feat: infra backup GFS retention + version history

New infra_backup_versions table with GFS pruning (~14 versions per
customer). Recovery endpoint supports ?version=ID. New /versions API.
Dashboard shows collapsible backup history with app names and disk count.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-26 14:47:48 +01:00
parent f82fa9be2c
commit f1212e6ba8
5 changed files with 341 additions and 30 deletions
+4
View File
@@ -231,6 +231,7 @@ func (s *Server) handleCustomerUnified(w http.ResponseWriter, r *http.Request, c
var recentNotifs []store.NotificationLogEntry
var infraMeta *store.InfraBackupMeta
var infraBackupAge string
var infraBackupVersions []store.InfraBackupVersion
var events []store.Event
var eventCounts map[string]int
@@ -244,6 +245,7 @@ func (s *Server) handleCustomerUnified(w http.ResponseWriter, r *http.Request, c
if infraMeta != nil {
infraBackupAge = timeAgo(infraMeta.UpdatedAt)
}
infraBackupVersions, _ = s.store.ListInfraBackupVersions(customerID)
events, _ = s.store.GetRecentEvents(customerID, 50)
eventCounts, _ = s.store.CountEventsBySeverity(customerID, time.Now().Add(-24*time.Hour))
appTelemetry, _ = s.store.GetCustomerAppSummary(customerID, time.Now().Add(-7*24*time.Hour))
@@ -274,6 +276,7 @@ func (s *Server) handleCustomerUnified(w http.ResponseWriter, r *http.Request, c
InfraBackup *store.InfraBackupMeta
InfraBackupAge string
InfraBackupVersions []store.InfraBackupVersion
NotifPrefs *store.NotificationPrefs
RecentNotifications []store.NotificationLogEntry
History []store.CustomerSummary
@@ -315,6 +318,7 @@ func (s *Server) handleCustomerUnified(w http.ResponseWriter, r *http.Request, c
InfraBackup: infraMeta,
InfraBackupAge: infraBackupAge,
InfraBackupVersions: infraBackupVersions,
NotifPrefs: notifPrefs,
RecentNotifications: recentNotifs,
History: history,
@@ -313,7 +313,34 @@
<span class="label">Disks</span>
<span class="value">{{.InfraBackup.DiskCount}}</span>
</div>
<div class="info-item">
<span class="label">Versions</span>
<span class="value">{{.InfraBackup.VersionCount}}</span>
</div>
</div>
{{if .InfraBackupVersions}}
<details style="margin-top: 0.75rem;">
<summary style="cursor: pointer; color: var(--text-secondary, #94a3b8); font-size: 0.85em;">Backup History ({{len .InfraBackupVersions}} versions)</summary>
<table style="width: 100%; margin-top: 0.5rem; font-size: 0.85em;">
<thead>
<tr>
<th style="text-align: left; padding: 0.25rem 0.5rem;">Date</th>
<th style="text-align: left; padding: 0.25rem 0.5rem;">Apps</th>
<th style="text-align: right; padding: 0.25rem 0.5rem;">Disks</th>
</tr>
</thead>
<tbody>
{{range .InfraBackupVersions}}
<tr>
<td style="padding: 0.25rem 0.5rem;">{{.CreatedAt.Format "2006-01-02 15:04"}}</td>
<td style="padding: 0.25rem 0.5rem;">{{.StackCount}}{{if .StackNames}}: {{range $i, $n := .StackNames}}{{if $i}}, {{end}}{{$n}}{{end}}{{end}}</td>
<td style="text-align: right; padding: 0.25rem 0.5rem;">{{.DiskCount}}</td>
</tr>
{{end}}
</tbody>
</table>
</details>
{{end}}
{{else}}
<p style="color: #facc15">No infra backup received yet</p>
{{end}}