# REPORT — Indítópult (app launcher) + universal app placeholder icon (v0.163.0)

**Date:** 2026-07-24 · **Repo:** felhom-controller · **Target:** v0.163.0 · No agent coupling; MinAgent unchanged.

## 1. Baseline used

- 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.

## 2. Files created / modified

**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.

**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 `<li>`, 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.

**Tests**
- `controller/internal/web/launcher_test.go` — NEW (8 test functions).
- `controller/internal/web/app_row_test.go` — 2 test functions added (Group D).

**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").

## 3. Commits pushed to `main`

- `987e915` — Indítópult launcher + universal app placeholder (code + CHANGELOG + CONTEXT + README).
- `<REPORT commit>` — this REPORT.md (see git log).
- felhom.eu: `<docs commit>` — capability-map + ROADMAP (explicit paths only; shared worktree).

## 4. Tests + red-proofs

All groups green. **4 red-proofs applied → observed FAIL → restored → re-green** (tree clean after each):

| # | 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 `<a>` 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 |

**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`.

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).

## 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

- Built + pushed `gitea.dooplex.hu/admin/felhom-controller:0.163.0` (145M) from the clean pushed tree
  (`4aa7d41`, HEAD == origin/main). `build.sh 0.163.0 --push` → "Build complete".
- Deployed to guest 9201 via the bootstrap flow (docker pull → `/etc/felhom-controller-image` →
  `systemctl restart felhom-controller-bootstrap.service`). Live:

  ```
  docker ps: gitea.dooplex.hu/admin/felhom-controller:0.163.0  Up (healthy)
  logs:      [INFO] Event pushed: controller_started (info) — Controller elindult (0.163.0)
             [INFO] [offsite-apply] settle-gate: GO — at/above floor 0.156.0 (we are 0.163.0)
  ```

## 7. Live validation

Method: **endpoint-level** (browser automation / `claude-in-chrome` is not available on DooPlex),
run with `curl` inside the `felhom-controller` container on guest 9201 (the controller listens in its
own container netns, not the guest's 127.0.0.1). Verified:

- `GET /static/app-placeholder.svg` → **200, `Content-Type: image/svg+xml`, 359 bytes**, body =
  the `<svg … stroke="#94A6BF" …>` app-grid glyph. The new asset serves live. (Confirms Group D's
  route on the real box.)
- `GET /launcher` (unauthenticated) → **302 → `/login?next=%2Flauncher`** — the page is correctly
  behind `RequireAuth`, and the `next=` round-trips the launcher path. A wrong-password POST to
  `/login` is rejected ("Hibás jelszó") — auth is live.
- `protectedStackSubdomains` on the live box maps only `filebrowser → files`, so FileBrowser is the
  single protected stack eligible for a launcher tile.

**Not done live: the authenticated `/launcher` visual render on this box.** The demo box's dashboard
password has been rotated/customer-claimed away from the stored `~/.config/credentials` value
(`effectivePasswordHash()` DB override now wins), so I could not log in, and I deliberately did NOT
reset the operator's own box password just to render a page. The authenticated render is covered by:
(a) the production-template render tests (`renderBackupPage` loads the real template tree — the HTML
the live box emits is byte-identical to what those tests assert), (b) `TestLauncherRoute_EndToEnd`
(the route + handler + template integrated on a seeded server), and (c) the live 302-to-login above
proving the route + auth-gate are wired. The residual is a manual operator click-through (§8).

## 8. Not yet live-validated (expected residual)

- **Authenticated `/launcher` visual render + a real tile click-through to an app** on guest 9201 —
  HUMAN (needs the current customer-owned dashboard password; browser automation unavailable). Everything
  else in the pipeline is proven (route, auth-gate, handler, template, placeholder asset).
- **Placeholder on a real list surface:** every currently-deployed catalog app on 9201 has a logo,
  so the placeholder does not appear on a live list today; it is covered by
  `TestAppRow_DefaultPlaceholderFallback` + the live `GET /static/app-placeholder.svg` (200,
  `image/svg+xml`, verified above). Stated explicitly per §15.8.

## 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.
