5271f2b1ec
- Wrap lobby and profile in a shared `lobby-shell.svelte` chrome: page-list sidebar (Overview/Profile) and a top "Player-xxxx" identity strip mirroring the project site's monospace look. - Strip the legacy `lobby.title`, device-session-id `<code>`, and `lobby.greeting` paragraph; the identity strip both names the user and opens the profile editor. - Add a top-level `profile` AppScreen with a three-field form (`display_name`, `preferred_language`, `time_zone`) backed by a new `src/api/account.ts` wrapper around `user.account.get`, `user.profile.update`, and `user.settings.update`. Saving switches the active i18n locale in-place when the new preferred language is one the UI ships translations for. - Update e2e fixture + auth-flow / lobby-flow specs to use the new `lobby-account-name` testid and wait for the loaded identity before releasing pending `SubscribeEvents` (webkit revocation race). New `profile-screen.spec.ts` covers navigation, edit-save, and cancel. - Sync `ui/docs/lobby.md` and `ui/docs/navigation.md` to the new layout. Closes #47
158 lines
8.4 KiB
Markdown
158 lines
8.4 KiB
Markdown
# Lobby UI
|
|
|
|
The lobby is the first authenticated view; the user lands here after
|
|
the email-code login completes (see
|
|
[`docs/auth-flow.md`](auth-flow.md)). This doc captures the shared
|
|
shell, the Overview sections, the profile sub-screen, and the
|
|
defaults baked into the create-game form.
|
|
|
|
## Shell
|
|
|
|
Lobby and profile share a single chrome implemented in
|
|
`lib/screens/lobby-shell.svelte`. The chrome mirrors the project
|
|
site's VitePress layout: a left page-list sidebar (Overview /
|
|
Profile), a top identity strip on the right, and the page content in
|
|
the right-hand column. The shell uses `var(--font-mono)` so the
|
|
post-login pages adopt the "nerdy" type stack that the public site
|
|
already uses.
|
|
|
|
The identity strip renders the caller's `display_name` (falling back
|
|
to the immutable `user_name` handle, then to a loading placeholder
|
|
while `user.account.get` resolves) as a `data-testid="lobby-account-name"`
|
|
button. Clicking it switches the top-level screen to `profile`
|
|
(`appScreen.go("profile")`); the e2e suites use that testid as their
|
|
lobby-loaded signal. The logout button sits next to it
|
|
(`session.signOut("user")`).
|
|
|
|
The sidebar always renders both pages; clicking the active page is a
|
|
no-op. The shell collapses to a horizontal scrolling strip below
|
|
640px.
|
|
|
|
## Overview sections
|
|
|
|
The Overview page renders one column of sections, top to bottom.
|
|
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`) |
|
|
|
|
## Profile sub-screen
|
|
|
|
`lib/screens/profile-screen.svelte` is a top-level `AppScreen` (peer of
|
|
`lobby` and `lobby-create`). The browser Back stack treats it the
|
|
same as the create screen — pushing a fresh history entry on entry,
|
|
falling back to lobby on Back/Forward (see
|
|
[`navigation.md`](navigation.md)).
|
|
|
|
On mount it issues `user.account.get` through `src/api/account.ts`
|
|
and renders an identity read-out (immutable `user_name`, `email`)
|
|
plus a three-field form:
|
|
|
|
| Field | Endpoint | Notes |
|
|
| --------------------- | --------------------- | -------------------------------------------------------------- |
|
|
| `display_name` | `user.profile.update` | Trimmed; empty value clears the stored name (backend PATCH semantics). |
|
|
| `preferred_language` | `user.settings.update`| `<select>` over `SUPPORTED_LOCALES`; if the stored value is unsupported the option is preserved verbatim so a round-trip save does not silently switch it. |
|
|
| `time_zone` | `user.settings.update`| Free-text IANA name. Placeholder shows the browser's current zone; backend validates with `time.LoadLocation`. |
|
|
|
|
Save fires `user.profile.update` and/or `user.settings.update`
|
|
conditionally on which fields actually changed, then returns to the
|
|
lobby (`appScreen.go("lobby")`). When the saved
|
|
`preferred_language` is one the UI also ships translations for, the
|
|
active i18n locale switches in-place so the rest of the session
|
|
matches the new preference.
|
|
|
|
`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`](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.
|