SQ6 over-capture FIXED for classified apps: the userdata root tar is exclude-scoped (keeps only
ancestors/descendants of a SELECTED bind relpath — R1-C, the tier2Reconcile keep-rule); no selected
userdata bind → no root tar (radarr state-only). New appbackup.ComputeFabBuckets (shared
resolveGuardCollapse pipeline; class buckets; guards over ALL classes; no cross-bucket containment).
appexport/fabplan.go: computeFabPlan + tarDirectoryExcluding + fabEstimateSplit. ExportRequest gains
DeselectOptional/OptInExcluded (both start handlers — two-call-site); mandatory is a server-side
floor. Manifest v1 + import UNTOUCHED; legacy apps byte-identical to v0.130.0. Estimate additive
class split; export UI: locked-mandatory/optional-checkboxes/excluded-opt-in + two-number warning.
All 6 §10 red-proofs verified. 6D Accept #1 now runs against this shape.
tier2_capture.go: classified apps get TierSecondary per-bind legs (paperless copy shrinks — export
drops); legacy apps keep the byte-identical resolver set. v2 relpath-mirroring layout
(backups/secondary/<stack>/{marker LAST, recovery-unit/, hdd/<rel>/, userdata/<rel>/}); N>1 native
(errTier2MultiDir/tier2AppDataName deleted). Migration=delete-and-rebuild + reconcile; all RemoveAll
via tier2SafeRemove (refuses outside backups/secondary/). SSD=state-only tier. selectTier2Target
never picks network storage (pinned+auto, F-6C-1). Restore reads v2 behind a marker gate.
Part 0: offbox_enlarge_blocked is a persisted one-time Load seed (opt-out sticks), not a getter
append. Part 0.5: offsite restore scratch prefers a local (non-network) path.
Full v2 test suite + all 10 §10 red-proofs verified. Destructive writes bounded to backups/secondary/.
PlaceOffsiteRestore: live target via raw GetStackHDDPath not AppNamespaceRoot (F-3a-1a: no SSD
merge; undeployed refused), placement headroom gate (F-3a-1b), stat pre-pass over all placements
before any copy (F-3a-4: no partial writes), scratch removed on success/kept on failure (F-3a-2).
mapOffsiteRestorePaths refuses the namespace root itself (F-3a-3).
Delivery chain: DefaultEnabledEvents + GetNotificationPrefs append-if-absent migration + settings
checkbox + handler slice; paired with hub v0.55.0 allowlist (no customerMessages entry — raw
dynamic message survives). +8 tests; all 6 controller §10 red-proofs verified.
Task 2 of the backup-classification-redesign arc. Ships the referential-
coupling classification as DATA + PARSER + PURE CLASSIFIER, deliberately
inert — no backup tier changes behavior. Task 3 (tier policy engine) and
Task 4 (manual .fab UI) consume it.
- appbackup/classify.go: BackupSpec/BindSpec/ComposeBind/ClassifiedBind;
ClassifyBinds (SQ5 two-level default — explicit beats :ro; unlisted
writable→mandatory, unlisted :ro→excluded; nil spec→legacy/false);
ValidateBackupSpec (whole-block-reject on any defect, first defect named).
- stacks/classify_binds.go: ParseComposeClassifiableBinds — ${VAR}-relative
binds + :ro flag (NOT ParseComposeHDDMounts/ExportDataMounts, the traps).
- Metadata.Backup + LoadMetadata as the single validation choke point (bad
catalog block → nil + one ERROR → legacy, within one sync cycle).
- Manager.ClassifiedBinds + StackDataProvider.GetStackClassifiedBinds seam
(delegated by stackAdapter, nil-stubbed in every fake) — wired + tested
now so Task 3 consumes a tested seam.
INERT: full pre-existing suite green with zero test-logic edits. +14 tests;
red-proofs RP-1..RP-4 confirmed. The 13 catalog backup: blocks ship in the
same app-catalog change (this controller deploys first).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A45Qop8YY8tS94bz63LFne
Both guests live+healthy on 0.131.0. Live functional legs (paperless
deploy → tier-2 backup → marker restore → storage page → F-S3 migration)
deferred to Viktor's supervised session — paperless-ngx is undeployed on
the demo and the API is container-internal; documented with rationale.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A45Qop8YY8tS94bz63LFne
The controller assumed an app's HDD appdata dir is always appdata/<stackName>.
paperless-ngx writes appdata/paperless (stack paperless-ngx), so every consumer
keying by stack name silently missed it via a stat-and-skip. One canonical
resolver appbackup.AppDataDirNames derives the real dir name(s) from the app's
compose ${HDD_PATH} binds; all consumers use it.
- F-S2 (tier-2): RunTier2 mirrors the resolved appdata/<name> (paperless docs
got NO tier-2 copy before). Tier2Info size + RestoreTier2Files live dir use it.
WARN when a declared appdata dir is absent. New tier2Mirror seam.
- F-S3 (migrate, NEW): all six per-app appdata legs (collision/size/copy/verify/
cleanup/skip-set) now loop resolved names. scope="app" migration of paperless
previously copied nothing and left an empty media dir (scope="all" was saved by
the merge walk). WARN on missing declared dir in the copy leg.
- Multi-dir (N>1) refusal: tier-2 backup/info/restore refuse loudly (Hungarian);
migrate supports N. No catalog app hits it today; lifted by Task 3.
- Display: storage page sums resolved dirs.
- Truth repair: the v0.130.0 "tier-2 copies the namespace wholesale" claim is
false; corrected in CHANGELOG + main.go export-adapter comment.
+9 tests; red-proofs RP-1..RP-5 all confirmed. Controller-only, no agent/hub
coupling. Task 1 of the backup-classification-redesign arc.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A45Qop8YY8tS94bz63LFne
handleNetStorageRemove now refuses (409, Hungarian, names the apps) when any
DEPLOYED stack's HDD_PATH is the share root or a subpath of it — the C6B live
event removed campaign6 under a running sonarr, and the agent's tolerated
best-effort stop steps then deleted the unit files under the busy mount,
leaving an unreapable orphaned autofs mount until host reboot. The guard cuts
that chain off at the product flow. The remove handler resolves the agent via
the netAgent seam (netAgentForAdd), making the negative control testable.
NOTE: the agent-side residual (tolerate-and-continue stop in felhom-agent
netmount.go RemoveNetworkMount) is out of this controller-only task's scope —
flagged in REPORT for a follow-up agent task. Red-proof recorded: disabling
the guard returns the live pre-fix removed:true.
assertBundleDataComplete's claimed-tar checks pass trivially when discovery
claims nothing (0 claims -> 0 checks) — the exact blind spot that shipped a
4.17 GB app as a 2308-byte config-only bundle. A needs_hdd manifest with
neither HDD data nor volume data now fails the job with an honest Hungarian
error. Red-proof recorded: removing the assertion returns scenario D to
silent hollow success.
executeExport no longer either/or gates user data on needs_hdd — a needs_hdd
app bundles BOTH its HDD mounts and its named volumes (sonarr_config = the
whole app DB was silently dropped pre-fix). exportHDDData returns error and
fails LOUDLY on a basename collision between mounts (the manifest keys tars by
basename; the old code silently overwrote the first tar). EstimateExport made
additive to match, so the fits-on-dest gate counts both. Round-trip placement
test proves a userdata tar restores to <HDD_PATH>/userdata through the
untouched import mapping. Red-proofs recorded: either/or revert fails
scenario A; collision-check removal fails the collision test.
exportAdapter.GetStackHDDMounts now returns stacks.ExportDataMounts — the
${HDD_PATH} binds UNIONed with the ${USERDATA_PATH} ROOT (captured at the
root, not per-bind, so the manifest's basename keying round-trips through the
existing import mapping without touching restore). Containment-aware dedupe
both directions. The backup-side stackAdapter is intentionally unchanged.
Red-proof: pre-fix behavior fails TestExportDataMounts_UserdataConvention/
MixedBindsUnion/LiteralUserdataBindDeduped (run->fail->revert recorded).
Only Observe-sourced drives ever carried class, so legacy PVE dir:-backed USB
drives showed 'lassú' while registry-sourced ones never did. The card already
carries the USB type tag. Hub-report ClassHint UNCHANGED (UI-only). Pinned by
TestStorageTemplate_USBClassBadgeSuppressed (red-proof run).
- offboxWarningDisplay(lastWarning, toggledCount) — pure DISPLAY pick, no state mutation:
a persisted 'nincs mentésre jelölt alkalmazás' run-result is replaced by
'A kijelölés módosult az utolsó futás óta — a következő távoli mentés már tartalmazza.'
once ≥1 app is toggled; 0 toggled keeps the v0.123.0 honesty verbatim; every other
warning (quota, partial) passes through untouched
- replacement renders NEUTRAL (exception-color principle: reassurance, not deviation);
the untouched original keeps the warn color
- unit + render tests; red-proven (pick removed → 1-enabled case fails at both levels)
- scans all templates + Go sources (291 files) for the double-encoding signature chars
(Ã Â Ă ă ˘ ˇ) + strict-UTF-8 decode; allowlist ZERO by design; Python per the
multibyte rule (this task exists partly because of a bash-era mojibake sweep)
- byte-level sweep result: the committed source contains NO mojibake — the 'Tárhely'-class
text on the live import page is the felhom-usb DRIVE-LABEL DATA (settings.json on the
demo box; default-label pattern settings.go GenerateLabel), repaired via the label-edit
UI in the live-validation step, not by code
- red-proven: reintroduced 'Tárhely' into app_import.html → exit 1 naming file:line; reverted → 0
- backups_restore: placeholder 'Opcionális jelszó' + helper under the field
('Üresen hagyva a csomag titkosítás nélkül készül.') — the long clipped placeholder is gone
- app_import: the bundle-password input picks up the standard .form-input class
(was a bare browser-default input)
- inframeta.go: static display-only map (name/description/Linked) + infraMeta template func;
filebrowser is the ONLY Linked stack (files.<domain> Megnyitás on the dashboard row;
the stacks card already links via Subdomains)
- generic infra icon: embedded /static/infra-logo.svg (Lucide-style server, monochrome)
wired through the app-row FallbackIcon slot + the stacks-card onerror chain
- dashboard rows + app cards: infra name + description + existing Védett chip
- guarded WRONG outcome: no customer link for cloudflared/traefik — render tests count
exactly ONE https:// link; red-proven (Linked:true on cloudflared → test FAILS)