From 7410f1c841798608383a6661b54205216377c08d Mon Sep 17 00:00:00 2001 From: kisfenyo Date: Sun, 13 Sep 2026 09:03:59 +0200 Subject: [PATCH] =?UTF-8?q?REPORT:=20v0.236.0=20(R-442)=20proven=20live=20?= =?UTF-8?q?on=20demo-hp=20=E2=80=94=20data=20gone,=20refusal=20shown,=20SS?= =?UTF-8?q?D=20app=20not=20refused?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS --- REPORT.md | 332 +++++++++++++++++++++++++----------------------------- 1 file changed, 151 insertions(+), 181 deletions(-) diff --git a/REPORT.md b/REPORT.md index b776b5b..128d730 100644 --- a/REPORT.md +++ b/REPORT.md @@ -1,204 +1,174 @@ -# REPORT — v0.235.0, update arc slice 3: freeze the version, keep the fixes flowing (2026-09-06) +# REPORT — v0.236.0, R-442: "delete my data too" deletes the data, or says that it could not (2026-09-13) *Overwritten each run. This records the most recent implementation only.* -> **Read this first — one claim in the task was wrong, and one thing this run FOUND that the task did -> not anticipate.** -> -> **The task's §7 Scenario B is incomplete, and the live run is what showed it.** It asks that a -> catalog version move freezes the app to "the **stored applied definition**", and says nothing about -> keeping that store current. But the store is written when the PIN is written, so a fix delivered -> afterwards (Scenario A's own case) lands in the live file and **not** in the store — and the first -> freeze then reverts it. **Observed live at 08:20:29Z**: the frozen file came back with -> `interval: 30s`, nineteen minutes after `45s` had been delivered. Left unfixed, slice 3 would have -> silently undone the half of the ruling that says fixes keep flowing. **The equal-images branch now -> refreshes the store as it delivers.** Everything else in §5's symbol table was accurate; the -> remaining corrections are in §10. +## For the operator — what changed, in plain words ---- +When a customer removes an app and ticks *also delete my data*, the box now deletes the data and lists +what went. If the box cannot work out where the data is, it refuses, keeps the app, and says so in one +sentence. It never again reports success over data left behind. Proven on the HP with a throwaway +Nextcloud. Live on both demo machines. Nothing is waiting on you. -## 1. Confirmed baselines — none had moved +## Claims in the task that turned out wrong or incomplete — named first -| repo | task's baseline | found | -|---|---|---| -| felhom-controller | `998aa319588f` | `998aa319588f` | -| felhom.eu | `bc47dd4ef997` | `bc47dd4ef997` | -| app-catalog-felhom.eu | `8220f8d82e53` | `8220f8d82e53` | -| felhom-agent | — | untouched | +1. **"Three lookups" was right, but the backup half needed the same fix, not only the same honesty.** + Under the global lookup, every `remove_backups: true` on every box was refused — not only paths + "outside the expected directory": with `hddPath == ""` the base was `/backups`, so nothing could + ever be under it. Making that refusal *visible* without fixing its *base* would have shown every + customer a wrong refusal. The base now follows the same per-app rule (the app's namespace root: + its own drive, or `/felhom-data` otherwise), which is exactly what the router's + `AppNamespaceRoot` produced the paths from. **This is one deliberate step beyond "and no more"** + (task §2, "the same honesty treatment and no more"), and the reason is above. +2. **"Deploy a small drive-backed app" — 8 of the 13 `needs_hdd` catalog apps have no `${HDD_PATH}` + bind at all.** They bind only `${USERDATA_PATH}` (the shared library) and named volumes. For them + "delete my data" correctly removes nothing on the drive, and the modal shows no checkbox. The + faithful throwaway was Nextcloud — the app R-442 was measured on. +3. **The task's §11 verifies the deploy on `felhom-pve` 9201 and validates on `demo-hp`.** Both guests + were deployed; the live validation ran on demo-hp as specified. +4. **"NOT ESTABLISHED: whether the fleet shares this config shape"** (the R-442 row) — now + established: demo-felhom also has 0 `hdd_path` lines and 0 `FELHOM_PATHS_*` variables. -Highest `R-` id: **457**, confirmed. Minted **R-458**. +## Baselines (verified before starting) -## 2. Files created and modified - -**Created:** `controller/internal/stacks/pin.go`, `controller/internal/stacks/pin_test.go`, -`controller/internal/sync/render_test.go`. - -**Modified:** `controller/internal/stacks/deploy.go` (the `PinnedImages` field + the deploy writer), -`controller/internal/stacks/manager.go` (`Stack.CatalogImages`, `ScanStacks`, `UpdateStack`), -`controller/internal/sync/sync.go` (the seam + the render table), -`controller/internal/web/updatebadge.go`, `controller/internal/web/updatebadge_test.go`, -`controller/cmd/controller/main.go` (the seam, adoption, and the startup ordering), plus `CHANGELOG.md`, -`CONTEXT.md`, `REUSE.md`, `controller/README.md`. - -## 3. Commits pushed to `main` - -| repo | commit | what | -|---|---|---| -| felhom-controller | **`8a0e0a59adc7`** | v0.235.0 — the pin, the render, adoption, the badge fix | -| felhom-controller | **`2a56f557d048`** | the stored definition must follow a delivered fix (found live) | -| app-catalog-felhom.eu | `dc7e548` / `09b4ff5` | the two live-test pushes | -| app-catalog-felhom.eu | `1798ce6` / `17cc784` / `7b9b9b3` | their reverts, and the CHANGELOG entry recording them as a measurement | -| felhom.eu | `417df06f3529` | the architecture doc, module map, register, roadmap, capability map, STATUS, live evidence | - -No branches. All gate runs passed on push. - -## 4. Tests: 1729 → 1746 (+17). 28 packages, 0 FAIL. - -`go build ./... && go vet ./... && go test ./...` green in the controller module. - -| group | what it pins | -|---|---| -| **A** | a non-image template change reaches a pinned, matching app | -| **B** | an image change freezes it — and the file content is the **stored definition**, with the new template's body (`NEXTCLOUD_TRUSTED_DOMAINS`) asserted **absent** | -| **C** | self-healing from a corrupted file in **both** branches | -| **D** | the update advances the pin, re-renders and stores, all before the pull; and **refuses** when the catalog cannot be read, while leaving an unpinned app alone | -| **E** | adoption skips an incomplete observation and a complete-but-mismatched one, manufactures no applied file, is idempotent, and runs **no docker command at all** | -| **F** | a restore's pin is reported to the syncer with its stored definition (R-441's contract) | -| **G** | the badge reads the catalog, not the rendered file; no catalog entry renders nothing | -| **H** | the AST walk: the seam and adoption are wired, **and their order** against the backfill and `syncer.Start()` | -| — | the render table's remaining rows, the nil seam, and an empty stored definition | - -### All three companion red-proofs — mutation, observed failure, revert - -| # | mutation | observed failure | reverted | +| Repo | `main` before | after | version | |---|---|---|---| -| **1** | `renderSource` returns the catalog template on the moved branch | `TestGroupB` — *"a pinned app must NOT receive the catalog's new version"* | yes | -| **2** | `observationCoversTemplate` guard removed from `AdoptPins` | `TestGroupE/incomplete_observation` — *"pinned 1, want 0 — only 1 of 2 services was observed"* | yes | -| **3** | `compareInstalledToTemplate` reads `TemplateImages` again | `TestGroupG` — *"THE FEATURE IS INVERTED"*, and *„Naprakész"* with no catalog entry | yes | +| felhom-controller | `bab82c471e03` | `42a73e6` (code) + this REPORT commit | v0.235.0 → **v0.236.0** | +| felhom.eu | `d6837d98ee24` | docs commit (see that repo's log) | — | -Full suite re-run green after each revert. +Highest `R-` id before: 464. **No new row minted.** R-442 closed. -**A fourth defect was caught by a test rather than by review:** the syncer trusted the applied path it -was handed and would have written an **empty compose file over a live app**. It now re-reads and falls -back to the catalog. +## Files -## 5. Deployed version +- `controller/internal/stacks/delete.go` — `appHDDPath`, `composeBindsDrive`, `hddPathForRemoval`, + `hddNoteFor`; `RemoveRefusedError` + reasons; the three `m.cfg.Paths.HDDPath` reads replaced; + `[]` init; `hdd_paths_missing`, `hdd_note`, `backup_paths_refused`; backup base via + `appbackup.NamespaceRootFor`. +- `controller/internal/api/router.go` — `errors.As` → 409 + `Message` in `removeStack` and `deleteStack`. +- `controller/internal/web/templates/layout.html` — both success modals render `hdd_note` and + „Nem törölt mentések". +- Tests: `controller/internal/stacks/delete_r442_test.go` (13), `controller/internal/api/remove_r442_test.go` (2). +- Docs: `CHANGELOG.md` (v0.236.0), `CONTEXT.md`, `REUSE.md` (row 120), `controller/README.md` (3 rows). + +## Design rule this brings the removal path into line with + +`07-backup-architecture.md` ~L437, *"[DESIGN] 2026-08-22 — the restore destination is resolved by the +same rule as the capture destination: the drive if the app declares one (`HDD_PATH`), the system data +path otherwise."* Deploy (`withPathVars`), the start gate (`startGatedByMissingDrive`) and the backup +destination (`GetAppDrivePath`) already read the app's own record; removal was the odd one out. +**No fallback to the global** — the helper's comment says why. + +## Tests — 1761 in the module, 15 new, all green; gates all OK + +| Scenario | Test | Asserts (the effect) | Result | +|---|---|---|---| +| A drive app, data removed | `TestRemoveStack_R442_RemovesDeclaredDriveData` | folder gone from a real tree; listed with size; app.yaml gone; compose ran; `appdata/` root intact | PASS | +| B same app, data kept | `…KeepsDataWhenNotAsked` | file present; listed under preserved; `"hdd_paths_removed":[]` on the wire | PASS | +| **C unresolvable** | `…RefusesWhenDriveUnresolvable` | typed error, reason, exact sentence; **compose marker absent**; app.yaml present | PASS | +| **D SSD app** | `…SSDAppIsNotRefused` | no error; non-nil empty list; `[]` on the wire; note; app removed | PASS | +| drive absent | `…RefusesWhenDriveAbsent` | drive-absent sentence naming the path; nothing ran; app kept | PASS | +| keep-data never refused | `…KeepDataNeverRefusedOnHDDGrounds` | drive absent + `false` → removed | PASS | +| folder already gone | `…MissingFolderIsStatedNotRefused` | `hdd_paths_missing`, note names it, app removed | PASS | +| E backup half | `…BackupRefusalReachesResponse` | inside path removed + listed; outside path KEPT + `backup_paths_refused` | PASS | +| E SSD app backups | `…SSDAppBackupsUnderSystemNamespace` | `/felhom-data/backups/…` removable | PASS | +| `${USERDATA_PATH}` convention | `…UserdataConventionFromPerAppPath` | appdata removed, shared `userdata/` untouched; `ExportDataMounts` from the per-app path includes `/userdata` | PASS | +| orphan delete C / A | `TestDeleteStack_R442_*` (2) | same shapes for `DeleteStack` | PASS | +| modal source | `TestGetStackHDDData_R442_ReadsPerAppRecord` | lists the folder while the global stays empty | PASS | +| **production wiring** | `TestRemoveHandler_R442_*` (2) | real handler → real `NewManager` + `ScanStacks` → **409**, exact sentence / drive-absent sentence, app.yaml still on disk | PASS | + +**Red-proof 1 (Scenario C — the pre-fix silent fallback to the global, and nil lists):** 5 tests +failed, with the wrong values visible: +`want *RemoveRefusedError, got err= resp.hdd_paths_removed=[] app.yaml present=false — a 200 over inaction`; +handler: `HTTP 200 ok=true — a 2xx over a removal that could not resolve the data location`; +B: `got {"…","hdd_paths_removed":null,…}`. Fix restored → green. + +**Red-proof 2 (Scenario D — make "declares no drive" refuse):** 2 tests failed: +`an SSD app must not be refused on HDD grounds, got: Az alkalmazás adatainak helye nem állapítható meg…`. +Fix restored → green. `git diff` clean after both. + +Green gate: `go build ./... && go vet ./... && go test ./...` rc=0. `controller_gates.py --fast`: all OK. + +## Deployed + +`gitea.dooplex.hu/admin/felhom-controller:0.236.0` — `Up … (healthy)` on **both** demo guests +(felhom-pve 9201 and demo-hp 9201), verified with `docker ps`. + +## Live validation on demo-hp — endpoint-level (the exact endpoints the UI's modal and buttons invoke: `GET …/hdd-data`, `GET …/backup-data`, `POST …/stop`, `POST …/remove`), driven from inside guest 9201 with the session cookie + `X-CSRF-Token`. Evidence: `felhom.eu/documentation/audits/R442-2026-09-13/`. + +**Scenario A — Nextcloud, deployed through the real deploy endpoint, data written BY THE APP (its +first-run install: 69 files, 63 MB under `/mnt/felhom-drives/hdd_1/appdata/nextcloud`), stopped, +removed with `{"remove_hdd_data":true,"remove_backups":true}`:** ``` -$ ssh hp "pct exec 9201 -- docker ps --filter name=felhom-controller --format '{{.Image}} {{.Status}}'" -gitea.dooplex.hu/admin/felhom-controller:0.235.0 Up 31 minutes (healthy) +{"ok":true,"data":{"removed":"nextcloud","volumes_removed":null, + "hdd_paths_removed":["/mnt/felhom-drives/hdd_1/appdata/nextcloud (63M)"], + "hdd_paths_preserved":[], + "backup_paths_removed":["/mnt/felhom-drives/hdd_1/backups/primary/nextcloud/db-dumps (8.0K)"]}, + "message":"Stack nextcloud removed"} HTTP 200 +ls: cannot access '/mnt/felhom-drives/hdd_1/appdata/nextcloud': No such file or directory +ls /mnt/felhom-drives/hdd_1/appdata → paperless romm +[INFO] Removed HDD data: /mnt/felhom-drives/hdd_1/appdata/nextcloud (63M) +``` +The db-dumps entry was a **planted** fixture file (`r442-fixture.sql`, stated) — no nightly had run +for a 3-minute-old app. The modal's `GET …/hdd-data` beforehand listed the folder with `63M` — under +0.235.0 that endpoint returned nothing for the same app. + +**Scenario C — Nextcloud redeployed, stopped, then `HDD_PATH` deleted from its `app.yaml` (a +test-only edit, backed up and restored), `{"remove_hdd_data":true}`:** + +``` +{"ok":false,"error":"Az alkalmazás adatainak helye nem állapítható meg, ezért semmit nem töröltünk. Az alkalmazás nem lett eltávolítva."} HTTP 409 +state=stopped deployed=True app.yaml present (1302 B) data files after: 69 (before: 69) +[ERROR] [stacks] RemoveStack nextcloud refused: data removal requested, the compose binds a drive path, but app.yaml records no HDD_PATH — nothing removed, app kept (R-442) +``` +`app.yaml` restored → the **same call** returned HTTP 200 with the 63M folder listed and gone +(positive control: the refusal was about the record, not the app). + +**Scenario D — Gokapi (SSD-only: 0 `HDD_PATH` lines in app.yaml, 0 drive binds in compose), +`{"remove_hdd_data":true}`:** + +``` +{"ok":true,"data":{"removed":"gokapi","volumes_removed":null,"hdd_paths_removed":[],"hdd_paths_preserved":[], + "hdd_note":"Az alkalmazás nem tárolt saját adatot külső meghajtón, így ott nem volt mit törölni."},…} HTTP 200 ``` -Previous: `0.234.0`. Fleet after the run: **21 containers up, none unhealthy.** +**ASCII-fragment controls** (`controls.txt`, counted on DooPlex over the saved bodies; `llap` is the +ASCII core of *állapítható*): C body = 2 (the JSON error + the router's ERROR line), A body = 0, +D body = 0 (in-guest count; the DooPlex file shows 1 from my own label line). Positive control that the +grep works: `hdd_paths_removed` = 1 in every file. `HTTP 409` appears only in C. -## 6. Live evidence +Not validated live: Scenario B (kept data) and the drive-absent refusal — unit-tested only; no drive +was unplugged on the demo box. -Full quotes: `felhom.eu/documentation/tests/VALIDATION-update-slice3-2026-09-06.md`. -**Method: endpoint level, plus two REAL catalog pushes travelling the REAL 15-minute cycle** — a -hand-edited file on the box would have proved nothing about a change to the syncer. +## Teardown — three layers -**Adoption:** `9 pinned, 0 already pinned, 0 left unpinned`, multi-service apps pinned per service. +1. **Apps:** both throwaway apps (`nextcloud`, `gokapi`) were removed BY THE FEATURE UNDER TEST; all + nine standing apps still deployed (`bentopdf bookstack calibre-web docmost kimai opengist + paperless-ngx privatebin romm`); `bentopdf` kept. +2. **Residue:** the deploy's recovery-unit capture left `backups/primary/nextcloud/{compose/,manifest.json}` + (not customer data; removal deletes only `db-dumps/` by design) — removed BY HAND, stated here. + `appdata/` holds only `paperless`, `romm` — unchanged. +3. **Helpers:** `/tmp/r442.sh`, `/tmp/r442.pw`, `/tmp/r442-*.json` deleted from guest and host + (count 0). Nothing else was provisioned. The password file travelled file→file and was never printed. -**Scenario A** — catalog `dc7e548` (healthcheck 30s → 45s, no image): -``` -08:01:51 [INFO] [sync] Updated bentopdf/docker-compose.yml -live: image …:v2.8.6 interval: 45s container: v2.8.6, started 03:30:20Z (untouched) -``` +## Rows -**Scenario B** — catalog `09b4ff5` (v2.8.6 → v2.8.5): -``` -08:20:29 [INFO] [sync] Updated bentopdf/docker-compose.yml -catalog: v2.8.5 live: v2.8.6 pin: v2.8.6 -``` -and then `POST /api/stacks/bentopdf/restart`: +- **R-442 → CLOSED** (`CLOSED-ITEMS.md`, top). **Opened: R-465** (the six remaining `Paths.HDDPath` readers — audit), **R-466** (recovery-unit residue after "delete backups"), **R-467** (v0.236.0 owes a golden) — all P3-LOW, owner CC. Register: OPEN 208 → 210, CLOSED 168 → 169. +- `00-capability-map.md` lifecycle row narrowed (remove proved the app, not the data) and re-proven. +- `STATUS.md`: item 13 (plain language) + **item 7 closed** (ruled 2026-09-02, `09` §3). +- Highest `R-` id now 467. -| | before v0.235.0 (spike §2) | now | -|---|---|---| -| elapsed | **18.3 s** | **0.1 s** | -| pulled? | yes, v2.8.5 entered the local store | **no** — `docker images` lists only v2.8.6 | -| container | recreated | **not recreated**, `started` unchanged | -| digest | changed | `sha256:eaeea1e4…`, identical to baseline | +## Observations -**Scenario D** — `POST …/update`, 16.7 s: -``` -08:22:21 update bentopdf: pin advanced to the catalog's current definition (…:v2.8.5) -08:22:38 Stack bentopdf updated successfully (took 16.6s) -container v2.8.5, digest sha256:2d867aac… live/pin/applied all v2.8.5 -``` -The pin advanced **17 s before** the update completed — i.e. before the pull, as the design requires. - -**Scenario G** — quoted from the live page while frozen: -```html -Frissítés elérhető — 56 napja -``` -ASCII fragments (`grep -oF`): `Naprak` **0** on the frozen app page and **8** on the list (the eight -current apps); `napja` **1**; positive control `BentoPDF` 4; negative controls `zzz-never-present` and -`Nem-karbantartott-XYZ` both **0**. - -**The freeze holds in BOTH directions:** with the catalog reverted to v2.8.6 and the app pinned to -v2.8.5, the sync logged `hash match, skipped` and the app stayed on v2.8.5. The pin is what the -customer HAS, not what is newest. - -**Teardown:** a final Update returned the container to `sha256:eaeea1e4…` — **byte-identical to the -pre-run baseline** — with `interval: 30s` restored. Both catalog commits reverted; the catalog tree is -byte-identical to `8220f8d`. **This run provisioned nothing.** - -## 7. R-441 closed by MEASUREMENT - -The restore writer now pins to the unit's captured compose and stores it, so the render obeys the -restored definition. **The closure rests on the render behaviour being measured live** (§6 Scenarios B -and 6b — a pinned app's file surviving a sync that would previously have overwritten it within 15 -minutes), plus `TestGroupF` for the contract the syncer relies on. **What was NOT done: a full live -restore.** That needs a restore rehearsal, which is a phase, not a check — named in §8. - -## 8. NOT yet live-validated — an explicit list - -1. **A live RESTORE setting the pin.** The mechanism is measured; this specific entry point is not. -2. **The adoption skips** — on demo-hp all nine apps were complete and matching, so `0 left unpinned`. - The skip branches are unit-tested with a red-proof; no live app exercised them. -3. **The `no stored definition` render row.** Every pinned app on the box has one. -4. **A multi-service app through a freeze.** bentopdf is single-service; the multi-service half is - covered by adoption (docmost, paperless-ngx, romm pinned per service) and by unit tests. -5. **Any box other than demo-hp.** demo-felhom is still on 0.234.0. - -## 9. Register — 203 open before, 203 after; closed 163 → 167 - -**Closed:** R-447 (slice 3 shipped), R-441 (by measurement), R-438 (both halves discharged), R-455 -(the operator added a Docker Hub PAT). **Opened:** R-458 — `.felhom.yml` keeps flowing to a frozen app -(P3-LOW, CC), with what would settle it by measurement rather than code. All four compressed into -`CLOSED-ITEMS.md`, each naming `bc47dd4ef997`. - -## 10. Every claim in the task that turned out to be wrong, named - -1. **§7 Scenario B is incomplete** — see the box at the top. The stored definition must FOLLOW a - delivered fix, or the first freeze reverts it. Found live, not by review. -2. **§5 says "the syncer must not import the stack manager".** It now imports the `stacks` PACKAGE for - two pure symbols — `RenderPlan` and `ParseComposeImages` — and never touches `Manager` or - `app.yaml`. The alternative was a second compose-image parser, which is the duplication `REUSE.md` - exists to prevent. **The intent is honoured; the letter is not, and the import carries a comment - saying so.** -3. **§2.2 says to call adoption "right after `BackfillInstalledImages()`" and stops there.** That is - necessary but not sufficient: `syncer.Start()` fires an immediate sync, and at its original - position (~L392) that first sync would have run while every app was still unpinned — copying the - catalog over a deployed app **once per boot**. `Start()` was moved to after adoption; the order is - pinned by `TestGroupH`. -4. **All line-number landmarks were accurate** (`AppConfig` ~L100, `ScanStacks` ~L468 with - `TemplateImages` at ~L539/~L550, the syncer wiring ~L389, `RecreateStackDefinitionFromUnit` - ~L2582), and **§3's warning was exactly right** — the badge would have inverted silently, and - red-proof 3 shows it doing so. - -## 11. Observations — noticed, documented, NOT acted on - -1. **The syncer's DEBUG hash line now prints two identical hashes and the word `(changed)`.** - `logFileHashes` re-reads src and dst *after* the copy, so they always match; with the render, `src` - is sometimes the applied file, which makes the oddity conspicuous. Cosmetic, DEBUG-only, and - pre-existing. **NOT-A-FINDING: it misleads no verdict — the `Updated /` INFO line above - it is the real signal, and changing a debug helper inside a release about the syncer would add - unreviewed noise to the one path that touches every app on every box.** -2. **An app pinned before v0.235.0's refresh logic shipped carries a store one fix behind** until its - next delivered fix, which self-corrects it. Observed on bentopdf at 08:20:29. **NOT-A-FINDING: the state converges by itself on the next delivered fix, and the only - alternative — rewriting every stored definition at boot — would touch every app on every box to - correct something that costs nothing.** -3. **The golden is three releases behind** (`0.232.0` vs `0.235.0`), per the push advisory. - **NOT-A-FINDING: the `golden-notice` gate raises it on every release and `STATUS.md` carries the - debt; a register row would duplicate an instrument that already fires.** +1. **`cfg.Paths.HDDPath` still has readers** — `report/builder.go:69`, `monitor/healthcheck.go:35`, + `api/router.go` system-info, `web/server.go:740`, `main.go` (auto-discovery seed + metrics). Each + reads an always-empty value on the fleet; whether any of them is silently inert the way removal was + is worth one short audit. Not touched here. FILED: R-465 +2. **Removal with "delete backups" leaves the recovery unit's `compose/` + `manifest.json`** — the + router passes only `AppDBDumpPath`. Small, pre-existing; a customer who asked for backups gone is + left with a manifest. FILED: R-466 +3. **8 of 13 `needs_hdd` apps bind only `${USERDATA_PATH}`**, so their modal never offers the data + checkbox. Correct by the placement design (`01` — userdata is the shared library), but the label + „Felhasználói adatok a merevlemezen" only ever appears for the 5 that bind `${HDD_PATH}`. NOT-A-FINDING: this IS the `01` placement design — userdata is the customer's shared library, and an app removal deleting it would destroy media other apps and the customer own; the modal correctly offers nothing. +4. `app.yaml` carries `HDD_PATH` twice (`env:` and `locked_fields:`); the test-only edit removed the + `env:` line only, which is the one `appHDDPath` reads — proven by the refusal firing. NOT-A-FINDING: `locked_fields` is a list of field NAMES (which fields are locked after deploy), not a second value; nothing reads a path from it. +5. **v0.236.0 owes a golden** (the golden-notice gate, advisory): newest golden baked is 0.232.0. A bake + vouch is the `RUNBOOK-manual-build.md` §4.1 procedure and is outside this task; until then a fresh install receives 0.232.0 and picks up 0.236.0 by self-update. FILED: R-467