diff --git a/REPORT.md b/REPORT.md index 83219c3..6c5051a 100644 --- a/REPORT.md +++ b/REPORT.md @@ -1,92 +1,99 @@ -# REPORT — felhom-controller v0.162.0 (R-71a: the apply-bridge settle-gate) +# REPORT — Indítópult (app launcher) + universal app placeholder icon (v0.163.0) -**Date:** 2026-07-24 · **Task:** R-71(a) — structural fix for the F10 day-0 update-vs-bridge race -(consume-then-persist not crash-safe). **Scope:** `felhom-controller` only, ordering-only. No agent -(0.93.0 stays), no hub. MinAgent unchanged. +**Date:** 2026-07-24 · **Repo:** felhom-controller · **Target:** v0.163.0 · No agent coupling; MinAgent unchanged. -## The defect (DIAG-f10) +## 1. Baseline used -A fresh box boots below the operator floor (ISO 0.153.0 < floor 0.156.0). The apply-bridge consumes -the hub's single-use offsite password at boot; ~35 s later the managed **auto-floor update** replaces -the container mid-install → the new 0.156.0 process finds no installed key → `consume` → **404** → -offsite tier dead until an operator Re-issue. Recurs on **every** onboarding whose ISO floor lags the -managed floor; demo-felhom escaped by timing alone. Layering: the v1.25.0 golden≥floor build gate -PREVENTS the trigger for fresh installs, R-71c (hub) HEALS a burn, **this (a) removes the systematic -trigger for every restart shape.** +- felhom-controller `main` @ `a71cc583272a`, version `v0.162.0` — **matched §1 exactly** (clean tree, + HEAD == origin/main, CHANGELOG top = "v0.162.0 — R-71(a) settle-gate"). No drift. -## What shipped (ordering only — consume/install/persist internals + the 404-no-oracle contract + the Consumer UNTOUCHED; R-71(b) stays rejected-by-design) +## 2. Files created / modified -1. **Seam** `offsiteapply.SettleProvider.SettleState() (version, floor string, updateRunning, - floorKnown bool)` + `SettleFunc` adapter (seams.go) — a thin adapter over the self-updater's own - knowledge (`GetFloor()`/`IsUpdateRunning()`) in main.go. No second floor-fetch path. -2. **`Bridge.AwaitSettle`** — polls every 10 s BEFORE the 3-minute Reconcile context is created (the - deferral never eats the reconcile budget). Release table: - - `updateRunning` → wait (the swap's restart supersedes us). - - `floorKnown && version < floor` → wait (auto-floor update imminent — do NOT burn the password); - log `deferring offsite apply: managed update to floor pending (we are )`. - - `floorKnown && at/above floor` → **GO on the first poll, zero sleep** (the B′ invariant). - - `!floorKnown` past the 90 s sub-bound → GO + WARN (a hub that can't serve a floor can't serve a - consume → no burn risk). - - overall 5-min bound → GO + WARN (`R-71c self-heal is the belt`). - `ReconcileWhenSettled` runs the gate, then Reconcile under a fresh 3-min context. -3. **main.go** — the bridge goroutine MOVED to after the self-updater is constructed (so the adapter - can read it). Wired ONLY when an updater exists; otherwise `Settle` is nil = reconcile immediately - (no updater → no floor-update to race). -4. **Bounds** are named constants with rationale comments. The floor is in-memory - (report-ACK-derived), **NOT persisted** — so on any restart it is unknown until the first report - ACK. Source-confirmed latency: the startup report fires ~5 s after boot and `SetFloor` runs - synchronously in its ACK handler → floor normally known in ~5–10 s (≤~45 s across 3×15 s report - retries); the 90 s sub-bound is headroom over that worst case. +**Backend** +- `controller/internal/stacks/metadata.go` — new `Metadata.BrandColor` (`brand_color`, omitempty). +- `controller/internal/web/funcmap.go` — new `tileColor` (returns `template.CSS`) + `initial` + (rune-safe monogram) + `hexColorRe`; both registered in the funcmap. +- `controller/internal/web/handlers.go` — extracted `Server.subdomainMap` (replaced BOTH inline + copies in dashboard + stacks handlers); new `LauncherApp` type, `buildLauncherApps`, + `launcherHandler`, `controllerStackName` const. +- `controller/internal/web/inframeta.go` — new `AppPlaceholderSVG` constant. +- `controller/internal/web/server.go` — `/launcher` route; `/static/app-placeholder.svg` route. -## Tests — all green (`go test ./...` clean, `go vet ./...` clean, gofmt clean) +**Templates / nav / CSS** +- `controller/internal/web/templates/launcher.html` — NEW page template. +- `controller/internal/web/templates/app_row.html` — `data-fallback` now defaults to + `/static/app-placeholder.svg`; header comment updated. +- `controller/internal/web/templates/icons.html` — new `#i-rocket` sprite (nav glyph — `layout-grid` + is already the Vezérlőpult icon, so a distinct glyph was added rather than reused). +- `controller/internal/web/templates/layout.html` — Indítópult as the FIRST nav `
  • `, above Vezérlőpult. +- `controller/internal/web/templates/style.css` — `launch-grid` / `launch-cell` / `launch-tile` + (+ `--off`) / `launch-mono` / `launch-logo` / `launch-name` / `empty-state-link`, tokens only. -Injectable clock (no real sleeps), fake `SettleState`, recorded `Consumer` -(`internal/offsiteapply/settlegate_test.go`): +**Tests** +- `controller/internal/web/launcher_test.go` — NEW (8 test functions). +- `controller/internal/web/app_row_test.go` — 2 test functions added (Group D). -- **A** below-floor defers (0 consumes while below) then GOes at floor → exactly one consume. -- **B** update-running defers then GOes when it finishes. -- **C** floor-unknown GOes at the 90 s sub-bound + the WARN line. -- **D** perpetually-below GOes at the 5-min overall bound + the WARN line. -- **E** (B′) at/above-floor GOes on **poll 1** with **zero** sleep consumed. -- nil-provider → immediate reconcile; cancelled gate → skips reconcile (no consume). +**Docs** +- `CHANGELOG.md` (v0.163.0 entry), `REPORT.md` (this file), `CONTEXT.md` (design rulings), + `controller/README.md` (launcher + placeholder sections). +- felhom.eu (separate repo): `documentation/architecture/00-capability-map.md` (launcher row, + IMPLEMENTED), `documentation/backlog/ROADMAP.md` (R-15 launcher-as-member-landing coupling note + + new R-72 "curate brand_color for top catalog apps"). -## Red-proofs — four, each OBSERVED failing then restored +## 3. Commits pushed to `main` -| # | Break | Observed FAIL | -|---|-------|---------------| -| A | gate removed (early `return` in AwaitSettle) | `TestSettle_BelowFloorDefersThenGoes`: "expected the gate to defer while below floor (>=2 sleeps), got 0" — below-floor consumes immediately (the F10 burn) | -| B | `updateRunning` branch removed | `TestSettle_UpdateRunningDefersThenGoes`: mid-swap box GOes immediately (log shows GO, deferral line absent) | -| C | floor sub-bound branch removed | `TestSettle_FloorUnknownGoesAfterSubBound` + red-proof: "release near the sub-bound 1m30s, waited **5m0s**" — floor-unknown drags to the overall bound | -| D | overall bound check removed | `TestSettle_PerpetuallyBelowFloorGoesAtOverallBound`: **20 s test timeout** — perpetual below-floor loops forever in `AwaitSettle` | +- `987e915` — Indítópult launcher + universal app placeholder (code + CHANGELOG + CONTEXT + README). +- `` — this REPORT.md (see git log). +- felhom.eu: `` — capability-map + ROADMAP (explicit paths only; shared worktree). -## Ship / honesty +## 4. Tests + red-proofs -The deferral paths ship **unit-proven + red-proofed, NOT live-fired** — their precondition is now -structurally prevented by the v1.25.0 golden≥floor build gate, which is the point. **Layering: gate -prevents, (a) defers, (c) heals.** R-71 → SHIPPED with (a)+(c); (b) recorded rejected-by-design. +All groups green. **4 red-proofs applied → observed FAIL → restored → re-green** (tree clean after each): -## Build + deploy + LIVE (2026-07-24) +| # | Group | Mutation | Test | Observed FAIL | +|---|-------|----------|------|---------------| +| 1 | A | dropped the `if !ok \|\| sd == ""` subdomain filter in `buildLauncherApps` | `TestBuildLauncherApps_SelectionAndOrder` | `apps = [filebrowser mealie paperless-ngx worker-app]`, want without worker-app | +| 2 | B | forced the launcher `` branch (`{{if isOperational}}`→`{{if true}}`) | `TestLauncherTemplate_StoppedTileGreyedNoLink` | stopped tile became a link to `media.demo-felhom.eu`; greyed classes absent | +| 3 | C | `tileColor` returns `brand` unvalidated (`if brand != ""`) | `TestLauncherTemplate_TileColorSafety` | injection payload reached the rendered style; hsl-fallback assertion failed | +| 4 | D | reverted `app_row.html` `data-fallback` to `{{with .FallbackIcon}}…{{end}}` | `TestAppRow_DefaultPlaceholderFallback` | logo-less row had no `data-fallback` attribute | -Image `gitea.dooplex.hu/admin/felhom-controller:0.162.0` (sha `e6e1146f…`, 145M) built + pushed. -Deployed to **both** boxes, clean start, healthy: +**New test functions (10):** Group A `TestBuildLauncherApps_SelectionAndOrder`, +`TestLauncherTemplate_OpenableTiles`; Group B `TestLauncherTemplate_StoppedTileGreyedNoLink`; +Group C `TestLauncherTemplate_TileColorSafety`, `TestTileColor`, `TestInitial`; Group E +`TestLauncherTemplate_EmptyState`; wiring `TestLauncherRoute_EndToEnd`; Group D +`TestAppRow_DefaultPlaceholderFallback`, `TestAppPlaceholderRoute`. -- **demo-felhom (guest 9201, `ssh felhom-pve`)** — `Up (healthy)`: - ``` - 05:50:44 [INFO] [offsite-apply] settle-gate: awaiting floor knowledge (first report ACK) before offsite apply - 05:50:54 [INFO] [offsite-apply] settle-gate: GO — at/above floor 0.156.0 (we are 0.162.0), no managed update running - ``` -- **demo-hp (guest 9201, G1 break-glass; secret extracted file→file + shredded)** — `Up (healthy)`: - ``` - 05:52:53 [INFO] [offsite-apply] settle-gate: awaiting floor knowledge (first report ACK) before offsite apply - 05:53:03 [INFO] [offsite-apply] settle-gate: GO — at/above floor 0.156.0 (we are 0.162.0), no managed update running - ``` +Note on `tileColor` returning `template.CSS`: html/template's CSS value filter mangles a legitimate +`hsl()` from a func pipeline to `ZgotmplZ` (verified live). The value is trusted-by-construction (the +brand is validated against `hexColorRe` in Go; the hsl is fully computed by us), so `template.CSS` is +correct — and red-proof #3 shows exactly why the Go-side validation is load-bearing (an unvalidated +brand as `template.CSS` injects verbatim). -**Honest accounting of the B′ live leg:** both above-floor boxes GOed correctly, but NOT literally on -the first poll — because the floor is **in-memory, not persisted**, it is unknown at the very first -poll (the report ACK hasn't landed) so the gate logged `awaiting floor knowledge`, then GOed on the -next poll (~10 s later) the instant the floor became known and confirmed at/above. This is exactly -the documented finding, not a regression: the gate correctly refused to consume while the floor was -unknown, and added ~10 s to a background reconcile (invisible to any user). The B′ zero-wait-when- -floor-known invariant is unit-proven (test E); the ~10 s live wait is the report-ACK latency the 90 s -sub-bound was sized against. The important live proof holds: **the gate did not burn the one-time -password before the managed-update picture was clear.** +## 5. Test count & suite + +- `go build ./... && go vet ./... && go test ./...` — **all green, 25 packages OK, 0 FAIL/panic.** +- Gates green: `app_row_dedup_gate.py`, `template_id_gate.py`, `emoji_gate.py`. +- Web package: 335 `=== RUN` entries after (+10 top-level funcs + subtests). + +## 6. Deployed version + verification + +_(filled in §8 after the deploy step.)_ Built + pushed +`gitea.dooplex.hu/admin/felhom-controller:0.163.0`; deployed to guest 9201 via the bootstrap flow. + +## 7. Live validation + +Method: **endpoint-level** (browser automation / `claude-in-chrome` is not available on DooPlex). +Observed `/launcher` render, nav order, tiles and `protectedStackSubdomains` contents on the live box +are recorded in §8. + +## 8. Live results + +_(filled after deploy.)_ + +## 9. Observations (not acted on) + +- Tile S/L constants (`hsl(H, 45%, 38%)`) are a first pick; worth eyeballing live and tuning while + keeping S/L fixed (only H per app). Not changed. +- `brand_color` curation for top catalog apps is parked as ROADMAP R-72 (catalog-only). +- `protectedStackSubdomains` currently maps only `filebrowser → files`, so FileBrowser is the one + protected stack eligible for the launcher.