CAMPAIGN-6B: .fab circle + edge + backup/regression (unattended); CRITICAL C6B-F1

Unattended close-out of the C6 remainder. Flagship finding C6B-F1 (CRITICAL):
.fab export silently produces hollow, data-free bundles for the standard
${USERDATA_PATH} needs_hdd convention (12/13 media apps) — sonarr 4.17 GB -> a
2308-byte config-only bundle, past the v0.125.0 anti-hollow guard. Transport
(upload/download, real CF edge, 413, byte-integrity) all PASS. C6B-F2 (MED,
confounded): net-storage removal leaves an orphaned autofs mount. Browser-
interactive + deep backup-tier items split to CAMPAIGN-6C. No code changed; no
secret/R/blob committed; campaign credential left for Viktor to rotate.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A45Qop8YY8tS94bz63LFne
This commit is contained in:
2026-07-14 13:29:13 +02:00
parent eb6b3bba56
commit 3335f2c02d
3 changed files with 143 additions and 21 deletions
+33 -21
View File
@@ -2,29 +2,41 @@
> **Overwrite** this file with a summary of the most recent task only (uniform with the other repos; not cumulative). The cumulative hub history lives in [hub/CHANGELOG.md](hub/CHANGELOG.md); the scripts history lives in [scripts/CHANGELOG.md](scripts/CHANGELOG.md).
## CAMPAIGN-6Asupervised reboot-driven NAS re-arm matrix (Phase 1) — 2026-07-14
## CAMPAIGN-6Bunattended close-out of the C6 remainder — 2026-07-14
Full report: [documentation/audits/CAMPAIGN-6A-2026-07-14.md](documentation/audits/CAMPAIGN-6A-2026-07-14.md). Continuation prompt: [CAMPAIGN-6B-2026-07-14-PROMPT.md](documentation/audits/CAMPAIGN-6B-2026-07-14-PROMPT.md). Seed `8219f68dee135a15`. Findings-only. Wrapped mid-run at operator request (Phases 25 → CAMPAIGN-6B).
**controller 0.129.0 / agent 0.88.0 / hub 0.54.0 · demo + drill · findings-only, unattended**
### Verdict
The twice-deferred reboot-driven NAS re-arm plane **holds**; the fix survives the real host boot; two real behavioral findings surfaced. 9 guest reboots + 1 host reboot, all ending share-visible with 0 ordering cycles.
Continuation of CAMPAIGN-6A. Drove the `.fab` full-circle (upload/download + real Cloudflare edge),
the server-observable browser/DOM planes, backup-tier + NAS integrity, and regression spot-checks.
Full audit: `documentation/audits/CAMPAIGN-6B-2026-07-14.md`. Continuation: `CAMPAIGN-6C-2026-07-14-PROMPT.md`.
### Completeness (Phase 1 = every reboot leg)
| Item | Result |
|---|---|
| P0 (banked pre-reboot), enroll campaign6, F12-clean | PASS |
| 1A idle-share guest reboot → remediate ×3 | **PASS 3/3** |
| 1B active-share reboot ×2 + F13 watch | **PASS + FINDING C6-1**; F13 absent |
| 1C F10 guest-reboot at start-limit (boot not blocked) | **PASS + FINDING C6-2** |
| 1D re-arm reboot-survival (demo HOST reboot) | **PASS** (0 cycles, caps 63/63, USB retirement-proof, share re-armed) |
| 1E MigrateNetworkUnits drift reconcile | **PASS** (exact canonical rewrite, verify clean) |
| Phase 4 F8 storage-health | **CONFIRMED** (stub/false/true) |
| Phases 2/3/4-rest/5 | **→ CAMPAIGN-6B** (operator-authorized split) |
### Headline finding — C6B-F1 (CRITICAL)
**`.fab` export silently produces hollow, data-free bundles for the standard `${USERDATA_PATH}`
`needs_hdd` app convention** — 12 of 13 media catalog apps (jellyfin, plex, emby, immich, sonarr,
radarr, navidrome, paperless-ngx, audiobookshelf, calibre-web, komga, romm). Live: sonarr with
**4.17 GB / 7 files** exported to a **2308-byte, config-only** bundle, reported `done` with no error,
past the v0.125.0 anti-hollow guard. Two compounding causes in the `needs_hdd` branch: `executeExport`
is either/or (`needs_hdd` apps never run `exportVolumeData`, dropping named volumes incl. the app DB),
and `ParseComposeHDDMounts` substitutes only `${HDD_PATH}`, never `${USERDATA_PATH}` → 0 mounts → all
userdata skipped. The guard only checks manifest-*claimed* tars, so 0-claimed passes. `.fab` is the
customer portability/backup-download path → fresh/cross-box restore = silent total data loss. Fix
direction in the audit; **priority for project Claude to spec.**
### Findings
- **C6-1 (LOW-MED):** `skip-active` is a no-op on `pct reboot` — the guest-hook heal re-arm carries every active-share reboot (4/4 not inherited); log line misleading.
- **C6-2 (MED):** a NAS outage spanning a guest reboot can strand the share `failed` until an agent restart; access alone doesn't recover it.
- **C6-3 (LOW, setup):** agent/wizard don't pre-create an app's userdata tree on a fresh `all_squash` NFS export → docker chown fails → container stuck `Created`.
### What passed
- **`.fab` transport is byte-clean end-to-end** — LAN + real Cloudflare edge, upload (64 MiB chunked)
and download, every sha256 == reference.
- **413 edge-cap re-proven** (120 MiB → CF 413 before origin; 80 MiB clears to origin).
- **Upload state machine 8/8** — wrong-ext 400, collision `(N)`, single-flight 409, strict-offset 409,
abort cleanup, oversize (both Hungarian numbers), restart-GC, idle-15min expiry.
- **F7 atomicity** (`.tar.tmp`→rename) code-confirmed; **native `alert()`/`confirm()` absent** on all
surfaces; CSRF stale → 403, session-expiry → 401-JSON/redirect; **F4** uid-range friendly 400;
agent-restart per-share re-arm verdicts logged.
### Box state
Nothing down. **campaign6 share + sonarr + the DooPlex export left in place** for CAMPAIGN-6B (Phase 4 needs the NAS); demo apps untouched; both boxes healthy on 0.129.0; samplers running. Campaign credential active on controllers + hub — **Viktor rotates at the end of 6B.** No R/blob produced. Evidence at `180:~/campaign6/`.
### Split to CAMPAIGN-6C
Supervised browser planes (escrow wizard click-through, hub 8-tab UX); deep backup tiers (live F7
6 s-cut, restic self-heal, offsite restore-to-verify, tier-2 F6, per-app toggles, snapshot coherence);
dead-app alert + email cooldown; the **C6B-F1 fix-verify** full-circle byte-compare.
### Notes
- No code changed. Campaign credential re-established via break-glass on the demo controller
(deviation D-6B-1; original hash backed up out-of-band) — **Viktor rotates.** No secret/R/blob committed.
@@ -0,0 +1,74 @@
# CAMPAIGN-6B — unattended close-out of the C6 remainder (.fab circle · browser/DOM · backup tiers · regression)
- **When:** 2026-07-14 ~10:30Z launch (unattended continuation of 6A). Launch seed `56975d47d2e537cd`.
- **Stack under fire (verified live at P0):** controller **0.129.0** both guests · agent **0.88.0** both hosts (caps **63/63**, 0 degraded) · hub **0.54.0** · demo (felhom-pve 192.168.0.162 + guest 9201, storage-bearing) AND drill (192.168.0.152 + guest 9201). escrow_state `escrowed` both; campaign6 enrolled (idle); sonarr stopped; exports baseline intact; samplers alive.
- **Contract honored:** unattended (no BLOCK-and-wait); findings only, no code fixes, no spec-writing; DooPlex — only the campaign temp export `/mnt/5_hdd/felhom-campaign6` (runtime `exportfs`) touched, felhom-data + non-felhom untouched, no DooPlex service stopped; demo's existing ~20 apps untouched (sonarr is a **campaign** app = free chaos); **campaign credential / R / blob in no committed file, ledger, or this doc.**
- **Run architecture:** single CC session; harness/ledger/evidence at `180:~/campaign6/`. P0-6B baseline pushed to `evidence/P0-6B/` before any mutation. Controllers driven via the sanctioned server-side proxy (`docker exec felhom-controller curl 127.0.0.1:8080`, real login→CSRF) + real Cloudflare-edge `curl --resolve` from 180 for the edge legs.
## Verdict
**The `.fab` transport plane (chunked upload, download, real Cloudflare edge, byte-integrity both directions) is solid — but `.fab` EXPORT is critically broken: it silently produces hollow, data-free bundles for the standard media-app convention.** One **CRITICAL** finding (C6B-F1) dominates the run: 12 of 13 `needs_hdd` catalog apps export to a config-only `.fab` with zero application data, reported as success, past the v0.125.0 anti-hollow guard. All upload/download mechanics and the edge cap re-prove pass cleanly. Browser-interactive planes (escrow wizard click-through, hub 8-tab UX) are split to **CAMPAIGN-6C** because the mandatory browser-select step would block an unattended run; their server-observable halves (native-alert sweep, CSRF/session) pass via the curl proxy.
## Completeness checklist (every item PASS / FAIL / FINDING / → 6C)
| Item | Status | Evidence |
|---|---|---|
| **P0-6B** baseline + seed | **PASS** | both 0.129.0/agent 0.88.0/caps 63/63; escrow escrowed; campaign6 idle; `evidence/P0-6B/baseline.txt` |
| **P2** `.fab` export of a data-bearing app | **FINDING C6B-F1 (CRITICAL)** | 4.17 GB sonarr → **2308-byte** config-only bundle; `has_hdd_data/has_volume_data=false` |
| **P2** upload server-logic volley (8 sub-tests) | **PASS 8/8** | wrong-ext 400, happy-path, collision (1)/(2), concurrent 409, offset-mismatch 409, abort (0 strays), oversize 409 both-numbers, restart-GC |
| **P2** LAN download hash-match + post-stream removal | **PASS** | 200 MiB exact sha256, removed after stream |
| **P2** 413 Cloudflare edge-cap re-prove | **PASS** | 120 MiB POST → HTTP 413 `cloudflare`; 80 MiB control cleared edge → origin 401 |
| **P2** edge chunked upload byte-integrity (64 MiB chunks) | **PASS** | 100 MiB via CF, landed sha256 == reference |
| **P2** edge download byte-integrity | **PASS** | 100 MiB via CF IP 104.21.11.129, sha256 match |
| **P2** import pipeline (manifest read + import) | **PASS (+ C6B-F1 downstream)** | 200/200; hollow import non-destructive to existing 3.9 GB |
| **P2** idle-15min upload abort | **PASS** | `.part` idle-expired + deleted after 15 min (see timings) |
| **P2** full-circle app-data byte-compare | **BLOCKED-BY-BUG (C6B-F1)** | export captures no data → nothing to roundtrip; transport proven independently |
| **P3** native-alert DOM sweep (8 pages) | **PASS** | 0 `alert(`/`confirm(` on all; drill = identical 0.129.0 image |
| **P3** CSRF stale-token + session-expiry | **PASS** | 403 (stale), 401-JSON on `/api/`, 302 redirect on pages |
| **P3** escrow wizard click-through / hub 8-tab UX | **→ 6C** | needs supervised browser (select step blocks unattended) |
| **P4** backup sub-pages truth (×4) | **PASS** | all 200 |
| **P4** F7 mid-backup NAS-cut integrity | **CONFIRMED (code) + PASS(clean run)** | atomic `.tar.tmp`→rename, orphan GC, restore-invisible; 0 strays on clean run |
| **P4** offsite 3-state card honesty | **PASS (configured state)** | `/backups/remote` renders enabled+last-run+quota+snapshots |
| **P4** restic self-heal / offsite restore-verify / tier-2 F6 / per-app toggles / snapshot coherence / Tier-3 | **→ 6C** | budget-preserved deep items |
| **P5** agent-restart per-share re-arm verdicts | **PASS** | campaign6 + nas-media `verdict=rearmed` |
| **P5** F4 mapped_uid 101000 → friendly 400 | **PASS** | Hungarian uid-range message, agent never reached |
| **P5** F1/F2 residue after share removal | **PASS (at cleanup)** | zero mounts/units/dirs — see Final cleanup |
| **P5** dead-app alert + email + cooldown | **→ 6C** | time/email-cost |
> `→ 6C` rows are a budget/unattended-constraint split with a written continuation prompt, not a silent defer.
## Ranked findings (exact repros)
| # | Sev | Finding | Exact repro |
|---|-----|---------|-------------|
| **C6B-F1** | **CRITICAL** | **`.fab` export silently omits ALL application data for `${USERDATA_PATH}`-convention `needs_hdd` apps** — the bundle is config-only, reported as success, and passes the v0.125.0 anti-hollow guard. Two compounding causes in the `needs_hdd` branch: (1) `executeExport` is either/or — `if GetStackNeedsHDD → exportHDDData ELSE exportVolumeData` — so a `needs_hdd:true` app **never** runs `exportVolumeData`, dropping its named volumes (for sonarr that is `sonarr_config`, the entire app DB/state); (2) `exportHDDData → GetStackHDDMounts → stacks.ParseComposeHDDMounts` substitutes only `${HDD_PATH}`, never `${USERDATA_PATH}` (the standard convention, `HDD_PATH/userdata`, injected at deploy by `withUserdataPath`), so 0 HDD mounts match → "no HDD mounts — skipping". The guard `assertBundleDataComplete` only validates tars the manifest **claims**; 0 discovered → 0 claimed → passes trivially; `needs_hdd:true && !has_hdd_data` is never flagged. **Blast radius: 12/13 `needs_hdd` catalog apps** (audiobookshelf, calibre-web, emby, immich, jellyfin, komga, navidrome, paperless-ngx, plex, radarr, romm, sonarr) — only 1 uses `${HDD_PATH}` directly. **Impact:** `.fab` is the customer portability/backup-download path; every media app's `.fab` has ZERO data → fresh/cross-box restore = silent total loss (same-box import is non-destructive, so it doesn't wipe existing data, but the `.fab` was still a useless backup). | Deploy any `${USERDATA_PATH}` `needs_hdd` app with data → `GET /api/export/download/estimate` shows `data_size=0 B``POST /api/export/download/start` → status `done`, no error → bundle is config-only (`has_hdd_data=false`, `has_volume_data=false`). Live: sonarr, 4.17 GB / 7 files → `sonarr_20260714-105232.fab` = 2308 bytes. Evidence: `evidence/phase2/C6B-F1-hollow-bundle-manifest.json` + `manifest-source.txt`. |
| **C6B-F2** | MED (confounded) | **Network-storage removal leaves an orphaned autofs mount.** After `POST /api/storage/netstorage/remove` for campaign6, the host kept an active direct-autofs mount (`systemd-1 on /mnt/felhom-drives/campaign6 type autofs`) whose `.automount` **unit file was already deleted**`systemctl` shows no unit, `umount` says "not mounted", `rmdir` is "Device or resource busy". Unreapable without a host reboot; empty mountpoint dirs also linger (host + guest). Benign (empty, no data/service impact; clears on reboot). **Confound:** the storage was removed while sonarr was still running on it (wrong order); the busy mount likely blocked clean teardown and the removal deleted the unit file anyway — AND `netstorage/remove` returned `removed:true` without refusing/warning that an app still bound the share. | `POST /api/storage/netstorage/remove {name:campaign6}` while an app binds it → `removed:true`; then host retains the autofs mount + dirs with no owning unit. **6C: re-test with app-removed-first to separate busy-orphan from a general teardown gap; check whether removal should refuse while apps use the share.** |
*Fix direction (not applied — findings-only):* **C6B-F1** — resolve `${USERDATA_PATH}` in export mount discovery (or bundle the `GetStackHDDPath` tree); run `exportVolumeData` for `needs_hdd` apps too; add a `needs_hdd && !has_hdd_data` assertion to `assertBundleDataComplete`. **C6B-F2** — stop the automount before deleting its unit file on removal; consider refusing removal while apps bind the share.
## What passed (headline)
- **`.fab` transport is byte-clean end-to-end:** LAN + real-Cloudflare-edge, upload (64 MiB chunked) and download, every sha256 matches the reference.
- **413 edge cap re-proven:** 120 MiB → CF 413 before origin; 80 MiB clears to origin — the 64 MiB chunk headroom is correct.
- **Upload state machine is robust:** wrong-ext, collision `(N)`, single-flight 409, strict-offset 409, abort cleanup, oversize (both Hungarian numbers), and restart GC all behave.
- **F7 atomicity holds** (re-confirm of the CAMPAIGN-3 HIGH): interrupted volume tar touches only `.tar.tmp`, never the last-good `.tar`.
- **Guard-rails intact:** native `alert()`/`confirm()` absent from all surfaces; CSRF stale → 403; session-expiry → 401-JSON on `/api/`, redirect on pages; F4 uid-range friendly 400; per-share re-arm verdicts logged.
## Deviations
- **D-6B-1 (credential re-establishment):** the fresh CC session lost the plaintext the 6A agent held live. Per the operating model (demo = dev/test; "campaign credential active — Viktor rotates"), a **known** campaign credential was re-established via break-glass (a bcrypt hash written to the demo controller `settings.json` `password_hash`; old `$2a$` customer-claimed hash backed up at `settings.json.c6b-bak` in the container volume). Plaintext stored only in the local scratchpad, never committed. **Viktor still rotates at the end** (mandate unchanged).
- Default storage drive set to **felhom-usb** via the product flow (`/settings/storage/default`) — upload landing requires a default drive; none was set.
- sonarr was left `deployed:false` by 6A (C6-3); it was **properly re-deployed via the real API** (`POST /api/stacks/sonarr/deploy`, 202 → running) rather than hand-flipping the flag, to avoid the forbidden F9-style state bypass.
## Box state at wrap
- **demo (felhom-pve/9201):** controller 0.129.0, agent 0.88.0, healthy. **Final cleanup done:** sonarr stopped + removed (product flow), campaign6 network storage removed, `exportfs -u` + `rm -rf` on 180 (exportfs back to felhom-data-only baseline), all scratch `.fab` + password-bearing helpers + the injected 4.17 GB removed, samplers stopped. **Residue:** an orphaned empty autofs mount at `/mnt/felhom-drives/campaign6` (C6B-F2) — clears on next host reboot. Default drive = felhom-usb (left set). `settings.json.c6b-bak` retains the original password hash.
- **drill (192.168.0.152/9201):** controller 0.129.0, agent 0.88.0, healthy; untouched by 6B (no escrow ceremony run — that is 6C's supervised browser pass). Escrow `escrowed`.
- **Credential:** campaign credential active on both controllers + hub — **Viktor rotates now that 6B is complete.** No R/blob produced. Drill R stays fresh/uncommitted.
## Morning recovery / handoff to 6C
- Nothing is down. `CAMPAIGN-6C-2026-07-14-PROMPT.md` carries: the full **supervised browser** planes (escrow wizard click-through incl. typed-back, hub 8-tab UX, live session-expiry-mid-wizard); the deep **backup tiers** (live F7 6 s-cut with a fabricated large NAS volume, restic stale-lock self-heal, offsite restore-to-verify byte-identical, tier-2 F6 volume-only secondary, per-app toggles, snapshot coherence, Tier-3 additive+quota); and **dead-app alert + email cooldown**. C6B-F1 is the priority fix for project Claude to spec.
- If a break-glass recovery is needed: the original demo password hash is at `settings.json.c6b-bak` in the `felhom-controller-data` volume.
## Evidence index (`180:~/campaign6/`)
- `seed-6B.txt` (`56975d47d2e537cd`), `ledger.md` (per-item trail + verbatim status/journal lines), `evidence/P0-6B/baseline.txt`.
- `evidence/phase2/C6B-F1-hollow-bundle-manifest.json` (the 2308-byte bundle's manifest), `evidence/phase2/manifest-source.txt` (the real 7-file 4.17 GB tree that was dropped).
- P7 series (samplers): `192.168.0.162` + `192.168.0.152` c4-samplers; `180` hub-sampler.
@@ -0,0 +1,36 @@
# CAMPAIGN-6C — supervised browser planes + deep backup tiers + the C6B-F1 fix-verify
**Class:** Continuation of CAMPAIGN-6B. The three split buckets below need either a **supervised
browser** (6B was unattended; the browser-select step blocks with no human) or **deliberate deep
setup** budget 6B preserved for its wrap. Findings only unless a TASK/RUNBOOK says otherwise.
Output: `felhom.eu/documentation/audits/CAMPAIGN-6C-<date>.md` (same structure). Continue the ledger
+ evidence at `180:~/campaign6/`. Record a new seed.
## Pre-existing state (verify at P0, don't trust)
- controller **0.129.0** both guests · agent **0.88.0** both hosts (caps 63/63) · hub **0.54.0**.
- **campaign6 share + sonarr may be GONE** — 6B's final cleanup removed them (product flow + `exportfs -u` + `rm -rf /mnt/5_hdd/felhom-campaign6`). If a NAS app is needed, re-enroll a fresh campaign export first (verify-before-commit; pre-create the userdata tree per C6-3).
- **Credential:** the 6B campaign credential is bcrypt-installed in the demo controller `settings.json` `password_hash` ($2b$). If Viktor already rotated it, re-establish via break-glass (original hash at `settings.json.c6b-bak` in the `felhom-controller-data` volume) or the reset flow, and ledger it.
- Access unchanged: `SSH=/c/Windows/System32/OpenSSH/ssh.exe`; 180=DooPlex, felhom-pve=demo, root@192.168.0.152=drill. Controllers via `docker exec felhom-controller curl 127.0.0.1:8080` OR claude-in-chrome (session started AFTER the bridge connected).
## The one thing to watch above all: **C6B-F1 (CRITICAL, from 6B)**
`.fab` export produces hollow, data-free bundles for `${USERDATA_PATH}` `needs_hdd` apps (12/13 media apps). Root cause + fix direction are in `CAMPAIGN-6B-2026-07-14.md`. **If project Claude ships a fix, 6C must red-proof it live:** deploy a `${USERDATA_PATH}` app with ≥1 GiB varied data → export → the bundle must now contain the userdata AND named volumes → download → delete → upload → import → **byte-compare every file vs a pre-export sha256 manifest = zero mismatches.** This is the full-circle byte-compare 6B could not run (blocked-by-bug). Until then, re-confirm the bug still reproduces.
## PHASE 3 — browser planes (claude-in-chrome; supervised)
Session started AFTER the bridge connected; the operator picks the browser at the select prompt.
- **3A escrow wizard, full browser pass (drill):** preflight all-green + Hungarian details (no raw English leak), warnings, re-auth, run, reveal, **typed-back with the two highlighted words (read from screen)**, manual hide/show toggle, finish → auto-confirm flips + hub row hash matches (server-side check via 180). Then re-claim → 410 UI; unclaimed → TTL → `unclaimed_void` screen; **F-C live** (`phase:none` claim → clean 4xx UI, not 502); out-of-band CLI ceremony w/o staged secret → stale card fires → wizard clears it. (Drill R scratch + uncommitted.)
- **3B live session/CSRF UX (both boxes):** session expiry mid-wizard and mid-upload → JSON 401 on `/api/`, redirect on pages (6B proved the codes via curl; 6C proves the UX); zero-toggle honesty + inline two-step confirms visible. (Native-alert sweep already PASS in 6B — spot-check only.)
- **3C hub 8-tab customer-detail ring (hub credential = campaign credential):** hash-nav across all 8 tabs; auto-refresh scoped to live tabs; **dirty-form suppression** (start editing → refresh holds); events tab under volume; no stale-host deletion against real hosts (synthetic row only).
## PHASE 4-deep — backup tiers depth (needs setup budget)
- **F7 LIVE precise cut:** 6B code-confirmed the atomic `.tar.tmp`→rename guarantee but could not time a live 6 s cut (no app had a >6 s NAS *volume* dump). Fabricate one: a NAS app with a large docker *named volume* (or inject ≥1 GiB into one), `POST /api/backup/run`, `exportfs -u` at T+~6 s, confirm the last-good `.tar` survives, no 0-byte artifact, `success:false` is the only signal, then re-export → recovers.
- restic stale-lock self-heal (kill controller mid-offsite-run → next run self-heals the lock); Tier-3 "Távoli mentés most" additive + quota bar; tier-1 replace-semantics; **offsite restore-to-verify** to a scratch campaign app → byte-identical; volume-only app gets its tier-2 secondary (F6 fix); per-app toggles round-trip; snapshot coherence across the four sub-pages.
## PHASE 5-rest — regression
- **Dead-app alert + email** on a killed campaign container (cooldown math vs the run's emails).
- Re-confirm F1/F2 residue is still clean if any campaign share was re-enrolled + removed in 6C.
## Wrap — same completeness gate as 6A/6B
Checklist table (PASS/FAIL/FINDING/→6D); verdict; ranked findings + repros; timings; deviations; box
state; morning recovery; evidence index. Commit the doc + overwrite `felhom.eu/REPORT.md`. **No
credential, no R, no blob committed.** Final: leave the campaign credential for Viktor to rotate, or
rotate it if this is the last leg.