Files
felhom.eu/documentation/audits/night2-2026-10-20/P2/live

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.