v0.317.0: a second copy counts only on a proven different disk; the no-drive install speaks in words (felhom.eu architecture/16, build A)
gates / gates (push) Successful in 46s

Tier-2 and the one-copy notice ask the host agent (POST /disks/same-disk, agent 0.157.0): different +
proven -> made and counted; same disk -> not made (D1); unknown (VM, hidden USB serial) -> made, not
counted, the row says why (D2); a NAS never counts and no longer hides the notice (R-1075). An older
agent keeps the st_dev rule, logged once. D4: a data-folder or media app on a box with no drive is
refused in words with a link to the drives page (R-1068's raw-error half). MinAgent 0.157.0.

Claude-Session: https://claude.ai/code/session_012qRErfCoiTkvDK9N5XHbzb
This commit is contained in:
2026-10-15 21:21:54 +02:00
parent c6a567122e
commit 0626b9fc8a
34 changed files with 665 additions and 200 deletions
+37 -163
View File
@@ -1,177 +1,51 @@
# REPORT — v0.316.x: the app page as one column; one image per version number (2026-10-15)
# REPORT — v0.317.0: a second copy counts only on a proven different disk; the no-drive install speaks in words (2026-10-15)
Written in ASD-STE100 Simplified Technical English. Design read first: felhom.eu `architecture/16-storage-physical-identity.md`
(§3, §4, §5 D4, §8, §10, §11), `07-backup-architecture.md` §6, `09-update-architecture.md` (MinAgent), `03-host-agent.md`.
Task: storage identity build A (operator rulings D1–D6, 2026-10-15). Agent counterpart: felhom-agent v0.157.0.
## 1. Baselines
| Repo | `main` at start | Version | → |
|---|---|---|---|
| felhom-controller | `8339206` | v0.315.0 | **v0.316.0 → .1 → .2 built; .3 committed, NOT built (§6)** |
| felhom.eu | `340804f9` (docs only) | — | — |
Architecture read first: `02-controller-module-map.md` „The app page", `09-update-architecture.md`,
the `felhom-ui-design` skill, and the controller's `CLAUDE.md` + `.claude/rules/*`.
Built and tested on DooPlex in an isolated scratch clone; edited on the Windows workstation. The
browser work is on the workstation, which is the reason CC runs here.
## 2. PART 0 — the diagnosis, before any fix
**Verdict: (a) a cache.** Full evidence: `felhom.eu/documentation/audits/app-page-2026-10-15b/P0/VERDICT.md`.
**(b) is ruled out.** Exactly one guest ran 0.315.0 — demo-hp 9201 — on digest
`sha256:8887343011d6…`, identical to the registry's 0.315.0 at the time of measuring. The **global
floor is 0.314.0** (read from the hub's `/configuration`), so no box could have taken 0.315.0 by
itself. demo-hp 9202 and felhom-pve (N100) 9201 are both on 0.314.0; Tester 1 (`c11-appliance`) was
unreachable and, by the floor, cannot be on 0.315.0.
**The mechanism, in four measured facts:**
1. `layout.html` asked for `/static/style.css?v={{.Version}}` → `?v=0.315.0` for **all three**
different 0.315.0 builds (`e66d466`, `fe0228c`, `db52f5a`); the first had no `.app-tabs` rules.
2. The origin sends **no validator**: measured on the box, `ETag present = 0`,
`Last-Modified present = 0`. A cached copy cannot be revalidated — only expire.
3. Cloudflare caches that URL (`cf-cache-status: HIT`, `age: 99 → 100` over three requests) and
**rewrites the browser TTL from the origin's `max-age=3600` to `max-age=14400`** — four hours.
4. The session's first request returned **`cf-cache-status: EXPIRED`**, which only happens when the
edge holds an entry that has aged past its TTL.
**What is true now, and the caveat stated rather than glossed:** origin, edge and box agree
byte-for-byte (`sha256 c1774f63…` both sides) and all carry the rules; the page itself is never
cached (`DYNAMIC`). The specific stale object is **no longer observable** — the first probe forced
its revalidation, which is what `EXPIRED` records — so I cannot show a `HIT` whose body lacks
`.app-tabs`. What is shown is every precondition, the proof an aged entry existed, and the
elimination of the only other candidate. No cache was purged by hand.
## 3. Boxes on 0.315.0, with digests
| Box | tag | digest | now |
|---|---|---|---|
| demo-hp 9201 | 0.315.0 | `8887343011d6` | **0.316.2** |
| demo-hp 9202 | 0.314.0 | — | unchanged |
| felhom-pve (N100) 9201 | 0.314.0 | — | unchanged |
| Tester 1 (`c11-appliance`) | unreachable | — | cannot be on 0.315.0 (floor 0.314.0) |
## 4. Files changed, commits on `main`
| Commit | What |
|---|---|
| `9cc7ffc` | build.sh refuses a repeated push; `assetTag` per build; every shell uses it; harness normalises it |
| `7f3e58f` | the one-column layout (the approved mockup) |
| `48d8674` | v0.316.0 CHANGELOG + the push-once fact in the rule file |
| `006ceab` | **v0.316.1** — the asset tag reaches the dashboard pages |
| `f908eee` | the settings tab loses the install page's boxes |
| `9e68e52` | two more data-tab leftovers |
| `8c7aa10` | **v0.316.3** CHANGELOG |
| felhom.eu `b6638ac4`, `22cd7c51` | the design docs, the audit, R-1080 closed, R-1081 filed |
Main files: `controller/build.sh`, `scripts/test_build_push_once.sh`, `internal/web/server.go`,
`asset_tag_test.go`, `app_layout_test.go`, `templates/{style.css,app_shell,app_settings,app_data,app_guide,app_logs,app_fields,deploy}.html`,
both locale files, `i18n_parity_test.go`, `i18n_wiring_test.go`.
## 5. Tests and red-proofs
| # | Break | Result |
| Repo | `main` at start | at commit |
|---|---|---|
| B1 | the whole registry check deleted | 5 of 6 cases fail; a present tag builds AND pushes |
| B2 | an unreachable registry treated as absent | the two unanswered-registry cases fail |
| C1 | `assetTag` from the version only | `TestAssetTag_ChangesPerBuild` fails on both same-version cases |
| C2 | one shell back on `?v={{.Version}}` | `TestAssetTag_EveryShellUsesIt` fails naming layout.html |
| C3 | `addShellAssetData` removed from `executeTemplate` | `TestAssetTag_ReachesTheRenderedPage` fails: empty buster on four pages |
| D1 | the auto-update success banner restored | `TestAppPage_AutoUpdateSuccessIsMetaNotBanner` fails |
| D2 | one tab loses its column | `TestAppPage_OneColumnLayout` fails naming the tab |
| D3 | a section drawn as a card again | same test fails on two tabs |
| E | an informational section drawn as a blue callout | `TestAppPage_CalloutsOnlyForAskAndWarn` fails on both counts |
| felhom-controller | `9cc7ffcf3b` (brief) → `48d8674876` read | rebased onto `c6a5671` (v0.316.4) |
| felhom-agent | `d2053e7d47` | `52ed52c` (v0.157.0 released) |
New: `TestBuildScript_RefusesExistingTag` (six cases, real script, fake docker),
`TestAssetTag_{ChangesPerBuild,EveryShellUsesIt,ReachesTheRenderedPage}`,
`TestAppPage_{OneColumnLayout,AutoUpdateSuccessIsMetaNotBanner,CalloutsOnlyForAskAndWarn}`.
## 2. What changed
Full gate: `go build ./... && go vet ./... && go test ./...` → **exit 0, 37 packages ok, zero
failures**, measured with `go test`'s own exit code (not a pipeline's). All controller gates OK; 53
decoys behaved.
- `internal/backup/backup.go`: `diskRelation` — the one „same disk?" decision; asks the agent through `SetDiskJudge`; an older
agent or an `unmapped` path keeps the st_dev rule (logged once). `offDriveCopyState` / `singleCopyWarning`: the notice by `16` §4,
skips a NAS (R-1075), i18n.
- `internal/backup/tier2.go`, `tier2_shares.go`: target choice — same disk never; unknown made, marked `Unproven`; proven beats unknown;
the guard re-asks right before the run. `settings.CrossDriveBackup.Unproven` records it; `handlers.go` + `backups_apps.html` show it.
- `internal/agentapi`: `FeatureSameDisk` (MinAgent 0.157.0), `Client.SameDisk`, the identity fields on `DiskInfo`.
- `internal/stacks/deploy.go`, `deploy_errors.go`, `internal/api/router.go`, `deploy.html`: D4 — `ErrNeedsDrive`, 400, `code: needs_drive`,
the sentence and a link to the drives page.
- `cmd/controller/main.go`: the judge wired. Locales: four new keys in both files; `scripts/i18n_go_keys.json` lists them.
## 6. Deployed — and what is NOT
## 3. Tests and red-proofs
**demo-hp guest 9201 runs `0.316.2`, healthy.** Tester 1 and demo-felhom untouched. The **floor stays
0.314.0** — raising it needs all three test boxes and a green box walk.
| Test | Red-proof (seen failing) |
|---|---|
| `TestDisk16_ProvenDifferent_CountedNoNotice` | — (positive control for the others) |
| `TestDisk16_SameDisk_NotMadeAndNoticeShown` | the same-disk refusal turned off → fails |
| `TestDisk16_Unknown_MadeNotCountedCannotCheck` | the unproven mark set false → fails |
| `TestDisk16_NetworkShare_NeverHidesTheNotice` | the NAS skip removed → fails (R-1075) |
| `TestDisk16_OldAgent_KeepsTheStDevRule` | fails with the same-disk refusal off |
| `TestDisk16_UnprovenCopyIsRecorded` | — |
| `TestD4_NoDriveRefusalSpeaksInWords` | the D4 branch off → fails with the OMV run's raw sentence |
| `TestD4_NeedsDriveIs400`, `TestD4_FilledPathKeepsItsOwnRefusal` | — |
| `TestDiskJudge_IsWiredAtStartup` | the judge made to never call the agent → fails |
**v0.316.3 is committed and pushed to git but NOT built.** Between the 0.316.2 build and this one,
another session's work in progress appeared in the shared DooPlex build clone (15 modified files,
the physical-disk storage work, R-1074..R-1079). The clean-tree gate forbids building over it and I
will not stash or revert another session's work. Every build I did make printed `porcelain=0` first.
`go test ./...` green; `controller_gates.py --fast` green except `observations` (this section fixes it). Ten i18n parity pages
refreshed with `-update-i18n-golden` after moving the old ones aside; the diff of each is ONLY the new lines (the stylesheet line
kept as stored).
What 0.316.3 adds, and therefore what the operator will NOT see tonight: on the data tab,
„Biztonsági mentés" is still drawn as a card, and the 2nd-copy heading still reads „2. mentés
beállítása — AdventureLog". Both are cosmetic; everything else is live.
## 4. Live proof
## 7. The browser check, through the PUBLIC address
Chrome on the workstation, `https://felhom.enkisfelhom.hu`, **normal loads — no hard refresh**:
- The stylesheet URL is `?v=20540d81` (an 8-hex per-build tag, not a version, not empty), and the
loaded sheet carries the `.app-tabs` rules. **This is the fix working:** before it, the same page
needed a hand-forced refetch.
- `.app-tab-active` computes `border-bottom: 2px rgb(46,168,245)` = `--blue-bright`; the column is
`max-width: 820px`; the hero has `border-top-width: 0px`.
- Across settings / data / guide / logs / plex / grocy / install: **1 column each, 1 `aria-current`
on an installed app and 0 on a not-installed one, 0 `.app-info-card`, 0 success banners.**
- Data tab: exactly **1 amber callout** (the same-disk warning). Settings, guide, grocy: **0 blue,
0 amber** — correct, nothing is asked and nothing is risky.
- **Console clean on a fresh load.**
Screenshots: `felhom.eu/documentation/audits/app-page-2026-10-15b/browser/`.
**Scenario E's blue callout was NOT exercised live.** Plex's setup gate is open (the operator's real
claim, R-1060), so `Needed()` is false and the card is correctly hidden. Showing it live would mean
un-claiming the operator's real Plex server; not done. The branch is covered by
`TestAppPage_CalloutsOnlyForAskAndWarn` against a harness Plex with a closed gate.
## 8. Fixtures replaced, by name
Recaptured (the pages this task restyles, per the standing exception): `app_settings_*` ×21,
`app_data_*` ×9, `app_guide_*` ×4, `app_logs`, `deploy_new{,_blocked,_memory_blocked}`. **All other
fixtures are byte-identical** — and now permanently so against build identity: the harness
normalises the asset tag on both sides of the comparison, as it already did for the per-session CSRF
token, so a build can no longer churn 148 files.
## 9. NOT yet validated
1. **The operator's look** at AdventureLog and Plex on demo-hp through the public address — asked
for in STATUS. The layout judgement is theirs.
2. **v0.316.3's two cosmetic fixes** (§6) — committed, unbuilt, so unseen.
3. **A blue callout in a real browser** (§7).
## 10. Teardown — three layers
- **Machine:** nothing provisioned, no app installed, no throwaway Plex. Probe scripts and fetched
pages removed from DooPlex `/tmp`.
- **Host:** the scratch clone and the `felhom.eu` sibling symlink are deleted. The shared DooPlex
checkout was never written to by me — which is how I could tell the other session's work from
mine. `build.sh` in the build directory is now a symlink to the repo's (deliberate, §11), with the
stale copy kept beside it.
- **Hub:** nothing created; no floor change.
Pending at this commit — the next REPORT revision carries it (Tester 1, demo-hp, the N100, a NAS on a scratch box).
## 11. Observations
- **FILED: R-1081** — the registry's `0.315.0` no longer holds the 0.315.0 release. **I did this**,
and the cause is worth stating plainly: I put the guard in `controller/build.sh` in the repo, but
the documented release command runs `$FELHOM_ROOT/build/felhom-controller/build.sh`, which was a
**separate copy taken 2026-07-18**. The very first thing I did with the new refusal — run it
against a real existing tag to prove it fires — therefore rebuilt and re-pushed 0.315.0 a fourth
time, with v0.316.0's code under it. A guard added to a script the runbook does not execute is the
seam-built-but-never-wired class, and I shipped one while fixing another. Root cause fixed in the
session (symlink), then the refusal proven from the documented path. No box is affected.
- **NOT-A-FINDING (fixed in session):** the asset tag shipped dead in 0.316.0 — every shell asked for
it, the template test passed, and the dashboard path never set it, so the first live load rendered
`?v=` (empty, i.e. a constant). Found in a browser; `TestAssetTag_ReachesTheRenderedPage` now
asserts the rendered value, not the markup.
- **NOT-A-FINDING (fixed in session):** `app_row_dedup_gate` convicted my `.app-row`, correctly —
that class already means „a row in the app list". Renamed `.app-field-row`; an over-broad rename
briefly took the list's own rule with it and was restored.
- **NOT-A-FINDING:** `page.title.tier2_config`-style leftovers — the 2nd-copy heading's locale key
carries „— {{.DisplayName}}" because it was a page title. New key `app_page.second_backup`.
- **Register: 169 before → 170 after; 1 opened (R-1081), 1 opened-and-closed (R-1080).**
- **System poster:** no fact in `felhom-system-poster.facts.md` changed. Neither file edited;
nothing added to STATUS about it.
- **Instruction files:** `.claude/rules/unprompted-work.md` item 6 in **all four** versioned copies
gains „a version number is pushed once", stated as a fact about what the build now enforces, not
as a new rule. The workspace-root copy is unversioned and not on this machine.
1. FILED: R-1082 — the drives page offers `/mnt/sys_drive` for attaching (R-280, the reinstall-restore way out) while `16` §11 rejects
it for household data, and the D4 sentence links to that page. Not changed here: source and design disagree, so it is reported.