Commit Graph

383 Commits

Author SHA1 Message Date
admin d9da5a0678 docs: v0.114.0 CHANGELOG (MinAgent header convention) + README NAS gate + CONTEXT + REUSE + coupled-feature rule in CLAUDE.md
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PSK5g6qYLknKj8u3QAFEr6
2026-07-11 13:54:09 +02:00
admin 4a5c40b7cb test: fix TestBackupTier2Restore_DoubleClickRefused scheduling flake
IsRunning flips before the restore goroutine parks in StopStack; under parallel
load the ==1 assertion raced it and read 0. Wait for the first stop to land, then
assert — the red-proof property (no-fast-path mutant → count 2) is preserved.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PSK5g6qYLknKj8u3QAFEr6
2026-07-11 13:50:35 +02:00
admin 50da7329ac feat(ui): agent-outdated banner on the NAS settings page (capability gate, Part 3)
- 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
2026-07-11 13:49:29 +02:00
admin d347dc48d2 feat: agent-capability gate for coupled features — typed StatusError + Supports probe/cache + netstorage add gate (option-1)
- agentapi: non-2xx GETs now surface as typed *StatusError (same text); features.go
  adds Feature/SupportState/SupportCache (route probe, TTL 5m, Yes/No cached,
  Unknown never cached or refused) + Client.Supports
- web: handleNetStorageAdd refuses up front (412, code agent_outdated, Hungarian
  message) when the agent predates /netstorage/verify-status (= pre-0.81 add
  semantics); gate runs BEFORE the single-flight claim; SupportUnknown passes
  through to the existing agent-error paths
