Files
galaxy-game/ui/docs/lobby.md
T
Ilia Denisov f57a290432 phase 8: lobby UI + cross-stack lobby command catalog + TS FlatBuffers
- Extend pkg/model/lobby and pkg/schema/fbs/lobby.fbs with public-games
  list, my-applications/invites lists, game-create, application-submit,
  invite-redeem/decline. Mirror the matching transcoder pairs and Go
  fixture round-trip tests.
- Wire the seven new lobby message types through
  gateway/internal/backendclient/{routes,lobby_commands}.go with
  per-command REST helpers, JSON-tolerant decoding of backend wire
  shapes, and httptest-based unit coverage for success / 4xx / 5xx /
  503 across each command.
- Introduce TS-side FlatBuffers via the `flatbuffers` runtime dep, a
  `make fbs-ts` target driving flatc, and the generated bindings under
  ui/frontend/src/proto/galaxy/fbs. Phase 7's `user.account.get` decode
  now uses these bindings as well, closing the JSON.parse vs
  FlatBuffers gap that would have failed against a real local stack.
- Replace the placeholder lobby with five sections (my games, pending
  invitations, my applications, public games, create new game) and the
  /lobby/create form. Submit-application uses an inline race-name
  form on the public-game card; create-game keeps name / description /
  turn_schedule / enrollment_ends_at always visible and the rest under
  an Advanced toggle with TS-side defaults.
- Update lobby/+page.svelte to throw LobbyError on non-ok result codes;
  GalaxyClient.executeCommand now returns { resultCode, payloadBytes }.
- Vitest binding round-trips, lobby.ts wrapper unit tests, lobby-page
  + lobby-create component tests, Playwright lobby-flow.spec covering
  create / submit / accept across all four projects. Phase 7 e2e was
  migrated to the FlatBuffers fixtures and to click+fill against the
  Safari-autofill readonly inputs.
- Mark Phase 8 done in ui/PLAN.md, mirror the wire-format note into
  Phase 7, append the new lobby commands to gateway/README.md and
  docs/ARCHITECTURE.md, add ui/docs/lobby.md.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-07 18:05:08 +02:00

6.0 KiB

Lobby UI

The lobby is the first authenticated view; the user lands here after the email-code login completes (see docs/auth-flow.md). Phase 8 introduced the live lobby with five sections, the create-game form, and the TS-side FlatBuffers integration the rest of the client builds on. This doc captures the sections, the application / invite lifecycle the user sees, and the defaults baked into the create-game form.

Sections

The lobby renders one column of sections, top to bottom, with the common content max-width capped at 32rem (same convention as the login page). Cards inside each section take the full available width.

Section Empty state Source Action
create new game (always visible) Navigates to /lobby/create
my games no games yet lobby.my.games.list Click → /games/:id/map (placeholder until Phase 10)
pending invitations no invitations lobby.my.invites.list Accept (lobby.invite.redeem) / Decline (lobby.invite.decline)
my applications no applications lobby.my.applications.list Status badge (pending / approved / rejected)
public games no public games lobby.public.games.list Submit application via inline race-name form (lobby.application.submit)

The header preserves the device-session-id <code> block from the Phase 7 placeholder (kept as a debug affordance) plus a greeting if the gateway returns a display_name for the caller.

Application lifecycle

Submit application on a public-game card toggles an inline race-name form on the same card (no overlay/modal infrastructure yet — the in-game shell that introduces overlays lands later). On submit:

  1. The page calls submitApplication(client, gameId, raceName) from src/api/lobby.ts.
  2. The wrapper builds an ApplicationSubmitRequest FlatBuffers payload, posts it through GalaxyClient.executeCommand, decodes the ApplicationSubmitResponse, and returns an ApplicationSummary plain object.
  3. The lobby page prepends the new application to the my applications list and collapses the inline form. The page does not refresh the public-games list — backend semantics are that the public game still exists and is still in enrollment_open.
  4. Status starts as pending. When the owner approves, backend creates a membership and the next refresh of lobby.my.games.list surfaces the game in my games. When the owner rejects, the application stays terminal in my applications with status rejected.

Invite lifecycle

A pending invite arrives in pending invitations either when the inviter targets the user by id (invited_user_id is set) or when the user redeems a code-based invite from somewhere outside the lobby. The user can accept (lobby.invite.redeem) or decline (lobby.invite.decline):

  • Accept — the invite card disappears, the page refreshes my games, and the freshly-joined game appears there.
  • Decline — the invite card disappears. No membership is created.

Create-game form

The form posts lobby.game.create through the gateway with visibility="private" hard-coded; the user surface never produces a public game (FUNCTIONAL.md §3.3). Fields:

Field Visibility Default Notes
game_name always "" Non-empty client-side check
description always ""
turn_schedule always 0 0 * * * Plain text input, hint says "five-field cron"
enrollment_ends_at always "" <input type="datetime-local">, RFC 3339 on submit
min_players Advanced toggle 2 <details> block
max_players Advanced toggle 8
start_gap_hours Advanced toggle 24
start_gap_players Advanced toggle 2
target_engine_version Advanced toggle v1 Falls back to v1 if blank

On success the page navigates back to /lobby and the new game shows up in my games once the lobby's onMount has had a chance to refresh the list.

Errors

Lobby errors raised by the gateway carry a canonical code (invalid_request, subject_not_found, forbidden, conflict, internal_error). The LobbyError thrown by lobby.ts exposes the code; the page maps it to the matching lobby.error.<code> i18n key and falls back to the gateway-supplied message via lobby.error.unknown for any unknown code.

Why FlatBuffers on the TS side

The gateway encodes lobby payloads through pkg/transcoder/lobby.go into FlatBuffers bytes; the browser must decode them with the same schema. Phase 8 ships:

  • flatbuffers runtime dependency in ui/frontend/package.json;
  • make -C ui fbs-ts driving flatc --ts to regenerate the bindings from pkg/schema/fbs/*.fbs into ui/frontend/src/proto/galaxy/fbs/;
  • a Vitest round-trip suite (tests/lobby-fbs.test.ts) that catches binding drift in CI.

Phase 7's user.account.get decode previously used JSON.parse(TextDecoder); that path was rewritten in Phase 8 to use the same generated AccountResponse table, so the lobby greeting now works against a real local stack as well as the mocked Playwright fixtures.