feat(lobby): F8-04b hierarchical sidebar + paid-tier gate for create-game
Tests · Go / test (push) Successful in 2m17s
Tests · UI / test (push) Waiting to run

Reshape the lobby UI from a single Overview into a two-level sidebar
(games · profile · DEV synthetic-reports) with four games sub-panels
(active-past · recruitment · invitations · private-games). Move the
`create new game` button into the private-games panel, merge the
applications section into recruitment cards as status chips, and add
DEV-only synthetic-report loader as a top-level screen.

Add a paid-tier gate at backend `lobby.game.create`: free callers get
`403 forbidden` before the lobby service is invoked. The UI hides the
private-games sub-panel + create button on free tier (DEV affordances
flag overrides). Update every integration test that creates a game to
use a new `testenv.PromoteToPaid` helper; add a new
`TestLobbyFlow_FreeUserCreateGameForbidden`.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Ilia Denisov
2026-05-26 23:53:53 +02:00
parent 98d1fe6cae
commit 009ea560f9
44 changed files with 2486 additions and 1118 deletions
+50 -11
View File
@@ -17,10 +17,25 @@ for the whole session. The only other routes are the dev/test-only
`/__debug/*` surfaces. What the URL used to encode now lives in two
rune singletons in `src/lib/app-nav.svelte.ts`:
- **`appScreen`** — the top-level screen
(`login` / `lobby` / `lobby-create` / `profile` / `game`) plus the
active `gameId`. It replaces the old `goto`-based redirects and the
`[id]` route param.
- **`appScreen`** — the top-level screen plus the active `gameId`. The
literal values are:
- `login` — anonymous entry point
- `lobby` — historical alias; the dispatcher renders a tiny resolver
that immediately navigates to the first visible games sub-panel
(kept for snapshots persisted before the F8-04b split)
- `lobby-create` — create-game form
- `profile` — profile editor
- `game` — in-game shell (drives `activeView`, see below)
- `games-active-past`, `games-recruitment`, `games-invitations`,
`games-private-games` — the four lobby sub-panels (F8-04b)
- `synthetic-reports` — DEV-only legacy-report loader, gated by
`VITE_GALAXY_DEV_AFFORDANCES === "true"`
It replaces the old `goto`-based redirects and the `[id]` route
param. Sanitize on session-restore allows every literal above, but
the lobby shell's $effect re-routes a restored
`games-private-games` (free tier) or `synthetic-reports` (prod
bundle) to the first visible games sub-panel silently — no toast.
- **`activeView`** — the in-game view (`map` / `table` / `report` /
`battle` / `mail` / `designer-science`) plus the sub-parameters the
old route segments carried (`tableEntity`, `battleId`, `turn`,
@@ -30,16 +45,40 @@ A single-route dispatcher (`src/routes/+page.svelte`) chooses what to
render: it gates on `session.status` (anonymous → login, authenticated
→ the `appScreen.screen`), and for the authenticated tree mounts the
matching screen component from `src/lib/screens/`
(`login-screen.svelte`, `lobby-screen.svelte`,
`lobby-create-screen.svelte`, `profile-screen.svelte`) or, for
`screen === "game"`, the in-game shell
`src/lib/game/game-shell.svelte`. Lobby and profile share a
post-login chrome (sidebar + identity strip) implemented in
`lib/screens/lobby-shell.svelte`; see [`lobby.md`](lobby.md). The game shell in turn renders
the active view from `activeView` (see below). Navigation is
(`login-screen.svelte`, `lobby-screen.svelte` resolver,
`lobby-create-screen.svelte`, `profile-screen.svelte`,
`games-active-past-screen.svelte`, `games-recruitment-screen.svelte`,
`games-invitations-screen.svelte`,
`games-private-games-screen.svelte`,
`synthetic-reports-screen.svelte`) or, for `screen === "game"`, the
in-game shell `src/lib/game/game-shell.svelte`. Every authenticated
non-game screen wraps its body in
`lib/screens/lobby-shell.svelte`, which renders the two-level sidebar
+ identity strip; see [`lobby.md`](lobby.md). The game shell in turn
renders the active view from `activeView` (see below). Navigation is
`appScreen.go(screen, { gameId })` and `activeView.select(view,
params)` — never `goto`.
### Lobby submenu
The lobby shell renders the `games` parent as an always-expanded
submenu on desktop (>640px) whenever the active screen is one of the
`games-*` literals. The submenu order is canonical
(`active-past``recruitment``invitations``private-games`), and
visibility is computed per-render from the
`account.entitlement.is_paid` flag, `lobbyData.myGames.length`, and
the build-time `VITE_GALAXY_DEV_AFFORDANCES` flag — see the
[Games panels](lobby.md#games-panels) table for the rules.
On mobile (≤640px) the sidebar collapses to a horizontal strip
(F8-04). The `games` entry then renders as a single
`games · {active-sub} ▾` button; tapping it opens a popover
(`role="listbox"`) of every visible sub-panel. Tapping a sub-panel
selects it and closes the popover; tapping outside or pressing
`Escape` closes it without changing the active page; re-tapping the
active sub-panel inside the popover is a no-op (the same idiom as the
F8-02 turn-navigator fix).
### Active-view dispatch
The client renders **one active view at a time**. The game shell