Evidence-survival check FIRST: HEAD == origin/main == 7efb7a5, and every artefact
confirmed present in origin/main with git cat-file, including all 27 evidence
files. Nothing died with the rig.
1. VM 311 destroyed by exact VMID. qm list now shows only drill-r50; 311.conf does
not exist; images/311 gone. 126 GB returned - nvme 133G used / 758G avail ->
6.7G / 884G. A first attempt stopped the VM but its destroy never ran, killed by
an unescaped paren in my own echo; caught by checking qm list rather than the
exit code.
2. c10-scratch removed by name. storage.cfg 5 -> 4 entries. felhom-backup shares
the same path and is intact, config unchanged.
3. PBS on DooPlex (Tier 2). Identity confirmed first - the target carried the
comment this campaign set at creation. Datastore felhom-c10, user c10@pbs,
token !box, both ACLs and the on-disk chunkstore all gone; felhom-spike,
felhom@pbs, felhom@pbs!n100 and /mnt/5_hdd/backup/demo-felhom all intact. Only
the campaign's entries disappeared. "user delete" is not a PBS subcommand - it
printed usage and the user survived rc=0; caught by re-reading the user list.
4. Restic subaccount 281530 deleted after asserting home==felhom-campaign10 and
the description names Campaign 10 - the script aborts otherwise. 4 -> 3
subaccounts; demo-felhom, peti-felhom and demo-hp all present.
5. Hub customer c10-soak. The ONLINE gate REFUSED with HTTP 409 for 18 consecutive
attempts, exactly as anticipated (customer_delete.go:98-102, :146). Predicted
deletable at last-report 09:38:19 + 30 min = 10:08:19; measured 303 at 10:08:23.
Verified positively: 0 customer rows, 0 host/appliance rows, GET
/customers/c10-soak -> 404, and the hub logged the DELETE cascade COMPLETE with
residue purged. No /appliances/discard was needed - the appliance record was
bound and went with the cascade. drill-r50 verified INTACT (200, VM 300 present).
All 12 credential files shredded and the directory removed.
Register rows NOT written: app-catalog-felhom.eu has an uncommitted working tree
including scripts/check-volume-persistence.py, so the parallel catalog session is
still open and owns the register. R-156/R-157/R-158 rows and rankings remain owed,
itemised in the doc - and that session's volume-persistence checker looks like
R-156's fix shape, so the two should be reconciled before filing.
14 KiB
Campaign 10 — closeout: two last measurements, then teardown (2026-08-02)
Companion doc (my call, per the task) to CAMPAIGN-10-two-storage-soak-2026-07-31.md and
SPIKE-recovery-unit-space-2026-08-02.md, rather than appending — those two are already long and this
session answers two specific questions and then dismantles the rig.
Part 1 headline: Q1 lowers R-158's rank — the failure is customer-visible, on one page, and it
persists and clears correctly. Q2 clears ValidateDump of shipping bad dumps, and simultaneously
kills C2's proposed gate on its current predicate.
Q1 — when a backup refuses for lack of space, what does the customer see?
Method: ballast /mnt/sys_drive to 699 MB free (homebox's unit needs 2.3 GB), run the real backup
through POST /api/backup/run, and read every customer-facing surface through the real UI endpoints.
The five points
| # | Surface | Result |
|---|---|---|
| 1 | /backups overview |
SHOWS IT — ✗ Adatmentés sikertelen |
| 2 | /backups/apps (per-app) |
shows nothing — Homebox tier-1 reads Utolsó: 3 perce |
| 3 | /stacks/homebox (app detail) |
shows nothing |
| 4 | / dashboard · /launcher |
show nothing — no banner |
| 5 | GET /api/backup/status |
db_dump.success: **false** |
| — | hub event / email | none (R-158) |
Exact copy, as rendered on /backups:
✗ Adatmentés sikertelen
The point-5 question — retry, stay failed, or quietly go green?
It retries, stays failed, and does not lie. Measured across three runs:
| run | condition | status.success |
/backups shows failure |
unit mtime |
|---|---|---|---|---|
| 1 | constrained | false | true | 07:30:50 |
| 2 | still constrained | false | true (persists) | 07:30:50 (unchanged) |
| 3 | space freed | true | false (cleared) | 07:37:38 (fresh) |
/backups/apps is honest, not deceptive. Its Utolsó: 3 perce tracks the unit's real mtime
(07:30:50), not the failed run's timestamp — so it correctly reports the age of the last good unit. It
simply never says the latest attempt failed. This is NOT the R-156 family: no surface claims a
fresh backup over a stale unit.
R-158's rank — LOWERED, with the reason
R-158 is a notification gap, not a silent-failure defect. It ranks BELOW R-157.
Because the customer can find out by looking, and the indicator behaves correctly — it persists while failing and clears on recovery. What is missing is that nothing pushes it: no hub event, no email, no dashboard banner. And the discovery path is poor in a specific way worth fixing: the per-app backup page — the natural place to ask "is homebox backed up?" — shows no failure at all, while the answer lives one page away on the overview.
R-157 outranks it because R-157's mechanism B leaves a deployed: true app not running, with the
deadapp check reporting 0 currently down — silent on every channel, including the one the customer
would look at.
Q2 — was ValidateDump's warning correct?
The warning, seen twice:
[WARN] [backup] ValidateDump: …/rallly-postgres.sql is structurally valid (32 tables) but its
accounts table has NO rows — the dump may predate the customer's data
It was factually correct, and the dump was NOT deficient
The live database genuinely had no accounts. Every non-empty table, from pg_stat_user_tables:
_prisma_migrations | 129
cc_proof | 82 <-- the campaign's own canary
instance_settings | 1
accounts | 0
An empty table proves nothing either way, so — as the task required — an account was seeded and the backup re-run:
INSERT users (id='c10user', …) -> 1 row
INSERT accounts (id='c10acct', …) -> 1 row
Result:
[INFO] [backup] DB dump: rallly-postgres → rallly-postgres.sql (100.6 KB, 396ms, 32 tables)
(no ValidateDump line at all — the warning STOPPED)
grep -c c10acct <dump> = 1 grep -c c10user <dump> = 2
dump size 102 766 B -> 103 029 B
Verdict: the validator is sound and no bad dumps are shipping. It correctly detected an empty
accounts table, and the moment real data existed the dump captured it and the warning cleared.
(Seeded data disclosed: one users row and one accounts row, both c10*-prefixed, on a scratch
customer that this session then deletes.)
What it does to C2's ordering — the proposed gate is DEAD on this predicate
C2 proposed dropping the DB volume tar (halving DB-app units, and closing the D5/R-127(b) initdb-skip
password trap) once ValidateDump is promoted from a warning to a gate. That ordering does not
survive Q2:
A fresh appliance legitimately has zero accounts. Gating on "the
accountstable has rows" would block the backups of every new customer until someone registers a user — refusing good backups for the entire period a box is most fragile.
The validator's fact is right and its inference ("may predate the customer's data") is wrong: there was no data to predate. So the dependency chain is longer than C2 assumed:
- a sound gate predicate first — a comparison against the live DB (per-table row counts in the
dump vs
pg_stat_user_tables) rather than an absolute expectation about one table; - then the warn→gate promotion;
- only then the tar-drop.
Until (1) exists, the DB volume tar stays load-bearing — not because dumps are bad, but because nothing can yet prove a given dump is good.
Findings
No new R-n. Register grepped (documentation/backlog/*.md) for ValidateDump, accounts table,
Adatmentés sikertelen, and the per-app backup page: no existing rows, and neither Q1 nor Q2 produced
a defect that is not already covered.
- Q1 does not mint an R — it re-ranks R-158 (filed in the spike) and adds the specific
improvement worth making: surface the per-app failure on
/backups/apps, where a customer looks for a specific app. - Q2 does not mint an R — it removes a proposed change (the gate) and lengthens C2's dependency chain. Recording that a design is wrong is the spike rule working.
Nothing fixed, per the fences.
Part 2 — teardown: five layers, each verified gone
0. Evidence-survival check — done BEFORE anything was deleted
HEAD == origin/main == 7efb7a5, tree clean, and each artefact confirmed present in origin/main
(not merely locally) with git cat-file -e origin/main:<path>:
CAMPAIGN-10-two-storage-soak-2026-07-31.md IN origin/main
CAMPAIGN-10-closeout-2026-08-02.md IN origin/main
SPIKE-recovery-unit-space-2026-08-02.md IN origin/main
REPORT-campaign10.md IN origin/main
documentation/tests/campaign10-evidence-2026-07-31/ — 27 files in origin/main
Everything the campaign and both spikes produced survives the rig.
1. VM 311 on demo-hp — GONE
| before | after | |
|---|---|---|
qm list |
300 drill-r50, 311 c10-appliance running |
300 drill-r50 only |
311.conf |
present | Configuration file 'nodes/felhom-host/qemu-server/311.conf' does not exist |
/mnt/nvme-1tb/images/311 |
127 G | No such file or directory |
/mnt/nvme-1tb |
133 G used / 758 G avail | 6.7 G used / 884 G avail |
Space returned: 126 GB. Deleted by exact VMID with qm destroy 311 --purge. drill-r50 untouched.
(A first attempt stopped the VM but its qm destroy never ran — an unescaped paren in my own echo
made the remote shell exit before it. Caught by checking qm list rather than the exit code, and
re-run.)
2. c10-scratch dir storage — GONE
before: dir: local | lvmthin: local-lvm | pbs: felhom-pbs | dir: felhom-backup | dir: c10-scratch (count=5)
after : dir: local | lvmthin: local-lvm | pbs: felhom-pbs | dir: felhom-backup (count=4)
Removed by name (pvesm remove c10-scratch). felhom-backup shares the same path
(/mnt/nvme-1tb) and is intact — path /mnt/nvme-1tb, content backup, is_mountpoint 1, unchanged.
3. PBS datastore + token on DooPlex (Tier 2) — GONE, production untouched
Identity confirmed before deleting. The target carried the comment this campaign set at creation:
felhom-c10 /mnt/5_hdd/backup/campaign10 "Campaign 10 scratch - DELETE at teardown" <-- target
felhom-spike /mnt/5_hdd/backup/demo-felhom (no comment) <-- PRODUCTION
| before | after | |
|---|---|---|
| datastores | felhom-c10, felhom-spike |
felhom-spike only |
| users | c10@pbs, felhom@pbs, root@pam |
felhom@pbs, root@pam |
| tokens | c10@pbs!box, felhom@pbs!n100 |
felhom@pbs!n100 |
| ACLs | 2 × /datastore/felhom-c10 + 2 × /datastore/felhom-spike |
2 × /datastore/felhom-spike only |
| on disk | /mnt/5_hdd/backup/campaign10 |
removed; /mnt/5_hdd/backup/demo-felhom INTACT |
Only the campaign's entries disappeared. (user delete is not a PBS subcommand — it printed
usage and the user survived. Caught by re-reading the user list rather than trusting rc=0; the
correct pair is user delete-token then user remove.)
4. Restic subaccount on the Storage Box — GONE, production untouched
Identity asserted programmatically before the DELETE — the script aborts unless both hold:
id=281530 username=u629488-sub4 home=felhom-campaign10 desc="Campaign 10 scratch - DELETE at teardown"
assert home == "felhom-campaign10" PASSED
assert "Campaign 10" in description PASSED
Its inability to reach any production path was already proven in isolation/restic-denial.txt.
| before | after | |
|---|---|---|
| subaccounts | 4 | 3 |
281530 (campaign) |
present | absent |
273581/269130/275124 (demo-felhom, peti-felhom, demo-hp) |
present | all present |
5. Hub customer c10-soak — GONE, after the ONLINE gate refused
The gate refused, exactly as anticipated. POST /configs/c10-soak/delete returned HTTP 409
for 18 consecutive attempts. Source: internal/web/customer_delete.go:98-102 sets
onlineBlocked = true while hostStatus(LastReportAt) == "ok", and :146 comments
"leg 1 refuses; decommission the agent first".
When it became deletable — predicted and confirmed. Last host-report 09:38:19; ok lapses at
30 minutes ⇒ deletable from 10:08:19. Measured:
09:50:17 attempt 1 -> HTTP 409
…
10:07:20 attempt 18 -> HTTP 409
10:08:23 attempt 19 -> HTTP 303 <-- 4 s after the predicted threshold
Verified positively, not by trusting the response:
customers list — c10-soak / "Campaign 10" rows: 0
hosts+appliances — c10-soak / 55ab3776 rows: 0
GET /customers/c10-soak -> HTTP 404
hub: delete c10-soak: residue purged (reports=198 app_telemetry=1099 notif_prefs=1
selfbind_tokens=1 appliance_registrations=1)
hub: customer DELETE cascade COMPLETE for c10-soak (journal #9) — full teardown
No separate /appliances/<id>/discard was needed: the appliance record was bound, so it left the
unclaimed list at bind time and was purged by the cascade (appliance_registrations=1). The task
anticipated /discard; on this path the cascade covered it.
Remaining customers, unchanged: Demo Ügyfél, Demo HP, Peti Proxmox, and
R-120 golden 0.186.0 proof (= sess-f) — the pre-existing R-131 orphan, not this campaign's and
deliberately not touched.
drill-r50 (R-93 fixture) verified intact: GET /customers/drill-r50 → HTTP 200, and VM 300
still present and stopped on demo-hp. (It is absent from the dashboard listing because it is DOWN, not
because it is gone — checked explicitly rather than inferred from the listing.)
6. Credentials
All 12 files under ~/.config/campaign10/ shredded (shred -u) and the directory removed —
host/dashboard passwords, API key, app secrets, PBS token, restic password, claim code. The guest-side
copies (/root/c10/, /root/c10api.sh) died with the VM. No campaign processes remain.
Register work still owed — NOT done this session, and why
The parallel catalog session is OPEN. app-catalog-felhom.eu has an uncommitted working tree:
CLAUDE.md and REUSE.md modified, plus untracked audits/, scripts/check-volume-persistence.py
and scripts/test_check_volume_persistence.py. One session owns the register at a time, so no rows
were written.
Still owed, for whoever takes the register next:
| finding | needs |
|---|---|
| R-156 | row + rank. papra's data neither persisted nor backed up, reports healthy. Note: the parallel session's check-volume-persistence.py looks like exactly this fix's shape — reconcile before filing. |
| R-157 | row + rank. bootrecon's start-once sweep, two mechanisms (A: Exited/race ~50%; B: zero-container, silent on every channel). A settle-condition fix closes A only. |
| R-158 | row + rank — ranked BELOW R-157 by Q1 (notification gap, not silent failure; visible on /backups, absent from /backups/apps). |
| spike C2 | not a finding: a dependency chain (sound predicate → warn→gate → tar-drop), lengthened by Q2. |
What did not happen, and why
- No register rows written — parallel session owns it (above).
- Nothing fixed — R-156/R-157/R-158 and everything Q1/Q2 surfaced are findings only, per the fences.
- No
/appliances/<id>/discardcall — there was no appliance row left to discard (§5). sess-f/ R-131 orphan not deleted — not this campaign's record; deliberately out of scope.- The ~5 GB linearity point — the earlier spike's brief said skip it; still skipped.