feat(frontend): brew frontend v1 slice — chat + status + brew-now + rating#24
Merged
Leolebleis merged 27 commits intomainfrom May 2, 2026
Merged
feat(frontend): brew frontend v1 slice — chat + status + brew-now + rating#24Leolebleis merged 27 commits intomainfrom
Leolebleis merged 27 commits intomainfrom
Conversation
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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/simplifyreview pass.Backend prep (Tasks 1–7)
/apiprefix;/healthstays at root for Docker HEALTHCHECKchat/projections.py—ModelMessage → ThreadMessageLikeprojection (pure function; pydantic-ai shape stays in DB, projection runs at read time)GET /api/chat/messagesexposes aprojectedfield alongside the raw payloadBagActivated,BagFinished,WaterRefilled) — published byBagService/WaterServiceand broadcast over/api/eventsStaticFilesmount at/for the SPA (gated onfrontend/dist/existing;BREW_FRONTEND_DISTenv var override for non-editable installs)Frontend foundation (Tasks 9–16)
prefers-color-scheme+ manual override, persisted to localStorage)@microsoft/fetch-event-sourceSSE helpers (handle auth header —EventSourcecan't)ExternalStoreRuntimeadapter —Threadcomposed from primitives because it's not a named export at@assistant-ui/react@0.12.28ThreadMessageLikerows on mount, filtering null projections (system prompts)Button,Sheet(Radix Dialog),ToastProvider/ToastItem(Radix Toast)Status, brew, rating, deploy, e2e (Tasks 18–26)
useDevice/useActiveBag/useWaterJournalEntryCreatedqueues the rating toastStatusHeader(live device state, active bag, water + refill, brew CTA, theme toggle)setTimeoutto expiry, not a 1Hz interval)BrewNowSheet— pre-flight gating (lid/basket/carafe), water mL editor,POST /api/schedules/brew-nowRatingToast— 5 stars →PATCH /api/journal/{id}with{rating: n}docker-compose.ymlforwardsFELLOW_API_KEYasVITE_FELLOW_API_KEYbuild arg/simplify pass
Reviewed by three agents (reuse, quality, efficiency). Applied:
apiJsoneverywhere (was silently swallowing 4xx/5xx in three places)pollUntilinterval with one-shotsetTimeoutElapsedleaf component to scope brew-time re-renders["bags","active"]invalidation (prefix match covers it)StatusEventconstants +BREW_POLL_UNTIL_KEYdecodeSseMessagehelper shared between chat runtime and status listenerappendOrExtendStreamParthelper in chat store_BROADCAST_EVENTStuple loop inmain.pyexceptclause + log shape driftThemeTogglenow usesButton variant="ghost"events_scope/close_asgi_task/ SSE helpers to shared e2e conftestDeferred (out of v1 scope, see PR discussion in chat):
activate/decrementModelMessagesTypeAdapter.validate_pythonin chat replayTest plan
uv run pytest— 424 passinguv run ruff check,ty check— cleannpm run lint,typecheck,test,build— all clean (35 tests, build OK)docker build --build-arg VITE_FELLOW_API_KEY=dev -t brew:test .— succeeds; runtime image contains/app/frontend/distdocker compose up, open via Tailscale, send a chat turn, hit Brew now (post-merge smoke)