Skip to content

feat(frontend): brew frontend v1 slice — chat + status + brew-now + rating#24

Merged
Leolebleis merged 27 commits intomainfrom
feat/frontend-v1-slice
May 2, 2026
Merged

feat(frontend): brew frontend v1 slice — chat + status + brew-now + rating#24
Leolebleis merged 27 commits intomainfrom
feat/frontend-v1-slice

Conversation

@Leolebleis
Copy link
Copy Markdown
Owner

Summary

Ships the full v1 frontend slice for the brew app — chat home, sticky status header, brew-now sheet, post-brew rating toast — plus the backend prep it depends on.

Implements docs/superpowers/plans/2026-05-02-brew-frontend-v1-slice.md (originally three PRs; consolidated here per request) followed by a /simplify review pass.

Backend prep (Tasks 1–7)

  • All API routers move behind /api prefix; /health stays at root for Docker HEALTHCHECK
  • New chat/projections.pyModelMessage → ThreadMessageLike projection (pure function; pydantic-ai shape stays in DB, projection runs at read time)
  • GET /api/chat/messages exposes a projected field alongside the raw payload
  • Three new domain events (BagActivated, BagFinished, WaterRefilled) — published by BagService/WaterService and broadcast over /api/events
  • StaticFiles mount at / for the SPA (gated on frontend/dist/ existing; BREW_FRONTEND_DIST env var override for non-editable installs)

