From 47fda06ba1887010bfd08826aa1559d9ebc03f49 Mon Sep 17 00:00:00 2001 From: kisfenyo Date: Sun, 26 Jul 2026 09:25:15 +0200 Subject: [PATCH] =?UTF-8?q?REPORT:=20R-77=20v0.173.0=20=E2=80=94=20Part=20?= =?UTF-8?q?0=20repair,=20red-proofs,=20live=20legs=201-4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- REPORT.md | 413 ++++++++++++++++++++++++------------------------------ 1 file changed, 187 insertions(+), 226 deletions(-) diff --git a/REPORT.md b/REPORT.md index 0699732..c6178d9 100644 --- a/REPORT.md +++ b/REPORT.md @@ -1,283 +1,244 @@ -# REPORT — R-75: canonical import root, catalog-derived skeleton, import surfaces (2026-07-26) +# REPORT — R-77: endpoint-drift detection, samba protected-set gate, channel log honesty (2026-07-26) -**Overwritten** per the standing rule. Controller **v0.172.0**, live on demo-felhom 9201 and -demo-hp 9201. MinAgent unchanged. +**Overwritten** per the standing rule. Controller **v0.173.0** live on demo-felhom 9201, demo-hp 9201 +and drill-r50 9201; hub **v0.74.0** live. MinAgent unchanged; `felhom-agent` untouched. --- ## 1. Baselines used -| Repo | start | end | -|---|---|---| -| felhom-controller | `3b672ba7` (v0.171.0) | `8fadbd9` (v0.172.0) | -| app-catalog-felhom.eu | `3067a946` | `4252121` | -| felhom.eu | `2d2050c3` | see §10 | -| felhom-agent | `dfd5d731` (v0.96.0) | untouched | +| Repo | start | end | version | +|---|---|---|---| +| felhom-controller | head after R-75 (`c7a3a90`) | `9056f01` | `v0.172.0` → **`v0.173.0`** | +| felhom.eu | `9e94479` | `9cfa619` | hub `v0.73.1` live → **`v0.74.0`** | +| felhom-agent | `dfd5d731` | untouched | `v0.96.0` | +| app-catalog-felhom.eu | `4252121` | untouched | — | --- -## 2. Part 1.0 probe result — and the STOP it triggered +## 2. Part 0 — operational repair (supervised, done first) -The brief anticipated an ambiguity between `cfg.Paths.SystemDataPath` and the *registered* path. The -probe found something bigger, and I stopped before Part 1.1 as instructed. +Its own STOP condition — *"if `fingerprint` or `token` also disagree, STOP"* — **did not trigger**: +only the address had moved on either box. | | demo-felhom 9201 | demo-hp 9201 | |---|---|---| -| `paths.system_data_path` | `/mnt/sys_drive` | `/mnt/sys_drive` | -| Registered storage paths | `/mnt/felhom-drives/hdd_1` ("USB HDD 1TB", default) | `/mnt/felhom-drives/nvme-1tb` ("NVME 1TB", default); `/mnt/felhom-drives/Felhom-Share` (network) | -| **System drive registered?** | **No** | **No** | +| endpoint BEFORE | `192.168.0.162:8443` | `192.168.0.87:8443` | +| endpoint AFTER | `169.254.253.1:8443` | `169.254.253.1:8443` | +| **fingerprint agrees** | **true** | **true** | +| **token agrees** | **true** | **true** | +| backup | `/var/lib/docker/volumes/felhom-controller-data/_data/controller.yaml.pre-r77.bak` | same path | +| `[channel]` lines in 90 s after restart | **0** | **0** | +| hub `agent_channel_*` events since | **0** | **0** | -`/mnt/sys_drive` is a real mountpoint (50 G, 4 % used) holding only -`felhom-data/backups/{primary,secondary}` and `lost+found`. **Neither `/mnt/sys_drive/userdata` nor -`/mnt/sys_drive/felhom-data/userdata` existed** on either box — the userdata convention had never been -exercised on the system drive. +Values were redacted at the point of collection — the probe compared `fingerprint`/`token` and emitted +booleans plus a 12-char digest; neither value left the box. -**Which string is the namespace root:** `/mnt/sys_drive/felhom-data`, per -`appbackup.NamespaceRoot(drivePath, inGuestDrive=false)`, confirmed by the on-disk -`felhom-data/backups/primary`. **But `withUserdataPath` disagrees** — it computes `USERDATA_PATH` as -`/userdata`, not `NamespaceRoot(hdd)/userdata`, so an app on the system drive would get -`/mnt/sys_drive/userdata` while its backups go to `/mnt/sys_drive/felhom-data/backups/`. Latent, and -left untouched; recorded in CONTEXT.md. +### On the verbatim `agent channel recovered` line the brief asked for: it does not exist, by design -**Why it blocked Part 6** — verified against the real guard with a passing control, not read from -source: +`Check` returns early on a healthy probe — `return nil // healthy first-obs / steady-up → no notify` +— and `NotifyRecovered` fires only on a `down→up` transition **within one process's lifetime**. The +repair required a restart, so the new process's first observation is healthy and therefore silent. +There is no recovery line to quote, and reporting one would mean fabricating it. -``` -sysroot NamespaceRoot shape -> REFUSED (Ez a mappa nem osztható meg.) -sysroot withUserdataPath shape -> REFUSED (Ez a mappa nem osztható meg.) -user drive (control) -> ACCEPTED (…/felhom-drives/hdd_1/userdata/import) -``` +The positive evidence instead: -And the obvious fix had a trap: if the system drive *were* registered, `SharingDeniedRoots` denies -`/felhom-data`, so the namespace-consistent shape is refused anyway. +- **zero** `[channel]` lines 90 s after restart on both boxes, where the pre-repair container logged + `transient down` within 60 s and confirmed `DOWN` within 120 s; +- the dashboard banner cleared (fetched through the authed endpoint, ASCII-safe grep); +- **zero** new `agent_channel_*` hub events since 06:45 UTC; +- demo-felhom health `ok` continuously. -``` -SharingDeniedRoots("…/sys_drive") = [appdata backups felhom-data felhom-data/appdata felhom-data/backups] - NamespaceRoot shape (felhom-data) -> REFUSED - withUserdataPath shape -> ACCEPTED -``` - -**Operator ruling:** leave the system drive unregistered; the controller writes the `beolvasas` share -directly. `sharingResolvePath` validates CUSTOMER-supplied picker paths — a controller-generated -constant is a different trust class — so **no guard was weakened, and none was bypassed for user -input**. +This is now written into the runbook, because "wait for the recovered line" would leave an operator +watching an empty log forever. --- -## 3. Exhaustive-switch audit +## 3. Files changed -| Site | Verdict | Action | -|---|---|---| -| `stacks/classify_binds.go:20` `composeVarRoots` | needs third entry | `{"${IMPORT_PATH}", RootImport}` added | -| `appbackup/classify.go` `ValidateBackupSpec` / `ClassifyBinds` | needs third root | `Import []BindSpec` + both loops extended | -| `appbackup/captureset.go` `resolveAbs` | **BROKEN for import — the sharp one** | fell through to `path.Join(hddPath, relPath)` → a directory on the WRONG DRIVE. Now takes `importRoot`; `RootImport` is its own case | -| `appbackup/captureset.go` `structuralGuard` | needs a case | an unresolvable import root is refused into `Skipped` (`reasonNoImportRoot`) rather than joined onto `""`. The `RootHDD` bare-root/`backups` rules deliberately do NOT transfer — `/userdata/import` nests no backups tree | -| `ComputeCaptureSet` / `ComputeFabBuckets` | needed the root threaded | `importRoot` param added; compile-forced at all 4 call sites (2 backup, 2 appexport) | -| `appexport/fabplan.go:53` | needs a case | import binds fell into `selectedHDD` and would tar from the wrong root. `excluded` makes them default-out, but `OptInExcluded` can pull them in | -| `appexport/fabplan.go:78` `classifiedHDD` | **correctly indifferent** | import binds are not `RootHDD`, so they are not listed | -| `backup/tier2_capture.go` `tier2DestRel` | **indifferent, but only by class** | TierSecondary is mandatory+optional and import is excluded. Left as-is and recorded rather than relied on silently | -| `stacks/samba_classify.go:44` | produces binds, does not switch | `shareRelPath` falls back to path-minus-leading-slash for a share under no registered root. **Known gap, not fixed here** — see §9.2 | -| `appexport/export.go:629` | **indifferent** | compares resolved paths, not the enum | -| `backup/appbackup_bridge.go` | **indifferent** | pure re-export shim | +**felhom-controller** (`9056f01`, plus `REPORT.md`): -`GetImportRoot()` added to both provider interfaces (`appbackup.StackDataProvider`, -`appexport.ExportStackProvider`) and both adapters in `cmd/controller/main.go`. +| file | change | +|---|---| +| `internal/bootstrap/bootstrap.go` | `DetectEndpointDrift`, `EndpointDrift` + its two message builders | +| `internal/bootstrap/drift_test.go` | NEW — Scenarios A/B/C/D + the secret-leak assertions | +| `internal/web/alerts.go` | `endpointDriftAlert` field, `SetEndpointDriftAlert`, ordered ABOVE the channel banner in `GetAlerts` | +| `internal/notify/notifier.go` | `NotifyEndpointDrift` + `EndpointDriftDetails` | +| `cmd/controller/main.go` | startup wiring (once, after config settles) | +| `internal/monitor/healthcheck.go` | `EffectiveProtected` gate + the corrected doc comment | +| `internal/monitor/effective_protected_test.go` | Scenario E; existing toggle test updated | +| `internal/channelhealth/checker.go` | `stateUnconfirmed`, `orUnseeded`, re-arm condition, field comment | +| `internal/channelhealth/checker_test.go` | Scenario F ×2 | + +**felhom.eu** (`9cfa619`): `hub/internal/api/handler.go` (allowlist), `hub/CHANGELOG.md`, +`manifests/hub.yaml`, `documentation/runbooks/RUNBOOK-local-api-endpoint-drift.md` (NEW), +ROADMAP (R-77 shipped + R-78/79/80), capability-map note. + +### Scope addition, flagged: the hub was NOT in the brief's scope, and had to be + +`handleEvent` validates `event_type` against `allowedEventTypes` and returns +`400 Invalid event_type` otherwise. Shipping `local_api_endpoint_drift` controller-side alone would +have produced an alert that never reaches the operator — the inert-seam class this project has hit +four times, and precisely the failure mode R-77 exists to prevent. I added the one-line allowlist +entry, bumped the hub to v0.74.0 and deployed it. Live leg 2 proves the end-to-end path. --- -## 4. Commits +## 4. Tests + the three mandatory red-proofs -| Repo | Commit | What | -|---|---|---| -| felhom-controller | `2958946` | v0.172.0 main implementation | -| felhom-controller | `4773809` | fixup 1 — `EnsureImportRoot` parent convention | -| felhom-controller | `8fadbd9` | fixup 2 — drop `import/*` from the carry-list | -| app-catalog-felhom.eu | `4252121` | ingest binds → `${IMPORT_PATH}`, `backup:` moved, `data_paths:` added | +**951 → 959 test functions; 27 controller packages green; hub green.** +`go build ./... && go vet ./... && go test ./...` clean in both repos. -**Push ordering was deliberate and load-bearing:** the controller was built and deployed to BOTH boxes -*before* the catalog was pushed. A catalog carrying `${IMPORT_PATH}` reaches a running v0.171.0 -controller within 15 minutes and would resolve the variable to `""`, binding a bogus root-owned dir at -the container root on the next deploy or restart. +### Red-proof A — run in BOTH failure directions -**Files:** 10 new (`stacks/{datapaths,skeleton_derive}.go`, `web/{datapath_card,filebrowser_link}.go`, -6 test files), 43 modified. +*(i) the pre-fix shape (v0.172.0: no detection at all):* +``` +--- FAIL: TestScenarioA_DriftDetectedAndNamed_NoWrite (0.00s) + drift_test.go:78: drift must be DETECTED — this is the exact live shape from the 2026-07-25 outage +``` +*(ii) the tempting wrong turn — a variant that detects **and helpfully corrects**, i.e. R-78 done +prematurely:* +``` +--- FAIL: TestScenarioA_DriftDetectedAndNamed_NoWrite (0.00s) + drift_test.go:101: controller.yaml was MODIFIED — detection must never write (that is R-78) +``` +The second is the one that matters: "an error was logged" is a hollow assertion; "and nothing was +written" is the contract. + +### Red-proof E +``` +--- FAIL: TestScenarioE_SambaProtectedOnlyWhenActuallyDeployed (0.00s) + effective_protected_test.go:99: (2) sharing ON, no password: samba protected = true, want false + — THE BUG: reconcileSambaAt refuses to deploy without a password — a deliberate state, not a fault +``` + +### Red-proof F +``` +--- FAIL: TestScenarioF_BornDownLogsUnseededNotUp (0.00s) + checker_test.go:308: an unseeded checker must not report state "up" after a suppressed first failure + checker_test.go:318: born-down must log "unseeded->down:unreachable"; got: + [WARN] [channel] agent channel DOWN (up->down:unreachable): dial tcp 192.168.0.87:8443: … +``` +And the non-change half: all **nine** pre-existing `channelhealth` tests still pass, including +`TestF2_BornDownNonTransient_AlertsOnce`, `TestF2_OldSeedSilentLogicWouldNotAlert` and +`TestF2_BornDownTransient_Debounced`. Scenario F asserts sink call **count and arguments** +(`reason`, `eventType`, `severity`, dashboard flag, probe count), not just the log string. + +### One pre-existing test was deliberately changed + +`TestEffectiveProtectedTracksSharingToggle` used `SMBSettings{Enabled: true}` and asserted samba WAS +watched — i.e. it encoded the bug. Updated to `Enabled: true, UserSet: true` with a comment saying +why, and the three-state matrix added alongside. Flagged here because "changed a passing test" always +deserves to be visible. + +### Gates + +`template_id_gate`, `emoji_gate`, `mojibake_gate`, `native_confirm_gate`, `app_row_dedup_gate`, +`offbox_rename_gate` — rc=0. `docker_run_volume_path_gate` **rc=1, PRE-EXISTING** (R-29(a), red since +v0.129.0, in `appexport/estimate.go` which this change does not touch). --- -## 5. Tests + the three mandatory red-proofs +## 5. Deployed versions -**915 → 951 test functions; 27 packages; `go build ./... && go vet ./... && go test ./...` all green.** - -### Red-proof B — classification regression -Pre-fix shape: revert the third-root plumbing in `ValidateBackupSpec`/`ClassifyBinds`. ``` ---- FAIL: TestScenarioB_ImportMoveKeepsClassification (0.00s) - import_root_classify_test.go:107: import/paperless: class = "mandatory", want "excluded" — the moved ingest bind must classify under the import root - import_root_classify_test.go:110: import/paperless: origin = "default_writable", want "explicit" — the moved ingest bind must classify under the import root +demo-felhom 9201 : gitea.dooplex.hu/admin/felhom-controller:0.173.0 Up (healthy) +demo-hp 9201 : gitea.dooplex.hu/admin/felhom-controller:0.173.0 Up (healthy) +drill-r50 9201 : gitea.dooplex.hu/admin/felhom-controller:0.173.0 Up (healthy) +hub : gitea.dooplex.hu/admin/felhom-hub:0.74.0 Synced / Healthy, rolled out ``` -The failure is *worse* than "degrades to legacy": the drop-zone silently becomes `mandatory`, i.e. a -transient consume inbox would be shipped OFFSITE. - -### Red-proof C — determinism (the P6 gate) -Pre-fix shape: remove `sort.Strings` from `BuildUserdataSkeleton`. -``` ---- FAIL: TestScenarioC_SkeletonDeterminism (0.00s) - skeleton_determinism_test.go:28: generation 2/20 differs — a non-deterministic skeleton force-recreates FileBrowser on every sync pass - first: [a/b media media/audiobooks media/books media/photos import import/paperless roms a/b/c …] - got: [a/b media media/music media/comics media/photos downloads import/calibre media/tv …] -``` -Failed on generation **2 of 20**. - -### Red-proof E — server-side share refusal -Pre-fix shape: remove BOTH server-side refusals (handler + store), leaving only the template gate. -``` ---- FAIL: TestScenarioE_SystemShareDeleteRefusedServerSide (0.07s) - import_share_test.go:63: the system share was DELETED by a direct POST — the refusal is not server-side ---- FAIL: TestScenarioE_StoreLayerRefusesSystemShare (0.06s) - import_share_test.go:84: RemoveSMBShare must refuse a System share -``` - -**Design gates:** `template_id_gate`, `emoji_gate`, `mojibake_gate`, `native_confirm_gate`, -`app_row_dedup_gate`, `offbox_rename_gate` — all rc=0. -`docker_run_volume_path_gate` **rc=1, PRE-EXISTING** (`internal/appexport/estimate.go:179`, a file -this change does not touch — `git status --porcelain` on it is empty). That is R-29(a), red since -v0.129.0; not fixed here. --- -## 6. Live validation (Part 8) +## 6. Live legs -Deployed `gitea.dooplex.hu/admin/felhom-controller:0.172.0` — both boxes `Up … (healthy)`. -Method: **endpoint-level** — the exact endpoints the UI invokes, driven with an authed session against -the controller container IP with the `Host:` header. No browser exists on DooPlex. +**Leg 1 — no drift alert on the aligned production boxes (Scenario B live).** Zero `DRIFT` lines and +zero `[channel]` lines on both after the v0.173.0 restart. Agreement really is silent; the check does +not cry wolf on every healthy boot. -**Leg 1 — canonical bind.** Catalog synced through the real button endpoint (`POST /api/sync` → -`updated: [calibre-web, paperless-ngx, romm]`), then `POST /api/stacks/paperless-ngx/update`: +**Leg 2 — `drill-r50`, the untouched real drift (Scenario A live).** It was left broken on purpose. + +Before (controller 0.161.0): ``` -/mnt/sys_drive/felhom-data/userdata/import/paperless -> /usr/src/paperless/consume -drwxrwsr-x 2 1000 1000 /mnt/sys_drive/felhom-data/userdata/import/paperless +controller.yaml sha256 BEFORE : 54892f1bc691c29b70ff9c323aab9e0b6ad758c602c533130064006e9a2af737 + controller.yaml endpoint : 192.168.0.176:8443 + bootstrap.json endpoint : 169.254.253.1:8443 + DRIFTED : True ``` -**PASS** — 2775, group 1000, on the system drive. - -**Leg 2 — ingest round-trip.** File written into `/srv/beolvasas/paperless` through FileBrowser's own -mount as uid 1000; paperless saw it as `paperless:paperless`, then: +After deploying v0.173.0: ``` -INGESTED+DELETED after ~30s -[paperless.consumer] Consuming r75-teszt.txt -[paperless.consumer] Document 2026-07-26 r75-teszt consumption finished -[paperless.tasks] ConsumeTaskPlugin completed with: Success. New document id 16 created -final drop-zone contents: (empty) +[ERROR] bootstrap: local_api endpoint DRIFT — /opt/docker/felhom-controller/controller.yaml says +"192.168.0.176:8443" but /etc/felhom-bootstrap/bootstrap.json says "169.254.253.1:8443"; the +controller is dialling the FORMER. Pin agrees: true. Not auto-corrected (R-78 owns the authority +ruling) — fix the intended file and restart the controller. +[INFO] Event pushed: local_api_endpoint_drift (error) — … + +controller.yaml sha256 AFTER : 54892f1bc691c29b70ff9c323aab9e0b6ad758c602c533130064006e9a2af737 ``` -**PASS** — the consume-and-delete contract holds across the new bind. - -**Leg 3 — app-page deep link**, rendered live on demo-hp: -```html -Beolvasandó dokumentumok ↗ -18.5 GB szabad -

