Files
felhom-controller/REPORT.md
T

7.2 KiB
Raw Blame History

REPORT — TASK C2: one-click class-C file restore from the Tier-2 copy (closes F2)

Date: 2026-07-05 · Class: implementation (code + tests + deploy + live validation) Baseline: main @ 0313ecd (v0.99.0, matched the task's confirmed baseline exactly; not behind origin) Shipped: v0.100.0, live on guest 9201 (gitea.dooplex.hu/admin/felhom-controller:0.100.0, healthy) Outcome: drill finding F2 CLOSED — the whole C-series (F1/F2/F3/O4 from DRILL-appdata-restore-2026-07-04.md) is now resolved.

Commits (all on main)

Hash What
30b1110 Part 1 — RestoreTier2Files engine + rsyncRestoreMissing + copier seam + tests
27aeb41 Parts 2+3 — POST /backup/tier2/restore + "Fájlok visszaállítása" button + handler-guard test
85a5727 Part 4 — CHANGELOG v0.100.0, CONTEXT (F2 closed), REUSE, controller/README
felhom.eu 7e42ca6 backup-architecture.md — class-C in-place path, additive-only semantics, reindex caveat

Files (code): internal/backup/{tier2_restore.go(new), backup.go(seam)}, internal/web/{server.go, handlers.go, templates/backups.html}. Tests (new): internal/backup/tier2_restore_test.go, internal/web/tier2_restore_handler_test.go. Template gates (id + emoji) green.

What shipped

  • Engine Manager.RestoreTier2Files(stack) (int, error): in-place, additive-only restore of missing user files from the recorded Tier-2 copy (CrossDriveBackup.DestinationPath + the backups/secondary/<stack>/appdata literals from RunTier2 — never a fresh selectTier2Target). Single-flight with backup/restore; all refusals before any stop, each with a customer-readable Hungarian reason (no copy / never ran / copy dir gone / Tier-2 drive disconnected / live drive disconnected or decommissioned); stop → copy → start → 90s health-wait; copy and restart errors surface (F17), health timeout only warns.
  • Copier rsyncRestoreMissing: rsyncMirror's exec shape (mkdir, 60m ctx, trailing-slash contents copy, rsync's own exit code) with the opposite-direction flags — -a --ignore-existing --itemize-changes. Existing live files never overwritten; nothing ever deleted (the --delete trap). Count = >f itemize lines (pure countRestoredFiles); file names logged at DEBUG only, never INFO.
  • Endpoint + UI: POST /backup/tier2/restore (backupRestoreHandler-shaped guards) + the button on the healthy Tier-2 row only (Tier2Configured branch already excludes disconnected/inactive; extra Tier2LastRun gate), with the confirm dialog naming the additive-only contract + last-copy timestamp. Flash strings exactly per spec; n==0 is a success.
  • Out of scope honoured: no overwrite/point-in-time mode, no per-file selection, recovery-unit/ untouched, offbox untouched, C1-report Obs 1/2/3 untouched.

Tests & the §10 companion

go build ./... && go vet ./... && go test ./... green after every commit. Top-level test functions: 286 → 293 (+7).

  • Orchestration (seam): stop→copy→start order recorded; copier src = the recorded Tier-2 layout, dst = live AppDataDir; count passthrough.
  • Refusals C1C5: each asserts the non-effectStopStack never called, copier never invoked (C5 via a held running flag). C6 at the handler (httptest; nil backupMgr would panic if reached — it isn't).
  • Scenario D: (0, nil) success; copy-error surfacing (app still restarted).
  • FS-level semantics against real rsync (LookPath-skipped on Windows; PASS on the build server): deleted file restored byte-identical + counted; differing live file keeps live bytes; live-only file survives.
  • Companion red-proof (run on the build server, where rsync exists): flags swapped to rsyncMirror's (-a --delete, no --ignore-existing) → the test failed on both harms: live-edited b.txt was CLOBBERED: "BACKUP-VERSION" and live/c.txt: no such file or directory (plus count 2≠1). Mutation reverted (git status clean on the build server), test green again.

Deploy + live validation (§13, felhom-pve / guest 9201 only — Peti's box untouched)

Build ./build.sh 0.100.0 --push on 180; bootstrap deploy; verify :0.100.0 Up (healthy), no fatal/panic.

Method note: the claude-in-chrome bridge disconnected mid-session, so the first pass verified the rendered UI HTML (button + confirm + hidden stack_name, exactly once on nextcloud's Tier-2 row) and exercised the restore as the exact form POST the button submits. After Chrome was reopened, the real click was also validated end-to-end (step 8 below) — the UI path is now fully click-proven, no proxy caveat remains.

  1. Fixtures: sentinel d982fa3b….bin (sha 1e5f82d7…) + modify_me.txt (sha c5ca964b…) planted in live appdata/nextcloud/drillPOST /api/backup/tier2 → both in the Tier-2 copy on felhom-flash, shas identical.
  2. Three fates staged after the copy: .bin deleted live; modify_me.txt edited live (new sha bd24729a…); live_only.txt created live (sha 3c3474d5…).
  3. Restore: 302 → flash "nextcloud: 1 fájl visszaállítva a másodlagos másolatból." Log: Tier-2 file restore for nextcloud: …/felhom-flash/…/appdata → …/felhom-usb/appdata/nextcloud (additive-only)1 file(s) restored (15s).
  4. All three fate assertions PASSED: .bin back byte-identical (1e5f82d7…); modify_me.txt kept its LIVE sha (bd24729a…, NOT the backup's c5ca964b…); live_only.txt survived (3c3474d5…). Nextcloud healthy after restart.
  5. Idempotency (Scenario D): immediate rerun → flash "Nincs hiányzó fájl — minden fájl megvan a helyén." (success, 0 files).
  6. Refusal non-effect: sparkyfitness (no Tier-2 record) — the button is absent from its row (rendered-HTML count = 1, nextcloud only); direct POST → flash_error "Fájl-visszaállítás sikertelen: nincs másodlagos fájlmásolat ehhez az alkalmazáshoz"; container IDs + uptimes unchanged (never stopped).
  7. Cleanup: drill dir removed live; fresh Tier-2 run purged the copy side (mirror semantics); zero residue both sides; final docker ps all healthy.
  8. Real-click UI validation (Chrome reopened): fresh sentinel (click.txt, sha 6ce3342e…) → Tier-2 run → deleted live → the actual "Fájlok visszaállítása" button clicked in the dashboard (Nextcloud row expanded; the native confirm dialog fired with the exact spec text incl. the live last-copy timestamp 2026-07-05T11:35:49Z, auto-accepted via a test-only window.confirm override to avoid the extension-blocking dialog) → flash banner "nextcloud: 1 fájl visszaállítva a másodlagos másolatból." rendered on the page (screenshot) → file back byte-identical (6ce3342e…), nextcloud healthy. Cleanup repeated: zero residue, all healthy.

Observations (not acted on)

  1. Reindex caveat is real but untested in-app — the restored file returns at filesystem level; whether Nextcloud's own UI lists it may require occ files:scan (documented in backup-architecture.md; not exercised — the drill fixture lives outside Nextcloud's indexed user dirs).
  2. Carried from C1 (still open, unchanged): O4-residual/F3 interaction, compose stderr truncation, compose volume-label warnings, demo-box unauth/empty-CSRF exposure.