hub v0.1.7: Infrastructure backup endpoints for disaster recovery

Add infra-backup push/pull API for controller DR:
- POST /api/v1/infra-backup — controller pushes infrastructure snapshot
- GET /api/v1/infra-backup/{customer_id} — fresh controller pulls backup
- infra_backups SQLite table with per-customer snapshots
- Customer detail page shows infra backup status card
- README.md with full API docs and DR flow

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-19 13:17:12 +01:00
parent d3d3044b98
commit 41e313bf36
5 changed files with 343 additions and 0 deletions
+12
View File
@@ -191,6 +191,9 @@ func (s *Server) handleCustomerDetail(w http.ResponseWriter, r *http.Request, cu
notifPrefs, _ := s.store.GetNotificationPrefs(customerID)
recentNotifs, _ := s.store.GetRecentNotifications(customerID, 10)
// Get infra backup metadata
infraMeta, _ := s.store.GetInfraBackupMeta(customerID)
type detailData struct {
Customer *store.CustomerSummary
Report map[string]interface{}
@@ -198,6 +201,8 @@ func (s *Server) handleCustomerDetail(w http.ResponseWriter, r *http.Request, cu
OverallStatus string
NotifPrefs *store.NotificationPrefs
RecentNotifications []store.NotificationLogEntry
InfraBackup *store.InfraBackupMeta
InfraBackupAge string
}
overallStatus := "ok"
@@ -211,6 +216,11 @@ func (s *Server) handleCustomerDetail(w http.ResponseWriter, r *http.Request, cu
overallStatus = "down"
}
var infraBackupAge string
if infraMeta != nil {
infraBackupAge = timeAgo(infraMeta.UpdatedAt)
}
data := detailData{
Customer: customer,
Report: report,
@@ -218,6 +228,8 @@ func (s *Server) handleCustomerDetail(w http.ResponseWriter, r *http.Request, cu
OverallStatus: overallStatus,
NotifPrefs: notifPrefs,
RecentNotifications: recentNotifs,
InfraBackup: infraMeta,
InfraBackupAge: infraBackupAge,
}
w.Header().Set("Content-Type", "text/html; charset=utf-8")
+23
View File
@@ -127,6 +127,29 @@
{{end}}
</section>
<!-- Infra Backup (Disaster Recovery) -->
<section class="card">
<h2>Infra Backup</h2>
{{if .InfraBackup}}
<div class="info-grid">
<div class="info-item">
<span class="label">Last Updated</span>
<span class="value">{{.InfraBackupAge}}</span>
</div>
<div class="info-item">
<span class="label">Deployed Stacks</span>
<span class="value">{{.InfraBackup.StackCount}}</span>
</div>
<div class="info-item">
<span class="label">Disks</span>
<span class="value">{{.InfraBackup.DiskCount}}</span>
</div>
</div>
{{else}}
<p style="color: #facc15">No infra backup received yet</p>
{{end}}
</section>
<!-- Health -->
<section class="card">
<h2>Health</h2>