- netAddSupport page-render helper lands here; its template consumer follows
- tests: T1 gate refusal (job never starts, slot free), T2 unchanged happy path +
  warm-cache negative assertion, T3 indeterminate never 'too old', T4
  classification incl. the string-match trap, T6 TTL, wire-level 404-typing;
  red-proofs RP1-RP4 run and reverted

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PSK5g6qYLknKj8u3QAFEr6
2026-07-11 13:45:49 +02:00
admin 5d06ecf374 docs: CHANGELOG v0.113.0 + REPORT + CONTEXT + README NAS section + REUSE seams
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PSK5g6qYLknKj8u3QAFEr6
2026-07-11 10:31:07 +02:00
admin a65dcff85a storage_network: full redesign on the canonical form pattern + protocol-honest guidance (v0.113.0 UI)
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
2026-07-11 10:05:08 +02:00
admin bb8737a81f netstorage: verify-before-commit orchestration — agentapi verify fields, uid-1000 re-exec probe, detached add job, orphan rows
Controller half of the verify pipeline (SPIKE-nas-verify b57f6c1): AddNetStorage
gains verify/job fields + typed NetAddRefusedError; NetVerifyStatus polls the
agent slot; --netprobe hidden re-exec mode (SysProcAttr.Credential uid/gid 1000,
no shell) proves in-guest writability; the add handler starts a detached
single-flight job (agent_add → verifying → probing → registering LAST) with full
rollback on any failure incl. verify-lost-after-restart (Scenario F); §3.2
Hungarian error map server-side; live-but-unregistered shares surface as remove-
only 'Árva megosztás' rows.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PSK5g6qYLknKj8u3QAFEr6
2026-07-11 09:59:33 +02:00
admin d910edd4ad docs: v0.112.0 CHANGELOG + README (registry access modes) + REUSE (runner vars seam, WWW-Authenticate parser)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PSK5g6qYLknKj8u3QAFEr6
2026-07-10 17:53:51 +02:00
admin b25ca60ab7 v0.112.0: self-update without credentials — anonymous Docker v2 token flow
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
2026-07-10 17:52:03 +02:00
admin 30927b6816 docs: v0.111.0 CHANGELOG + README (telemetry context/tails) + REUSE entries
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PSK5g6qYLknKj8u3QAFEr6
2026-07-10 15:45:45 +02:00
admin 544c42a618 v0.111.0: remote app-log diagnostics — error context capture (±5 lines, capped+redacted) + on-demand log tails via ACK pull pattern
- metrics: LogIssue.Context on first-occurrence errors (≤11 lines, ≤400 chars/line, warns carry none); RedactLine sanitizer (password/token/api-key/bearer/64-hex) applied to everything shipped; FetchContainerLogTail
- report: 16KB per-report context budget (lowest-count issues dropped first); log_tail_requests ACK flag → next report ships log_tails (200 lines, ≤64KB/app head-truncated, ordered, redacted); consume-once drain
- tests: synthetic-window context capture, caps, redaction, budget order, consume-once, fetch-error skip

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PSK5g6qYLknKj8u3QAFEr6
2026-07-10 15:43:29 +02:00
admin cb9e992c7a v0.110.0: offbox stale-lock self-heal (campaign C2) + crash-truthful status (C1)
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
2026-07-10 07:02:23 +02:00
admin 0bd4cd02be docs: v0.109.1 README + REPORT (SLICE 4 deployed + live-verified)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PSK5g6qYLknKj8u3QAFEr6
2026-07-10 00:24:57 +02:00
admin ddac21cd91 v0.109.1: re-apply preserves escrow custody + runtime status (live finding)
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
2026-07-10 00:06:06 +02:00
admin f830325ca3 offsiteapply: include QuotaGB in the descriptor hash — quota changes re-apply
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
2026-07-09 23:58:30 +02:00
admin 8917014991 v0.109.0: soft-quota gate + usage bar + offsite report status (SLICE 4)
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
2026-07-09 23:48:42 +02:00
admin 994e15896f docs: v0.108.0 README (auto-confirm) + REPORT (deployed, Scenario E live)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PSK5g6qYLknKj8u3QAFEr6
2026-07-09 23:24:38 +02:00
admin febf6757dc v0.108.0: hub-verified escrow auto-confirm on current-password hash match (SLICE 3)
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
2026-07-09 23:19:17 +02:00
admin fec2e8fd84 docs: v0.107.0 README (key-auth-first + wipe) + REPORT (deployed, one-off cleanup done)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PSK5g6qYLknKj8u3QAFEr6
2026-07-09 22:46:25 +02:00
admin a38c743926 v0.107.0: key-auth-first bridge + staged-secret wipe on escrow confirm
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
2026-07-09 22:26:53 +02:00
admin 42af088308 readme: document the offsite provisioning apply-bridge (SLICE 2, v0.106.x)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PSK5g6qYLknKj8u3QAFEr6
2026-07-09 22:07:28 +02:00
admin 0c4a4ec02c v0.106.1: ssh-copy-id -s requires ~/.ssh locally — create it in the installer
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
2026-07-09 21:29:38 +02:00
admin 9a34887acc harden offsite apply-bridge: pin verified host key on the install/verify sessions (no TOFU)
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
2026-07-09 19:24:17 +02:00
admin aa61fb3411 v0.106.0: offsite provisioning SLICE 2 — controller apply-bridge
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
2026-07-09 19:14:14 +02:00
admin 0b09a799cb v0.105.0: fork-4 offsite password custody — hand-off + atomicity gate + DR inject + coord
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
2026-07-09 15:13:59 +02:00
admin 908e4b906a v0.104.0: off-box unit discovery (durable, deployment-independent) + no-silent-success
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
2026-07-09 12:32:05 +02:00
admin 02d37a110b fix(config): F-C2-1 — stop os.ExpandEnv corrupting the bcrypt password_hash (v0.103.0)
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
2026-07-07 18:34:52 +02:00
admin c529a455af feat(backup): async restore family — no proxy-timeout error page on a succeeding restore (v0.102.0)
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
2026-07-06 20:23:49 +02:00
admin c997d79246 feat(agentapi): surface agent disk-op refusal reasons; v0.101.0 + CHANGELOG/REPORT
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
2026-07-06 14:11:44 +02:00
admin fc28033fe8 fix(sync): 120s deadline on git subprocesses — hung remote no longer wedges sync (campaign F3)
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
2026-07-06 14:05:12 +02:00
admin 85a5727123 docs: v0.100.0 — CHANGELOG/CONTEXT/REUSE/README for the Tier-2 in-place file restore (F2 closed)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PSK5g6qYLknKj8u3QAFEr6
2026-07-05 13:23:03 +02:00
admin 27aeb415f4 feat(web): C2 Parts 2+3 — POST /backup/tier2/restore + "Fájlok visszaállítása" button
- 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
2026-07-05 13:20:56 +02:00
admin 30b11100e0 feat(backup): C2 Part 1 — RestoreTier2Files: in-place, additive-only class-C file restore engine
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
2026-07-05 13:17:40 +02:00
admin 88362dac0a docs: v0.99.0 — CHANGELOG/CONTEXT/REUSE/README for the restore-path fixes (F1/F3/O4)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PSK5g6qYLknKj8u3QAFEr6
2026-07-05 11:56:48 +02:00
admin a52851e79e fix(backup): O4 — generate a replacement for unrecoverable resettable secrets on restore
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
2026-07-05 11:52:04 +02:00
admin 73378a812c fix(backup): F3 — wire named-volume dumps into the app-data backup run
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
2026-07-05 11:46:37 +02:00
admin f413f9539d fix(api): F1 — register GET /api/backup/snapshots so the restore panel can populate
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
2026-07-05 11:42:39 +02:00
admin d0c70a7da6 ux(storage): hide 'Eltávolítás a listából' on wizard-enrolled drives
List-removal only deletes the registry entry — it's the undo of a
manual path add. On an enrolled drive (/mnt/felhom-drives/) it leaves a
de-registered-but-still-bound limbo nobody wants; the real lifecycle
there is Biztonságos leválasztás / Végleges leszerelés. New
StoragePathView.IsEnrolled gates the button; manual paths keep it; the
decommissioned-branch removal (final cleanup) is unchanged. Endpoint
untouched.
2026-07-02 21:29:14 +02:00
admin d256166fdd ux(storage): dedupe Leválasztás + self-documenting drive action labels
Two Leválasztás buttons rendered per USB drive (registry safe-disconnect
+ the agent eject the D1 merge carried over like-for-like) and four
near-synonymous labels covered very different operations. The agent
eject now hides when the card offers the registry safe-disconnect (one
detach per card; non-USB + unregistered drives keep eject). Labels are
self-documenting with explanatory title tooltips: Új telepítések
letiltása/engedélyezése, Biztonságos leválasztás, Eltávolítás a
listából, Végleges leszerelés, Formázás…. Endpoints and semantics
unchanged.
2026-07-02 20:36:48 +02:00
admin 611c5ffbdf fix(storage): spacing between the enrichment tag row and action rows
The .drive-agent-extra slot stacked the agent tag row and the agent
action row with no vertical gap (and sat flush against the registry
action row). The slot is now a flex column with a .6rem gap + top
margin, hidden when empty; enrichCard's inline margin removed.
2026-07-02 20:25:49 +02:00
admin 1b954e9217 docs: CHANGELOG v0.98.0 + REPORT + CONTEXT + README (Tárhely subpage split) 2026-07-02 19:37:42 +02:00
admin d3c97c62fe storage IA: split /storage into Meghajtók + /storage/network (NAS)
User feedback on D1: the NAS-add button and the local-drive enrollment
buttons sat side by side with no separation — two different storage
classes interleaved on one page. Now two subpages under Tárhely:

