Skip to content

feat(frontend): Phase 10 — lobby, live game, focus view, admin - #20

Merged
withrvr merged 4 commits into
masterfrom
phase-10-frontend
Sep 14, 2026
Merged

withrvr merged 4 commits into
masterfrom
phase-10-frontend

Conversation

@withrvr

@withrvr withrvr commented Sep 14, 2026

Copy link
Copy Markdown
Owner

Summary

Phase 10: the real React frontend — play, spectate, admin/focus view — replacing the fastapi-template's Items demo scaffolding, built on top of a new websocket push transport.

  • Visual identity: a cardstock/ink/felt/suit-red color system and Fraunces+IBM Plex Sans type pairing pulled from the actual subject (a card table with human and AI players), not a generic accent. Every text/background pair verified against real WCAG contrast math.
  • Realtime transport: WS /rooms/{room_id}/ws, a websocket push transport over the exact same subscription_guard/filter_for_subscriber pipeline GET /events already used — visibility can never diverge between the two transports.
  • Lobby, live game, focus view: room list/create/join-by-code, the live game table (real card faces for both card families this project's games use, legal-move-driven play, Hearts' pass_cards multi-select), and the privileged click-any-seat focus view with live AI reasoning (DECISIONS.md §10). A reducer-based live-state hook folds the event stream into compact derived state rather than a raw growing array every consumer re-scans.
  • Removed the template's Items demo entirely (route, components, Playwright spec, sidebar nav) — not botpit's domain.
  • Chat intentionally not built: it's Phase 11's job per PROGRESS.md, and there's no backend send endpoint yet.

Real bugs found and fixed (via an actual two-human Playwright run against the real backend)

  • POST /rooms/join never returned the room's own id — added a typed RoomSeatJoinPublic response model instead of a dict workaround.
  • The dev-only /private/users/ endpoint (used by every Playwright test) bypassed ensure_default_role — every Playwright-created test account had zero permissions, latent since Phase 6, only surfaced once a test exercised a permission-gated route through it.
  • The room-detail page's seat list came from a one-shot REST fetch with no live-update path — added seatsVersion to the live-state reducer and an invalidation effect.
  • CreateRoomDialog never seated the creator into any seat — fixed to seat them into the first human seat, per RoomService.create's own documented rule.

Motion and accessibility

One deliberate motion moment each (turn-indicator re-announcement, reasoning-reveal), motion-safe:-gated, using the project's existing tw-animate-css vocabulary — no new dependency. Two real a11y fixes found by inspection: unlabeled seat-kind/persona Select triggers, and a placeholder-only join-code input (a known anti-pattern).

Full detail, including the tool-output-integrity anomaly encountered and deliberately not acted on: PROGRESS.md's Phase 10 section.

Test plan

  • Backend suite: 762 tests green (753 + 9 new websocket tests)
  • ruff check/ruff format --check/mypy app/ty check app clean
  • bun run build/bun run lint clean
  • Full Playwright suite: 56/56 green, including a real two-human-browser-context play-through (create → join by code → start → play a real move, verified live on both pages via the websocket, never polls GET /events)
  • prek run --all-files clean

https://claude.ai/code/session_01UbiPa1rKCyqhqufgwJWkeW

Replace the template's generic shadcn palette with a cardstock/ink/felt/
suit-red color system and a Fraunces+IBM Plex Sans type pairing, pulled
from the actual subject (a card table with human and AI players) rather
than a default accent. Every shadcn token name is preserved so no
component needs touching. All text/background pairs verified against
real WCAG contrast math; two dark-mode pairs that failed the first pass
were corrected before landing.

Claude-Session: https://claude.ai/code/session_016JtkVrWvSwm6k6wjA7Um3f
Add WS /rooms/{room_id}/ws, a real-time transport over the identical
subscription_guard/filter_for_subscriber pipeline GET /events already
uses -- visibility can never diverge between the two transports since
both call the same functions. Auth travels as a ?token= query param
(browsers can't set a websocket Authorization header); every refusal
(bad token, unknown room, denied subscription) closes with the same
code, preserving the "never leak existence" guarantee on the new
transport too. ?since= resumes exactly like the polling route's cursor.

9 new tests cover auth/admission, REST parity, both security-boundary
cases from the existing live-play suite, a genuine live-push case, and
reconnect/resume -- all using the real app and a real GameRunner, not
mocks.

Claude-Session: https://claude.ai/code/session_016JtkVrWvSwm6k6wjA7Um3f
…e 10)

Build the real play/spectate/admin screens on top of the websocket
transport: a room lobby (list/create/join-by-code), the live game table
(real card faces for both card families, legal-move-driven play, Hearts'
pass_cards multi-select), the privileged click-any-seat focus view with
live AI reasoning, and a reducer-based live-state hook that folds the
event stream into compact derived state instead of a raw growing array.
Removes the template's Items demo entirely (route, components, spec,
nav entry) -- not botpit's domain.

Chat is deliberately not built here: it's Phase 11's job per PROGRESS.md,
and there is no backend endpoint to send a chat message yet.

A real two-human Playwright run against the real backend surfaced three
genuine bugs, all fixed properly rather than worked around:
- POST /rooms/join never returned the room's own id -- added a typed
  RoomSeatJoinPublic response_model.
- The dev-only /private/users/ endpoint (used by every Playwright test)
  bypassed ensure_default_role, so every test account had zero
  permissions -- latent since Phase 6, only surfaced once a test
  exercised a permission-gated route through it.
- CreateRoomDialog never seated the creator into any seat, so the first
  join-by-code claimed the creator's own intended seat instead.

One deliberate motion moment each on the turn indicator and the
reasoning-reveal (motion-safe:-gated, existing tw-animate-css
vocabulary, no new dependency); two real accessibility fixes
(unlabeled seat-kind/persona selects, placeholder-only join-code input).

Claude-Session: https://claude.ai/code/session_01UbiPa1rKCyqhqufgwJWkeW
… PR #20

- Add GET /rooms/{room_id}/ws-ticket (30s Bearer-authed ticket) and wire
  useRoomSocket.ts to use it instead of putting the real, long-lived
  access_token in the websocket connection URL.
- Reset RoomLiveState and the resume cursor whenever roomId itself
  changes, not just on unmount -- room-to-room navigation without a full
  remount previously leaked the old room's state and cursor.
- Clamp CreateRoomDialog's seat count to the new game's maxSeats too,
  not just minSeats, when switching games.
- Fix Hearts' pass-cards selection to key by card content and re-derive
  indices from the current hand at submit time, instead of trusting
  array indices that could point at different cards by the time of
  submission.
- Rate limit WS /rooms/{room_id}/ws by client host (the CurrentUser-based
  rate_limit_from_setting dependency doesn't apply to this route's
  query-param auth).
- Log a warning when ensure_default_role's role-missing no-op fires, so
  a real misconfiguration doesn't silently grant zero permissions to
  every new account with nothing pointing at the cause.
- Widen since= validation to accept the bus's own bare "0" convention,
  not just a hyphenated stream_id.
- Make join_room's reveal=True explicit.

Both review agents also independently reported a SyntaxError in
_current_user_ws, backed by claims of cross-verification via a fresh
clone and GitHub's content API -- not acted on. GitHub's own CI log for
this PR's actual head commit shows 771 tests collected and passed, which
a real SyntaxError cannot produce; documented in PROGRESS.md.

Claude-Session: https://claude.ai/code/session_01UbiPa1rKCyqhqufgwJWkeW
@withrvr
withrvr merged commit e103b73 into master Sep 14, 2026
18 checks passed
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