Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
12 KiB
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.
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.
Claims in the task that turned out wrong or incomplete — named first
- "Three lookups" was right, but the backup half needed the same fix, not only the same honesty.
Under the global lookup, every
remove_backups: trueon every box was refused — not only paths "outside the expected directory": withhddPath == ""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<system data>/felhom-dataotherwise), which is exactly what the router'sAppNamespaceRootproduced 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. - "Deploy a small drive-backed app" — 8 of the 13
needs_hddcatalog 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. - The task's §11 verifies the deploy on
felhom-pve9201 and validates ondemo-hp. Both guests were deployed; the live validation ran on demo-hp as specified. - "NOT ESTABLISHED: whether the fleet shares this config shape" (the R-442 row) — now
established: demo-felhom also has 0
hdd_pathlines and 0FELHOM_PATHS_*variables.
Baselines (verified before starting)
| Repo | main before |
after | version |
|---|---|---|---|
| felhom-controller | bab82c471e03 |
42a73e6 (code) + this REPORT commit |
v0.235.0 → v0.236.0 |
| felhom.eu | d6837d98ee24 |
docs commit (see that repo's log) | — |
Highest R- id before: 464. No new row minted. R-442 closed.
Files
controller/internal/stacks/delete.go—appHDDPath,composeBindsDrive,hddPathForRemoval,hddNoteFor;RemoveRefusedError+ reasons; the threem.cfg.Paths.HDDPathreads replaced;[]init;hdd_paths_missing,hdd_note,backup_paths_refused; backup base viaappbackup.NamespaceRootFor.controller/internal/api/router.go—errors.As→ 409 +MessageinremoveStackanddeleteStack.controller/internal/web/templates/layout.html— both success modals renderhdd_noteand „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 |
<sys>/felhom-data/backups/… removable |
PASS |
${USERDATA_PATH} convention |
…UserdataConventionFromPerAppPath |
appdata removed, shared userdata/ untouched; ExportDataMounts from the per-app path includes <hdd>/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=<nil> 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}:
{"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
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.
Not validated live: Scenario B (kept data) and the drive-absent refusal — unit-tested only; no drive was unplugged on the demo box.
Teardown — three layers
- 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);bentopdfkept. - Residue: the deploy's recovery-unit capture left
backups/primary/nextcloud/{compose/,manifest.json}(not customer data; removal deletes onlydb-dumps/by design) — removed BY HAND, stated here.appdata/holds onlypaperless,romm— unchanged. - Helpers:
/tmp/r442.sh,/tmp/r442.pw,/tmp/r442-*.jsondeleted from guest and host (count 0). Nothing else was provisioned. The password file travelled file→file and was never printed.
Rows
- R-442 → CLOSED (
CLOSED-ITEMS.md, top). Opened: R-465 (the six remainingPaths.HDDPathreaders — 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.mdlifecycle 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.
Observations
cfg.Paths.HDDPathstill has readers —report/builder.go:69,monitor/healthcheck.go:35,api/router.gosystem-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- Removal with "delete backups" leaves the recovery unit's
compose/+manifest.json— the router passes onlyAppDBDumpPath. Small, pre-existing; a customer who asked for backups gone is left with a manifest. FILED: R-466 - 8 of 13
needs_hddapps 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 the01placement 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. app.yamlcarriesHDD_PATHtwice (env:andlocked_fields:); the test-only edit removed theenv:line only, which is the oneappHDDPathreads — proven by the refusal firing. NOT-A-FINDING:locked_fieldsis a list of field NAMES (which fields are locked after deploy), not a second value; nothing reads a path from it.- 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