v0.172.0 — R-75: canonical import root, catalog-derived skeleton, import surfaces
${IMPORT_PATH} = <system namespace root>/userdata/import — ONE drop-zone per box,
on the system drive, injected at BOTH compose-env builders with NO per-drive
fallback (unresolvable leaves it unset so compose fails loudly rather than
quietly building a second, dead drop-zone).
Third BindRoot (RootImport) + Import list in BackupSpec, extended through
ValidateBackupSpec/ClassifyBinds. Load-bearing: a stale `userdata: import/<app>`
entry against the moved bind would be a WHOLE-BLOCK reject, taking the app's
mandatory hdd classification with it.
Exhaustive-root audit: resolveAbs/structuralGuard/ComputeCaptureSet/
ComputeFabBuckets now take importRoot explicitly (an import bind resolved
against hddPath would name a directory on the wrong drive); unresolvable is
refused loudly into Skipped. GetImportRoot added to both provider interfaces.
Catalog-derived skeleton: UserdataSkeleton() -> UserdataSkeletonCarry() +
BuildUserdataSkeleton(), SORTED. The carry-list makes zero-removals true by
construction (`documents` is in no catalog app but on both boxes) and is the
fresh-box floor. The sort is not tidiness: the naive map-order derivation
measured 20 distinct outputs from 20 identical runs, which with fbNeedsRecreate
is a fleet-wide FileBrowser restart loop.
One authoritative compose parser: ParseComposeUserdataMounts now delegates to
ParseComposeClassifiableBinds. Import root excluded from per-app migration.
Surfaces: FileBrowser /srv/beolvasas source; app-page "Hova tegyem a fajlokat?"
with PathEscape deep links (never QueryEscape) and class-driven copy;
data_paths: annotation with the Fork-3 asymmetry; system-owned beolvasas SMB
share refused server-side at handler AND store, button omitted in template.
Caught on the way: the sharing template's row struct was function-local, so
adding {{if .System}} would have 500'd every share row. ShareRow is now
package-level and the render test uses the handler's own type.
Tests 915 -> 949, all green. MinAgent unchanged.
This commit is contained in:
@@ -1,5 +1,84 @@
|
||||
## Changelog
|
||||
|
||||
### v0.172.0 — R-75: canonical import root, catalog-derived skeleton, import surfaces (2026-07-26)
|
||||
|
||||
Spike: `felhom.eu/documentation/audits/SPIKE-catalog-data-paths-2026-07-26.md`.
|
||||
|
||||
**The drop-zone is now ONE canonical location on the system drive.** New `${IMPORT_PATH}` =
|
||||
`<system namespace root>/userdata/import`, injected at BOTH compose-env builders
|
||||
(`withUserdataPath` → `withPathVars`, `deploy.go` + `manager.go`) — the initial-deploy path missing
|
||||
`USERDATA_PATH` once bound a bogus root-owned dir at the container root, and `IMPORT_PATH` has the
|
||||
identical failure mode. It is derived from the SYSTEM drive, never from `HDD_PATH`, and has **no
|
||||
per-drive fallback**: an unresolvable root leaves the variable UNSET so compose fails loudly instead
|
||||
of quietly building a second, non-functional drop-zone. *Operator ruling, overriding the spike's
|
||||
Fork-1 recommendation:* each drop-zone app has exactly one ingest bind, so a per-drive `import/`
|
||||
would put a folder that LOOKS like a drop-zone on every drive while only one works — and since
|
||||
import paths are `class: excluded`, files stranded in a dead one are never backed up either.
|
||||
|
||||
**Third `BindRoot` + the whole-block regression it prevents.** `RootImport` / `${IMPORT_PATH}` in
|
||||
`composeVarRoots`, an `Import []BindSpec` list in `BackupSpec`, and `ValidateBackupSpec` /
|
||||
`ClassifyBinds` extended. This is load-bearing: `ValidateBackupSpec` rejects an entry matching no
|
||||
compose bind and the rejection is WHOLE-BLOCK, so moving paperless's ingest bind while leaving
|
||||
`userdata: import/paperless` in place would have discarded the entire block — taking
|
||||
`hdd: appdata/paperless/media class: mandatory` with it and silently degrading the customer's
|
||||
document originals to legacy handling. `TestScenarioB_*` is the gate.
|
||||
|
||||
**Exhaustive-root audit — `resolveAbs` was the sharp one.** An import bind resolved against `hddPath`
|
||||
would name a directory on the WRONG DRIVE. `resolveAbs`, `structuralGuard`, `ComputeCaptureSet` and
|
||||
`ComputeFabBuckets` now take `importRoot` explicitly (compile-forced at all 4 call sites), and an
|
||||
unresolvable root is refused LOUDLY into `Skipped` (`reasonNoImportRoot`) rather than joined onto "".
|
||||
`GetImportRoot()` added to both provider interfaces + both adapters. `fabplan`/`tier2DestRel`/
|
||||
`export.go`/`appbackup_bridge.go` audited and recorded in REPORT.md.
|
||||
|
||||
**Catalog-derived skeleton, deterministic by construction.** `UserdataSkeleton()` →
|
||||
`UserdataSkeletonCarry()` (the v0.171.0 list verbatim, retained forever) + `BuildUserdataSkeleton()`,
|
||||
which merges it with `DeriveUserdataDirs(stacksDir)` and **sorts**. The carry-list makes zero-removals
|
||||
true by construction — `documents` is implied by no catalog app yet exists on both demo boxes — and
|
||||
doubles as the fresh-box floor. The sort is not tidiness: the spike measured the naive map-order
|
||||
derivation at **20 distinct outputs from 20 identical runs**, and `fbNeedsRecreate` force-recreates on
|
||||
any byte difference across ~14 `SyncFileBrowserMounts` call sites — a fleet-wide FileBrowser restart
|
||||
loop. `TestScenarioC_SkeletonDeterminism` pins 20/20. The catalog sync is deliberately **still not**
|
||||
wired to `SyncFileBrowserMounts`. The canonical import root is excluded from per-app migration
|
||||
(`appDataSkipSet`) so it never moves with an app.
|
||||
|
||||
**One authoritative compose parser.** `ParseComposeUserdataMounts` is now a thin resolver over
|
||||
`ParseComposeClassifiableBinds`. The classifier won because it is the richer of the two byte-identical
|
||||
scanners (it keeps the root and the `:ro` flag). One deliberate behaviour drop, recorded not hidden:
|
||||
the old textual replace also accepted a LITERAL absolute path under `userdataPath`; no catalog
|
||||
template has ever used that form and such a compose would be pinned to one machine's drive layout.
|
||||
The deploy belt now handles both roots, gated differently — the drive-absent gate applies to the app's
|
||||
data drive and must NOT suppress a system-drive import dir.
|
||||
|
||||
**Surfaces.** FileBrowser gains a separate `/srv/beolvasas` bind + a „Beolvasás" sidebar source
|
||||
(separate, not nested — a nested source is indexed twice). New app-page block **„Hova tegyem a
|
||||
fájlokat?"** for DEPLOYED apps declaring `data_paths`, with a deep link built from the shipped
|
||||
Quantum router template, `url.PathEscape` per segment (**never `QueryEscape`** — it encodes space as
|
||||
`+`, a literal plus in a path), the system-drive free space on import rows, and a **class-driven**
|
||||
consequence line so the UI can never promise a backup the engines do not make. Copy does not promise
|
||||
one click: a cold deep link goes through the FileBrowser login.
|
||||
|
||||
**`data_paths:` annotation** (`stacks.Metadata.DataPaths`) — role + Hungarian label over paths that
|
||||
must ALREADY exist as compose binds; it can never declare one. Fork-3 asymmetry, deliberate: a
|
||||
malformed PATH is a whole-block reject (data handling; reuses `ValidateBackupSpec`'s refusal set via
|
||||
the extracted `appbackup.ValidateRelPath` — no second validator), an unknown ROLE fails OPEN with one
|
||||
WARN (presentation; the `Lifecycle` precedent). Catalog: paperless-ngx, calibre-web, romm.
|
||||
|
||||
**System-owned import share.** `SMBShare.System`; a `beolvasas` share auto-created when sharing is
|
||||
ENABLED (never before — deploying an app must not put SMB on the household LAN), `Offsite: false`
|
||||
because the data is `class: excluded`. Deletion refused **server-side at both the handler and the
|
||||
store**, and the button omitted in the template — three checks proving different things (the v0.70.1
|
||||
ghost-delete lesson: a render gate is not enforcement, a handler test is not reachability). The share
|
||||
is written directly rather than through `sharingResolvePath`: that guard validates CUSTOMER-supplied
|
||||
picker paths, and the system drive is deliberately not a registered StoragePath.
|
||||
|
||||
**A latent 500 caught on the way:** the sharing template's row struct was function-local, so adding
|
||||
`{{if .System}}` would have failed at render for every share. `ShareRow` is now package-level and the
|
||||
render test constructs the exact type the handler passes.
|
||||
|
||||
Tests 915 → 949, all green. Red-proofs recorded in REPORT.md for Scenario B (classification),
|
||||
C (determinism) and E (server-side share refusal). No destructive filesystem call was added anywhere
|
||||
in this arc. **MinAgent unchanged.**
|
||||
|
||||
### v0.171.0 — Disk-health card: device-model label (pairs with agent v0.95.0) (2026-07-25)
|
||||
|
||||
`agentapi.SmartSummary` gains `ModelName` (mirrors the agent v0.95.0 `model_name`); the "Lemezek
|
||||
|
||||
Reference in New Issue
Block a user