- /storage — Tárhely — Meghajtók: physical drive registry, migrate,
  wizard entry points (Új meghajtó inicializálása / Meglévő meghajtó
  csatolása), unified agent view, manual add. The enrollment buttons
  now live unambiguously in the local-drive context.
- /storage/network — Tárhely — Hálózati tárhely (NAS): the NAS share
  list (NAS-megosztások) + add form + its JS (moved verbatim, incl.
  its own openDialog copy for the remove overlay).
- layout.html: Tárhely main-nav item gains two always-visible nested
  sub-links (Meghajtók / Hálózati tárhely, .nav-links-nested CSS);
  parent stays highlighted on both.
- handlers.go: NetworkStoragePaths moves out of storagePageData into
  the new networkStoragePageData (page key storage-network) +
  storageNetworkPageHandler; GET /storage/network route.
- Tests: /storage must NOT render the NAS section, /storage/network
  renders it and nothing drive-related; inventory + no-native-confirm
  scans cover the new template. Both template gates green.
2026-07-02 19:34:27 +02:00
admin c427b40b2f docs: CHANGELOG v0.97.0 + REPORT (TASK-D1) + CONTEXT + README page-IA section 2026-07-02 19:26:07 +02:00
admin 622d9328f8 D1 Part 3 leftovers: remove 8 residual emoji + Python emoji gate
- The D0 grep gate false-negatived multibyte emoji on Windows (its zero
  was wrong). scripts/emoji_gate.py scans by Unicode codepoint; it found
  8 survivors: backups.html (📁→file-text icon, 🔄 restore-info text),
  debug.html (🔄→'fut', 🔒→'titkosított'), deploy.html (📦→upload icon,
  ★ default-marker → '(alapértelmezett)'), storage.html (📦→upload icon,
  📦 in the migrate-target option → plain text). All → sprite icons or
  plain words.