Ide másold a feldolgozandó fájlokat. Az alkalmazás beolvassa, majd törli innen - — ez a mappa átmeneti, és nem készül róla biztonsági mentés.

+**Checksums identical — zero write, proven on a live divergence.** And hub-side, proving the +allowlist end-to-end: ``` -**PASS.** *Honest scope:* the link was verified as rendered and correctly encoded, **not followed in a -browser** — see §8. - -**Leg 4 — the share.** `POST /sharing/enable` → the `beolvasas` share auto-appeared at -`/mnt/sys_drive/felhom-data/userdata/import`, row marked `rendszer` with no delete button. A direct -`POST /sharing/shares/delete name=beolvasas` returned 303 and the share **survived**: +events: 2026-07-26 07:21:31 drill-r50 local_api_endpoint_drift error +notification_log: 2026-07-26 07:21:32 drill-r50 local_api_endpoint_drift operator sent ``` -[('beolvasas', True, '/mnt/sys_drive/felhom-data/userdata/import')] +`drill-r50` was **left drifted** — repairing it is the operator's call, and it is now the only live +fixture for this alert. + +**Leg 3 — demo-hp's false samba alarm is gone, sharing still on without a password.** ``` -**PASS.** Note `felhom-samba` is **not running** — sharing is on but no household password is set, so -the stack correctly stays undeployed. Enabling the toggle therefore did **not** put SMB on the LAN. +[DEBUG] [monitor] Checking 4 protected containers: [traefik cloudflared felhom-controller filebrowser] +[DEBUG] [monitor] All protected containers running +[DEBUG] [monitor] Final status: ok (issues=0, warnings=0, info=5) +``` +Hub-side the flip is unambiguous: `fail` on 0.172.0 at 07:15:56 → **`ok` on 0.173.0 at 07:18:27**. -**Leg 5 — zero removals, both boxes.** - -| Box | paths removed | paths added | -|---|---|---| -| demo-felhom | **none** | none | -| demo-hp | **none** | `media/podcasts` | - -`documents` intact on both. `media/podcasts` is the one genuinely derived addition (audiobookshelf's -second bind, in no hardcoded list) — and its appearance on demo-hp but not demo-felhom, where it -already existed, is the clean control that the derivation works. System-drive trees on both boxes: -`userdata` and `userdata/import` at `2775 root:1000`, plus `import/paperless` at `2775 1000:1000` on -demo-hp where paperless runs. +**Leg 4 — the other half of Scenario E, live (operator-present state change).** Household password +set through the real `POST /sharing/password` endpoint (303): +``` +felhom-samba Up 45 seconds +[DEBUG] [monitor] Checking 5 protected containers: [traefik cloudflared felhom-controller filebrowser felhom-samba] +[DEBUG] [monitor] Final status: ok (issues=0, warnings=0, info=5) +``` +Samba deployed and **is watched again** — the fix suppresses the deliberate state, not the real one. --- -## 7. Two defects found DURING the live legs, fixed in the same version +## 7. NOT yet live-validated -1. **`EnsureImportRoot` left the parent at 755.** `EnsureUserdataDir` MkdirAll's intermediates at plain - `0755` and chmods only the leaf, so `/userdata` came out `755 root:root` — the one userdata - root on the box outside the 2775/gid-1000 convention. Both now carry it - (`TestEnsureImportRoot_ParentCarriesTheConvention`). -2. **The carry-list re-created the dead drop-zone forever.** With `import`, `import/paperless` and - `import/calibre` carried, the derived skeleton would re-create a per-drive drop-zone on every drive - in perpetuity — the exact lookalike this arc removes, and unbacked (`class: excluded`). Dropped from - the carry-list. **Not a zero-removals violation:** nothing deletes what a box already has, and both - boxes' old drop-zones were verified to hold **zero files** first. - `TestSkeletonNeverCreatesAPerDriveDropZone` pins it; `TestUserdataSkeleton_List` was updated to - assert their absence (a deliberate behaviour change, not a weakened assertion). - -**One latent 500 caught before it shipped:** the sharing template's row struct was function-local, so -adding `{{if .System}}` would have failed at render for every share row. `ShareRow` is now -package-level and the render test constructs the handler's own type. +- **A drift where the FINGERPRINT also moved.** Unit-tested + (`TestDrift_FingerprintDisagreementIsSurfacedNotLeaked`); no live fixture exists, and manufacturing + one would mean deliberately corrupting a pin. +- **The drift banner rendered in a browser.** The alert and event are proven; the Hungarian dashboard + line is unit/`AlertManager`-level only. No browser on DooPlex. +- **Drift on a box where `controller.yaml` is the CORRECT file.** The detector is symmetric by + construction, but every live case so far has `bootstrap.json` correct. This is exactly the asymmetry + R-78 must rule on. +- **A recovery-line transition** (`down→up` within one process). The repair path restarts, so it is + structurally unreachable; the `recovered` branch is unit-tested only. +- **The samba gate through a full enable→password→disable cycle.** Enable-without-password and + password-set were both exercised; disabling sharing again was not. +- **Whether `drill-r50`'s channel actually recovers** once its endpoint is aligned — it was left + drifted on purpose. --- -## 8. NOT yet live-validated +## 8. Observations — noticed, not acted on -- **No browse through the FileBrowser UI.** The source, the bind and the deep-link URL are verified; - nothing drove FileBrowser's HTTP UI or API. The capability-map row *File access via browser* is - therefore **deliberately left at IMPLEMENTED**, with a note. Needs a human click-through. -- **The deep link was not followed.** Rendered and encoding-verified only; the cold-link → login → - redirect path is proven from the shipped router source (spike P2), not exercised live. -- **calibre-web's ingest bind** was not redeployed — calibre-web runs on demo-felhom, and leg 1 - exercised paperless-ngx on demo-hp. The catalog change is identical in shape. -- **SMB access to the `beolvasas` share** — the samba stack is not deployed on demo-hp (no household - password), so no Windows/Explorer leg ran. Config-level only. -- **`data_paths` roles `library` and `export`** were not seen live: paperless declares only `import`. - calibre-web (`library`) and romm (`library`) are unit-tested but not rendered on a box. -- **A multi-drive box.** Both demo boxes have one non-network data drive, so the canonical root's - central benefit — no dead drop-zone on drive #2 — is proven by construction and unit test, not in - the field. -- **The `.fab` export path with an opted-in import bind** — `resolveAbs`'s third case is unit-tested, - never exercised through a real export. -- **A migration off the system drive** (the `appDataSkipSet` exclusion) — unit-tested only. - ---- - -## 9. Observations — noticed, not acted on - -1. **`withUserdataPath` puts userdata outside the namespace on the system drive** (`/userdata` vs - `NamespaceRoot(hdd)/userdata`). Latent — no app with a userdata bind has been deployed there — but - it is a real inconsistency and would surface the day one is. -2. **`samba_classify.shareRelPath` has no case for a share under no registered root.** It falls back to - path-minus-leading-slash, which for the system share yields a bogus `RootHDD` relpath. Only reached - for backup classification of shares, and the share is `Offsite: false`, so it is inert today. Left - alone deliberately (out of scope); worth its own item. -3. **A customer with pending files in an OLD per-drive drop-zone would find them never ingested** after - this upgrade — the bind moved, and nothing migrates the files. Both demo boxes were verified empty - so no action was needed here, but a real box would need an operator-run move. **No destructive or - move operation was added anywhere in this arc.** -4. **`docker_run_volume_path_gate` is still red** (R-29(a), since v0.129.0), in a file this change does - not touch. -5. **R-76 untouched** (FileBrowser's `0755`/no-setgid, and `import/calibre` at 755 on demo-felhom — - still `755 1000:1000` after this deploy, since nothing here chmods it). Nothing built in this arc - assumes an `import/*` directory stays 2775. -6. **Sharing is now ENABLED on demo-hp** (leg 4, explicitly authorised by the brief). No household - password is set, so `felhom-samba` is not deployed and SMB is not on the LAN. Reverting would be a - further unrequested state change, so it was left as-is — flagging for the operator's decision. -7. **I re-hit the accented-Hungarian grep trap myself** while checking leg 3: an accented pattern - through `ssh → pct exec → bash -c` returned nothing and briefly read as "the consequence line is - missing". It was rendering correctly all along. ASCII-only gating patterns, every time. - ---- - -## 10. felhom.eu docs - -- `documentation/controller/import-and-data-paths.md` — NEW: the canonical root, the three roles, the - `data_paths` contract, the class-driven copy rule, and the seven invariants a future change must not - break. -- `documentation/architecture/00-capability-map.md` — *File access via browser* note added, **status - unchanged** (§8). -- `documentation/backlog/ROADMAP.md` — R-75 collapsed to its shipped one-liner; **R-76 left open**. +1. **`manifests/hub.yaml` was pinned to `0.73.1` while `hub/CHANGELOG.md` topped out at `0.73.2`** — + the documented "live image can lag the CHANGELOG" trap, found while bumping. v0.73.2's content is + in the 0.74.0 image now, but **v0.73.2 as a released artifact was never deployed**. Worth a glance + at what else it contained. +2. **`expected_backup_missed` still fires nightly on all three customers** — filed as **R-80**, with + the arithmetic the DIAG hedged on: 7.3 days of stale backup materially exceeds the ~1.5-day channel + outage, so backups were already stale ~6 days *before* the channel broke. R-77's repair will not + resolve it, and it reaches a **customer** channel. It plausibly outranks this task. +3. **`report.Issues`/`Warnings` remain English on a Hungarian surface** — R-79, whole-surface, + deliberately not swept here. +4. **The drift check is startup-only.** Correct today (both files are read at boot and neither changes + under a running controller), but if anything ever rewrites `bootstrap.json` live, the check would + not notice until the next restart. Named in the code comment so the assumption is visible. +5. **`mergeLocalAPI` replaces the whole block**, so endpoint/fingerprint/token cannot be reconciled + independently today. That constrains R-78's design space and is recorded in its ROADMAP entry. +6. **Sharing on demo-hp now has a household password and samba is running** (leg 4). That is a real, + deliberate state change from the brief, and it changes the box's LAN exposure — SMB is now actually + served. Flagging so it is a decision, not a surprise.