diff --git a/CONTEXT.md b/CONTEXT.md index ce01c06..f2982db 100644 --- a/CONTEXT.md +++ b/CONTEXT.md @@ -7,7 +7,41 @@ > > Ask Claude Code: "Please update CONTEXT.md with what we did today" -Last updated: 2026-07-18 (v0.145.0 — R-7b: share data enters the live backup runs, Model B′) +Last updated: 2026-07-19 (v0.147.3 — feedback slice 1: pressing a button says something) + +> **2026-07-19 — v0.147.0 → v0.147.3: feedback slice 1.** The systemic complaint, twice in one +> evening: you press a button and nothing happens. Three worst offenders fixed on the two patterns +> that already existed (deploy 3-step panel; storage-init status poll). **Deliberately NOT a +> framework** — that is ROADMAP **R-45**, and the two lessons it must encode are already written +> down there: a terminal state must be **probed, not inferred** (`compose up -d` exits 0 on a +> crash-loop), and a progress source reporting nothing is **normal, not broken** (restic reports 0 +> bytes for a whole incremental run). +> +> **4a** — the offsite verification restore names its **full path** in the flash, and +> `/backups/restore` lists existing verification copies (app · size · date · path) with a +> double-confirmed per-copy delete. That delete takes a **stack name, never a path**; red-proofed +> (neutralise the name guard and `stack:""` resolves to the offsite-restore ROOT and takes every copy +> with it). `backups/offsite-restore` now has ONE home, `offsiteRestoreRootFor`. +> **4b** — Megosztás enable/password no longer reconcile inside the POST; detached job + poll, with +> „képfájl letöltése" vs „indítás" decided BEFORE the work starts (afterwards the image is always +> present and the distinction is unrecoverable). +> **4c** — „Távoli mentés most" streams restic `--json`. **Manual only**; the nightly stays silent, +> pinned by a test. +> +> **Three of the four versions exist because the cards were watched against real runs on the demo +> box** — none of these would have surfaced from unit tests: (.1) an incremental run reports 0 bytes +> for its whole duration, so a byte-only bar looks hung in the COMMON case; (.2) restic 0.14 counts a +> file only when it completes, so one big archive freezes the file counters too — fall back to +> current file + elapsed; (.3) the run does not end with the last app — the shares leg and +> `forget --prune` took 40 of a 57-second run, and the card froze on the last app until phases were +> added. +> +> Also: `infra.Images()` + `--print-infra-images` close the golden/controller infra-image drift at +> the source. The golden's own copy had already drifted (felhom-samba missing → 3 of 4 baked), which +> is **why** enabling Megosztás pulled at runtime in the first place. Effective at the next bake +> (`felhom-agent` build-golden v2.1.0); no golden rebuilt. **Floor NOT raised — Viktor decides.** +> +> Earlier: 2026-07-18 (v0.145.0 — R-7b: share data enters the live backup runs, Model B′) > **2026-07-18 — v0.145.0: R-7b — the „Felhőmentés" toggle is now TRUE (Model B′), + samba liveness.** > Until v0.144.0 a share could be marked „Felhőmentés: bekapcsolva" while its files were in NO backup: diff --git a/controller/README.md b/controller/README.md index 84ecb92..3c34d3b 100644 --- a/controller/README.md +++ b/controller/README.md @@ -44,6 +44,8 @@ backups, monitoring and notifications. All Proxmox/disk operations are delegated - [Debug Mode](#13-debug-mode) - [Geo-Restriction](#14-geo-restriction) - [App-to-App Integrations](#15-app-to-app-integrations) + - [Network File Sharing — SMB](#16-network-file-sharing--smb-megosztás-v01440-r-7-slice-1) + - [Async-job feedback](#17-async-job-feedback-v0147x-feedback-slice-1) - [Repository Layout](#repository-layout) - [Configuration](#configuration) - [REST API](#rest-api) @@ -2415,6 +2417,64 @@ e-mail. Note the container name is NOT the stack name (`samba` vs `felhom-samba` --- +### 17. Async-job feedback (v0.147.x, feedback slice 1) + +Three long operations that used to be silent now report what they are doing. **These are three +targeted cards on the two existing patterns** (the deploy 3-step panel and the storage-init status +poll), NOT a framework — a unified async-job layer is ROADMAP R-45. + +**Verification-restore visibility** (`backup/offbox_verify_copies.go`, `web/offbox_handlers.go`, +`templates/backups_restore.html`). The offsite verification-restore flash now names the **full path** +it wrote to, and `/backups/restore` lists existing verification copies (app · size · date · path) +with a per-copy delete. + +- `offsiteRestoreRootFor(drivePath)` is **THE** place `backups/offsite-restore` is spelled; + `offboxRestoreScratchDir` builds on it so listing and delete resolve byte-identical paths to what + the restore wrote. +- `ListOffsiteRestoreCopies()` sweeps every candidate drive in the same preference order the restore + path uses to *choose* one, so it can never miss a copy the restore was capable of creating. +- `DeleteOffsiteRestoreCopy(stack)` takes a **stack name, never a path** — the customer cannot hand + the controller a directory to remove. Guarded by `isSafeStackName` plus a containment assertion on + the resolved path. `POST /backup/offbox/verify-copy/delete` additionally requires `confirm=1`, is + double-confirmed in the UI, and refuses while any backup/restore op is running. + +**SMB bring-up progress** (`web/samba_ensure_job.go`, `templates/sharing.html`). `/sharing/enable` +and `/sharing/password` no longer run `ReconcileSamba()` inside the POST — they start a detached +single-flight job (the `storage_init_job.go` shape) and the page polls `GET /sharing/status`. + +- The opening phase is decided **before** the work starts, from `stacks.SambaImagePresent()`: + `pulling` („képfájl letöltése") when the pinned image is not in local Docker storage, else + `starting`. Afterwards the image is always present, so the distinction is unrecoverable later. +- Terminal success is **probed** via `stacks.SambaRunning()` — `compose up -d` exits 0 on a + crash-loop. A `nil` reconcile with `UserSet == false` reports `needs_password`, not `running`. +- `/sharing/status` lets live container state win over a stale/absent job, so a page loaded after a + restart still tells the truth. + +**Offsite backup progress** (`backup/offbox_progress.go`, `templates/backups_remote.html`). +`RunOffboxBackupWithProgress` (the **manual** trigger only; the nightly `RunOffboxBackup` is +unchanged and stays silent) installs a progress sink, adds `--json` to the app-backup leg and scans +restic's stdout line-by-line through `offboxStreamRunner` — a streaming sibling of the existing +`offboxRunner` seam, injectable for tests. Published on the existing `GET /backup/offbox/status` +under `progress`. + +- Restic's status object is only partly usable in practice, and the fallbacks matter more than the + percentage: on an **incremental** run restic transfers no bytes (`bytes_done` is `omitempty`, so + absent) and `percent_done` stays 0 for the whole run; and because restic 0.14 counts a file only + when it **completes**, an app dominated by one large archive freezes the file counters too. The + card therefore degrades: bytes -> files -> current file + elapsed seconds. +- A run is **not** only the per-app loop. `Phase` (`shares`, `retention`) names the post-app stages + and clears app-scoped counters, so the card never shows the last app's finished numbers against + work that is no longer about that app. + +**Infra image pins** (`internal/infra`). `infra.Images()` returns every controller-managed infra +image, derived from the existing pinned consts. `felhom-controller --print-infra-images` prints it +(config-free by design — no `controller.yaml`, data dir or settings are touched) so the golden bake +(`felhom-agent configs/build-golden.sh`) can ask the controller image it is about to bake instead of +keeping its own list, which had already drifted. A `go/ast` test fails if a `*Image` const is added +without reaching `Images()`. + +--- + ## Repository Layout ``` @@ -2646,6 +2706,9 @@ All daily jobs use Europe/Budapest timezone. Skip-if-running prevents concurrent | POST | `/api/stacks/{name}/cross-backup/run` | Trigger cross-drive backup | | GET | `/api/stacks/{name}/cross-backup/status` | Cross-drive status | | POST | `/api/backup/cross-drive/run-all` | Run all scheduled cross-drive backups | +| GET | `/backup/offbox/status` | Offsite run status **+ live `progress` for a manual run** (v0.147.x) | +| POST | `/backup/offbox/verify-copy/delete` | Delete ONE verification copy (stack name + `confirm=1`; v0.147.0) | +| GET | `/sharing/status` | SMB bring-up phase + live container state (v0.147.0) | ### Storage