feat(lobby): F8-04b hierarchical sidebar + paid-tier gate for create-game #62

Merged
developer merged 4 commits from feature/f8-04b-lobby-restructure into development 2026-05-27 07:18:33 +00:00
Owner

Summary

Reshape the lobby UI from a single Overview into a two-level sidebar (games · profile · DEV synthetic test 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 the 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. UI hides the private-games sub-panel + create button on free tier; VITE_GALAXY_DEV_AFFORDANCES overrides for owner testing.

Backend

  • backend/internal/lobby/deps.go: extend EntitlementProvider with IsPaid(ctx, userID) (bool, error).
  • backend/internal/server/handlers_user_lobby_games.go::Create: check h.svc.IsPaid before svc.CreateGame, return 403 forbidden for free callers.
  • backend/cmd/backend/main.go::userEntitlementAdapter: implement IsPaid over GetEntitlementSnapshot.
  • backend/openapi.yaml: document the new 403 response.

Integration

  • New helper testenv.PromoteToPaid posts to /api/v1/admin/users/{user_id}/entitlements with the permanent tier.
  • Every game-creating integration test (lobby_flow, runtime_lifecycle, notification_flow, engine_command_proxy, lobby_open_enrollment, admin_global_games_view, lobby_my_games) calls PromoteToPaid on the owner before lobby.game.create.
  • New integration/lobby_free_tier_test.go::TestLobbyFlow_FreeUserCreateGameForbidden covers the rejection path.

UI

  • ui/frontend/src/lib/app-nav.svelte.ts: extend AppScreen with games-* literals and synthetic-reports; sanitize fallback resolves invisible screens to lobby.
  • ui/frontend/src/lib/lobby-data.svelte.ts: new session-wide store owning the four lobby panels' fan-out.
  • ui/frontend/src/lib/screens/lobby-shell.svelte: two-level sidebar with desktop always-expanded submenu + mobile dropdown popover.
  • Five new screen components (games-active-past, games-recruitment, games-invitations, games-private-games, synthetic-reports); lobby-screen.svelte becomes a tiny resolver.
  • lobby-create-screen.svelte: inline lobby.create.error.forbidden on LobbyError.code === "forbidden"; refresh lobby data after success.
  • ui/frontend/src/api/account.ts: decode entitlement.is_paid from the FBS EntitlementSnapshot.
  • New i18n keys for the new nav items / empty states.

Docs

  • docs/ARCHITECTURE.md, docs/FUNCTIONAL.md (§3.3), docs/FUNCTIONAL_ru.md mirror, ui/docs/lobby.md, ui/docs/navigation.md, backend/openapi.yaml — all updated to describe the new structure and the tier gate.

Test plan

  • Vitest: decodeAccountView extracts entitlement.isPaid (tests/account-decode.test.ts).
  • Playwright e2e: existing lobby-flow, new lobby-tier-gate and lobby-recruitment-badges specs.
  • Backend integration: TestLobbyFlow_FreeUserCreateGameForbidden + every existing create-game flow with PromoteToPaid (runs in this PR via integration.yaml).
  • Manual smoke on dev-deploy once merged: free / paid scenarios in browser, mobile dropdown via DevTools emulation.

Decisions baked into the diff

  • Tier gate is backend-only (gateway has no is_paid propagation; matches the diplomail precedent).
  • Mobile submenu is a dropdown, not a second scroll row (owner-requested).
  • rejected application keeps the inline race-name form visible (re-apply path).
  • Account cache is not invalidated on mid-session tier change — documented as a known limitation, out of scope for this PR.
## Summary Reshape the lobby UI from a single Overview into a two-level sidebar (`games` · `profile` · DEV `synthetic test 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 the 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. UI hides the `private-games` sub-panel + create button on free tier; `VITE_GALAXY_DEV_AFFORDANCES` overrides for owner testing. ## Backend - `backend/internal/lobby/deps.go`: extend `EntitlementProvider` with `IsPaid(ctx, userID) (bool, error)`. - `backend/internal/server/handlers_user_lobby_games.go::Create`: check `h.svc.IsPaid` before `svc.CreateGame`, return `403 forbidden` for free callers. - `backend/cmd/backend/main.go::userEntitlementAdapter`: implement `IsPaid` over `GetEntitlementSnapshot`. - `backend/openapi.yaml`: document the new `403` response. ## Integration - New helper `testenv.PromoteToPaid` posts to `/api/v1/admin/users/{user_id}/entitlements` with the `permanent` tier. - Every game-creating integration test (`lobby_flow`, `runtime_lifecycle`, `notification_flow`, `engine_command_proxy`, `lobby_open_enrollment`, `admin_global_games_view`, `lobby_my_games`) calls `PromoteToPaid` on the owner before `lobby.game.create`. - New `integration/lobby_free_tier_test.go::TestLobbyFlow_FreeUserCreateGameForbidden` covers the rejection path. ## UI - `ui/frontend/src/lib/app-nav.svelte.ts`: extend `AppScreen` with `games-*` literals and `synthetic-reports`; sanitize fallback resolves invisible screens to `lobby`. - `ui/frontend/src/lib/lobby-data.svelte.ts`: new session-wide store owning the four lobby panels' fan-out. - `ui/frontend/src/lib/screens/lobby-shell.svelte`: two-level sidebar with desktop always-expanded submenu + mobile dropdown popover. - Five new screen components (`games-active-past`, `games-recruitment`, `games-invitations`, `games-private-games`, `synthetic-reports`); `lobby-screen.svelte` becomes a tiny resolver. - `lobby-create-screen.svelte`: inline `lobby.create.error.forbidden` on `LobbyError.code === "forbidden"`; refresh lobby data after success. - `ui/frontend/src/api/account.ts`: decode `entitlement.is_paid` from the FBS `EntitlementSnapshot`. - New i18n keys for the new nav items / empty states. ## Docs - `docs/ARCHITECTURE.md`, `docs/FUNCTIONAL.md` (§3.3), `docs/FUNCTIONAL_ru.md` mirror, `ui/docs/lobby.md`, `ui/docs/navigation.md`, `backend/openapi.yaml` — all updated to describe the new structure and the tier gate. ## Test plan - [x] Vitest: `decodeAccountView` extracts `entitlement.isPaid` (`tests/account-decode.test.ts`). - [x] Playwright e2e: existing `lobby-flow`, new `lobby-tier-gate` and `lobby-recruitment-badges` specs. - [ ] Backend integration: `TestLobbyFlow_FreeUserCreateGameForbidden` + every existing `create-game` flow with `PromoteToPaid` (runs in this PR via `integration.yaml`). - [ ] Manual smoke on dev-deploy once merged: free / paid scenarios in browser, mobile dropdown via DevTools emulation. ## Decisions baked into the diff - Tier gate is backend-only (gateway has no `is_paid` propagation; matches the diplomail precedent). - Mobile submenu is a dropdown, not a second scroll row (owner-requested). - `rejected` application keeps the inline race-name form visible (re-apply path). - Account cache is not invalidated on mid-session tier change — documented as a known limitation, out of scope for this PR.
developer added 4 commits 2026-05-26 22:35:07 +00:00
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
009ea560f9
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>
test(ui): update profile-screen e2e for F8-04b sidebar rename
Tests · UI / test (push) Failing after 11m56s
058c4fcf69
`lobby-nav-overview` is replaced by `lobby-nav-games` (the new parent),
and the empty-games active-past sub-panel is hidden entirely so the
landing testid becomes `lobby-recruitment-empty` (the always-visible
sub-panel for a no-games session).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- lobby-create-screen: call lobbyData.refresh() after a successful
  POST so the new game shows up in the private-games panel
  immediately. The shared lobby-data store is otherwise lazy
  (ensure-on-first-mount), which rendered a stale list across the
  post-create navigation in the e2e suite.
- e2e tests that move between lobby sub-panels now go through
  `window.__galaxyNav.go(...)` rather than clicking the sidebar
  items. The mobile sidebar tucks the submenu behind a dropdown, so
  testid-based clicks fail on the mobile-iphone-13 / pixel-5
  viewports — the dev nav surface bypasses that UX (which has its
  own coverage in `lobby-tier-gate` / future submenu specs).
- game-shell-map missing-membership test: assert
  `lobby-account-name` instead of `lobby-create-button` on
  drop-back-to-lobby (the button moved into the paid-only
  private-games sub-panel; the identity strip is the constant lobby
  chrome).
- inspector-ship-group + ship-group-send synthetic loader specs:
  jump straight to the dev-only `synthetic-reports` top-level
  screen via the dev nav surface before looking for the file
  input (the loader moved off Overview in F8-04b).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
test(ui): F8-04b mobile-safe assertion for free-tier private-games entry
Tests · UI / test (push) Successful in 2m52s
Tests · Integration / integration (pull_request) Successful in 1m50s
Tests · Go / test (pull_request) Successful in 2m3s
Tests · UI / test (pull_request) Successful in 2m52s
f42ab87233
The desktop submenu (.desktop-only) is CSS-hidden on mobile
viewports — the mobile sidebar tucks the same sub-panel entries
behind a dropdown popover. Assert `toBeAttached()` instead of
`toBeVisible()` so the dev-bundle smoke check works on every
viewport.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
owner approved these changes 2026-05-27 07:17:53 +00:00
developer merged commit 3153a95292 into development 2026-05-27 07:18:33 +00:00
developer deleted branch feature/f8-04b-lobby-restructure 2026-05-27 07:18:33 +00:00
Sign in to join this conversation.
No Reviewers
2 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: developer/galaxy-game#62