Commit Graph

744 Commits

Author SHA1 Message Date
admin 4646be1515 docs: REPORT for the 2026-07-20 remote-site remediation + F3 fix (v0.149.0) 2026-07-20 09:09:14 +02:00
admin c059fe4c28 v0.149.0 — F3: the dashboard backup card tells the truth about the last run
The card claimed "Utolsó mentés: Még nem futott" on every box, forever, even
with dumps on disk and db_dump_completed events in the hub. dashboard.html
branches on {{if .BackupStatus}} and reads .Success/.LastRun, but
dashboardHandler never set that key — so the {{if}} arm was unreachable and
the "never ran" else-branch rendered unconditionally. The neighbouring
"Adatbázisok: N mentve" row kept working because it reads DBDumpStatus, which
was passed; that is the contradiction the audit caught on the live box.

Fix is the one-line pass-through the template always expected:
data["BackupStatus"] = fullStatus.LastDBDump. *DBDumpStatus nil/non-nil maps
onto the template's branch, so a fresh box still reads "Még nem futott" and no
zero-value timestamp is fabricated. No template change, no new view-model.

Tests drive the real handler through ServeHTTP so they bite on the handler
wiring, not the template alone. Red-proofed: deleting the assignment fails
TestDashboardBackupCard_ShowsLastRun.

Origin: felhom.eu/documentation/audits/AUDIT-vacation-remote-ops-2026-07-20.md (F3)
2026-07-20 09:04:11 +02:00
admin 9d001771ea docs: TEMPORARY remote-site access for felhom-pve (tailnet) — see AUDIT-vacation-remote-ops-2026-07-20
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Nhk3eBHT8Mg5L8c2aj57aU
2026-07-20 08:07:38 +02:00
admin 29eda5d86e docs(runbook): e2e live-drive commands are DooPlex-local (plain ssh felhom-pve) 2026-07-19 12:27:42 +02:00
admin 062357f778 v0.148.0 — coherent snapshot pairs + an offsite restore that actually restores (R-43 + R-44)
Closes the two findings from DIAG-immich-restore-2026-07-19. Viktor deleted 11
immich photos to test offsite restore; both runs flashed success and the photos
stayed gone. Two independent defects.

R-43 — no offsite path could restore a database. All three buttons were
file-only: the two "visszaállítás" actions staged to a scratch folder and never
touched postgres, and place-to-live merged only MISSING files. For a DB-indexed
app the bytes returned and the app still could not see them. The dump was
carried INTO every snapshot and could never be replayed OUT of one.

New ReconstituteFromOffsite (/backup/offbox/reconstitute): safety dump → stop →
files overwritten to the snapshot version → start → the snapshot's own dump
replayed → health wait. Two invariants:
  - nothing is ever deleted (-a, no --ignore-existing, no --delete): a file
    created after the snapshot survives as an extra;
  - the undo exists before the act — the pre-restore- dump is verified ON DISK
    before anything is stopped, overwritten or replayed; if it cannot be taken
    the operation refuses with zero changes.
The replay reads the SCRATCH unit: the live unit is never overwritten, so
replaying from it would replay the current DB over itself and restore nothing.

R-44 — a manual push shipped an unrefreshed dump (up to ~24h old). That day's
predated the customer's account by four hours and probed to asset:0/user:0/
album:0 inside 52MB whose bulk was immich's shipped geodata. Every run, manual
AND nightly, now refreshes dumps + units BEFORE capturing. Order is the
mechanism: the gap can only ADD files the DB does not reference yet, never
remove one it does. Manifests carry offsite_run_id + dumps_at, so coherence is
verifiable at restore time rather than assumed; the periodic refresh carries a
prior stamp forward and never invents one.

Honesty surfaces, all warn-level and none a gate: unstamped (pre-v0.148) pairs
report their skew, ValidateDump gained an EXACT-match accounts-table sniff for
customer-empty dumps, the completion flash states an outcome instead of a
mechanism, and the missing-only button now says what it does NOT do.

11 tests; 5 red-proofs run and reverted. Two of those found real test weaknesses
rather than confirming strength — the first undo mutation was caught by a second
guard, and the first table-matching test did not discriminate between the two
matchers at all. Both tests were rewritten to the cases that separate them.

NOT in scope: R-41's catalog invariant check, nightly cadence, retention, quota
math, tier-2, and v0.147.x progress semantics beyond one added phase line.

Live acceptance (§9) has NOT run: no capability-map flip, customer-restore row
stays MISSING, R-3 stays DRAFT.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01P9Nn14TWGzKoqAJAiVwC2s
2026-07-19 12:21:16 +02:00
admin 2fcae041ae chore: mark build-samba-image.sh executable (mode-only, set on Linux) 2026-07-19 12:17:39 +02:00
admin ac7323dc9a docs: migrate workflow to DooPlex-local execution 2026-07-19 12:15:52 +02:00
admin da56c3e994 docs(reuse): register the v0.147.x helpers, patterns and seams
offsiteRestoreRootFor as THE home for backups/offsite-restore (it was open-coded
in three places). Two patterns: the detached-job+poll shape, now noting that FIVE
of them exist and agree on nothing so a sixth should extend rather than clone;
and streaming subprocess progress, which encodes the two traps tonight found —
a source reporting nothing is normal, and progress may only update on unit
completion, so degrade rather than fake a percentage. Seams: sambaImgFn and
offboxStreamRunner.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Nn3VgQk9iwEGgyx6QJ2NvE
2026-07-19 10:06:05 +02:00
admin 86de16f6dd docs: README feature 17 (async-job feedback) + CONTEXT for v0.147.x
README gains the async-job feedback section, the TOC entries it was missing
(SMB was also absent), and the three new endpoints. CONTEXT records the slice
and, more usefully, that three of its four versions exist because the cards were
watched against real runs — the failure modes were all invisible to unit tests.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Nn3VgQk9iwEGgyx6QJ2NvE
2026-07-19 10:05:07 +02:00
admin 10ca8ac884 docs(report): v0.147.0-.3 feedback slice 1
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Nn3VgQk9iwEGgyx6QJ2NvE
2026-07-19 10:03:17 +02:00
admin 63a22e5911 v0.147.3 — 4c follow-up 3: the run does not end with the last app
Third real run, third thing only a live run could show. The per-app legs
finished in ~15 seconds; the remaining 40 of the 57-second run was the shares leg
and forget --prune, during which the card sat frozen on "calibre-web — 8 / 8
fájl". The same frozen-looking silence 4c exists to remove, relocated to the end.

Progress now carries a phase. The post-app stages announce themselves and the
app-scoped counters are cleared when a phase starts, so the last app's finished
numbers are never shown against work that is no longer about that app. Starting
the next app clears the phase again. Pinned by a test.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Nn3VgQk9iwEGgyx6QJ2NvE
2026-07-19 09:54:18 +02:00
admin 111369dd10 v0.147.2 — 4c follow-up 2: when NO counter can move, say what is being worked on
The v0.147.1 file-count fallback fixed the incremental case but not the one the
demo box actually hits. Watching a second real run: bookstack reported clean byte
progress (100%, 154.0 MB, 7/7 files — the byte path works), while immich sat at
files_done 1 of 46, bytes_done 0, for 42 seconds.

restic 0.14 only counts a file into bytes_done/files_done when it COMPLETES, so
an app dominated by a single large archive (immich's ~430MB volume tar) freezes
both counters. No percentage can move in that window, so stop trying to fake one.

restic keeps reporting current_files and seconds_elapsed throughout. The card now
names the file being processed and the elapsed time: "1 / 46 fájl (430.2 MB) ·
feldolgozás alatt: immich_upload.tar · 42 mp". "Working on this file for 42
seconds" is a completely different message from "0%", and it is the honest one.

The last known current_files value persists across ticks that omit it (restic
does not send it every tick, and blanking the label every other second is its own
flicker); switching app clears it so one app's file is never shown against
another. Both pinned by tests, with the real 42-second status line shape.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Nn3VgQk9iwEGgyx6QJ2NvE
2026-07-19 09:49:28 +02:00
admin 77e8d5590b v0.147.1 — 4c follow-up: the bar must move on an INCREMENTAL run
Found by watching the v0.147.0 card during a real manual run on the demo box,
which is the only way this was going to surface: a 430MB immich push reported 0%
for 40+ seconds and then completed.

The parser was not broken. restic was genuinely reporting no transferred bytes —
on an incremental run where nothing changed, bytes_done is omitempty on restic's
side so it is not even in the JSON, and percent_done stays 0 for the whole run.
Confirmed against the real schema by capturing backup --dry-run --json from
restic 0.14.0 in the controller image rather than guessing; those captured lines
are now quoted verbatim in the type's doc comment.

Why it mattered: a byte-only bar is indistinguishable from a hang in the COMMON
case, which is precisely the silence 4c set out to remove. Shipping it would have
traded "no feedback" for "feedback that says 0% and looks stuck".

files_done/total_files are now parsed and published alongside the bytes; the card
prefers bytes when bytes move, otherwise drives the bar from files and says
"N / M fájl ellenőrizve". parseResticStatus returns a struct instead of four
positional values, and a new test pins the real incremental line shape (bytes
absent, files climbing) so a refactor cannot quietly restore the stuck bar.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Nn3VgQk9iwEGgyx6QJ2NvE
2026-07-19 09:44:21 +02:00
admin b5d78d1e0f v0.147.0 — feedback slice 1: pressing a button says something
The systemic complaint, twice in one evening: you press a button and nothing
happens. No progress, no ETA, no named result. Three worst offenders, fixed on
the two patterns already here (deploy 3-step panel, storage-init status poll).
No new framework — that is a ROADMAP item; three targeted cards ship tonight.

4a — a verification restore names its result. The flash said the app had been
restored "to a verification folder on the drive"; which folder, on which drive,
was invisible, so the customer could not go and look at what they had just asked
for. Full path now. The restore page gained a listing of existing verification
copies (app, size, date, path) — nothing anywhere showed these, so they piled up
and the only way to find them was SSH — each with a double-confirmed delete.

That delete is the only one this release adds, so it names a STACK, never a
path: the Manager resolves the name inside a backups/offsite-restore root it
computed itself and refuses anything landing outside. Red-proofed — neutralise
the name guard and stack:"" resolves to the offsite-restore ROOT and takes every
copy with it. Refusals are asserted as non-effects.

4b — Megosztás enable shows what it is waiting for. Enabling ran ReconcileSamba
synchronously inside the POST handler; on a golden without felhom-samba baked
that is compose pulling ~100MB, i.e. minutes of an apparently-hung form post
followed by "Beállítás mentve." whether or not anything came up. Detached +
polled now, distinguishing "képfájl letöltése" from "indítás" — decided BEFORE
the work starts, since afterwards the image is always present. Success is
probed, not inferred (compose up -d exits 0 on a crash-loop). The password form
starts the same job: with UserSet false reconcile deploys nothing, so on a fresh
box that is where the pull actually happens.

4c — "Távoli mentés most" streams real progress. restic was already reporting
bytes and percent; the runner seam used CombinedOutput() and discarded them. The
manual run now passes --json and scans stdout line-by-line: total bytes, percent,
current app. Manual only — the nightly stays silent, pinned by a test that fails
if it ever passes --json. The poll now arms unconditionally, closing a race the
manual trigger always ran: the redirect rendered before the goroutine wrote
LastStatus=running, so the poll never armed and the page sat static during the
very run just started. Red-proofed twice.

Also closes the golden/controller infra-image drift at the source: infra.Images()
derives from the existing pins and --print-infra-images exposes it, so the golden
bake can stop carrying its own copy. That copy had already drifted — felhom-samba
was never added, so the golden baked 3 of 4, which is why enabling Megosztás
pulled at runtime in the first place.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Nn3VgQk9iwEGgyx6QJ2NvE
2026-07-19 09:30:30 +02:00
admin 7f0b41c3e7 docs(report): v0.146.0 nav polish
Overwrites REPORT.md per convention. Records the two changes, the four design
decisions behind the accordion (real <button>, landing-pages-survive check,
server-side open state, grid-rows instead of max-height), and — importantly —
what could NOT be verified and why: the demo controller's password is
customer-owned since the claim flow, so the build-server credentials are stale
and a curl-login returns the Bejelentkezés page. The four red-proofed render
tests stand in for the server-side half; the visual leg needs Viktor's browser.

Also notes the fleet gap: v0.146.0 is live on the demo box but the golden still
bakes 0.143.0, deferred to the next session.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Nn3VgQk9iwEGgyx6QJ2NvE
2026-07-18 21:04:06 +02:00
admin fd93020f39 test(nav): pin the v0.146.0 accordion's server-side half
The collapsible sidebar is a progressive enhancement: the server marks the group
owning the active page with .is-open, so the correct group is open before any JS
runs and stays open if JS never runs. That server-side half is what these tests
pin — the part a browser screenshot confirms only for whichever page happened to
be open when the screenshot was taken.

Four properties, rendered through the real shared layout rather than a hand-built
fragment:
 - every sub-page opens its own group, with aria-expanded=true and an .active
   toggle, and EXACTLY ONE group open (the count is asserted, not just the group
   we expected);
 - a page outside any group (dashboard) opens nothing;
 - each group's landing page still exists as a sub-link — the property that made
   converting the headers from <a> to <button> safe. If someone drops one of
   those sub-links the destination becomes unreachable from the sidebar
   SILENTLY, because the header still looks clickable;
 - the toggle is a real <button> and its aria-controls targets an element that
   actually exists.

Red-proofed: removing `{{if $storageOpen}} is-open{{end}}` from layout.html
fails both the open-group assertion and the exactly-one-open count on the two
storage pages, then passes again when restored.

Note on verification: the live authenticated render could NOT be checked from
here — the demo controller's password is customer-owned since the claim flow
(Viktor set it during the 2026-07-18 rehearsal), so the credentials on the build
server are stale and a curl-login returns the Bejelentkezés page. These render
tests exercise the same template through the same loadTemplates() path the server
uses; the visual leg needs Viktor's browser.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Nn3VgQk9iwEGgyx6QJ2NvE
2026-07-18 20:57:39 +02:00
admin 24d23b80bd v0.146.0 — nav polish: styled scrollbars + collapsible sidebar groups
UI-only; no behavioural or backup/restore surface touched. Green: build, vet,
tests all pass; template_id_gate, emoji_gate, native_confirm_gate,
offbox_rename_gate, mojibake_gate, app_row_dedup_gate all PASS.

Scrollbars (style.css): the platform default reads as a bright chunky stripe
against the navy and competes with the content it scrolls. Now thin and
hairline-coloured — scrollbar-width/scrollbar-color for Firefox AND
::-webkit-scrollbar (8px, thumb --line, hover --text-3, --radius) for
WebKit/Blink, because neither alone covers the browsers customers use. The two
surfaces that actually scroll take their own panel background as the track
(.sidebar -> --bg-2, html -> --bg-0) so the gutter never shows as a lighter
channel. Tokens only, no raw hexes.

Collapsible nav groups (layout.html + style.css, vanilla JS, no framework):
Tárhely, Biztonsági mentés and Megosztás become accordions with a chevron;
exactly one open at a time, clicking the open one closes it. Groups without
sub-items are untouched plain links. Hungarian labels unchanged.

 - The header is a real <button>, so keyboard and AT reachability come for free
   instead of being simulated with tabindex/role on a div. aria-expanded +
   aria-controls + a :focus-visible outline.
 - Nothing became unreachable when the header stopped being a link: every
   group's landing page is ALSO its first sub-item (/storage -> Meghajtók,
   /backups -> Áttekintés, /sharing -> Hálózati megosztás). Checked before the
   conversion, not assumed.
 - Progressive enhancement: the group containing the active page is rendered
   open SERVER-SIDE (.is-open), so the right group is open before any JS runs
   and stays open if JS never does. The listener only handles clicks.
 - No layout jump: collapse animates grid-template-rows 0fr -> 1fr (with
   min-height:0 + overflow:hidden on the sub-list) rather than max-height, so
   it animates to the content's REAL height and there is no magic number to
   drift when a group gains or loses an item — the specific way a max-height
   accordion rots. The toggle reserves its 3px active border as transparent so
   becoming active adds no width shift. .18s transitions, and both the collapse
   and the chevron rotation are disabled under prefers-reduced-motion.

Pre-existing and deliberately NOT bundled: docker_run_volume_path_gate.py still
fails on internal/appexport/estimate.go:179. That is ROADMAP R-29, unrelated to
this change, verified to fail identically on the untouched tree, and R-29 itself
says not to bundle it into an unrelated feature commit.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Nn3VgQk9iwEGgyx6QJ2NvE
2026-07-18 20:50:46 +02:00
admin 57be785bad docs(R-7b): finding (f) now points at ROADMAP R-29, with the dating and diagnosis
The gate landed v0.125.0 (2026-07-13), realVolumeSize landed v0.129.0 (2026-07-14)
-> red for 16 releases unnoticed. Records that the flagged mount is a NAMED VOLUME
(the safe shape) so the fix is an allowlist entry, not a rewrite of correct code.
2026-07-18 15:17:12 +02:00
admin cc50918244 docs(R-7b): CORRECTION — offsite leg + restore round-trip ARE live-proven
The prior revision claimed the demo box had no offsite target and that both legs
were unexercised. All three clauses were false on a box that had run a successful
offsite backup minutes earlier.

Evidence: offsite snapshots e0b9d723 (Viktor 12:18:16Z) and 4e2b15ec, tagged
felhom-offbox,_shares, each carrying the payload dir (manifest + passdb.tar, 0600)
plus both share folders. Restore round-trip through the real routes returned a
deleted probe byte-identical and a deleted DEFINITION with its original flags,
without overwriting the two pre-existing files. R-7b is fully PROVEN-LIVE.

Root cause in new REPORT section 7b: the check read a GUESSED settings key
('offbox_target') where the real tag is 'offbox' (settings.go:96), got None for
every field, and read that as 'not configured' instead of 'wrong key'.
2026-07-18 14:32:28 +02:00
admin a5d870ea0e docs(shares): REPORT.md — R-7b v0.145.0 ship report
Findings, all six red-proof outcomes, deployment, live-validation evidence, and an
explicit list of what was NOT live-exercised (offsite leg + restore round-trip: the
demo box has no offsite target).
2026-07-18 13:34:59 +02:00
admin d0c1d77491 fix(shares): reserved key leaked into the crossdrive_completed hub event
Found by LIVE VALIDATION, not by a unit test: the first demo tier-2 run pushed
'Masodlagos mentes elkeszult: _shares' — the reserved key reached Hungarian
customer/operator copy.