- Gates: scripts/emoji_gate.py = 0; new Go TestNoEmojiInTemplates
  codepoint scan keeps it enforced (allowlist: ✓✗✔✘•●○■▶ monochrome
  text marks). go build/vet/test ./... green (18 pkgs).
2026-07-02 19:19:23 +02:00
admin cb6f04c8fc D1 Part 3: unified drive view + overlay migration
- storage.html: the registry cards and the separate 'Meghajtók (ügynök
  nézet)' merge into ONE view. Each connected registry card gets an
  #agent-extra-<path> slot; the enrichment JS joins the agent /api/disks
  list on mount path and decorates the matching card in place (role tag
  via i-lock, drive class, durable-id mono line, agent-only
  register/eject/wipe actions). Two extra groups render below:
  'Rendszermeghajtók' (system/backup — read-only, lock tag, NO actions)
  and 'Nem regisztrált meghajtók' (unregistered user-data — register
  action only). Agent-down: one warn note into #agent-warn-note, all
  registry cards still render from server data (graceful degradation).
  The agent-view helpers now emit design-system .tag markup instead of
  .badge (roleTag/classTag/dataTag/regTag/appBackingTag); the 🔒 lock
  emoji is gone (sprite i-lock).
- Overlay migration: every native confirm()/prompt() on the four pages
  routes through a light .confirm-overlay dialog (openDialog; texts
  verbatim) — storage remove forms, netStorageRemove, storageMigrateAll,
  storageDisconnect, storageDecommission (migrate + the type-to-confirm
  anyway branch preserved like-for-like), storageReEnroll; and on the
  system page triggerUpdate + controller/server restart; on the security
  page the two geo Hungary-removal confirms. Scenario F grep: zero
  native confirm/prompt in the four templates.
- Deleted the now-orphaned .badge-lock/.lock-ico CSS (grep-zero first).
- Tests: no-native-confirm scan, agent-down warn-note static assertion;
  integrity gate stays green.
2026-07-02 19:17:19 +02:00
admin f8e18a9ec9 D1 Part 2: settings.html split into four pages + sidebar restructure
- settings.html (1451 lines) deleted; sections moved verbatim into
  settings_system.html (Rendszer konfiguráció, Verzió és frissítés,
  Vezérlő/Kiszolgáló újraindítása + update/restart JS),
  settings_notifications.html (Értesítések, Alkalmazás-email),
  settings_security.html (Jelszó módosítás, Földrajzi korlátozás + geo
  JS, Vészhelyzeti információk — heading + section copy accents fixed),
  storage.html (Adattárolók, NAS, migrate progress, agent view + all
  storage JS; wizard entry links now /storage/init|attach with sprite
  icons instead of emoji). The NAS + migrate sections were nested inside
  {{if .StoragePaths}} in the monolith and vanished with zero drives —
  now unconditional on /storage.
