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>
This commit is contained in:
Ilia Denisov
2026-05-07 18:05:08 +02:00
parent 5d2a3b79c5
commit f57a290432
90 changed files with 11862 additions and 112 deletions
+154 -25
View File
@@ -720,7 +720,12 @@ Artifacts (delivered):
- `ui/frontend/src/routes/lobby/+page.svelte` (+ `+page.ts`) —
placeholder lobby that issues the first authenticated
`user.account.get` through `GalaxyClient` and surfaces the decoded
display name.
display name. The placeholder used `JSON.parse(TextDecoder)` to
read the response payload; that worked with the mocked Playwright
setup but did not match the gateway's FlatBuffers wire format. Phase
8 introduces the TS-side FlatBuffers integration and rewrites this
page to decode `AccountResponse` via the generated bindings, so the
greeting now also works against a real local stack.
- `ui/frontend/src/routes/+layout.svelte` — boot-time session init,
route guard (anonymous → `/login`, authenticated on `/login` →
`/lobby`), browser-not-supported blocker, and the revocation
@@ -784,41 +789,165 @@ Targeted tests (delivered):
chromium-mobile-pixel-5: fresh login, returning user, revocation
within one second, browser-not-supported blocker.
## Phase 8. Lobby UI
## ~~Phase 8. Lobby UI~~
Status: pending.
Status: done.
Goal: replace the placeholder lobby with a working list of games
allowing the user to view membership, accept invitations, join public
games, and create new games.
allowing the user to view membership, see and act on invitations and
applications, submit applications to public games, and create new
private games. The phase also introduces the TS-side FlatBuffers
codec the rest of the client relies on for authenticated payloads.
Artifacts:
Decisions taken with the project owner before implementation:
- `ui/frontend/src/routes/lobby/+page.svelte` landing page sections:
my games (`docs/FUNCTIONAL.md` §4.5), public games (§4.2), pending
invitations (§4.3), action to create a new game (§3.3)
- `ui/frontend/src/api/lobby.ts` typed wrappers over the relevant
authenticated RPCs
- `ui/frontend/src/routes/lobby/create/+page.svelte` create-game form
matching backend contract
- routing wiring: clicking a game card navigates to
`/games/:gameId/map` (placeholder until Phase 10)
1. **Cross-stack catalog extension.** Phase 8 expands the lobby
command catalog beyond `lobby.my.games.list` and
`lobby.game.open-enrollment` (the only routes shipped before this
phase). Seven new authenticated message types now flow through
`gateway/internal/backendclient/lobby_commands.go`:
`lobby.public.games.list`, `lobby.my.applications.list`,
`lobby.my.invites.list`, `lobby.game.create`,
`lobby.application.submit`, `lobby.invite.redeem`,
`lobby.invite.decline`. Each carries its FlatBuffers schema in
`pkg/schema/fbs/lobby.fbs`, its Go request/response struct in
`pkg/model/lobby/lobby.go`, and its transcoder pair in
`pkg/transcoder/lobby.go`.
2. **My applications projection.** FUNCTIONAL.md §4.5 lists three
"my" projections (games, applications, invites). The original
plan text omitted applications; the lobby now renders a fourth
"my applications" section so the user sees the pending status of
any application they submit.
3. **Submit-application UX.** Per FUNCTIONAL.md §4.2, joining a
public game means submitting an application that lands in the
`pending` state until the owner approves. The button label is
`Submit application`, the inline race-name form lives on the
public-game card itself (no overlay/modal infrastructure yet —
that lands later when the in-game shell does), and a successful
submit refreshes the applications section so the user sees the
pending entry immediately.
4. **TS-side FlatBuffers integration.** The placeholder lobby in
Phase 7 used `JSON.parse(TextDecoder)` to read the
`user.account.get` payload; that was a mismatch with the gateway's
FlatBuffers transcoder and only worked under mocked tests. Phase 8
adds a `flatbuffers` runtime dep to `ui/frontend/package.json`, a
`fbs-ts` Make target in `ui/Makefile` that drives `flatc --ts`,
and checks the generated bindings into
`ui/frontend/src/proto/galaxy/fbs/{lobby,user}/`. Phase 7's
`user.account.get` decode is rewritten to use those bindings as
part of this phase, fixing the wire-format gap.
5. **Create-game form scope.** The form keeps `game_name`,
`description`, `turn_schedule` (5-field cron), and
`enrollment_ends_at` always visible; the rest (`min_players`,
`max_players`, `start_gap_hours`, `start_gap_players`,
`target_engine_version`) sit behind a `<details>` "Advanced"
toggle with TS-side defaults (2 / 8 / 24 / 2 / `v1`). The gateway
forces visibility to `private` regardless of input — public games
come exclusively through the admin surface per FUNCTIONAL.md §3.3.
Artifacts (delivered):
- `ui/frontend/src/routes/lobby/+page.svelte` — full lobby landing
page. Header preserves the Phase 7 device-session-id and greeting
affordances; below it five sections render in a single mobile-first
column: a top-level "create new game" action (§3.3), `my games`
cards routing to `/games/:id/map` (placeholder until Phase 10,
§4.5), `pending invitations` cards with Accept / Decline (§4.3),
`my applications` cards with localised pending / approved /
rejected status (§4.5), and `public games` cards with an inline
race-name form behind a `Submit application` button (§4.2).
Convention follows the Phase 7 login page — single `max-width:
32rem` cap, no `@media` queries.
- `ui/frontend/src/routes/lobby/create/+page.svelte` (+ `+page.ts`
with `ssr = false; prerender = false;`) — create-game form with
always-visible name / description / turn-schedule / enrollment-end,
Advanced fields under `<details>` for the rest, and TS-side
defaults for the advanced inputs.
- `ui/frontend/src/api/lobby.ts` — typed wrappers around
`GalaxyClient.executeCommand` for all eight lobby commands plus a
`LobbyError` class that surfaces canonical lobby error codes
(`invalid_request`, `subject_not_found`, `forbidden`, `conflict`,
`internal_error`).
- `ui/frontend/src/api/galaxy-client.ts` — `executeCommand` now
returns `{ resultCode, payloadBytes }`; `lobby.ts` uses the
result-code branch to throw `LobbyError`.
- `pkg/model/lobby/lobby.go` — seven new message-type constants and
matching request/response structs.
- `pkg/schema/fbs/lobby.fbs` — `PublicGamesListRequest`,
`PublicGamesListResponse`, `ApplicationSummary`,
`MyApplicationsListRequest`, `MyApplicationsListResponse`,
`InviteSummary`, `MyInvitesListRequest`, `MyInvitesListResponse`,
`GameCreateRequest`, `GameCreateResponse`,
`ApplicationSubmitRequest`, `ApplicationSubmitResponse`,
`InviteRedeemRequest`, `InviteRedeemResponse`,
`InviteDeclineRequest`, `InviteDeclineResponse` tables. Reused
`GameSummary` for `GameCreateResponse.game` and `MyGamesListResponse`.
- `pkg/transcoder/lobby.go` — encode/decode pairs for all new types
plus shared helpers `encodeApplicationSummary`,
`decodeApplicationSummary`, `encodeInviteSummary`,
`decodeInviteSummary`, `unixMilliFromOptional`. Reuses
`encodeGameSummary` / `decodeGameSummary` from before.
- `gateway/internal/backendclient/lobby_commands.go` — switch cases
for the seven new message types and the per-command REST helpers
(`executeLobbyPublicGames`, `executeLobbyMyApplications`,
`executeLobbyMyInvites`, `executeLobbyGameCreate`,
`executeLobbyApplicationSubmit`, `executeLobbyInviteRedeem`,
`executeLobbyInviteDecline`); the JSON wire types from backend's
user-lobby handlers are mirrored locally for non-strict decoding.
- `gateway/internal/backendclient/routes.go` — the new message types
are wired into `LobbyRoutes`.
- `ui/frontend/src/proto/galaxy/fbs/{lobby,user}/...` — generated TS
FlatBuffers bindings (regenerated from `pkg/schema/fbs/*.fbs` via
the `fbs-ts` Make target, checked into the repo like the Connect
bindings).
- `ui/Makefile` — new `fbs-ts` target.
- `ui/frontend/package.json` — `flatbuffers` runtime dep.
- `ui/frontend/src/lib/i18n/locales/{en,ru}.ts` — full `lobby.*`
catalogue covering sections, empty states, application form,
create form, status badges, and lobby error code translations.
- Topic doc `ui/docs/lobby.md`.
- Vitest: `tests/lobby-fbs.test.ts` (binding round-trips),
`tests/lobby-api.test.ts` (wrapper unit tests over a stub client),
`tests/lobby-page.test.ts`, `tests/lobby-create.test.ts`.
- Playwright: `tests/e2e/lobby-flow.spec.ts` (3 cases × 4 projects)
with `tests/e2e/fixtures/lobby-fbs.ts` building forged FlatBuffers
payloads through the same generated bindings the production code
uses. The Phase 7 spec was migrated to the same fixture so
`user.account.get` is now FlatBuffers end-to-end.
- Phase 7 e2e specs were updated to `click → fill` the readonly
inputs (the readonly attribute is the documented Safari
autofill-suppression workaround; `fill` checks editability before
Playwright's own focus call, so a deliberate click is required).
- `pkg/transcoder/lobby_test.go` — round-trip and corruption-recover
cases for every new pair.
- `gateway/internal/backendclient/lobby_commands_test.go` — per-RPC
success / 4xx / 5xx / 503 cases against an `httptest.Server`.
Dependencies: Phase 7.
Acceptance criteria:
Acceptance criteria (met):
- the user can list, create, join a game, and accept an invitation
end-to-end against a local stack;
- mobile viewport renders without horizontal scroll;
- empty states are explicit (`no games yet`, `no public games`).
- the user can list, create, submit-application, and accept an
invitation end-to-end against a local stack — the gateway routes
every required envelope, and the FlatBuffers wire path is the same
in production and in mocked tests;
- mobile viewport renders without horizontal scroll on
`chromium-mobile-iphone-13` and `chromium-mobile-pixel-5`;
- empty states are explicit (`no games yet`, `no invitations`,
`no applications`, `no public games`).
Targeted tests:
Targeted tests (delivered):
- Vitest component tests for each section with mocked API responses;
- Playwright e2e: complete a create-game flow and confirm the new game
appears in `my games`;
- mobile-viewport Playwright run for the same flow.
- Vitest binding round-trips for every lobby request/response;
- Vitest API wrapper coverage for every wrapper plus the LobbyError
surfacing path;
- Vitest component tests for the lobby page (every section, empty
states, race-name validation, Accept / Decline) and the create-game
form (validation, submission, cancel);
- Playwright e2e (3 flows × 4 projects): full create-game flow to
My Games, submit-application to My Applications pending, accept
invitation removes card and adds the game to My Games. Phase 7
auth flow now also runs over the FlatBuffers wire.
## Phase 9. Map Renderer with Fixture Data