@@ 1,6 1,6 @@
# lethe-web-ui-foundation
-**Status:** Design
+**Status:** Execute (verify pending)
**Module:** `sourcecraft.dev/bigbes/lethe`
**Branch:** master
**Worktree:** none
@@ 295,3 295,33 @@ Approach: six commits — Phase 1 ships the backend aggregates Home depends on (
### Backwards-compat check
Restating from Design: only `GET /api/v1/sessions` changes shape, additively. The five new fields (`summary`, `turn_count`, `tokens_in_total`, `tokens_out_total`, `model`) are added; nothing is removed or renamed. No client today depends on the response shape (only consumer is the integration test, which we update). No schema migrations; no wire-type changes; no config-key changes.
+
+## Conclusion
+
+### Deviations from plan
+
+- **Phase 2 — vite outDir**: set to `../internal/server/web/dist/` so vite output lands directly in the directory the Go embed will read in Phase 3. Plan implied a `web/dist/` location with an unspecified copy step; collapsing to a single dist location removes that step entirely. Root `.gitignore` updated accordingly (`internal/server/web/dist/*` with `.gitkeep` exception).
+- **Phase 2 — eslint v8 (vs v9)**: ESLint v9 dropped `.eslintrc.cjs` support in favor of flat config; downgrade to `^8.57.1` keeps the plan's config format. Acceptable until a future cleanup migrates to flat config.
+- **Phase 2 — separate `web/vitest.config.ts`**: the TanStack Router vite plugin fails at config-resolve when `src/routes/` is missing or empty; vitest uses its own config that omits the Router plugin so `npm test` runs in this phase before routes exist. Phase 4 will populate routes; the separate file remains useful for test-only config divergence.
+- **Phase 2 — `web/src/routes/__root.tsx` placeholder**: required to satisfy the TanStack Router plugin scan at build time; minimal stub that Phase 4 replaces with the real shell-mounted root.
+- **Phase 2 — `web/src/test-setup.ts`**: implied by the testing infrastructure; sets up `@testing-library/jest-dom` matchers.
+
+- **Phase 3 — `r.Get("/*", …)` vs `r.Handle("/*", …)`**: plan said `Handle` (any-method); implementation uses `Get` (and HEAD). Reason: `Handle` made `POST /healthz` return 200 (SPA HTML) instead of 405 problem+json, regressing the existing `TestRouter_MethodNotAllowedReturnsProblemJSON` invariant. SPA only needs GET (browser navigation); restricting the catch-all is more correct and preserves the 405 behavior for non-GET on unknown paths.
+- **Phase 3 — `TestRouter_NotFoundReturnsProblemJSON` retargeted**: with `/*` mounted, chi returns 405 (not 404) for non-GET on unknown paths because `/*` matches the path. The existing `notFoundHandler` is still wired (in case chi reaches it) but the routing path that previously hit it no longer does. The test now invokes `notFoundHandler` directly to assert its output.
+- **Phase 5 — keyboard `cursor` wiring (retroactive Phase 4 amendment)**: Phase 4 left a static no-op `cursor: { move, activate }` in `__root.tsx`'s keyboard controller. Phase 5 added `cursorRef` + `KeyboardCursorContext` so route-local components can register their cursor and the controller delegates through the ref. This is the pattern the dispatcher's Phase 5 brief instructed; recording so it isn't conflated with Phase 4's omission.
+- **Phase 5 — `useHomeCursor.jumpTo(i)`**: plan specified `move`/`activate` only; `jumpTo` added so `SessionsTable.onCursor` can set the cursor on mouse hover/click. Pure additive surface; no plan invariant violated.
+- **Phase 5 — `FilterChips` "active filter" set is component-local**: plan said filters are URL-driven. The actual filter values (`since`, `tool`, `host`) ARE URL-driven; what's local is "which chips are shown" (e.g. `+ filter` adds `host` to the displayed chip list). No URL representation was specified for that and adding one is gratuitous.
+- **Phase 5 — `ToolDot` tool prop widened from literal union to string**: real ingest data may include tools outside the prototype's five. CSS class still falls through cleanly.
+- **Phase 5 — `session.$tool.$host.$id.tsx` placeholder created**: needed so TanStack Router types accept `Link` navigation from Home. Phase 6 replaces the placeholder with the real Session view.
+- **Phase 6 — `SessionWithTurns extends Omit<Session, 'turns'>`**: `Session.turns` is the aggregate count (number) from Phase 5; `SessionWithTurns.turns` is `Turn[]`. `Omit` resolves the conflict cleanly. SubBar turn-count display reads `session.turns.length`.
+
+### Known minor gaps
+
+- **Turn meta line lacks timestamp**: the `Turn` TS interface in the plan's 6.1 spec did not include `timestamp`, so the meta line shows `# seq · model · tokens-in→tokens-out` only. The DTO has it; adding the field to `Turn` and the meta line is a 5-line follow-up if desired (deferred).
+- **Manual interactive smoke not run** (Phases 4–6): the implementers had no display environment for `vite dev` browser checks. `up:uverify` will cover it.
+- **Docker image not built** (Phase 3): the multi-stage Dockerfile change compiles in principle but was not exercised. `docker build .` smoke recommended before deploy.
+
+### Deferred (needs user input)
+
+- **Phase 3 — CI configuration**: the plan calls for `.github/workflows/*` or `.sourcecraft/ci.yml`; neither directory exists in the repo. The CI job (`npm ci && npm run build && npm test && npm run lint`) is not implemented in this phase. To resume: either confirm sourcecraft.dev's CI file format and create `.sourcecraft/ci.yml`, or add a `.github/workflows/web.yml` if GitHub mirroring is in scope.
+- **Phase 3 — Dockerfile not docker-built**: the multi-stage `node:20-alpine` builder + `COPY --from=web-builder /internal/server/web/dist /src/internal/server/web/dist` was added but not exercised via `docker build`. Recommend a docker-build smoke before the binary is deployed.