- layout.html: Tárhely main-nav item (hard-drive icon) + the
  'Beállítások' sidebar group with Rendszer / Értesítések / Biztonság és
  hozzáférés sub-links (active-state per page key); orphaned
  .sidebar-settings-link CSS deleted (grep-zero), .nav-group-label /
  .nav-links-sub added.
- Handlers wired to their own builders + templates; the legacy
  settingsData() merge deleted.
- scripts/template_id_gate.py: the §10 JS element-ID integrity gate
  (getElementById/querySelector('#…') must resolve in the SAME template;
  JS-created + template-parameterized IDs handled; layout modal IDs
  allowlisted). Red-proven: a storage function planted in the
  notifications template failed the gate with 'static #migrate-progress
  not defined'.
- Tests: per-page section markers + cross-leak assertions, h3 section
  inventory (all 11 old headings accounted for; typo rename asserted).
2026-07-02 19:07:32 +02:00
admin d50a919404 D1 Part 1: settings-split routes + per-page data builders
- server.go: GET /storage (Tárhely page), GET /settings/notifications
  (GET->page, POST->save dispatch on the same path), GET
  /settings/security; the enrollment wizards move to /storage/init +
  /storage/attach with 301s from the old /settings/storage/* URLs.
- handlers.go: settingsData() decomposed into settingsBaseData +
  systemPageData / storagePageData / notificationsPageData /
  securityPageData; the legacy merge remains only while the monolithic
  settings.html exists (Part 2 deletes it). All five storage action
  redirects (add/remove/default/schedulable/label) now land on
  /storage?storage_msg=... (incl. the two error-branch redirects).
- Every page keeps rendering the full legacy template in this commit —
  the site stays functional; the split lands in Part 2.
- Tests: four pages 200, wizard 301s + new URLs render, storage-label
  redirect Location prefix + flash renders on /storage, wrong-password
  inline re-render. Red-proven vs pre-split code (Location was
  /settings?..., no 301s).
2026-07-02 19:00:29 +02:00
admin 52e97b15ca docs: CHANGELOG v0.96.0 + REPORT (TASK-D0) + CONTEXT + README design-system section 2026-07-02 15:04:10 +02:00
admin 4906524c11 fix(backups): off-box LastRun is an RFC3339 string — 500'd the backups page
Found during the D0 click-through: OffboxTarget.LastRun persists as an
RFC3339 string (settings.json), but backups.html passed it to timeAgo
(expects time.Time), so GET /backups returned 500 on any node where an
off-box backup had ever run. Pre-existing since v0.93.0 (NAS Part B),
unrelated to the re-skin. New timeAgoStr funcmap helper parses the
string (falls back to the raw value); also v2-token fix for the offbox
error hint (var(--danger) fallback hex) and the dead stat-running/
stat-stopped classes on the offbox status card.
2026-07-02 14:47:22 +02:00
admin 7df061c00f D0 Part 4: settings visual pass + remaining pages; grep gate zero
- settings.html: Aktív/Inaktív rows -> run-blue check / neutral gray
  (Inaktív no longer red), update states -> run/crit/progress with
  sprite check/x/spinner, pencil + cancel buttons -> icons, storage
  badges -> tags, host-disk bar + JS drive capBar -> meters
  (usageColorClass -> nominal/warn/crit), state-text-* consumers on the
  new suffixes incl. JS-built class names.
- debug.html, app_info.html, storage_init/attach.html, logs.html:
  emoji -> sprite icons or plain text in templates AND JS strings.
- catchall.html (standalone, no sprite): v2 token sweep of its inline
  style, status emoji -> inline SVGs; a stopped app renders neutral,
  not red.
- login.html: two-tone H1 (last word blue-bright).
- setup_hub_versions.html: stale var(--border,#30363d) fallback -> v2.
- Test Group F grep gate: all 34 banned patterns at ZERO across
  internal/web + internal/setup; Scenario E test now asserts the
  '--bg-0: #0A1220' token literal.
2026-07-02 14:38:51 +02:00