controller v0.93.0: NAS Part B off-box backup target (restic-over-SFTP)

Encrypted restic repo over SFTP for the app-data tier (the off-site 3-2-1 leg). A dead
NAS fails fast via -oConnectTimeout (spike Q8), never hangs the runner; secrets are 0600
files (ride DR via PBS whole-CT); init-if-absent, retention forget --prune, restore,
single-flight, per-app toggle + UI. restic re-added to the image.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HxLA1mZurFq9kt8hneFeCs
This commit is contained in:
2026-06-30 15:26:38 +02:00
parent ddeb509d1d
commit 2a7deadc93
11 changed files with 1059 additions and 0 deletions
+31
View File
@@ -635,6 +635,11 @@ func (s *Server) backupsHandler(w http.ResponseWriter, r *http.Request) {
dbDumpTotalBytes += f.Size
}
data["DBDumpTotalBytes"] = dbDumpTotalBytes
// Off-box (NAS) restic-SFTP backup (Part B): the target status + per-app off-box toggles.
data["Offbox"] = s.settings.GetOffboxTarget()
data["OffboxConfigured"] = s.backupMgr.OffboxConfigured()
data["OffboxApps"] = s.buildOffboxApps()
} else {
data["Backup"] = nil
}
@@ -642,6 +647,32 @@ func (s *Server) backupsHandler(w http.ResponseWriter, r *http.Request) {
s.executeTemplate(w, r, "backups", data)
}
// OffboxAppRow is one deployed app's off-box toggle state for the backups page.
type OffboxAppRow struct {
Name string
DisplayName string
Enabled bool
}
// buildOffboxApps lists deployed, non-protected apps with their off-box toggle state.
func (s *Server) buildOffboxApps() []OffboxAppRow {
var out []OffboxAppRow
if s.stackMgr == nil {
return out
}
for _, st := range s.stackMgr.GetStacks() {
if !st.Deployed || st.Protected {
continue
}
dn := st.Meta.DisplayName
if dn == "" {
dn = st.Name
}
out = append(out, OffboxAppRow{Name: st.Name, DisplayName: dn, Enabled: s.settings.IsAppOffbox(st.Name)})
}
return out
}
// AppBackupRow holds per-tier backup information for one app on the backup page.
type AppBackupRow struct {
StackName string