diff --git a/CHANGELOG.md b/CHANGELOG.md index 772a730..509c5d9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -27,7 +27,11 @@ reader of the empty `cfg.Paths.HDDPath` already had (R-465). - **R-489 — `volumes_removed` reports the volumes a removal removed**, `[]` when none — never `null`: the project's volumes are listed before and after `down --volumes` and the difference is reported - (compose's progress lines are printed to a TTY it does not have here). + (compose's progress lines are printed to a TTY it does not have here). **Measured limit, live on + 9202 the same night (row kept open):** the listing filters on the compose project LABEL, and a + volume recreated by a unit restore (`docker volume create `, `restore.go`) carries no labels + — compose still removes it, and the response says `[]`. A fresh compose-created volume is + reported. Next release lists by the `_` name prefix as well. - **R-456 — the boot-orphan rule is pinned:** an absent member does not make a stack degraded, so a partly-dead stack is not a boot orphan; a present-but-dead member is (`internal/bootrecon/r456_partly_dead_test.go`). diff --git a/CONTEXT.md b/CONTEXT.md index 16eb32a..04e2fe4 100644 --- a/CONTEXT.md +++ b/CONTEXT.md @@ -7,7 +7,17 @@ > > Ask Claude Code: "Please update CONTEXT.md with what we did today" -Last updated: 2026-09-13 (v0.237.0 — update arc slice 4: the guarded update) +Last updated: 2026-09-13 night (v0.242.0 — R-487 removed-app listing + R-491/R-490/R-489/R-476/R-456) + +> **2026-09-13 night — v0.242.0 (R-487 / R-491 / R-490 / R-489 / R-476 / R-456).** The local backup +> lists are keyed on the DRIVES, not on what is deployed (`ListRemovedAppUnits`, the R-237 rule one +> tier down): a removed app whose unit was kept is listed with its restore, the picker and +> `/api/backup/snapshots` answer for it, and the restore opens the unit where it sits +> (`primaryUnitDirFor`). A removal clears an update hold. `/api/system/info` reaches the API router. +> `volumes_removed` is a before/after difference — **but a volume recreated by a unit restore has no +> compose label and is missed (R-489 open, residual: list by `_` prefix too)**. Tier-2 copy +> date = newest dump (`UnitDataDate`) unless the leg was preserved. **The scratch guest 9202 on demo-hp +> is where releases are validated now** (hand-set image; hub/tunnel off) — `felhom.eu/CONTEXT.md`. > **2026-09-13 — v0.237.0 (slice 4: R-448, R-443, R-439).** Update is a guarded 202 job: > refusals (hold, busy, memory, disk, no restorable Tier-2 copy) → backup-first if the proven copy is diff --git a/REPORT.md b/REPORT.md index 6267e46..c2719e4 100644 --- a/REPORT.md +++ b/REPORT.md @@ -1,33 +1,53 @@ -# REPORT — controller v0.241.0: a bind-data app leans on off-site before its own unit (2026-09-13, evening) +# REPORT — controller v0.242.0: a removed app is listed with its kept backup, and five small ones (2026-09-13 night) *Overwritten each run. This records the most recent implementation only.* -> **Operator ruling R-479, shipped as v0.241.0 and delivered by the managed floor** (declared MinAgent -> 0.129.0): demo-hp +16 s, demo-felhom +18 s. Proven live on demo-hp with a nextcloud throwaway on the -> registered drive. Evidence: `felhom.eu/documentation/audits/v0241-2026-09-13/`. +> **Nightly session under `.claude/rules/unprompted-work.md`.** Architecture read and named: +> `felhom.eu/documentation/architecture/07-backup-architecture.md` (§6.3 restore destination, the +> R-237 store-keyed list rule) and `09-update-architecture.md` (holds). Shipped as **v0.242.0** +> (`d698ce3`), delivered by the managed floor with the declared MinAgent 0.129.0: demo-hp +16 s, +> demo-felhom +17 s; hand-set on the scratch guest 9202 (the one place that is allowed). Evidence: +> `felhom.eu/documentation/audits/v0242-2026-09-14/`. -## What changed (`3e81330`) +## What changed -- `UpdateTierOrderFor` / `DataOutsideUnit`: an app with classified binds walks 2 → 3 → 1; a volume app - keeps 2 → 1 → 3. `UpdateRestorePoints` reads it. -- `RestoreHold.CopyHolds` + `UpdateCopyHolds`: the hold sentence ends with what the chosen copy holds; - the adapter passes it through `HoldAfterFailedUpdateHolding`. Holds from v0.239.0–v0.240.0 keep the - tier-only sentence (`UpdateHoldTierFmt`). +- **R-487 (P2)** — `backup.ListRemovedAppUnits` walks `backups/primary/` on the system path and every + connected registered drive and returns the units whose app is not deployed. The Mentések page lists + them after the deployed rows („Eltávolítva — visszaállítható", one action: *Visszaállítás a + mentésből* = `POST /backup/restore`), the Visszaállítás picker lists them in their own group, + `GET /api/backup/snapshots` answers for them, and `RestoreFromRecoveryUnit` opens the unit where it + sits (`primaryUnitDirFor`) — a unit kept on a data drive was unreachable before. The claim text is + registered with the retrieval-promise gate as conditional on the readable unit. +- **R-491 (P2)** — `removeStack` clears an UPDATE hold (`Settings.ClearUpdateHold`), never an R-379 + restore hold. Logged. +- **R-490** — `/api/system/info` mounted exactly ahead of the web layer's `/api/system/` prefix; + `systemInfo` reads the default storage path like every other reader of the empty global. Nil-safe + on the syncer. **The global's deletion is R-492.** +- **R-489** — `volumes_removed` is the before/after difference of the project's volumes, `[]` when + none. **Measured limit, row kept open:** the listing filters on the compose project label and a + volume recreated by a unit restore (`docker volume create`) carries none — compose removes it, the + response says `[]`. A fresh compose volume is reported. +- **R-476** — `Tier2Coverage.UnitDataDate`: a refreshed leg is dated by its newest dump, a preserved + package keeps the manifest date (R-403). +- **R-456** — the boot-orphan rule pinned by a test; design unchanged. -## Gates, tests, red-proof, live +## Proof -- `go build ./... && go vet ./... && go test ./...` green; `controller_gates.py --fast` green. -- `internal/backup/r479_tier_order_test.go`: order per layout, the consequence (Tier 2 absent, unit - and off-site both fresh → the bind app leans on off-site), the phrases, the sentence verbatim, the - older sentence; the adapter wiring test demands the phrase. -- Red-proof: a layout-blind order fails the bind case (`rp-v241-R479-order-layout-blind.txt`). -- Live (`13-R479-live.txt`): nextcloud with `HDD_PATH` on the drive (2 classified binds), Tier 2 off, - backup now (unit with db-dump and volume tars), a never-healthy update → held, the sentence names - „saját meghajtó, 2026-09-13 21:51 — ez a másolat csak a beállításokat és az adatbázist tartalmazza, - a fájlokat nem." The 2→3→1 preference is unit-proven only: a live off-site run would touch the - standing apps' leg. +- Red-proofs (each a compiling mutation that made its test fail, restored byte-identical): + `rp-v242-*.txt` — R-487 ×6 (lister inert, picker 404, wrong unit dir, row not built, row not + rendered, picker not rendered), R-491, R-490 ×2, R-489, R-476. +- Full gate green before the build (`go build/vet/test ./...`, `controller_gates.py --fast`). +- Live on 9202 (endpoint-level, the exact endpoints the UI invokes; no browser on DooPlex): an + opengist throwaway — `GET /api/system/info` 200 with the drive figures; a seeded update hold + cleared by the removal (log line + store), the app redeployed by the restore without refusal; the + removed app's row, badge, form, picker option and snapshot API all present, „Visszaállítás a + mentésből" reinstalled it running in 9.1 s; two captures under one definition dated the Tier-2 + confirm by the second capture's dump (22:58 against a manifest from 22:52). First pass was refused + 409 (a running app must be stopped first) and repeated; the R-489 cause was then isolated in a + third pass (`19-R489-cause.txt`). -## Observations +## Teardown (three layers) -1. **A removed app keeps its `update_failed` hold in the store; a reinstall under the same name would start held.** FILED: R-491 -2. **The 2→3→1 preference was not exercised live (no off-site run on a box with standing apps).** NOT-A-FINDING: the unit test and red-proof pin it; a live pass belongs to a night whose throwaway has off-site of its own. +Machine: the opengist throwaway removed with data and backups, no volume and no unit left; the +scratch guest 9202 persists on purpose with filebrowser + traefik + the controller. Host: nothing +changed. Hub: floor raised to 0.242.0 (that is the delivery), nothing else touched. diff --git a/controller/README.md b/controller/README.md index 80c132a..832eacb 100644 --- a/controller/README.md +++ b/controller/README.md @@ -3786,7 +3786,7 @@ All daily jobs use Europe/Budapest timezone. Skip-if-running prevents concurrent | GET | `/api/stacks/{name}/logs` | Container logs (`?raw=1` for plain text) | | GET | `/api/stacks/{name}/hdd-data` | HDD data paths + sizes — resolved from the app's OWN `app.yaml` `HDD_PATH` (v0.236.0, R-442), never the global config | | GET | `/api/stacks/{name}/backup-data` | Backup data paths + sizes (DB dumps, cross-drive rsync) | -| POST | `/api/stacks/{name}/remove` | Remove deployed stack (revert to "not deployed"). `remove_hdd_data: true` deletes the app's folders under its recorded `HDD_PATH` and lists them; **409 + a Hungarian sentence when the data was asked for but its location cannot be resolved or the drive is absent — nothing is touched, the app is kept** (v0.236.0, R-442). `hdd_paths_removed` is `[]` for an SSD app (never `null`); `hdd_paths_missing`, `hdd_note`, `backup_paths_refused` state what was not found / not removed. `remove_backups: true` (v0.240.0, R-474) deletes the app's whole recovery unit, its Tier-2 mirror(s) on any registered drive and its backup preferences (`backup_paths_removed` lists them); **without it the backups AND the Tier-2 record are kept, so the removed app can still be restored from the second drive (R-486)**. Off-site snapshots are never touched by removal. Since v0.242.0 a removal also clears the app's update hold (R-491) and `volumes_removed` lists the named volumes actually removed, `[]` when none (R-489) | +| POST | `/api/stacks/{name}/remove` | Remove deployed stack (revert to "not deployed"). `remove_hdd_data: true` deletes the app's folders under its recorded `HDD_PATH` and lists them; **409 + a Hungarian sentence when the data was asked for but its location cannot be resolved or the drive is absent — nothing is touched, the app is kept** (v0.236.0, R-442). `hdd_paths_removed` is `[]` for an SSD app (never `null`); `hdd_paths_missing`, `hdd_note`, `backup_paths_refused` state what was not found / not removed. `remove_backups: true` (v0.240.0, R-474) deletes the app's whole recovery unit, its Tier-2 mirror(s) on any registered drive and its backup preferences (`backup_paths_removed` lists them); **without it the backups AND the Tier-2 record are kept, so the removed app can still be restored from the second drive (R-486)**. Off-site snapshots are never touched by removal. Since v0.242.0 a removal also clears the app's update hold (R-491) and `volumes_removed` lists the named volumes actually removed, `[]` when none (R-489 — **a volume recreated by a unit restore carries no compose label and is not listed yet**, measured 2026-09-13; the row stays open) | | DELETE | `/api/stacks/{name}` | Delete orphaned stack — same R-442 resolution and refusal shape as `/remove` | | POST | `/api/sync` | Trigger catalog sync | | GET | `/api/system/info` | System info + sync status |