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:
2026-07-26 08:12:57 +02:00
parent 3b672ba74c
commit 2958946517
57 changed files with 2228 additions and 151 deletions
+22 -2
View File
@@ -393,8 +393,18 @@ Each app can define rich metadata in `.felhom.yml`:
surfaces it on `/apps/{slug}` as a "Kezdeti belépési adatok" card (masked password + reveal/copy),
labelled as the *initial* credential (stays valid only until the customer changes it in-app). Hidden
when the container is down / file missing / parse fails. Reuse for any future self-seeding app.
- `backup` (v0.132.0): the **referential-coupling classification** block (Task 2). Two optional lists,
`userdata:` (relative to `${USERDATA_PATH}`) and `hdd:` (relative to `${HDD_PATH}`), each of
- `data_paths` (v0.172.0, R-75): the **customer-facing folder annotation**`{path, root, role, label}`
where `root ∈ {import, userdata, hdd}` and `role ∈ {import, library, export}`. It ANNOTATES paths that
must already exist as compose binds and can never declare one (so no new filesystem-write primitive
comes from catalog data). Validation is deliberately ASYMMETRIC: a malformed **path** is a whole-block
reject (reusing `appbackup.ValidateRelPath`, the same refusal set as `backup:`), an unknown **role**
fails OPEN with one WARN (the `Lifecycle` precedent — presentation, not data handling). Rendered on
`/apps/{slug}` as „Hova tegyem a fájlokat?" for DEPLOYED apps only, each row a FileBrowser deep link
plus a **class-driven** consequence line. Full contract:
`felhom.eu/documentation/controller/import-and-data-paths.md`.
- `backup` (v0.132.0; **three** lists since v0.172.0): the **referential-coupling classification** block
(Task 2). Optional lists `userdata:` (relative to `${USERDATA_PATH}`), `hdd:` (relative to
`${HDD_PATH}`) and `import:` (relative to `${IMPORT_PATH}`, R-75), each of
`{path, class}` where `class ∈ {mandatory, optional, excluded}` (COUPLED / DECOUPLED-precious /
DECOUPLED-bulk). `LoadMetadata` validates the block against the app's compose binds and **rejects the
WHOLE block** (→ nil + one `[ERROR]`, app behaves as legacy) on any defect. Semantics
@@ -501,6 +511,16 @@ The `/apps/{slug}` page renders hero section, screenshots, setup guide, and opti
#### Base-infrastructure bring-up (`stacks/infra.go` + `internal/infra/`, v0.41.0)
**Canonical import root (v0.172.0, R-75).** `${IMPORT_PATH}` = `<system namespace root>/userdata/import`
— ONE drop-zone per box, on the always-available system drive, never per data drive (each drop-zone app
has exactly one ingest bind, so a per-drive `import/` would put a dead lookalike on every other drive,
and `import/*` is `class: excluded` so files stranded there are unbacked too). Injected at BOTH
compose-env builders; **no per-drive fallback** — unresolvable leaves it unset so compose fails loudly.
The system drive is deliberately NOT a registered `StoragePath`, so the FileBrowser bind
(`/srv/beolvasas`, sidebar „Beolvasás"), the skeleton and the system-owned `beolvasas` SMB share each
reach it explicitly. The **userdata skeleton is catalog-derived** (`DeriveUserdataDirs` +
`UserdataSkeletonCarry`, sorted — the sort is load-bearing, see REUSE.md) and can only ever ADD.
The controller stands up its own base stack — **traefik** (reverse proxy), **cloudflared** (external tunnel), **filebrowser** — instead of relying on the bare-metal `scripts/docker-setup.sh` (which a Proxmox-provisioned guest never runs). `internal/infra` renders the compose + config files from `controller.yaml` via embedded `text/template`s (lifted from `docker-setup.sh`); image tags are **pinned constants there** (`TraefikImage`/`CloudflaredImage`/`FileBrowserImage`) and the web FileBrowser sync path delegates to the same renderers, so the pinned versions can never diverge.
`Manager.EnsureBaseStack()` creates the `traefik-public` network, then deploys traefik → cloudflared → filebrowser under `${stacks_dir}/<name>`. It is: