# CONTEXT.md — Project Memory > This file serves as persistent project memory across Claude Code sessions. > It replaces the auto-generated "Memory" from the claude.ai Project. > **Update this file at the end of each working session** with current state, > recent decisions, and anything the next session needs to know. > > Ask Claude Code: "Please update CONTEXT.md with what we did today" Last updated: 2026-07-24 (v0.163.0 — Indítópult launcher + universal app placeholder) > **2026-07-24 — v0.163.0 (Indítópult app launcher + universal placeholder icon).** New > customer-facing `/launcher` page: the FIRST sidebar item (above Vezérlőpult), a grid of large > tappable tiles for openable deployed apps. `/` stays the Vezérlőpult — the launcher is ADDITIVE. > Design rulings recorded here: > - **(a) The felhom brand mark is NEVER an app placeholder** — brand = platform identity only. The > logo-less fallback everywhere is the new generic `AppPlaceholderSVG` (a 2×2 app-grid glyph, > `/static/app-placeholder.svg`), now the DEFAULT `FallbackIcon` on `app_list_row` (was > `visibility:hidden`). On the launcher tile the fallback is the **monogram**, not the placeholder. > - **(b) A launcher tile exists ⟺ a „Megnyitás" button would** — subdomain presence (env `SUBDOMAIN` > > `.felhom.yml` subdomain > `protectedStackSubdomains`) is the single openability criterion. The > controller stack is excluded by name. The subdomain assembly was extracted to > `Server.subdomainMap` (3 callers: dashboard, Alkalmazások, launcher; priority byte-unchanged). > - **(c) Colored-tile + mono-glyph design.** `tileColor` = validated `.felhom.yml` `brand_color` > (`#rgb`/`#rrggbb`, new `Metadata.BrandColor`, omitempty) OR a deterministic FNV-1a-of-slug HSL > (fixed S/L, hue per app). Invalid `brand_color` silently falls back to the hash color (the one > §8 exception to no-silent-failure — cosmetic). `tileColor` returns `template.CSS` (we > validate/compute in Go; html/template's CSS filter mangles a legit `hsl()` from a func pipeline). > - **(d) `/` remains the Vezérlőpult.** No role/auth gating — member-role gating is a future arc > (ROADMAP: member role → launcher becomes the member landing page). No catalog app sets > `brand_color` yet (curation parked). > No agent coupling; MinAgent unchanged. 10 new test functions + 4 red-proofs (all observed FAIL then > restored). Gates green (app_row_dedup / template_id / emoji). > **2026-07-24 — v0.162.0 (R-71a), SHIPPED + deployed BOTH boxes (demo-felhom 9201 + demo-hp 9201 > via G1 break-glass), clean+healthy, settle-gate GO line captured on both.** B′ live note: both > above-floor boxes GOed correctly but NOT literally first-poll — the floor is in-memory (not > persisted), unknown at t=0, so the gate logged `awaiting floor knowledge` then GOed ~10 s later the > instant the report ACK landed (report-ACK latency = exactly what the 90 s sub-bound is sized to; > zero-wait-when-floor-known is unit-proven, test E). The gate correctly did NOT burn the one-time > password before the update picture was clear. > The structural fix for the F10 day-0 race (DIAG-f10): the apply-bridge no longer consumes the > single-use offsite password while a managed floor-update is in flight or imminent (below floor). > New seam `offsiteapply.SettleProvider.SettleState()` + `SettleFunc` adapter over the updater's own > `GetFloor()`/`IsUpdateRunning()` (no second floor path); `Bridge.AwaitSettle` polls 10 s BEFORE the > 3-min Reconcile ctx (deferral never eats the reconcile budget), bounds 90 s floor sub-bound / 5 min > overall (both GO+WARN — the "hub that can't serve a floor can't serve a consume → no burn" argument, > R-71c is the belt). At/above floor → GO first poll, zero wait (B′). Bridge goroutine MOVED after the > updater in main.go; wired only when an updater exists. **Ordering-only** — consume/persist/404 > contract untouched; R-71(b) rejected-by-design. **FINDING:** the floor is in-memory > (report-ACK-derived ~5–10 s), NOT persisted → unknown on any restart until the first ACK (sized the > 90 s sub-bound to that). 5 test scenarios (A–E) + nil-provider + cancelled-gate; **4 red-proofs all > observed FAIL then restored** (gate/updateRunning/sub-bound/overall-bound). Deferral paths NOT > live-fired (precondition now structurally prevented by the v1.25.0 build gate). **Layering: gate > prevents, (a) defers, (c) heals.** ROADMAP R-71 → SHIPPED (a)+(c). Live leg = the B′ first-poll GO > line on both above-floor boxes. > **2026-07-23 — v0.161.0 (R-70 controller leg), SHIPPED + deployed BOTH boxes.** When > `offsite.enabled` is in controller.yaml but no `offbox` target exists (pre-apply window / burned > credential — the F10 shape), Távoli mentés now shows „Felhom offsite tárhely kiépítve — a > beállítás automatikus, folyamatban…" on BOTH empty surfaces (status card + target line) instead > of „igényelhető" / „Még nincs beállítva". Data key `OffsiteHubEnabled` (from `Server.cfg`, no new > wiring); render tests per gate branch; banner leg is unit-proven/live-pending (no healthy box > occupies the window; next fresh onboarding is the natural live leg). Hub sibling v0.72.0 carries > the detector + `offsite_delivery_stuck` + the R-71c self-heal. Origin + rulings: > `felhom.eu/documentation/audits/DIAG-f10-demo-hp-offsite-2026-07-23.md`. > **2026-07-22 — v0.160.0 (R-67), SHIPPED + deployed BOTH boxes, full live leg on demo-hp.** > Network shares now bind their share ROOT into FileBrowser (`…/:/srv/:rslave`) — no > skeleton/userdata toward the NAS, ever. Pure assembly = `buildFileBrowserPaths` + `fbPathDeps` > (handlers.go), returning mounts AND config sources together so they can't disagree. > > **DECISION — two classes, two gates:** drives keep the drive-absent gate (byte-identical, > tested + observed live: demo-felhom logged a no-op sync); network shares use the STUB classifier > gate instead (stub ⇒ excluded from both lists + WARN — an exposed stub swallows uploads the real > mount later shadows; idle autofs is HEALTHY and included; unknown fails open). Never force-wake > in the sync (doctrine). > > **Phase-0 probe = GO:** in-container access through an rslave bind WAKES an idle autofs trigger > (proved on demo-hp against the real Felhom-Share). Live leg: upload from demo-hp's filebrowser > container (uid 1000) landed on demo-felhom's share dir and deleted clean; dead-NAS gave > `Host is down` in seconds (no hang) and recovered unaided after samba restart. RESIDUAL for the > operator: the FileBrowser HTTP click-through — its admin credential is customer-held (CC got 401 > on admin/admin and the demo password; by design). ROADMAP R-67 SHIPPED (coupled to R-64). > **2026-07-22 — v0.159.0 (R-66), SHIPPED + deployed to BOTH boxes.** Three legs: „Hálózat" card on > Beállítások → Rendszer (Helyi cím / Hálózati név only-while-Megosztás / Átjáró; „—" fallback), > `network` section in the Debug dump (best-effort per item), and the NetBIOS trap named on the NAS > add form (Szerver helper text + a purely lexical hint on `unreachable` for single-label non-IP > names). > > **DECISION (the load-bearing one): all guest-net reads go through the samba netns door.** The > controller is bridge-netns'd, so `/proc/net/route`/resolv.conf/net.Interfaces in-process answer > for the CONTAINER (172.x / 127.0.0.11) — the S-2 trap. `internal/stacks/guestnet.go` docker-execs > into host-networked felhom-samba (one `guestNetExecFn` seam); Megosztás off ⇒ door closed ⇒ „—" / > in-place error strings, never a plausible-wrong substitute (S-5). Nothing stored anywhere. > > Deploy: 0.159.0 on demo-felhom 9201 (open-door path live: .104/.1/\\FELHOM) AND demo-hp 9201 via > G1 break-glass (closed-door path live: dashes, no name row, in-place dump errors; secret shredded). > demo-hp gotcha worth keeping: the controller 404s on direct container-IP probes without the > customer-domain Host header (`felhom.enkisfelhom.hu` there). Red-proofs A2 + C2 run and recorded. > ROADMAP: R-66 SHIPPED; R-64 (pairing blessed, drill = evidence leg) + R-65 (buddy-box replication, > post-alpha spike-first) minted. NAS doc gained the naming-caveat paragraph. > **2026-07-21 — v0.155.0.** v0.154.0's wizard sourced "is an op running" from `Manager.IsRunning()` > — the CONCURRENCY single-flight, acquired inside the goroutine, and **`RestoreOffboxScratch` never > acquires it**. So the execution step was unreachable for „Ellenőrzés" and the full-restore > preparation: live buttons while a restore downloaded, with the progress banner contradicting the > phase strip on the same screen. Found by the operator on the first live click-through. > > **DECISION: display reads `RestoreStatus()` (the `opRunning` flag), never `IsRunning()`**, through > the named `restoreOpInFlight` seam, and the handler reads the status ONCE per render so the strip, > the suppression and the running-op name cannot diverge. The lesson generalises: `opstatus.go` is the > DISPLAY surface and says so in its own header — the concurrency flag is not a substitute. > > **The test lesson:** a table test over a pure function proves the function, not the caller. Scenario > E passed throughout because it injected `OpRunning=true` directly. The new test drives a real > `Manager` through `BeginRestoreOp` and asserts the render. > > **DECISION: „Eredmény" earns its place.** The strip's highlight is now `Phase`, derived separately > from `Step`: a finished restore is back on the intent step while the strip reads „Eredmény" and an > outcome card shows the result — window-bounded (10 min) and app-bound. > **2026-07-21 — v0.154.0 (R-48).** Collapses the offsite restore controls to a single > „Visszaállítás…" entry per app row plus a per-app wizard at `GET /backups/restore/app?name=`. > The defect it closes is the CAUSE of the round-2 incident: the list rendered up to five inline > forms per row, two of which — the missing-only merge and the true reconstitution — were sibling > buttons whose difference is whether the data comes back. The rule it establishes: *two adjacent > controls whose difference is "your data comes back" vs "your data cannot come back" must never be > distinguishable only by layout.* > > **DECISION: the wizard is server-rendered on the EXISTING endpoints.** No new mutation endpoint, > no JSON state API, no client router. Every card is a real form POST to > `/backup/offbox/{restore,place,reconstitute}` with the same field names and gates, and the server > renders the next step — so it works with JavaScript disabled. `TestRestoreWizard_NoNewMutationEndpoints` > makes that structural: adding a form that posts somewhere new fails the suite by design. > > **DECISION: R-45 stays its own item.** The wizard polls the two existing status surfaces as-is; the > generalized job registry (and with it a real per-phase progress feed) is not built here. > > **DECISION: the step is derived, never requested.** `deriveWizardStep` is pure over (op running, > size-gate flash, scratch ready). Precedence is load-bearing — a running op outranks a stale > `?full_prep=` in the URL, or a commit button reappears mid-restore. While ANY op runs every > mutation form is suppressed server-side rather than offered and then refused with a 409. > > Latent bug found and fixed on the way: `offboxRedirectTo` hardcoded `"?"` when appending its flash, > which would have buried the flash inside `?name=`. **No agent coupling — MinAgent stays > 0.90.0.** 9 new tests + the Group-B red-proof; full suite green. > > **NOT live-validated at commit time by design:** v0.154.0 is published but deliberately NOT > hand-deployed — the operator's hub floor save (0.153.0 → 0.154.0) pulls it via the self-update > path, and that swap IS the R-23(a) single-fire validation (STOP-1). > **2026-07-20 — v0.153.0 (R-47).** Closes the H4 race on **BOTH** restore paths. The replay needs a > running DB container, so both paths started the WHOLE stack first — giving the application a window > to rebuild the schema objects the dump was about to create. Measured at 8 s on 2026-07-19 > (`DIAG-immich-restore-round2-2026-07-19`): immich-server rebuilt `clip_index` two seconds before > the dump's `CREATE INDEX`, the replay aborted `already exists` under `ON_ERROR_STOP=1`, and immich > then reported schema drift. The photos came back **by accident** — `pg_dump` emits COPY before > CREATE INDEX, so the abort landed after the rows; a collision earlier in the script would have left > a genuinely half-restored database, reported identically. > > **DECISION: the DB-only bring-up is done by compose SERVICE scoping**, not by container tricks — > `StartStackServices(name, []string{svc})` → `compose up -d `. Every catalog template's > dependency direction is app→db, so naming the DB starts the DB and nothing else. `docker start > ` was never an option: `StopStack` is `compose down`, so the containers no longer exist. > `RestartStack`/`RedeployFromEnv` are traps here — both end in a full `up -d`. > > **DECISION: fail-closed.** A `.sql` dump with no identifiable DB service refuses BEFORE the first > mutation, on both paths (one Hungarian string, shared). The alternative would be to start everything > and replay into the race. It should be structurally unreachable — `dbTypeForImage` is now shared by > `DiscoverDatabases` and `DBServiceNames`, and a dump can only exist because discovery matched the > container's image, which IS the compose `image:` value — so this is the belt for template drift. > > Enablers: `RedeployFromEnv` split into `PersistUnitRedeployConfig` (persist, starts nothing) + the > unchanged tail; `StackDataProvider.RecreateStackFromUnit` renamed to > `RecreateStackDefinitionFromUnit` because the old name promised less than the method did — the > hidden `up -d` inside it is what carried the defect on the local path. `StartStackServices` REFUSES > an empty list (argument-less `up -d` is a full start). **No agent coupling — MinAgent stays 0.90.0.** > 19 new tests, 3 red-proofs, 23/23 green. **NOT live-validated yet:** STOP-1 supervised reconstitute, > golden 0.153.0 bake (P3 registry-reachability probe from the vacation site is load-bearing), Viktor's > two hub saves, and his C6 customer-restore UI run. > **2026-07-20 — v0.152.0 + felhom-samba 1.1.0 (Megosztás on a Mac).** Closes **S-3**. **A capture > on the box overturned the earlier guess:** macOS DOES send a correct NBNS query for `<20>` and > nmbd DOES answer it correctly in 140 µs (flags `0x8580`, RCODE=0, right address) — macOS simply > never acts on it. NetBIOS there feeds legacy browsing, not `smb://` URL resolution, so **the bare > `smb://` can never work from a Mac** and nmbd was never the broken part (it is what serves > Windows). felhom-samba 1.1.0 adds **avahi + dbus**, templating `avahi-daemon.conf` and the > `_smb._tcp` service file from `FELHOM_SERVER_NAME` so a rename re-advertises; both daemons are > non-fatal on failure. v0.151.0's card had offered `smb://` for Mac — the one dead form — now > `smb://.local`; Windows keeps flat `\\`. Spiked live by hand and confirmed from the > operator's Mac BEFORE publishing the image (the operator's call, and it chose the design too). > **STILL OPEN: Finder-sidebar discovery is NOT shipped** — the record is published and answers > browse queries, but was never observed working; likely a Finder Settings → Sidebar toggle, but > unverified. **Windows was not retested.** Two test bugs fixed en route, neither a production > defect: `TestRenderSambaCompose` pinned a literal image tag, and `TestFabUpload_GCAndIdleTimeout` > asserted an async unlink synchronously (it passed alone, failed in the full package once the new > render tests made `web` heavier). 23/23 green twice; 2 red-proofs. > **2026-07-20 — v0.151.0 (Megosztás).** Closes **S-1/S-2/S-4-core/S-5** of > `felhom.eu/documentation/audits/DIAG-sharing-2026-07-20.md`; **S-3 (no mDNS/Bonjour) stays OPEN**, > awaiting Viktor's `smbutil lookup FELHOM` + `dns-sd -B _smb._tcp` from the Mac. **The `/sharing` > page had been reload-looping at ~1.2 s for every customer with sharing enabled since v0.147.0** — > `/sharing/status` coerced `idle`→`running` on the JOB phase channel, and the client answers a > terminal `running` with a one-shot `location.reload()`, so the first poll of every steady-state > page load re-armed it. The rule this leaves behind, now recorded against R-45 too: **a phase a > client answers with a one-shot action is an EDGE — never synthesise it from a level, and serve it > exactly once.** Both halves are server-side; `sharing.html`'s `