Two defects E-2d found on a real box, fixed in this order deliberately: the
message is corrected BEFORE it is put on screen, because switching on a banner
that lies is worse than a silent one.
R-114 — the third state. resolveBackupTargetState had two outcomes: a disk
claims the target (healthy), or nothing does (degraded, "the backup is on the
system disk"). The state "configured, and its drive is gone" had no branch, so
it fell into the second and inherited its message AND its offer. Observed live
with the target detached: degraded:true, target:"felhom-backup" plus the
system-disk copy (false -- the backup was on a drive that had vanished) plus
offer_path naming that same vanished drive as the remedy.
New BackupTargetState.TargetAbsent discriminates. Degraded keeps its meaning
("is there a problem") so the wire contract is unchanged for every consumer;
TargetAbsent answers "which problem", because the two have opposite remedies --
attach any second drive, versus reconnect THAT one. Copy routed through
degradedMessageFor so one place still decides what a customer reads. The offer
is suppressed on the branch itself, NOT left to firstOfferableDrive's
Disconnected skip: that flag is set by the agent-side gate in another repo
(R-113), and this state must be correct independently of it.
R-112 — the state finally has a consumer. The endpoint was byte-correct and
nothing in the product ever asked for it: templates fetch 18 distinct
/api/storage/* endpoints and backup-target[/assign] were the only two with zero
references. Server-rendered on /backups now, following the existing
SingleCopyWarning banner pattern -- not a 19th JS fetch, because a banner that
needs JavaScript to appear is one more thing that can silently not happen.
backupTargetView returns nil for healthy and unknown so those render nothing at
all. The offer control POSTs to the existing assign endpoint behind the standard
inline confirm, never auto-submits, and surfaces restart_required honestly
instead of adding a self-restart.
Scenario E (the seam test) drives backupsHandler over httptest and asserts the
RENDERED HTML -- handler -> view -> resolver -> template. It deliberately does
not call the resolver and assert a string, which would prove the resolver that
was never broken. Deleting the one line that sets data["BackupTarget"]
reproduces the R-112 state and fails every render assertion.
Tests 326 -> 338 (+12) in internal/web; suite green (27 packages); both template
gates pass. Three red-proofs run and reverted, files byte-identical after.
MinAgent unchanged at 0.113.0: R-114 reads BackupTarget/MountPath/GuestPath/Role,
none of which R-113 altered (it changed BoundUnderParent, which this code does
not read). demo-hp on agent 0.113.0 is not held.
The absent copy is verbatim the hub's customerMessages["backup_target_absent"]
so the banner and the email tell one story -- filed as a two-repo drift risk,
not solved.
NOT LIVE-VALIDATED. Scenario C cannot occur on a healthy box; Session C proves it.
ONE setting (window start W) drives every nightly leg at fixed, never-stored
offsets: DB dump at W, tier-2 at W+60m, off-box at W+105m (wrap-safe). Precedence
settings > controller.yaml db_dump_schedule > 02:30.
- scheduler.UpdateDaily: retime a daily job at runtime (no restart) via a per-job
buffered resched chan + a select case in runDailyJob.
- new pure package internal/backupwindow (LegTimes/GateWindow/EffectiveWindow).
- quiesce disk-tier window gate: scheduled cycles run only inside [W+2h,W+6h) with a
safety valve (age>cadence+24h runs regardless); manual TriggerNow never gated.
Backend.Due now also returns the backup age (from the agent's own /backup/due).
- backup page: Mentési időablak card (time input + derived leg/gate rows); POST
/backups/window validates -> saves -> UpdateDaily x3 -> flash.
Tests: 5 groups, all red-proofed. Agent/cadence//backup/due untouched.
QuotaGB rides the descriptor into OffboxTarget; RepoSizeBytes persisted
from restic stats. Pre-run gate: >=100% refuses NEW backups (Hungarian
notice + operator alert) but prune STILL runs (red-proofed) and restore is
never gated; >=80% warns. /backups usage bar (quota>0 only). The hub
report gains the non-secret offsite status object for the OffsiteChecker.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PSK5g6qYLknKj8u3QAFEr6
Pairs with agent v0.77.0. StageEscrowSecret pushes the repo password to the
agent (POST /escrow/stage-secret) at offsite-enable → EscrowState="pending".
Atomicity gate: RunOffboxBackup (scheduler + handler) refuses until
EscrowState="escrowed" (operator POST /backup/offbox/confirm-escrow after the
escrow ceremony) — no un-recoverable offsite ciphertext can exist. DR:
POST /backup/offbox/inject-password pre-places a recovered 64-hex password 0600
(honored by WriteOffboxSecrets' IsNotExist guard; refuses clobber without
force). DR recipe gains non-secret offsite_restic coords (DRResticCoord); SFTP
key regenerated at DR, not escrowed. New settings.OffboxTarget.EscrowState.
Tests + atomicity & inject companion red-proofs green; UI gates pass. NOT yet
live-validated (supervised ceremony).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PSK5g6qYLknKj8u3QAFEr6
offbox located each toggled app's recovery unit via AppNamespaceRoot→GetAppDrivePath,
which reads the app's LIVE app.yaml HDD_PATH and silently falls back to systemDataPath
when the app isn't deployed → looked on the wrong drive, backed up nothing, reported
ok/0 (DIAG root cause). Now:
- discoverOffboxUnit/offboxCandidateNSRoots scan the durable storage registry
(schedulable non-decommissioned paths ∪ systemDataPath) for backups/primary/<app>,
independent of deploy state; newest-by-manifest-CreatedAt wins on drive churn.
- RunOffboxBackup: runOffboxInternal returns (backedUp, missing, err); 0-of-N toggled →
hard error + operator alert; partial → ok + new OffboxTarget.LastWarning (shown on
/backups, preserved across config edit).
- AppNamespaceRoot + primary WRITE paths unchanged.
- 6 non-hollow tests (A-E + edge) + both companion red-proofs run (reverted).
- NOT yet live-validated against the Storage Box (spike creds torn down).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PSK5g6qYLknKj8u3QAFEr6
Re-adjudicates F4: /backup/restore, /backup/tier2/restore, /backup/offbox/restore
blocked the HTTP request until completion, so through cloudflared's 100s cap a
customer got an error page while the restore succeeded (offbox worse — bounded
on r.Context(), canceling the SFTP restore mid-flight). Convert all three to the
offboxRun async shape: fast-path IsRunning refuse, background goroutine
(offbox ctx off r.Context() -> Background+30m), instant redirect. Add mutex-
guarded op-status (opstatus.go) + GET /api/backup/restore-status + a 3s-polling
backups.html banner (neutral running, red on failure). Restore single-flight
unchanged. Tests + red-proof (sync handler blocks indefinitely vs <500ms async).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PSK5g6qYLknKj8u3QAFEr6
- Endpoint next to /backup/restore; handler mirrors backupRestoreHandler
(ParseForm → validStackName → backupMgr guard → WARN with RemoteAddr →
RestoreTier2Files → flash). Flash strings: "<stack>: N fájl visszaállítva a
másodlagos másolatból." / "Nincs hiányzó fájl — minden fájl megvan a helyén."
/ "Fájl-visszaállítás sikertelen: <err>" (refusals carry the Hungarian
reasons from the engine).
- backups.html: the button on the healthy Tier-2 layer row only (the
Tier2Configured branch already excludes disconnected/inactive; additionally
gated on Tier2LastRun), inline POST form with CSRF + confirm dialog naming
the additive-only semantics and the last-copy timestamp. Template gates
(id + emoji) green.
- Handler guard test (C6): traversal/empty → exact Hungarian flash, no work
started (nil backupMgr would panic if reached).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PSK5g6qYLknKj8u3QAFEr6
- The D0 grep gate false-negatived multibyte emoji on Windows (its zero
was wrong). scripts/emoji_gate.py scans by Unicode codepoint; it found
8 survivors: backups.html (📁→file-text icon, 🔄 restore-info text),
debug.html (🔄→'fut', 🔒→'titkosított'), deploy.html (📦→upload icon,
★ default-marker → '(alapértelmezett)'), storage.html (📦→upload icon,
📦 in the migrate-target option → plain text). All → sprite icons or
plain words.
- Gates: scripts/emoji_gate.py = 0; new Go TestNoEmojiInTemplates
codepoint scan keeps it enforced (allowlist: ✓✗✔✘•●○■▶ monochrome
text marks). go build/vet/test ./... green (18 pkgs).
Found during the D0 click-through: OffboxTarget.LastRun persists as an
RFC3339 string (settings.json), but backups.html passed it to timeAgo
(expects time.Time), so GET /backups returned 500 on any node where an
off-box backup had ever run. Pre-existing since v0.93.0 (NAS Part B),
unrelated to the re-skin. New timeAgoStr funcmap helper parses the
string (falls back to the raw value); also v2-token fix for the offbox
error hint (var(--danger) fallback hex) and the dead stat-running/
stat-stopped classes on the offbox status card.
- settings.html: Aktív/Inaktív rows -> run-blue check / neutral gray
(Inaktív no longer red), update states -> run/crit/progress with
sprite check/x/spinner, pencil + cancel buttons -> icons, storage
badges -> tags, host-disk bar + JS drive capBar -> meters
(usageColorClass -> nominal/warn/crit), state-text-* consumers on the
new suffixes incl. JS-built class names.
- debug.html, app_info.html, storage_init/attach.html, logs.html:
emoji -> sprite icons or plain text in templates AND JS strings.
- catchall.html (standalone, no sprite): v2 token sweep of its inline
style, status emoji -> inline SVGs; a stopped app renders neutral,
not red.
- login.html: two-tone H1 (last word blue-bright).
- setup_hub_versions.html: stale var(--border,#30363d) fallback -> v2.
- Test Group F grep gate: all 34 banned patterns at ZERO across
internal/web + internal/setup; Scenario E test now asserts the
'--bg-0: #0A1220' token literal.
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
Part A of the UI-fixes/storage-spike spec.
A1: enrichHostStorageTargets sorts /api/host-metrics storage_targets
server-side and attaches friendly Hungarian labels + purpose, fixing the
#host-storage-bars reorder-on-poll bug. Display labels only — PVE storage
ids are never renamed.
A2: new GET/POST /stacks/{name}/backup Tier-2 config panel; the "2. mentés"
Beállítás button is repointed there from the dead-end deploy page. Customer
can pin a target drive or disable Tier 2; preference is preserved across the
runner's status writes. Always visible (single-SSD + non-HDD apps included).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Tier 2 rsync-mirrors each HDD app's recovery unit + appdata to a DIFFERENT physical
disk (the only off-drive protection bind-mounted userdata can get; PBS can't reach it).
Auto-enabled, auto-target: prefer another registered drive (different physical disk via
system.SamePhysicalDevice), else the internal SSD for SMALL units only — with a
size-aware headroom guard that REFUSES rather than fill the ~8G guest rootfs, recording
an honest "needs 2nd HDD" status. Status persisted via the surviving CrossDriveBackup;
"2. mentés" UI card now populated. Daily tier2-backup job + POST /api/backup/tier2.
- backup/tier2.go (engine+selection+headroom), tier2_test.go (headroom arithmetic)
- system.SamePhysicalDevice (linux Stat_t.Dev + stub)
- handlers.go Tier2 UI population + tier2DestLabel; backups.html honest no-target reason
- fixed stale TestBackupCopiesOnPath (old felhom-data layout -> in-guest layout)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Backups page: whole-guest backup shown as real DR — target label "Biztonsági szerver –
külön hardver (PBS)"; app-data "Távoli mentés" card now reflects the PBS offsite tier
(guestBackupView.Offsite) instead of "nincs beállítva".
- Model-A double-nest fix: appbackup path helpers take a felhom-data NAMESPACE ROOT (no
internal felhom-data join); backup.Manager.namespaceRoot/AppNamespaceRoot resolve
HDD-vs-systemDataPath provenance so a drive-resident app's backups land single-nested
(<drive>/backups/... on the guest = <drive>/felhom-data/backups/... on the host) instead
of .../felhom-data/felhom-data/.... Writes, deletion (GetStackBackupData/RemoveStack/
ProtectedHDDPaths), wipe-warning scan, and export updated coherently; legacy double-nest
dirs kept protected. New appbackup test asserts no doubled segment.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Part 2 of the USB/backup spec. agentapi: StatusResponse.Backup record, DueResponse
age_seconds, RestoreTestStatus(). New "Rendszermentés (teljes mentés)" section
(read-only: last backup/target PBS-vs-local/next-due/restore-test) + "Mentés most"
manual trigger that goes through the quiesce loop (controller owns quiescing):
quiesce.Loop gains mutex + TriggerNow() (single-flight, async). New
/api/guest-backup/{trigger,status} (distinct from apiRouter's /api/backup/*).
App-data rows relabeled under an "Alkalmazás-mentések" divider. Config → slice 10.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
backups.html still referenced .Backup.{RepoStats,LastBackup,ResticSchedule,
NextBackup,PruneSchedule,Retention,SnapshotHistory,LastCheckTime,LastCheckOK} —
fields removed from FullBackupStatus in the 8C de-privileging (disk-tier backup
moved to the agent). Field access on the slimmed struct 500s. Removed the dead
restic/snapshot/repo-stat sections; kept the app-data (DB dumps + per-app) view.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Add Docker named volume backup to Tier 1 (dump to tar, include in restic)
and Tier 2 (copy tars to rsync mirror _volumes/ dir)
- Fix volume name resolution: use project-prefixed names (mealie_mealie_data)
- Fix double Tier 1 in restore dropdown: filter snapshots by app's home drive
- Add Tier 2 restore: RestoreAppFromTier2() restores from rsync mirror
- Show Tier 2 entry in restore dropdown when cross-drive backup succeeded
- Add .fab import link in restore section
- Volume-aware restore type banners and backup content labels
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Deactivated drives (Schedulable=false) now treated like disconnected for
Tier2 backups. New IsStoragePathSchedulable() checks active+connected+not
decommissioned. UI shows yellow "Cél meghajtó inaktív" badge, scheduler
skips silently with WARN log.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- IsUSBDevice/diskModel: strip findmnt bind-mount suffix [/subdir] before
parsing device path (fixes USB badge not showing for attach-wizard drives)
- crossdrive.go: skip disconnected src/dest drives with WARN log instead of
returning error (prevents noisy error status in settings.json)
- handlers.go: detect Tier2 destination disconnection, set yellow status dot
instead of red, skip ValidateDestination for disconnected paths
- backups.html: new template branch showing "Cél meghajtó leválasztva" badge
with grayed-out info and hidden "Futtatás most" button
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Phase 1: Deprecate restic as Tier 2 method (rsync only), auto-migrate on startup
Phase 2: Enhanced per-app migration with backup awareness, DB dump copy, auto-cleanup
Phase 3: Full drive migration with decommissioned state, rollback support, wizard UI
Phase 4: Hub report includes decommissioned drive state
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add StorageBars to backupsHandler so all registered storage paths appear
- Update backups.html to use StorageBars loop (replacing single HDDConfigured block)
- Rename "SSD (/)" → "Rendszer (/)" on backup, monitoring, and dashboard pages
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Major refactor of backup and storage paths:
- Per-drive restic repos at <drive>/backups/primary/restic/
- Per-app DB dumps at <drive>/backups/primary/<app>/db-dumps/
- Remove global BackupDir, DBDumpDir, ResticRepo config fields
- Add SystemDataPath config (fallback for apps without HDD)
- New backup/paths.go with pure path computation helpers
- Add GetStackHDDPath to StackDataProvider interface
- Restic methods now accept repoPath as parameter
- Cross-drive backup uses new secondary path structure
- Rename storage/ to appdata/ in scripts and compose templates
- Update protected HDD paths (storage → appdata + backups)
- Simplify backup UI (remove global path displays)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Tier 2 cross-drive backup now configurable for all apps (not just HDD apps)
- Non-HDD apps (Mealie, Gokapi) can back up config + DB to secondary drive
- Status dot: removed "auto" gray — all apps start yellow, green = 2+ tiers OK
- Backup page: Tier 2 row always shown, Tier 3 placeholder added
- Deploy page: cross-drive config visible for all deployed apps
- Meta badges: non-HDD apps show "Konfig" or "Konfig + DB" instead of "Auto"
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Cross-drive now copies DB dumps (_db/) and config (_config/) alongside user data
- restic cross-drive includes config dir + full DB dump dir
- UI: per-tier rows (1. mentés / 2. mentés) instead of per-layer (DB/Konfig/Data)
- UI: BackupContents label shows what each tier protects (DB + Konfig + Adatok)
- UI: rsync backups show browsable indicator (📁)
- Cleanup: removed unused filterSnapshotsByPaths + pathCovers from router.go
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Fix 1: HDD data backup is now mandatory for all deployed apps.
resolveAppBackupPaths() iterates ListDeployedStacks() directly — no
longer reads GetAppBackupMap() or checks the Enabled flag. DiscoverAppData()
drops backupPrefs parameter; BackupEnabled is set from HasHDDData.
Five dead settings methods removed: IsAppBackupEnabled, SetAppBackup,
GetAppBackupMap, SetAppBackupBulk, GetAppBackupPrefs.
Fix 2: Cross-drive backup now triggers a fresh DB dump (DumpStackDB)
before running. DBDumper interface added to crossdrive.go; Manager
implements it; SetDBDumper wired in main.go. Non-fatal — proceeds with
user data backup even if DB dump fails.
Fix 3: Restore dropdown shows ALL deployed apps (not just HDD+enabled).
restore.go rewritten: always restores config+DB, adds user data if hasHDD.
UI shows restore type banner (full / config+DB / config only) with
color-coded styling. Snapshot API clarified for non-HDD apps.
Fix 4: "Docker kötetek" → "Konfiguráció" — named volumes are not in
the restic backup paths; compose files + app.yaml are what's backed up.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- StackDataProvider interface extended with StopStack/StartStack
- backup.Manager.GetStackHDDMounts() delegates to stackProvider
- RestoreApp() auto-stops app before restic restore, restarts after (even on failure)
- stackAdapter in main.go wires StopStack/StartStack through to stacks.Manager
- GET /api/backup/snapshots?stack={name} filters snapshots by app HDD paths via filterSnapshotsByPaths()
- Restore section simplified: no path list, per-app filtered snapshots, human-friendly timestamp format, single calm warning, empty-result inline message
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Bug fixes:
- GetFullStatus() returns deep copy; CrossDriveSummary/UnconfiguredApps/CrossDriveWarnings
are always nil in the copy so the handler builds them fresh (fixes duplicate-apps bug)
- Replace binary IsMountPoint check with tiered CheckBackupDestination() — path-not-exist,
not-writable, system-drive (warning), disk >90-95% full; shown as warning vs critical
- Remove dead settingsAppBackupHandler / POST /settings/app-backup route (toggle wrote
to settings.json but nothing consumed the flag)
Architecture:
- Unified per-app backup rows: new AppBackupRow struct + buildAppBackupRows() replaces
the two old sections with expandable rows showing all 3 layers per app
- Sequential backup chaining: cross-drive runs immediately after restic (removed
independent cross-drive-daily/cross-drive-weekly scheduler jobs)
- Deploy page: remove "Csak kézi indítás" schedule option; add weekly consistency note
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Replace the complex "Alkalmazás adatok" form (checkboxes, paths, volumes,
save button) with a clean read-only status list. Each app shows its name
(linked to its deploy page) and a simple status: Aktív / Inaktív / N/A.
Also include ALL deployed stacks in the list (not just HDD-capable ones),
so apps without user data appear with N/A status.
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
- Health severity fix: mount-point check downgraded from issue (FAIL) to warning (WARN)
- All storage health messages translated to Hungarian
- Success flash messages for all storage operations
- Edit storage path labels (inline edit UI + backend)
- App details per storage path on settings page (expandable list with names + sizes)
- Storage badge on stacks page showing which storage each app uses
- Deploy dropdown with free space display and low-space warning (<20%)
- Filesystem & disk info on settings page (ext4/btrfs, device, model via findmnt)
- Backup page storage context with per-app storage label badges
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add 4-branch template guard to handle zero-value DumpValidation
(shows "–" instead of misleading "Hiba" when validation never ran)
- Add debug logging to ValidateDump for all code paths
- Add cross-check re-validation in RefreshCache to heal stale
lastDBDump validation state from disk
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
New /backups page with full backup system visibility:
- Status overview cards (local/remote backup, DB count, repo size)
- Schedule section with next-run times and retention policy
- Database table with type, size, validation (table count), status
- Snapshot history table with per-snapshot stats
- Repository info card with paths, integrity status, remote placeholder
- "Mentés most" button with auto-refresh polling
- Empty state when backup not configured
Backend: SnapshotRecord history (ring buffer), DumpValidation,
ListDumpFiles, ListSnapshots, GetFullStatus, restic check tracking.
Server accepts scheduler for next-run time calculation.
Sidebar nav updated with 3rd item, dashboard backup card title clickable.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>