docs(R-7b): share backup EXECUTION shipped (controller v0.145.0, Model B')

- capability map: SMB row KNOWN GAP cleared -> share data rides both tiers; the
  offsite leg + restore round-trip flagged as not-yet-live-exercised
- ROADMAP R-7b: idea -> SHIPPED, with the Model B' rationale and the live evidence
- controller/sharing.md: the KNOWN GAP block replaced by the execution contract;
  operator note corrected — samba IS liveness-monitored since v0.145.0
This commit is contained in:
2026-07-18 13:35:00 +02:00
parent b76dad0c4e
commit 4e3f6c48d4
3 changed files with 48 additions and 13 deletions
+46 -11
View File
@@ -142,21 +142,56 @@ The new-folder flow uses a separate, strictly tighter check (`sharingResolveStor
`smb.conf` and the passdb are config, not customer data, and are never classified.
> **KNOWN GAP — share data is classified but not yet backed up.** Making this seam correct does not by
> itself put share data into a live run: `backup.RunTier2` short-circuits on a missing *recovery unit*
> before it ever calls `GetStackClassifiedBinds`, and the offsite runner enumerates
> `settings.GetOffboxApps()`. Both engines are recovery-unit shaped, which a share-only infra stack
> has not. Teaching them about one is a structural change, so it was reported as a design fork rather
> than improvised inside the engines. **Until it is resolved, do not tell a customer that files
> dropped on a share are backed up.** See `felhom-controller/REPORT.md` §4.
## Backup EXECUTION — the sibling shares source (R-7b, controller v0.145.0)
The classification above is now *executed*. It did not get there through `ClassifiedBinds` — the
engines stayed recovery-unit shaped and Model B deliberately left every per-app path byte-identical.
Instead `internal/backup` runs a **sibling shares source** off the same registry:
| Tier | Entry point | What it writes |
|---|---|---|
| 2 (cross-drive) | `RunSharesTier2`, after the per-stack loop in the same run | `backups/secondary/_shares/<sourceDriveKey>/<share>` per source drive, `_payload/`, `.felhom-tier2-layout` written **LAST** |
| 3 (offsite) | `runOffboxSharesLeg`, after the per-app loop and **before** retention | ONE `restic backup --tag felhom-offbox --tag _shares` = payload dir + every MANDATORY share |
| restore | „Megosztások" on `/backups/restore` | scratch → missing-only merge, each destination prefix-asserted against LIVE storage roots |
**The payload is the point.** `_shares-manifest.json` carries the share *definitions* (sorted →
byte-deterministic, so an unchanged registry gives the mirror nothing to rewrite) and `passdb.tar`
carries the household credential, best-effort. Without it a restore hands the customer their files
back and an empty „Megosztás" page. The credential copy is **secret-bearing**: 0600, encrypted inside
restic, never logged at INFO and never in a report or a committed file.
**Degradation contract.** A quota-blocked offsite push falls back to the **manifest only, never to
nothing** — definitions protection must not regress because the files stopped fitting.
**Restore semantics.** Files merge missing-only (a live file is never overwritten). Definitions merge
**existing-wins**: a restore must never silently flip a live share's read-only or „Felhőmentés"
setting, and anything skipped is named in the flash. Then `ReconcileSamba` re-renders `smb.conf`, then
the credential goes back into the named volume.
**`_shares` is a reserved key** (restic tag, dest root, status record). `ValidateSMBShareName` refuses
a leading underscore — note this was a REAL gap, not a formality: `nbNameRe` begins with
`[A-Za-z0-9_]`, so „_shares" was previously an accepted share name. The key never reaches a customer
surface; `backup.DisplayStackName` maps it to „Megosztások" at the notification and prose boundaries,
while the persisted set, the tag and the paths keep the raw key.
> **Live-validation status (2026-07-18).** The tier-2 leg is PROVEN-LIVE on the demo box through the
> real `/api/backup/tier2` endpoint (tree + marker + payload on the cross-drive target, mirrored file
> md5-identical, payload 0600 preserved). **The offsite `_shares` leg and the restore round-trip are
> unit-covered and red-proofed but NOT yet live-exercised** — the demo box has no offsite target
> configured. They need a box with an escrowed offsite repo.
## Operator notes
- samba is protected in **code** (`config.alwaysProtectedStacks`), because `cfg.Stacks.Protected` comes
from the golden-generated `controller.yaml` and predates the feature. Consequence:
`monitor.EffectiveProtected` does **not** monitor samba liveness — a dead samba container raises no
"protected container missing" issue. Deliberate for slice 1 (the safe direction: no false alarms
while the feature is off).
from the golden-generated `controller.yaml` and predates the feature. **Since v0.145.0 (R-7b) it is
also monitored for liveness:** `monitor.EffectiveProtected` takes the SMB settings and adds the
container `infra.SambaContainerName` exactly while sharing is ON, so a dead sharing service raises
the standard protected-container issue → `health_critical` → alert → Hungarian degradation e-mail,
while a box that never enabled sharing stays quiet (both directions unit-tested and red-proofed).
Note the CONTAINER name (`felhom-samba`) is deliberately not the stack name (`samba`) — the health
check docker-inspects container names. PROVEN-LIVE 2026-07-18: stopping the container produced
`Protected container not running: felhom-samba`, status `fail`, and a hub-accepted `health_critical`
event; the next tick's `EnsureBaseStack` self-healed it.
- Discovery depends on the household LAN. The R-6 spike's §7d caveat applies: a customer network with
IGMP snooping enabled and **no querier** could prune the discovery multicast, and a BYO topology may
put the guest on a bridge with no L2 path to the household LAN. The page carries an honest hint that