# REPORT — v0.205.0: a backup that skipped an app the customer chose is not „Rendben" (R-234) 2026-08-06. Controller **v0.204.0 → v0.205.0**. MinAgent unchanged (**0.127.0**). `felhom-agent`, `app-catalog-felhom.eu` untouched. No hub change. ## The correction that outranks the task §3 stated the mechanism as established: *"the off-site copy sends the local recovery bundle an app already has; switching an app on does not create one, so the first run after the toggle finds nothing to send."* **Measured on demo-hp, that is not what the code does.** The off-site run's own **pre-dump phase** calls `captureAllRecoveryUnits()`, which writes a unit for **every deployed stack** — through `admitApp` — *before* the per-app push loop. I moved `calibre-web`'s unit aside (never deleted) on demo-hp and triggered a run through the real endpoint: ``` BEFORE : units = calibre-web, opengist, privatebin status=ok snapshots=9 moved : calibre-web unit → .calibre-web.R234-aside RUN : POST /backup/offbox/run → 302, finished ~90 s, status=ok AFTER : status=ok snapshots=9 warning=(none) unit : /mnt/sys_drive/felhom-data/backups/primary/calibre-web → RECREATED ``` So "selected but no bundle yet" **does not survive a run** for a deployed app. The filed mechanism could not have produced the 2026-08-06 sequence. **What did.** `POST /backup/offbox/run` launched the run in a goroutine and immediately answered „A távoli mentés elindult". Inside, `acquireRunning` refused (a run was already in flight) and `runOffboxBackup` returned **nil** — no error, no signal. The card then showed the **previous** run's „✓ Rendben · 1 pillanatkép", read as covering the app just selected. It did not: the restore refused for that app minutes later, and a third run carried it. That is R-234's real cause, and it is the same family — a skip that reached a log and not a verdict. ## Live validation (§12) | # | what | observable | |---|---|---| | 1 | selected app with no unit → run | the run **recreated the unit** and reported ok — the measurement above. The verdict change is exercised by the run-level tests, because production cannot easily be held in that state | | 2 | counters intact | `snapshot_count` 9 → 9, `last_success` advanced to `2026-08-06T19:34:27Z` — what was captured is still recorded | | 3 | the third-run behaviour arriving sooner | **not applicable as filed**: there is no wait for a deployed app; the unit is created by the run itself (§7.3) | | 4 | a box with nothing selected | unchanged — Scenario D is pinned by test, and demo-hp's other apps stayed `ok` | **Access note:** demo-hp's dashboard password had been changed by the customer claim, so I reset it through the documented `--print-reset-code` escape hatch (code streamed file→file, extracted by shape, shredded; the new password is a 24-char generated value in a `0600` file on DooPlex). That is a deliberate change to a Tier-0 demo box, recorded here. ## §7.2 — which skips count, as decided | skip | counts? | why | |---|---|---| | selected + **deployed**, no recovery unit | **YES** → `incomplete` | the app the customer chose is not in the snapshot at all | | selected but **not deployed** | **no** — named, with what to do | a box left amber forever by an app somebody removed is a status nobody reads | | drive disconnected / decommissioned | **no** | it has its own card and its own signal; re-reporting it here would double-count | | nothing selected at all | **no** | unchanged: the existing „nincs mentésre jelölt alkalmazás" notice | ## §7.3 — the measurement, and the decision it forced `CaptureRecoveryUnit` writes `compose/` (docker-compose.yml, .felhom.yml, app.yaml) + `manifest.json` — **a few KB**, per `admission.go`'s own note — and **enumerates** the DB/volume dumps already present rather than creating them. It is idempotent (skips all writes when current) and **does not stop the app**; the stopping work belongs to the separate dump flow. **Decision: nothing to build.** The cheap, non-stopping capture already runs for every deployed stack inside the off-site run's pre-dump phase, through `admitApp`. For a deployed app there is no wait to remove, and the §7.3 branch that would have added an inline capture would have duplicated it. ## §7.4 — every changed Hungarian string - „Ezek az alkalmazások NEM kerültek be a távoli mentésbe, mert még nincs helyi mentési egységük: %s. A következő mentés általában már elkészíti — ha a második futás után is itt szerepelnek, szólj az üzemeltetőnek." - „Ezek az alkalmazások ki vannak jelölve távoli mentésre, de nincsenek telepítve, ezért nem menthetők: %s. Ha már nincs rájuk szükséged, vedd ki a kijelölésüket a Távoli mentés oldalon." - „Már fut egy távoli mentés — ez a kérés nem indított újat. A most látható eredmény még a korábbi futásé; várd meg, míg ez befejeződik." - **sibling, extended so both read alike:** „Figyelmeztetés: a(z) %s alkalmazás egyes adatmappái nem kerültek a távoli mentésbe: %s. **Ellenőrizd, hogy a mappák megvannak-e a meghajtón; ha igen és ez a következő mentés után is látszik, szólj az üzemeltetőnek.**" The replaced sentence was: „Figyelmeztetés: %d alkalmazásnak nincs elérhető mentése, ezek kimaradtak: %s" — a count with no next step, reading the same whether the customer must act or simply wait. ## Tests and red-proofs `go build` · `go vet` · `go test ./...` → **28 packages ok**. `controller_gates.py --fast` → **9/9 OK**. New: `internal/backup/offbox_verdict_r234_test.go` (Scenarios A, C, D, F — run-level) and `internal/web/offbox_run_inflight_test.go` (handler-level). | # | mutation (each asserted to have applied) | result | |---|---|---| | RP-1 | drop `unprotected` from the verdict — the defect itself | `SkippedSelectedAppIsIncomplete` **FAIL** — „LastStatus = ok, want incomplete" | | RP-2 | remove the classification switch; count **every** skip | `SelectedButUndeployedIsNamedNotCounted` **FAIL** — a removed app turns the box amber | | RP-3 | stop adding skipped apps to the operator signal | `SkippedSelectedAppIsIncomplete` **FAIL** — „operator signal … got map[]" | | RP-4 | remove the synchronous in-flight check in the handler | `InFlightRequestIsNotReportedAsStarted` **FAIL** | All restored; `grep -c RED-PROOF` = 0 in both files afterwards; suite green again. **Fixture note:** the shared `offbox3aProvider.ListDeployedStacks()` returned nil, so my first run of Scenario A passed *for the wrong reason* and F passed vacuously. Fixed with an **opt-in** `deployed` map that defaults to nil, so no existing fixture's behaviour moves. ## Files - `internal/backup/offbox.go` — `ErrOffboxRunInFlight`, `offboxWholeUnitGap`, the skip classification, `missingUnprotected`/`missingNotDeployed`, `stackDeployed`, `driveUnavailableFor`, the verdict, the messages - `internal/backup/offbox_capture.go` — the sibling message - `internal/backup/backup.go` — `AcquireRunningForTest`/`ReleaseRunningForTest` (test-only seams) - `internal/web/offbox_handlers.go` — the synchronous single-flight refusal - new `internal/backup/offbox_verdict_r234_test.go`, `internal/web/offbox_run_inflight_test.go` - `internal/backup/offbox_3a_test.go` — opt-in deployed set - `CHANGELOG.md`, `controller/README.md`, `CONTEXT.md` ## Observations, not acted on - **The zero-selection notice reads „Sikeres — nincs mentésre jelölt alkalmazás"** while the status is `ok`. It is honest, but "Sikeres" beside "nothing is covered" is the same rhetorical shape this session is about, one notch weaker. Not touched: Scenario D forbids changing that path. - **`res.missing` is still used verbatim** for the no-silent-success error text (every app missing → `error`). Correct as-is, and deliberately left, since that path already refuses loudly.