# P2 live: R-893 option A, proved on scratch guest 9202 (2026-10-20, by day) Night brief 2026-10-20 part 2, Part 2 (operator answer „D20 A"). Controller **0.336.0-rc.1** (commit `93fb251`, image id `sha256:0756d4d2…`) on Tier 0 only: demo-hp, scratch guest 9202. Code read live: `felhom-controller/controller/internal/backup/offbox_restore_undo.go` and `offbox_reconstitute.go` at `93fb251`. Architecture: `07-backup-architecture.md` §6.3 (R-893); `09` §3 decision 192 and D20 = A. The method repeats `night-2026-10-19/P3/` (the measurement of the old behaviour) step for step, so the two sets of numbers can be compared. ## Method (endpoint level, the household's buttons; no browser) - 9202 was on **0.331.0**. For the run it went to 0.336.0-rc.1: pull, write `/etc/felhom-controller-image`, restart `felhom-controller-bootstrap.service`. Healthy, 0 `[ERROR]` (`01-upgrade.txt`). It was put back after the run. - **Scratch off-site target:** `atmoz/sftp:alpine` container `r893b-sftp` on 9202 itself (port 2222). The repo is on 9202's own disk at `/var/lib/felhom/r893b-sftp` (nvme-scratch, not `pve/data`). The controller reached it at `172.17.0.1:2222`, which does not depend on DHCP. Set up through `POST /backup/offbox/config` with a throwaway ed25519 key and the server's own host key pinned. 9202 has no agent, so the escrow was confirmed with the legacy manual `POST /backup/offbox/confirm-escrow`. Only `rallly` was switched on (`POST /backup/offbox/toggle`). - **Throwaway app:** Rallly 4.15.3 + postgres:18-alpine. Its DB is in the named volume `rallly_rallly_postgres_data`. Deployed through the product (`box_walk.deploy`). - **Observables, all positive** (`tool-obs.sh.txt`): - the `r893_marker` rows; - tables that exist only at certain times; - the public base-table count; - an md5 of `pg_dump --schema-only`, with pg 18's random `\restrict` lines removed and **a second dump as its control** (equal every time); - an md5 of the per-table row counts; - an md5 of `pg_dump --data-only`; - the definition's image lines; - the `backups/restore-undo/rallly` folder. - **Forced error:** the same as last night. One line, `SELECT r893_forced_replay_failure();`, goes into the PREPARED scratch copy of the snapshot's dump, just before its completion marker. The completeness gate still passes, and the replay fails **after the volume was replaced**. - **Restore:** `POST /backup/offbox/reconstitute` with `app=rallly confirm=1`. The outcome was read from `/api/backup/restore-status`, the source the restore page polls. - **Watch on the undo folder:** while each restore ran, a watcher on 9202 listed `/mnt/sys_drive/felhom-data/backups/restore-undo` every 0.3 s. This shows the copy from a different channel than the controller's log. ## Timeline (UTC) | Time | Act | File | |---|---|---| | 07:25:33 | baseline: 0.331.0, no off-site target, `settings.json` and the `offbox/` folder copied | `00-baseline.txt` | | 07:25:47 | 9202 on 0.336.0-rc.1, healthy | `01-upgrade.txt` | | 07:27:37–07:27:58 | SFTP target up; off-site configured, escrow confirmed, `rallly` switched on | `02-sftp-target.txt` | | 07:28:03–07:28:53 | Rallly deployed | `03-deploy.txt` | | 07:29:13 | marker **A** (34 tables) | `04-marker-A.txt` | | 07:29:29–07:30:38 | off-site run → snapshot 1 | `05-snapshot-1.txt` | | 07:30:50 | marker **B** + `r893_after`; `r893_only_old` + marker **C** (36 tables) | `05-snapshot-1.txt` | | 07:30:56–07:32:04 | off-site run → **snapshot 2** (`c3d94d03`) | `06-snapshot-2.txt` | | 07:32:38 | `r893_only_old` DROPPED, marker **D**, `r893_new2`: the LIVE record | `07-live-before-restore.txt` | | 07:32:57–07:33:13 | full restore of snapshot 2 prepared (66.1 MB; content checked: C present, D absent); the failing line injected | `08-prepare-and-inject.txt` | | 07:33:28 | reconstitute (forced failure) | `10-forced-failure-http.txt`, `11-controller-log-window*.txt` | | 07:33:29 | undo dump of the live DB; app stopped 07:33:29–07:33:40 | | | 07:33:40.9–41.6 | **live volume copied** to `restore-undo/rallly/20261020T073340Z/rallly_rallly_postgres_data.tar` (69 198 848 B), tmp then rename | `09-undo-folder-watch.txt` | | 07:33:42 | snapshot's volume in place (`Restored 1 Docker volume(s)`) | | | 07:33:44 | replay FAILED (`function r893_forced_replay_failure() does not exist`, exit 3); rows rolled back 07:33:45 | | | 07:33:46–47 | app stopped; **the copy poured back** (`Restored 1 Docker volume(s)`, second time); „the live volumes and definition are put back; starting the app (R-893)" | | | 07:33:53 | `Stack rallly started successfully` | | | 07:33:54 | restore op ends ok=false; the copy folder **gone** (watcher: absent at 07:33:54.45) | | | 07:34:25 | after state read: identical to 07:32:38 | `12-after-failed-restore.txt` | | 07:36:09–07:36:15 | full restore prepared again, with no injection (injected-line count 0) | `13-normal-prepare.txt` | | 07:36:28–07:36:57 | normal reconstitute: copy made 07:36:41–42, volume + dump replayed, app restarted, copy removed 07:36:57.9 | `14-…`, `15-…`, `16-controller-log-normal*.txt` | | 07:38:31 | after state read = snapshot 2 | `17-after-normal-restore.txt` | | (stop) | the session was paused by an API rate limit. When it resumed, 9202 was re-read first: nothing was in progress, no restore-undo copy, step 4 already recorded | | | 08:43–08:45 | teardown, three layers | `18-teardown.txt` | ## Step 3: the forced failure, before and after | | LIVE before the restore (07:32:38) | After the failed restore + put-back (07:34:25) | 2026-10-19 on 0.335.0 (old behaviour) | |---|---|---|---| | marker rows | A, B, C, D | **A, B, C, D** | A, B, C, D | | `r893_only_old` (in snapshot 2 only) | absent | **absent** | present, 1 row | | `r893_new2` (written after snapshot 2) | present, 1 row | **present, 1 row** | present, 1 row | | public base tables | 36 | **36** | 37 | | schema-only md5 | `05828c77…` | **`05828c77…`** (control equal) | — | | per-table row-count md5 | `d8bcbc4b…` | **`d8bcbc4b…`** | — | | data-only md5 | `e5c1a72f…` | **`e5c1a72f…`** | — | | definition | rallly 4.15.3, postgres 18 (digest pins) | same | same | | app | running | **running, healthy**; controller `state=running`; front door 200 | stopped, held (`restore_mixed`) | | restore hold for rallly | none | **none** (holds: chaoscrash, chaosoom, chaosoomb, gokapi, as at baseline) | `restore_mixed` | | `backups/restore-undo/rallly` | absent | **absent** (it existed 07:33:40–07:33:54) | (did not exist) | The DB volume's `CreatedAt` moved from 07:28:21 to 07:33:46. That is expected: the put-back recreates the volume from the copy. Every content observable is equal, the data-only dump included. ## Step 4: the normal restore of snapshot 2 | | Recorded just before snapshot 2 (07:30:50) | After the normal restore (07:38:31) | |---|---|---| | marker rows | A, B, C | **A, B, C** | | `r893_only_old` | present, 1 row | **present, 1 row** | | `r893_new2` | absent | **absent** | | public base tables | 36 | **36** | | schema-only md5 | `f872bd54…` | **`f872bd54…`** | | per-table row-count md5 | `c032377e…` | **`c032377e…`** | | data-only md5 | `db5903ab…` | `1e688d1b…` (differs; see below) | | app | running | **running, healthy**; front door 200; no hold | | restore-undo folder | — | **absent** (existed 07:36:41–07:36:57) | - **Table count.** Snapshot 2 has **36** base tables, not 37. It held `r893_only_old` but not `r893_new2`. The „37" in the brief is last night's MIXED state (36 live tables + the stray `r893_only_old`), not the snapshot's. - **The data-only md5.** It differs from the read taken 6 s before the snapshot's own dump started, while every row count and the schema are equal. Not explained here. It may be a row value that Rallly touched between that read and the dump (the off-site run restarted the app), or row order after the reload. It was not part of the acceptance list, and no row count moved. In step 3 the same observable was byte-equal, because there the volume itself was put back. ## The sentences the household sees - **Failed restore, put back** (`err.backup.restore_failed_put_back`, from `/api/backup/restore-status`): „A teljes visszaállítás sikertelen: a(z) rallly visszaállítása sikertelen. Nem változott semmi: az alkalmazás adatai visszakerültek a visszaállítás előtti állapotba, és az alkalmazás újra fut". Measured **true**: the data is identical and the app runs. It does **not** contain „vedd fel velünk a kapcsolatot". - **Normal restore:** „A(z) rallly: 0 fájl és 1 adatkötet és az adatbázis visszaállítva (mentés: 2026. 10. 20. 09:30) — az alkalmazás újraindult." True. 09:30 is local time (CEST) for snapshot 2's dump at 07:30 UTC. - Flash keys seen on the way: `flash.offbox.target_saved_escrow_agent_down` (no agent on 9202, expected), `escrow_confirmed`, `run_started`, `restore_started`, `full_restore_started`. ## Not measured (said plainly) - **The version-change branch** (the snapshot's older definition written, then the live one written back). It needs an app at an older catalog step, the same limit as last night. Pinned by `r893_putback_test.go`. - **The `_files` sentence** (`restore_failed_put_back_files`). Rallly has no drive files, so 0 files were placed. - **The refusals before the stop:** the fit check refusing (`restore_undo_no_space`), an unmeasurable volume (`restore_undo_unmeasured`) and a failed copy (`restore_undo_copy_failed`). Only the positive path of the fit check ran: it passed and the app was stopped after it. - **A put-back that itself fails** (hold + kept copy). - **The operator notification** for a put-back. 9202 has no hub, so it was not observable. ## Side observations - Each manual off-site run restarted Rallly (its dump leg), as last night. - The controller logged one expected `[ERROR]` pair per failure (the import and the rollback line) and one `[ERROR] [web] off-box reconstitute … (async)` line that carries the household sentence. The sentence is good news, so ERROR is a loud level for a put-back that worked. This is noted for the parent, not changed. - The empty folder `backups/restore-undo/` is left behind after the per-app folder is removed (the code removes the per-app folder only). This drill removed it by name. ## Teardown (three layers) - **Machine (9202):** - Rallly was removed through the product (`POST /api/stacks/rallly/stop`, then `/remove` with `remove_hdd_data` + `remove_backups`). Result: `volumes_removed: rallly_rallly_postgres_data`; 0 containers, 0 volumes; no `backups/primary/rallly`, no `offsite-restore/rallly`. The template folder `/opt/docker/stacks/rallly` (`.felhom.yml`, `docker-compose.yml`, `hold-logs`) was there before and stays. - `r893b-sftp` was removed. So were the images `atmoz/sftp:alpine` and `lukevella/rallly:4.15.3` (both absent at baseline), the repo folder, the key and the empty `backups/restore-undo/`. - `settings.json` was put back from the 07:25 copy (sha256 `cb6b18f8…` = baseline) and `offbox/` from its tar, with the controller stopped. - The controller is back on **0.331.0**: healthy, 0 `[ERROR]`. Off-site target unset, no per-app off-site switch, restore holds as at baseline. - The 0.336.0-rc.1 image was removed by name. The image list is **identical to the baseline**. All `/root/r893b-*` and `/tmp/r893b-*` files are gone. - Not put back: `restore-status.json`, the controller's record of the last op. It now names this drill's last restore, as it named last night's at baseline. - **Host (demo-hp):** only the per-call `/tmp/r893b-g-*` script copies, each removed after its call; 0 left. Nothing on `pve/data`. No whole-guest backup taken. - **Hub:** nothing. 9202 has no hub. - **DooPlex:** the throwaway key was shredded from the session scratchpad.