Frontend foundation (Tasks 9–16)

  • React 19 + Vite 7 + TypeScript + Tailwind v4 + Vitest + ESLint
  • Light/dark palette via CSS variables (prefers-color-scheme + manual override, persisted to localStorage)
  • API client + @microsoft/fetch-event-source SSE helpers (handle auth header — EventSource can't)
  • Zustand chat store with streaming-aware reducers (text deltas, tool-call args streaming, thinking, completion/error)
  • assistant-ui ExternalStoreRuntime adapter — Thread composed from primitives because it's not a named export at @assistant-ui/react@0.12.28
  • Replay loads server-projected ThreadMessageLike rows on mount, filtering null projections (system prompts)
  • Generic primitives: Button, Sheet (Radix Dialog), ToastProvider/ToastItem (Radix Toast)

Status, brew, rating, deploy, e2e (Tasks 18–26)

  • TanStack Query hooks: useDevice / useActiveBag / useWater
  • SSE event → query invalidation router; JournalEntryCreated queues the rating toast
  • Sticky StatusHeader (live device state, active bag, water + refill, brew CTA, theme toggle)
    • Polling activates only after a brew launch (one-shot setTimeout to expiry, not a 1Hz interval)
    • Elapsed-time leaf component scopes re-renders during a brew
  • BrewNowSheet — pre-flight gating (lid/basket/carafe), water mL editor, POST /api/schedules/brew-now
  • RatingToast — 5 stars → PATCH /api/journal/{id} with {rating: n}
  • Multi-stage Dockerfile (Node frontend build → Python builder → runtime); docker-compose.yml forwards FELLOW_API_KEY as VITE_FELLOW_API_KEY build arg
  • CI frontend job (lint/typecheck/test/build)
  • Playwright config + theme-persistence spec (chat/brew specs deferred — needed backend Fellow-mock orchestration we didn't want to build for v1)

/simplify pass

Reviewed by three agents (reuse, quality, efficiency). Applied:

  • apiJson everywhere (was silently swallowing 4xx/5xx in three places)
  • Replace 1Hz pollUntil interval with one-shot setTimeout
  • Extract Elapsed leaf component to scope brew-time re-renders
  • Drop redundant ["bags","active"] invalidation (prefix match covers it)
  • Extract StatusEvent constants + BREW_POLL_UNTIL_KEY
  • decodeSseMessage helper shared between chat runtime and status listener
  • appendOrExtendStreamPart helper in chat store
  • _BROADCAST_EVENTS tuple loop in main.py
  • Narrow projection except clause + log shape drift
  • ThemeToggle now uses Button variant="ghost"
  • Lift events_scope / close_asgi_task / SSE helpers to shared e2e conftest

Deferred (out of v1 scope, see PR discussion in chat):

  • Type-universe rewrite (snake_case store ↔ camelCase assistant-ui translation layer)
  • Bag repo signature changes to avoid post-write reads in activate/decrement
  • Batch ModelMessagesTypeAdapter.validate_python in chat replay

Test plan

  • Backend: uv run pytest — 424 passing
  • Backend: uv run ruff check, ty check — clean
  • Frontend: npm run lint, typecheck, test, build — all clean (35 tests, build OK)
  • Docker: docker build --build-arg VITE_FELLOW_API_KEY=dev -t brew:test . — succeeds; runtime image contains /app/frontend/dist
  • Manual: docker compose up, open via Tailscale, send a chat turn, hit Brew now (post-merge smoke)

Leolebleis added 27 commits May 2, 2026 12:01
Frees root path for the SPA static mount in PR-2/3.
/health stays at root for Docker HEALTHCHECK.
Pure function. Maps pydantic-ai message parts to the shape the frontend
will consume (assistant-ui ThreadMessageLike-ish). Raw payload stays in DB.
GET /api/chat/messages now returns each row's payload AND a projected
field shaped for the frontend. Raw payload stays available for forward
compatibility.
Status header in the upcoming frontend needs these to live-update.
Bag and Water services now accept an EventBus and emit status-relevant
events. Auto-finish on decrement-to-zero also fires BagFinished.

Frontend status header (PR-3) consumes these via the broadcaster fanout.
BrewCompleted, BagActivated, BagFinished, WaterRefilled now fanned out
to SSE subscribers (frontend status header in PR-3 consumes them).
StaticFiles mount with html=True at / serves the React build when present,
falls back to index.html for unknown paths (SPA client routing).
Mount is gated on dist/ existing — tests still pass without a build.
Vite 8 + React 19 + TypeScript + Vitest + ESLint. Dev proxy /api -> :8000.
Tracked source only; node_modules + dist are gitignored.
Light/dark palette wired via CSS variables, mode toggle persists to
localStorage, prefers-color-scheme respected when mode=system.
Wraps fetch with API_KEY header. fetch-event-source for both GET /events
and POST /chat/messages SSE streams (native EventSource can't send headers).
Reducer functions per SSE event variant; pure, unit-tested. Store rows
are ThreadMessageLike-shaped so assistant-ui can render them directly.
handleSseEvent routes typed SSE events into the zustand store.
runChat orchestrates a turn (POST + stream consumption).
useChatRuntime exposes the store as an assistant-ui ExternalStoreRuntime.
Loads server-projected ThreadMessageLike rows into the store on mount,
filtering null projections (system prompts).
All Radix-based, palette-aware via CSS vars. Tests cover Button only;
Sheet/Toast exercised by upstream features in PR-3.
QueryClient + AssistantRuntime providers, replay on mount, Thread renders
from the zustand store. Status header / brew flows land in PR-3.

Composed Thread from ThreadPrimitive + MessagePrimitive + ComposerPrimitive
since @assistant-ui/react@0.12.28 doesn't export a styled Thread component
(only primitives + AssistantRuntimeProvider).

Also: rewrite ApiError parameter properties as explicit field
declarations so tsc honours erasableSyntaxOnly during `npm run build`.
Stage 1 builds the SPA via npm; runtime image gets dist/ copied in.
HEALTHCHECK unchanged at /health.

main.py now respects BREW_FRONTEND_DIST env var so the static mount
works with --no-editable installs (Docker) where __file__-relative
path resolution doesn't reach the dist copy.
Minimal Playwright setup: dev-server-only webServer + a single theme
spec that exercises the React app end-to-end without backend.
Backend-dependent specs (chat, brew-now) deferred.
- Use apiJson for brewNow/refill/rating (no more silent failures)
- Replace 1Hz pollUntil interval with one-shot setTimeout
- Extract Elapsed leaf component to scope re-renders
- Drop redundant ['bags','active'] invalidation (prefix match covers it)
- Extract StatusEvent constants + BREW_POLL_UNTIL_KEY
- Add decodeSseMessage helper + share between chat runtime / status listener
- Extract appendOrExtendStreamPart from chat store
- Declarative _BROADCAST_EVENTS tuple in main.py
- Narrow projection except clause + log shape drift
- ThemeToggle uses Button variant=ghost
- Lift _events_scope/_close_asgi_task to shared e2e helper
- tests/chat/test_projections.py: import order (local ruff cache hid I001)
- vite.config.ts: exclude e2e/ — Vitest was collecting Playwright spec
- test-setup.ts: polyfill hasPointerCapture (JSDOM lacks it; Radix uses it)
…echeck

App.tsx: /simplify pass G2 inlined the TextPart wrapper as
`Text: MessagePartPrimitive.Text`, but that primitive types as a span
ref-forwarder, not a TextMessagePartComponent. Restore the function-component
shim so the components.Text slot's type contract holds.

package.json: typecheck was running `tsc --noEmit` against the root
tsconfig.json which has "files": [] and only project references — it
checked nothing. Switch to `tsc -b --noEmit` so project references
are honored. Verified the new script catches the App.tsx regression
cleanly. Build (which uses tsc -b) was the only thing actually
typechecking before this.
@Leolebleis Leolebleis merged commit 553bf6c into main May 2, 2026
4 checks passed
@Leolebleis Leolebleis deleted the feat/frontend-v1-slice branch May 2, 2026 15:00
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant