Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PSK5g6qYLknKj8u3QAFEr6
7.2 KiB
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+ thebackups/secondary/<stack>/appdataliterals fromRunTier2— never a freshselectTier2Target). 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--deletetrap). Count =>fitemize lines (purecountRestoredFiles); 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; extraTier2LastRungate), 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 C1–C5: each asserts the non-effect —
StopStacknever 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"andlive/c.txt: no such file or directory(plus count 2≠1). Mutation reverted (git statusclean 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.
- Fixtures: sentinel
d982fa3b….bin(sha1e5f82d7…) +modify_me.txt(shac5ca964b…) planted in liveappdata/nextcloud/drill→POST /api/backup/tier2→ both in the Tier-2 copy on felhom-flash, shas identical. - Three fates staged after the copy:
.bindeleted live;modify_me.txtedited live (new shabd24729a…);live_only.txtcreated live (sha3c3474d5…). - 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). - All three fate assertions PASSED:
.binback byte-identical (1e5f82d7…);modify_me.txtkept its LIVE sha (bd24729a…, NOT the backup'sc5ca964b…);live_only.txtsurvived (3c3474d5…). Nextcloud healthy after restart. - Idempotency (Scenario D): immediate rerun →
flash "Nincs hiányzó fájl — minden fájl megvan a helyén."(success, 0 files). - 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). - Cleanup: drill dir removed live; fresh Tier-2 run purged the copy side (mirror semantics); zero residue both sides; final
docker psall healthy. - Real-click UI validation (Chrome reopened): fresh sentinel (
click.txt, sha6ce3342e…) → 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 timestamp2026-07-05T11:35:49Z, auto-accepted via a test-onlywindow.confirmoverride 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)
- 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). - Carried from C1 (still open, unchanged): O4-residual/F3 interaction, compose stderr truncation, compose volume-label warnings, demo-box unauth/empty-CSRF exposure.