Files
galaxy-game/ui/docs/lobby.md
T
Ilia Denisov e31fb2c17a
Tests · UI / test (push) Failing after 9m28s
docs(ui): sync docs to the app-shell; fix stale nav comments
Rewrite ui/docs (navigation, order-composer, auth-flow, pwa-strategy,
game-state + secondary topic docs) and ui/README for the single-URL
app-shell (in-memory screens/views, Back→lobby via shallow routing,
sessionStorage restore + validation, return-to-lobby). ui/PLAN.md gets a
Phase-10 supersede note (implemented; standalone-compatible). Fix stale
code comments (session-store auth gate, report-sections spec contract).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-23 21:04:11 +02:00

6.2 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). 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) Opens the create screen (appScreen.go("lobby-create"))
my games no games yet lobby.my.games.list Click → enters the game on the map view (activeView.reset() + appScreen.go("game", { gameId }))
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 (kept as a debug affordance) plus a greeting if the gateway returns a display_name for the caller.

GameSummary carries a current_turn field that the lobby UI does not display directly — the in-game shell reads it from the same payload to load the matching user.games.report for the map view without an additional gateway call. See game-state.md for the consumer's view.

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 create screen returns to the lobby (appScreen.go("lobby")) and the new game shows up in my games once the lobby's onMount has had a chance to refresh the list (the lobby screen remounts on return, so its onMount re-fires).

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. The TS integration 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.

user.account.get decodes through the generated AccountResponse table, so the lobby greeting works against a real local stack as well as the mocked Playwright fixtures.