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
@@ -40,6 +40,15 @@
function describeLobbyError(err: unknown): string {
if (err instanceof LobbyError) {
// Free-tier callers reach this branch when the backend gate
// at `lobby.game.create` rejects them. Show the dedicated
// inline message instead of the generic "operation
// forbidden" — the user got to this screen via the
// `private games` panel, so we want to spell out that the
// gate is the tier (not a permission misconfig).
if (err.code === "forbidden") {
return i18n.t("lobby.create.error.forbidden");
}
const key = `lobby.error.${err.code}` as TranslationKey;
const translated = i18n.t(key);
if (translated !== key) {
@@ -93,7 +102,10 @@
turnSchedule: trimmedSchedule,
targetEngineVersion: targetEngineVersion.trim() || DEFAULT_TARGET_ENGINE_VERSION,
});
appScreen.go("lobby");
// Land on the private-games panel where the freshly created
// game shows up — the lobby-data store will refresh on next
// mount.
appScreen.go("games-private-games");
} catch (err) {
formError = describeLobbyError(err);
} finally {