- networkStoragePageData threads NetAddSupport (yes/no/unknown) via the short-budget
netAddSupport probe (2 s ctx + cache — a down agent cannot stall the page)
- storage_network.html: support=no replaces the add form with the Hungarian
alert-warning banner; share list + remove render in every state; yes/unknown
render the form unchanged; load-time JS guarded for the formless render
- T5 render test (banner/no-form on 'no', form on yes/unknown); red-proof RP5 run
(conditional dropped → banner assertions fail) and reverted
- template_id_gate + emoji_gate green
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PSK5g6qYLknKj8u3QAFEr6
Kills the details/summary-button hack and the nonexistent form-row/form-input
classes (the unstyled-look root cause); rebuilds on the storage_attach pattern
(settings-card / form-group / form-control / form-actions / alert-*). SMB listed
first (consumer reality), NFS two-recipe guidance (map-all-users simple recipe +
full-fidelity anonuid=<uid+100000> with a live computed host-id), §3.2 staged
poll progress (Kapcsolódás/Csatolási teszt/Írásteszt/Regisztrálás), categorized
Hungarian errors with a collapsible raw-detail block, orphan rows with an Árva
badge + remove-only action. C8 render smoke guards the class regression.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PSK5g6qYLknKj8u3QAFEr6
Root cause (live on Peti's box): the updater piggybacked on Git Sync creds and refused when absent,
but the registry serves the public package anonymously (verified 2026-07-10). Credentials become what
they were meant to be — optional, for private catalogs only.
- queryRegistry: both creds empty → anonymous flow (plain GET → parse WWW-Authenticate realm/service
from the header, never hardcoded → credential-free token → Bearer retry); creds present → BasicAuth
path unchanged; half-configured pair → loud incomplete-credentials error
- pullImage: no creds → skip docker login entirely (docker's native anonymous flow); denied anonymous
access → clear 'registry denied anonymous access — a private registry requires Git Sync credentials'
- settings page: 'Registry: nyilvános (hitelesítés nélkül) / hitelesített' mode line — credential-less
is no longer an error state; DryRun.PullCapable counts anonymous as capable
- tests: fake registry httptest token dance (zero creds, no auth on token request, correct scope),
creds path unchanged (BasicAuth, no dance), both denial paths, WWW-Authenticate parser table
(quoted/bare/order/comma-in-quotes/missing-realm), fake-runner pull tests (no login invoked
anonymously; login/pull/logout order + stdin token with creds; partial creds refuse)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PSK5g6qYLknKj8u3QAFEr6
resticStep escalates a restic lock error to `unlock --remove-all` + one
retry (safe: single-writer repo — sub-account isolation + single-flight
mutex); plain `unlock` is stale-only and can't clear a crash lock across a
container-hostname change. Pre-run stale unlock hygiene on run+restore.
C1: NewManager flips a persisted LastStatus=running to a truthful error.
Both red-proofed (A reproduces the exact campaign backup failure).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PSK5g6qYLknKj8u3QAFEr6
The QuotaGB hash change triggered a live re-apply that demoted the
escrowed demo to pending and wiped its runtime status. ApplyOffsiteTarget
now carries over EscrowState (custody tracks the preserved repo password,
not the coords) + status fields; fresh guests still land pending.
Red-proofed.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PSK5g6qYLknKj8u3QAFEr6
A hub-side quota raise now reaches the target: the marker hash changes and
the bridge re-applies via key-auth-first (no password consumed). Test:
quota-only change remaps the new quota with a panicking consumer.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PSK5g6qYLknKj8u3QAFEr6
QuotaGB rides the descriptor into OffboxTarget; RepoSizeBytes persisted
from restic stats. Pre-run gate: >=100% refuses NEW backups (Hungarian
notice + operator alert) but prune STILL runs (red-proofed) and restore is
never gated; >=80% warns. /backups usage bar (quota>0 only). The hub
report gains the non-secret offsite status object for the OffsiteChecker.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PSK5g6qYLknKj8u3QAFEr6
EscrowAutoConfirmer flips pending->escrowed ONLY when sha256(local repo
password) matches the ACK's restic_pw_sha256 (blob-presence alone never
confirms — red-proofed). Mismatch warns once per hash naming the ceremony;
never un-confirms; wipes the staged secret on flip. Pinned cross-repo hash
vector; manual confirm deprecated to a legacy-blob fallback.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PSK5g6qYLknKj8u3QAFEr6
Key-auth-first: a KeyAuthProber seam lets the bridge skip consume+install
when the already-installed key still authenticates (pinned to the freshly
verified host key) — descriptor changes on provisioned guests no longer
loop on consume-404. Fingerprint verify still precedes everything.
Wipe-on-escrowed: confirm-escrow now calls the agent's new
DELETE /escrow/stage-secret (v0.78.0) best-effort, closing the hygiene gap
where a ceremony-less confirm left the staged password file behind.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PSK5g6qYLknKj8u3QAFEr6
Live finding: SFTP-mode ssh-copy-id mktemp's under ~/.ssh; the container
image has no /root/.ssh, so the install died locally AFTER the one-time
password was consumed (fail-safe held; password never transmitted).
Probe confirmed the pinned known_hosts + StrictHostKeyChecking=yes chain
is sound against the real box once ~/.ssh exists.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PSK5g6qYLknKj8u3QAFEr6
The SSHCopyIDInstaller used StrictHostKeyChecking=accept-new on the ssh-copy-id
and sftp-verify connections, so even though the bridge verifies the box host-key
fingerprint against the hub descriptor BEFORE installing, the actual install
connection was not pinned to that verified key — a MITM could substitute a
different key in the gap between the scan and the install (TOCTOU).
Now the bridge threads the scanner-verified known_hosts line into KeyInstaller,
which writes it to a temp known_hosts and connects with StrictHostKeyChecking=yes
+ UserKnownHostsFile — the install/verify sessions refuse any key but the one the
bridge already matched. Empty known_hosts now refuses to install.
Test asserts the installer receives the pinned known_hosts; red-proofed by passing
an empty line (the pre-fix TOFU shape) → test fails. Addresses the security-review
"host-key TOFU after verify" finding on internal/offsiteapply/seams.go.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PSK5g6qYLknKj8u3QAFEr6
On startup reconcile the hub-served offsite: descriptor into a key-only offbox
target. internal/offsiteapply.Bridge: verify-pin box host key vs host_fingerprint
(NO blind TOFU) → consume the one-time password (single-use, never logged) →
sshpass ssh-copy-id -s -f install + verify → configure offbox → EscrowState=pending
(fork-4 via Manager.ApplyOffsiteTarget) → persist a descriptor-hash marker LAST.
Idempotent + fail-safe. Seams faked in tests; both red-proofs run+reverted.
Dockerfile + sshpass. NOT yet live-applied (supervised end-to-end next runbook).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PSK5g6qYLknKj8u3QAFEr6
Pairs with agent v0.77.0. StageEscrowSecret pushes the repo password to the
agent (POST /escrow/stage-secret) at offsite-enable → EscrowState="pending".
Atomicity gate: RunOffboxBackup (scheduler + handler) refuses until
EscrowState="escrowed" (operator POST /backup/offbox/confirm-escrow after the
escrow ceremony) — no un-recoverable offsite ciphertext can exist. DR:
POST /backup/offbox/inject-password pre-places a recovered 64-hex password 0600
(honored by WriteOffboxSecrets' IsNotExist guard; refuses clobber without
force). DR recipe gains non-secret offsite_restic coords (DRResticCoord); SFTP
key regenerated at DR, not escrowed. New settings.OffboxTarget.EscrowState.
Tests + atomicity & inject companion red-proofs green; UI gates pass. NOT yet
live-validated (supervised ceremony).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PSK5g6qYLknKj8u3QAFEr6
offbox located each toggled app's recovery unit via AppNamespaceRoot→GetAppDrivePath,
which reads the app's LIVE app.yaml HDD_PATH and silently falls back to systemDataPath
when the app isn't deployed → looked on the wrong drive, backed up nothing, reported
ok/0 (DIAG root cause). Now:
- discoverOffboxUnit/offboxCandidateNSRoots scan the durable storage registry
(schedulable non-decommissioned paths ∪ systemDataPath) for backups/primary/<app>,
independent of deploy state; newest-by-manifest-CreatedAt wins on drive churn.
- RunOffboxBackup: runOffboxInternal returns (backedUp, missing, err); 0-of-N toggled →
hard error + operator alert; partial → ok + new OffboxTarget.LastWarning (shown on
/backups, preserved across config edit).
- AppNamespaceRoot + primary WRITE paths unchanged.
- 6 non-hollow tests (A-E + edge) + both companion red-proofs run (reverted).
- NOT yet live-validated against the Storage Box (spike creds torn down).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PSK5g6qYLknKj8u3QAFEr6
loadAndParse + LoadFromBytes ran os.ExpandEnv over the whole YAML before parse;
a bcrypt hash ($2a$10$...) is full of $word sequences that get replaced with
empty env values, silently corrupting web.password_hash on load (a silent
auth-integrity bug: $2a$10$N9qo8uL... -> "a0"). Remove both ExpandEnv calls;
parse raw bytes. The typed applyEnvOverrides path (FELHOM_WEB_PASSWORD_HASH) is
the sanctioned env mechanism and is unchanged. Tests + red-proof.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PSK5g6qYLknKj8u3QAFEr6
Re-adjudicates F4: /backup/restore, /backup/tier2/restore, /backup/offbox/restore
blocked the HTTP request until completion, so through cloudflared's 100s cap a
customer got an error page while the restore succeeded (offbox worse — bounded
on r.Context(), canceling the SFTP restore mid-flight). Convert all three to the
offboxRun async shape: fast-path IsRunning refuse, background goroutine
(offbox ctx off r.Context() -> Background+30m), instant redirect. Add mutex-
guarded op-status (opstatus.go) + GET /api/backup/restore-status + a 3s-polling
backups.html banner (neutral running, red on failure). Restore single-flight
unchanged. Tests + red-proof (sync handler blocks indefinitely vs <500ms async).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PSK5g6qYLknKj8u3QAFEr6
EjectDisk/Decommission switched from c.post (drops non-2xx body) to
postWithStatus + shared refusalError, so the agent's informative 403 body
("…decommission refused (role: X)") reaches the operator instead of a bare
"HTTP 403" (campaign F2 evidence gap). Generic post + other callers untouched.
Tests T-D1/T-D2/T-D3 + ok:false case; T-D1 red-proof shows the pre-fix bare
"HTTP 403". Bundles the v0.101.0 CHANGELOG entry (this + the F3 sync deadline).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PSK5g6qYLknKj8u3QAFEr6
runGitInDir had no context: a hanging remote parked the sync goroutine in
cmd.Run(), the doSync defer never ran, `syncing` stayed true, and every manual +
periodic sync was refused with "Szinkronizálás már folyamatban" until a
controller restart. Each git command now runs under exec.CommandContext with a
fresh per-command gitCmdTimeout (120s); the deadline error names the timeout and
the (masked) git args. Debounce and failed-sync-arms-debounce unchanged.
Tests: T-C1 cancelled-context kills the subprocess promptly (red-proof: pre-fix
exec.Command shape runs to completion → test FAILS); T-C2 failed sync releases
`syncing` and a post-debounce retry EXECUTES.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PSK5g6qYLknKj8u3QAFEr6
- Endpoint next to /backup/restore; handler mirrors backupRestoreHandler
(ParseForm → validStackName → backupMgr guard → WARN with RemoteAddr →
RestoreTier2Files → flash). Flash strings: "<stack>: N fájl visszaállítva a
másodlagos másolatból." / "Nincs hiányzó fájl — minden fájl megvan a helyén."
/ "Fájl-visszaállítás sikertelen: <err>" (refusals carry the Hungarian
reasons from the engine).
- backups.html: the button on the healthy Tier-2 layer row only (the
Tier2Configured branch already excludes disconnected/inactive; additionally
gated on Tier2LastRun), inline POST form with CSRF + confirm dialog naming
the additive-only semantics and the last-copy timestamp. Template gates
(id + emoji) green.
- Handler guard test (C6): traversal/empty → exact Hungarian flash, no work
started (nil backupMgr would panic if reached).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PSK5g6qYLknKj8u3QAFEr6
Closes the engine half of drill finding F2: user files under appdata/<stack>
had no customer recovery path (operator copy-back only).
- RestoreTier2Files(stack): single-flight with backup/restore; ALL refusals
before any stop (no Tier-2 record / LastRun empty / copy dir absent →
"nincs másodlagos fájlmásolat"; Tier-2 drive disconnected; live drive
disconnected/decommissioned — Hungarian, flash-ready); source is the
RECORDED CrossDriveBackup.DestinationPath (never a fresh selectTier2Target);
stop → copy → start → waitForHealthy; copy/restart errors surface (F17).
- rsyncRestoreMissing: rsyncMirror's exec shape with the OPPOSITE-direction
flags: -a --ignore-existing --itemize-changes — existing live files are
never overwritten, nothing is ever deleted (the --delete trap this task
exists to avoid). Count = ">f" itemize lines (pure countRestoredFiles).
- restoreFilesCopier seam so orchestration tests never shell out; the one
FS-level test of the real rsync is LookPath-guarded (runs on the Linux
build server + live validation).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PSK5g6qYLknKj8u3QAFEr6
The proceed-path for a missing RESETTABLE secret redeployed the app with the
secret blank (compose "Defaulting to a blank string" → exit 1, live-hit in the
2026-07-04 drill Phase 5). Now the restore generates a fresh credential instead:
- stacks.Manager.GenerateSecretForField: replacement value from the field's
catalog generate spec via the deploy flow's generateValue (no logic copied);
refuses data-keys (defense-in-depth), spec-less and non-secret fields.
- backup.Manager.SetSecretGenerator seam (wired in main.go), consulted in
RestoreFromRecoveryUnit AFTER the untouched fail-closed gate, for missing
names NOT in DataKeyEnvVars. The generated value rides fullEnv into
RecreateStackFromUnit → RedeployFromEnv → SaveAppConfig, so it persists
encrypted in the guest app.yaml and round-trips on the next backup/restore
(no second write path). reconcileRestoreSecrets stays pure and untouched.
- WARNs now discriminate: "generated replacement for X (credential was reset)"
vs "X unrecoverable and has no generator — app may fail to start". Values are
never logged (asserted in test).
- Residual case (documented, not pretended away): if a restored volume tar
carries the OLD internal credential hash, the app may still fail auth until a
manual in-DB reset — generation fully fixes only the fresh-init case.
Companion red-proof: pre-fix behaviour (generation skipped) fails
TestRestoreGeneratesMissingResettableSecret on the non-empty DB_PASSWORD
assertion (verified, reverted). Data-key gate proven unreachable by generation
in TestRestoreGenerationNeverReachesDataKeys.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PSK5g6qYLknKj8u3QAFEr6
DumpAppVolumesSafe had NO production caller: no trigger ever produced
volume-dumps/, so named-volume app data (e.g. nextcloud's html volume) was never
captured into the recovery unit and the granular restore silently restored
nothing for class-B data (drill finding F3).
- runVolumeDumps: per-stack loop in runDBDumpsInternal, BEFORE
captureAllRecoveryUnits (so manifests enumerate the fresh tars). Gate order is
load-bearing: protected-stack and volume-check gates precede DumpAppVolumesSafe
(which stops the stack before its own check — unconditional calls would bounce
every volume-less app nightly). Disconnected/decommissioned drives skip with
the same summary style as the DB loop.
- No silent partials: a per-stack failure lands as a FAIL summary entry, flips
Success, and fails the run ("some backup steps failed: ..."), without aborting
the other stacks.
- Zero-DB early return removed: volume-bearing apps without a database still get
their class-B dump + unit refresh.
- dumpVolumesSafe seam (same style as the F17 discoverDBs/importDBDump seams) so
the gating is unit-tested without Docker. Companion red-proof: neutering the
volume gate fails TestRunVolumeDumps_GatesPrecedeDump (dump fired for the
volume-less stack) and _VolumelessNeverStopped (verified, reverted).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PSK5g6qYLknKj8u3QAFEr6
The backups.html restore panel fetched /api/backup/snapshots (a restic-era route
that no longer existed), so the snapshot dropdown never populated and the
"Visszaállítás indítása" button could never enable — customers could not restore
anything from the UI (drill finding F1, DRILL-appdata-restore-2026-07-04).
- backup.Manager.ListRestorePoints: the keep-side restore has exactly ONE restore
point per app (the current recovery unit); time = newest artifact mtime among
manifest/db-dumps/volume-dumps; tier always 1 (Tier-2 copies are NOT restorable
via POST /backup/restore — never listed); drive_label from the storage registry,
empty for the SSD fallback.
- api: /backup/snapshots route + validStackParam guard (same semantics as
web.validStackName; traversal → 400, unknown stack → 404, no unit → ok+[]).
- Tests dispatch through Router.ServeHTTP (the bug WAS a missing route) + unit
tests for newest-mtime/label/empty semantics. Companion red-proof: hollow
always-[] implementation fails TestListRestorePoints_UnitOnDisk +
TestBackupSnapshots_UnitOnDisk (verified, reverted).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PSK5g6qYLknKj8u3QAFEr6
Points to the DRILL audit in felhom.eu. Flags F1 (HIGH): the keep-side
restore UI is dead because /api/backup/snapshots has no handler, so the
"Visszaállítás indítása" button never enables (server-side POST /backup/restore
works). Validation-only run; no code changed.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PSK5g6qYLknKj8u3QAFEr6