Mapped at the SOURCE of the notification (RunSharesTier2's tier2Notify calls) so no
future notifier wiring can reintroduce it, plus DisplayStackName at the main.go
wiring as idempotent defense in depth. Regression test added with a red-proof.
2026-07-18 13:13:51 +02:00
admin 3b70a9e9ab docs(shares): R-7b v0.145.0 — CHANGELOG, CONTEXT, REUSE, README; caveats cleared
The samba_classify.go SCOPE NOTE and the README KNOWN GAP both described a gap that
R-7b closes; both now describe the sibling-shares-source execution instead.
2026-07-18 13:06:41 +02:00
admin 900c870212 feat(shares): R-7b Parts 4-6 — shares restore, samba liveness, UI truth-up
Part 4 — restore: RestoreSharesScratch + PlaceSharesRestore as SIBLINGS of the
per-app scratch/place pair. Files merged missing-only (never overwriting), each
destination PREFIX-ASSERTED against registered LIVE storage roots; definitions
merged with existing-wins; ReconcileSamba via a seam (backup must not import
stacks); credential restored best-effort into the samba named volume.
New routes POST /backup/shares/{restore,place} + a restore-page entry that renders
'Megosztasok', never the raw reserved key.
Also adds scratchJoin: reconstructing an absolute captured path under a scratch
must strip the volume name rather than rely on filepath.Join.

Part 5 — liveness: EffectiveProtected gains a settings-backed dynamic extra so the
samba CONTAINER (not the stack name — they differ) is watched exactly while sharing
is on. FINDING: the issue -> health 'fail' -> existing health_critical event ->
alert -> Hungarian degradation e-mail path needs NO further change, and introduces
no new event type, so the allowlist gotcha does not apply.

Part 6 — UI: per-tier backup status lines on the Megosztas page (amber only on
deviation). Verified the two warning-prose sites (offbox_capture/tier2_capture)
only ever receive per-app stack names, so no mapping is needed there.

RED-PROOFS RUN AND REVERTED (both fired):
  4. prefix-assert removed        -> place-guard traversal test FAILS
  5. dynamic samba extra removed  -> Scenario E enabled-case FAILS
2026-07-18 13:02:41 +02:00
admin 85b76e0fc3 feat(shares): R-7b Part 3 — offsite shares leg (Model B') + B' isolation proof
ONE additional restic call tagged [felhom-offbox, _shares] carrying the payload
staging dir + every mandatory share folder. Hooked into runOffboxInternal AFTER the
per-app loop and BEFORE retention, so forget --group-by host,tags covers the _shares
group with no flag change. Reuses resticStep, the caller's repo-ensure and
single-flight, and the SAME enlargement-gate arithmetic.

- quota gate degrades the push to MANIFEST-ONLY, never to nothing
- EnlargedBlocked keeps the RAW _shares key (templates index by it); the display
  mapping applies only at the notification + Hungarian-prose boundaries
- OffboxTarget gains SharesLastRun/Status/Count for per-tier page truth
- zero-toggle notice suppressed when the shares leg provided coverage
- reserved-name defense: an app keyed _shares is excluded from the run loudly

RED-PROOFS RUN AND REVERTED (all fired):
  1. shares leg appends into the app's argv -> isolation test FAILS
  2. mandatory->offsite mapping inverted    -> Scenario A + B FAIL
  3. manifest-only degradation dropped      -> Scenario C FAILS
2026-07-18 12:51:46 +02:00
admin c81df55dcb feat(shares): R-7b Parts 1-2 — shares payload builder + tier-2 shares job (Model B')
Sibling shares source for the local cross-drive tier. Reuses the tier2Mirror seam,
selectTier2TargetFrom (narrow source-drive seam extracted from selectTier2Target),
tier2ReconcileRoots (pure extraction), tier2SafeRemove, the marker-LAST discipline
and the recordTier2* helpers. Per-app paths are untouched.

- shares_payload.go: deterministic _shares-manifest.json + best-effort passdb capture
- tier2_shares.go: per-source-drive legs -> cross-drive target, payload, marker LAST
- infra.SambaContainerName/SambaPassdbVolume/Mount: single source of truth for the
  container identity (renderer, stacks execs, backup execs, monitor all read it)
- RESERVED-NAME finding: ValidateSMBShareName did NOT exclude a leading underscore,
  so "_shares" was an accepted share name. Now refused; RunAllTier2 additionally
  skips a "_shares" stack loudly as defense in depth.
- fix: shareSourceDrive returned a slash-normalised path, which made the target
  selector's source-drive equality check miss (a group could target its own drive)
2026-07-18 12:45:57 +02:00
admin 3dfc49e578 docs(samba): Explorer human leg PASSED — R-7 slice 1 fully PROVEN-LIVE
Viktor confirmed on the demo box: Network -> FELHOM -> both shares open; an
INTERACTIVE Explorer save into dokumentumok succeeded and landed owned 1000:1000
(force user holds for a real Explorer write, not just a scripted one); a write
into the read-only filmek was refused by Windows with the folder left empty on
disk. Capability-map row flipped to PROVEN-LIVE in felhom.eu.
2026-07-18 12:12:51 +02:00
admin a4c82a2651 docs(samba): CHANGELOG v0.144.0 + REPORT + CONTEXT + README §16 + REUSE
Records the R-7 slice 1 ship, the four red-proof outcomes, live-validation
evidence, the two bugs live validation caught, and the Part-4 Step-1 enumeration
finding + the reported design fork (share data classified but not yet in a live
backup run — needs a Viktor ruling, suggested R-7b).
2026-07-18 12:03:24 +02:00
admin b409f5eee2 fix(samba): separate storage-ROOT validation from share-TARGET validation
Live validation caught it: the 'new folder' flow passed the storage root through
sharingResolvePath, which (correctly) refuses the drive root as a share target —
so share creation silently failed. sharingResolveStorageRoot accepts EXACTLY a
registered live root (strictly tighter) and is used only as the new-folder parent.
Regression test asserts both halves.
2026-07-18 11:54:35 +02:00
admin 2eef9b2e4a fix(samba): register /api/sharing/ on the mux, not the web switch
The /api/ subtree is routed on the main mux, so the browse case in the web
ServeHTTP switch was shadowed by the apiRouter catch-all and 401'd. Moved to
ServeSharingAPI behind RequireAuth+CsrfProtect, matching /api/storage/.
Found by live validation.
2026-07-18 11:51:12 +02:00
admin 4f08e5e7c3 feat(samba): Megosztas page + guarded folder picker (R-7 slice 1, Part 3)
New top-nav category with the Halozati megosztas page: enable/server-name card,
household password, shares table (Nev/Mappa/Irasvedett/Felhomentes/Torles), and
a create flow (new folder under <storage>/shares or an existing folder via the
browse modal). Every customer path goes through sharingResolvePath: absolute ->
EvalSymlinks -> containment in a registered live storage root -> deny-listed
system subtree check -> is-a-directory. Refusals are UNIFORM so the picker is
never a filesystem oracle. Deny-list derived from ProtectedHDDPaths (provably a
subset); the drive root is an exact-match denial so user-data folders under it
stay shareable. samba infra metadata + i-share icon. Gates green.
2026-07-18 11:45:18 +02:00
admin 1d26a69dd4 feat(samba): backup classification from the shares registry (R-7 slice 1, Part 4)
ClassifiedBinds("samba") resolves from the shares registry instead of catalog
metadata (samba has no .felhom.yml; its binds are absolute share paths). [R4]
Offsite ON -> mandatory (offsite + tier-2); OFF -> optional (tier-2 only);
smb.conf/passdb never classified. Verified through the REAL ComputeCaptureSet
tier filter incl. the negative (optional NOT in offsite). Zero engine edits.

Part-4 Step-1 finding: tier-2 (RunTier2) short-circuits on os.Stat(unitDir)
BEFORE GetStackClassifiedBinds, and the offsite runner enumerates
settings.GetOffboxApps() — both are recovery-unit shaped, which a share-only
infra stack has not. Wiring share data into a live run is therefore more than an
enumeration tweak; reported as a design fork per the STOP clause, not improvised.
2026-07-18 11:35:09 +02:00
admin 0dcbea90b2 feat(samba): lifecycle — ensureSamba/ReconcileSamba/password/disable (R-7 slice 1, Part 2)
ensureSamba joins EnsureBaseStack after filebrowser, gated on SMB.Enabled
(cloudflared conditional precedent); reconcile is idempotent (unchanged config +
running container = ZERO compose calls, asserted via seam). Atomic tmp+fsync+
rename config writes. Password applied via smbpasswd on STDIN (never argv/log/
settings). Disable = compose down, volumes + folders KEPT. samba added to
IsProtectedStack in code (controller.yaml is golden-generated and predates it),
which also makes the app-backup loops correctly skip it.
2026-07-18 11:30:04 +02:00
admin b0c5ef4823 feat(samba): settings registry + smb.conf/compose renderers (R-7 slice 1, Part 1)
SMBSettings + SMBShare registry in settings (password never stored — only UserSet);
NetBIOS-safe name validation. Pure infra renderers: RenderSambaConfig (hardened
global block: SMB2 floor, bind interfaces only=lo eth0, disable netbios=no, force
user block) + RenderSambaCompose (network_mode host, pinned image, :ro bind for
read-only shares, passdb volume). Exact smb.conf golden + CRUD/validation tests.
2026-07-18 11:21:05 +02:00
admin f42f3e0e08 feat(samba): felhom-samba infra image (R-7 slice 1, Part 0)
Own pinned alpine image (3.21@sha256:48b0309c) + smbd/nmbd/wsdd/tini. Dumb by
design: smb.conf bind-mounted read-only, no baked name/password, passdb on a
volume. Three-daemon stack per the R-6 spike verdict (nmbd required alongside
wsdd, else Explorer double-click 0x80070035). build-samba-image.sh helper.
2026-07-18 11:11:43 +02:00
admin ac13966f30 build: repoint build.sh to /mnt/5_hdd/felhom.eu (DooPlex build root moved off SSD 2026-07-18) 2026-07-18 09:55:16 +02:00
admin a4a7de3d8f docs: v0.143.0 REPORT + CONTEXT + README (guest RAM resize UI, R-24; deployed + live-validated on demo) 2026-07-17 19:27:47 +02:00
admin 3286c7faaf v0.143.0 — guest RAM resize UI (R-24) — MinAgent: 0.90.0
The customer sees the guest's current memory + allowed range on the Rendszer settings
page and resizes it. The controller proxies + maps the agent's machine code to Hungarian;
the agent (felhom-agent v0.90.0) enforces every bound and applies live (no reboot).

agentapi: GuestMemory + ResizeMemory; ruled 412 -> *MemoryRefusedError (code+bounds);
pre-0.90 agent 404 -> typed *StatusError. Capability: FeatureGuestMemoryResize +
featureMinAgent 0.90.0 + a featureProbes row (type-asserts GuestMemory so the shared
SupportProber/netAgent are untouched).

UI (internal/web/system_memory_handlers.go, settings_system.html): "Szerver memoria (RAM)"
card + number input; POST /api/system/memory/resize -> capability gate -> agent -> flash.
JS confirm only on shrink. Code->Hungarian map; agent-outdated hides the control;
agent-unreachable falls back to the guest's /proc/meminfo. Agent English never shown raw.

Tests: agentapi (decode, 404, refusal-code, capability table) + web handler (success/
below_usage_floor/agent_outdated). Gates + go build/vet/test all pass.
2026-07-17 19:10:00 +02:00
admin f900c83eed docs: v0.142.0 REPORT + CONTEXT (offsite repo continuity A+C; live leg staged for the rehearsal; security-review Observation) 2026-07-17 10:55:30 +02:00
admin 596505ed64 v0.142.0: offsite repo continuity — orphaned-repo guard (A) + run-status auto-refresh (C)
- Part A: classify restic cat-config failure (wrong-password=orphaned vs no-repo vs other); ORPHANED state + Hungarian card + offbox_repo_orphaned/reset events (once, not nightly); reset = move-aside (never delete) + init, unclaimed auto / claimed confirm. Red-proofs TestOffbox_OrphanDetection_* + ConfirmedReset.
- Part C: GET /backup/offbox/status + poll on backups_remote → flips Fut→Rendben/Hiba without manual reload.
2026-07-17 10:47:30 +02:00
admin 1452dd2b17 docs: v0.141.0 REPORT + CONTEXT + README (F6 detached init job + format-status poll, F7; live-validated on demo scratch USB; security-review TOCTOU acknowledged) 2026-07-17 09:49:45 +02:00
admin 0515d153db F6 deeper half: poll agent /disks/format/status on the 15s client timeout (slow USB mkfs runs detached) — agentapi.FormatStatus + awaitAgentFormat; then mount+register. Found on the live leg. 2026-07-17 09:41:50 +02:00
admin 0025a3b090 v0.141.0: F6 initialize-to-usable (detached crash-safe init job + status poll) + F7 Vissza back-routes
- F6: POST /api/storage/init runs format→mount→register as a DETACHED single-flight job (context.Background, netAddState shape) the wizard polls via GET /api/storage/init/status; 3-step progress; register-last marker-last crash-safety. Fixes the client-disconnect-aborts-mount bug. No agent change (chain reaches FileBrowser sync = controller-only). Red-proof TestStorageInit_DetachedSurvivesClientDisconnect.
- F7: storage_init/attach Vissza → /storage (was /settings). Test TestStorageWizardBackAnchors_PointToStorage.
2026-07-17 09:20:59 +02:00
admin 5be2449267 docs: v0.140.0 Direction-2 waiter — REPORT + CONTEXT (live: 240s no-annotation hold, 0.047s wake, restart no-storm) 2026-07-16 21:10:42 +02:00
admin 2dd05670ae feat(report): Direction-2 immediate-sync wait channel client (v0.140.0)
report.Waiter holds a hanging GET against hub /api/v1/wait?gen=N (same hub
URL+key as the pusher). On a generation change it fires the v0.139.0
report.Trigger — nothing else; the report ACK delivers everything through the
unchanged machinery. No overall client timeout (held GET); per-request ctx
bounds a dead connection. First-observation records-not-fires; same-gen
timeout fires nothing; errors (incl. 404 from a pre-v0.58.0 hub) back off
5s->5min while the 15-min cycle reconciles. Wired beside the trigger under the
same hubPusher!=nil && Hub.Enabled gate.

Red-proof: disable the baseline branch -> first observation fires
(TestWaiter_FirstObservationRecordsNoFire), run-fail-reverted.

Copy soften: backups_remote/escrow "néhány másodperc, legfeljebb 15 perc".
Pairs with hub v0.58.0. Grounding:
felhom.eu/documentation/audits/SPIKE-immediate-sync-transport-2026-07-16.md
2026-07-16 20:55:30 +02:00
admin f665bbed45 docs(report): v0.139.0 live verification — 2s save-to-hub round-trip proven on 9201
Out-of-cycle pushes at 17:42:19/17:42:34Z (quiet-window + min-interval
pacing exactly as designed, trailing edge carried the final state);
scheduled 15-min cycle unaffected (17:49:00Z tick on both ends).
Red-proof outcomes and the NOT-yet-live-validated list recorded.
2026-07-16 19:49:46 +02:00
admin fe9266f53f feat(report): v0.139.0 — immediate out-of-cycle hub report on user actions (Direction 1)
New report.Trigger (buffered-1 chan + worker; quiet 2s, min spacing 15s,
trailing-edge coalescing) generalizes the v0.70.0 geo out-of-band push.
One canonical fire closure in main.go; wired: geo save/sync + app
deploy/remove/delete (api reportPushNow), escrow recovery-code claim,
notification-prefs save, app-email toggle, offsite config + per-app
toggle, customer claim (web SetReportTrigger seam, nil-safe, fired only
after a successful local commit). 15-min hub-report cycle untouched as
the reconciliation backbone; hub.enabled=false stays a strict no-op.
Tests: trigger engine (2 red-proofs), seam fires-after-commit-only,
nil-seam no-ops.
2026-07-16 19:31:19 +02:00
admin 8f3564c137 docs(report): v0.138.0 live verification — 0.136→0.138 deployed, escrowed end-state confirmed
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017qDiBqKKQ5vPB5fXBqu7Kp
2026-07-16 18:48:00 +02:00
admin 120332103a feat(escrow): v0.138.0 — "awaiting hub confirmation" waiting state
After a completed escrow ceremony the Távoli mentés page showed the yellow
"Helyreállítási kód szükséges" card for ~15 min until the next hub-report ACK
flipped pending→escrowed. Phase-0 diagnosis (read-only) = verdict A (report-cycle
lag), already resolved on the demo box (escrow_state:"escrowed"); hub Hypothesis B
verified false (SaveHostEscrow ON CONFLICT already clears stale_at on upload) → no
hub change.

- settings.OffboxTarget.CeremonyCompletedAt: stamped on the recovery-code claim,
  zeroed on the auto-confirmer Flip + the deprecated manual confirm; persisted.
- web/handlers.go: offboxCeremonyWaitState + escrowCeremonyGraceWindow (35m).
- backups_remote.html: info "megerősítésre vár, legfeljebb 15 perc" card → warn
  "a megerősítés nem érkezett meg" past the window. Existing branches untouched.
- backups_escrow.html: "Mi történik ezután?" note on the wizard's final step.
- Test web/escrow_wait_state_test.go (truth table + red-proof recorded in REPORT).

No scheduler/agent/hub/endpoint changes.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017qDiBqKKQ5vPB5fXBqu7Kp
2026-07-16 18:44:13 +02:00
admin e99c675fe4 docs: v0.137.0 cleanup bundle — REPORT + CONTEXT (email-wipe guard + Parts 2-4 disposition) 2026-07-15 19:50:13 +02:00
admin c124d0adf2 v0.137.0: guard empty-email notification save (prevents alert-delivery wipe)
Saving the notifications form with a blank email box while events are enabled
wiped the customer's hub-side alert address (SyncPreferences pushed empty) —
the 2026-07-15 demo incident. settingsNotificationsHandler now refuses that
save before SetNotificationPrefs + hub sync, re-renders a Hungarian error, and
repaints the submitted checkboxes. Empty email + zero events (clear-all) still
proceeds. Tests + red-proof (remove guard -> stored email wiped to empty).
2026-07-15 19:40:01 +02:00