Realtime turn notifications via Firestore, auto-resolve on last submit#169
Realtime turn notifications via Firestore, auto-resolve on last submit#169tsg21 wants to merge 20 commits into
Conversation
…ions
- PRD 03: document auto-resolution on last submission, turn-0 race selection path
- PRD 50: remove POST /resolve; update POST /commands response (turn_resolved, new_turn);
add POST /auth/firebase-token endpoint; update WebSockets design decision; update
GET /games and GET /games/{id} Firestore sourcing note
- PRD 06: add Firestore/GameDirectory section with doc model, security rules, write paths;
remove meta.json.gz from bucket layout; update Storage Adapter Contract; add Firebase
custom token auth section; update Local Development with emulator env vars; update
architecture diagram; remove WebSockets from out-of-scope
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
… deps - New package backend/openstars/game_directory/ with: - base.py: GameDirectory ABC, GameSummary Pydantic model (incl. all_turns_submitted property) - memory.py: InMemoryGameDirectory for unit tests / memory dev mode - firestore.py: FirestoreGameDirectory wrapping firebase_admin Firestore client - factory.py: build_game_directory() reading GAME_DIRECTORY_BACKEND env var - backend/openstars/server/deps.py: add get_game_directory() FastAPI dependency - backend/pyproject.toml: add firebase-admin dependency - tests/server/test_game_directory.py: 19 unit tests covering factory, memory impl, GameSummary.all_turns_submitted, and Firestore mock assertions Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…to game lifecycle POST /commands now resolves the turn synchronously when the last player submits (no separate POST /resolve endpoint). The GameDirectory abstraction is wired into all game-lifecycle routes (create, list, get, commands, designs, race) so that game-summary metadata flows through the directory rather than meta.json.gz. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Mints a Firebase custom token with a `games` claim listing the caller's game IDs, so the frontend can subscribe to Firestore game documents. Warns when the games list hits the 200-entry cap to flag potential truncation. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…layer Game listing and metadata are now owned entirely by GameDirectory (Firestore / InMemory). The meta.json.gz blob format and the storage methods that read/write it are gone. Existing GCS meta blobs become harmless orphans. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- provider.tf: add google-beta provider (required for Firestore/Firebase resources) - firebase.tf: enable Firestore/Firebase/IdentityToolkit APIs, create native-mode Firestore DB in eur3, deploy security rules restricting client reads to token- scoped game IDs - firestore.rules: clients can only read games listed in their custom-token claim - iam.tf: grant backend SA roles/datastore.user and self-referencing roles/iam.serviceAccountTokenCreator (needed for signing Firebase custom tokens) - cloud_run.tf: add GAME_DIRECTORY_BACKEND=firestore and FIREBASE_PROJECT_ID env vars to the backend Cloud Run service Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- firebase.json: emulator config for auth (9099), firestore (8085), UI (4001) - .firebaserc: pin demo project id openstars-local for emulator CLI - docker-compose.yaml: add firebase-emulators service; wire backend to emulators with GAME_DIRECTORY_BACKEND=firestore and emulator host env vars; add Firebase env vars to frontend for emulator mode - AGENTS.md: document emulator ports, full-stack compose command, and env vars Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Initialises Firebase app, Auth, and Firestore with env-var config and optional emulator connections for local dev. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Hook fetches a custom token, signs into Firebase Auth, reads game claims, and schedules proactive token refresh before expiry. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Subscribes to games/{gameId} via onSnapshot; fires onTurnAdvanced when
current_turn increases and onSubmissionsChanged on every snapshot update.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replaces 10s setTimeout polling and manual resolve button with Firestore realtime listener; submit eagerly reloads when turn resolves immediately. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Removes calls to the deleted POST /resolve endpoint from all integration tests; extend int_tests docker-compose with firebase-emulators service; all 36 integration tests pass against the emulator stack. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: dc4c3234eb
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| ); | ||
|
|
||
| return unsubscribe; | ||
| }, [gameId, auth]); |
There was a problem hiding this comment.
Stabilise listener dependencies to avoid resubscribing each render
The Firestore subscription effect depends on auth as an object, and useFirebaseAuth returns a fresh object every render; this causes onSnapshot to be torn down and recreated on unrelated UI state changes (for example command edits), which can increase read churn and create gaps where updates are missed. Depend on stable primitives/callbacks (or memoise the auth object) so the listener only reattaches when auth state actually changes.
Useful? React with 👍 / 👎.
firebase_admin.initialize_app() with no credential falls through to Application Default Credentials (ADC), which don't exist in CI. The Firestore emulator bypasses auth entirely, so ADC are never actually used for requests — but firebase_admin still calls get_credential() during client construction, causing a 500 on every create_game call. When FIRESTORE_EMULATOR_HOST is set, supply a minimal _EmulatorCredential stub that returns AnonymousCredentials instead of trying to load ADC. Production paths (no emulator host) continue to use ADC as before. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…r deps
P1 auth.py: count-based truncation at 200 game IDs does not prevent the
Firebase custom-claims 1 000-byte limit from being exceeded (e.g. 200 UUIDs
exceed it many times over). Replace with _fit_game_ids(), which greedily
builds the longest prefix whose JSON-encoded {"games":[...]} payload fits
within 960 bytes. Also rename _GAMES_LIMIT to _GAMES_FETCH_LIMIT to
clarify its role, and add unit tests for the helper.
P2 useGameNotifications.ts: the useEffect dep on the whole auth object
caused onSnapshot to be torn down and recreated on every unrelated render
because useFirebaseAuth returns a fresh object reference each time.
Destructure stable primitives (claims from React state, refresh from
useCallback) and depend on those instead.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Summary
POST /commandsnow resolves the turn synchronously when the last player submits; the separatePOST /resolveendpoint is removedgames/{gameId}viaonSnapshot— no more 10s polling; both turn advancement and per-player submission status update in real timePOST /api/v1/auth/firebase-tokenendpoint mints custom tokens withgamesclaims;useFirebaseAuthhook manages token lifecycle and proactive refreshmeta.json.gzreplaced entirely by Firestore;GameDirectoryabstraction owns game-summary dataTest plan
cd backend && uv run pytest— 611 unit tests passcd backend && uv run ruff check . && uv run ruff format --check .— clean./backend/int_tests/run.sh— 36 integration tests pass (int_tests docker-compose extended with firebase-emulators)cd frontend && npm run typecheck && npm run lint && npm test— 355 tests pass, no type or lint errorsNotes
terraform apply→ Firestore exists; (2) ship backend + frontend together; (3) optionally delete orphanedmeta.json.gzblobs in GCS🤖 Generated with Claude Code