diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..567c5b2 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,60 @@ +name: CI + +on: + push: + branches: [main] + pull_request: + branches: [main] + +jobs: + go-build: + runs-on: ubuntu-latest + defaults: + run: + working-directory: services/realtime + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-go@v5 + with: + go-version: '1.25' + cache-dependency-path: services/realtime/go.sum + - run: go build ./... + + go-test: + runs-on: ubuntu-latest + defaults: + run: + working-directory: services/realtime + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-go@v5 + with: + go-version: '1.25' + cache-dependency-path: services/realtime/go.sum + - run: go test ./internal/... ./cmd/... -v -count=1 -timeout 120s + + web-lint: + runs-on: ubuntu-latest + defaults: + run: + working-directory: apps/web + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: '22' + - run: corepack enable && pnpm install + - run: pnpm run lint + + web-build: + runs-on: ubuntu-latest + defaults: + run: + working-directory: apps/web + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: '22' + - run: corepack enable && pnpm install + - run: pnpm run build diff --git a/.gitignore b/.gitignore index c7b20d6..9594d8e 100644 --- a/.gitignore +++ b/.gitignore @@ -8,6 +8,7 @@ coverage *.exe *.exe~ .env.local +.pg-creds.env package-lock.json client/build/ redis-unstable/ diff --git a/DEPLOY_RAILWAY.md b/DEPLOY_RAILWAY.md index ef66bac..4e72a99 100644 --- a/DEPLOY_RAILWAY.md +++ b/DEPLOY_RAILWAY.md @@ -121,6 +121,7 @@ MATCH_CLAIM_STORE_TTL_SECONDS=43200 ```env MATCHMAKING_TICKET_STORE_BACKEND=redis MATCHMAKING_TICKET_STORE_REDIS_URL=${{redis.REDIS_URL}} +MATCH_SERVICE_INTERNAL_URL=http://${{match-service.RAILWAY_PRIVATE_DOMAIN}}:${{match-service.PORT}} ``` ## First staging checklist @@ -151,3 +152,61 @@ Still recommended after first successful staging deploy: - add custom domains - add monitoring and alerting - add load testing before public launch + +## Rated beta launch checklist + +Use this checklist before calling the hosted `/play` flow launch-ready. + +1. Verify Railway service health: + - `web` loads its public domain + - `gateway` returns `200` from `/healthz` + - `match-service` returns `200` from `/healthz` + - `platform-service` returns `200` from `/healthz` + - `matchmaking-service` returns `200` from `/healthz` +2. Verify backend wiring from the web app: + - open `/status` + - confirm gateway, platform, match, and matchmaking all report healthy + - confirm `/api/gateway/bootstrap` returns healthy downstream state instead of localhost connection errors +3. Verify data backends: + - Railway Redis is attached to `matchmaking-service` and `platform-service` + - Railway Postgres is attached to `platform-service` and `match-service` +4. Run the hosted `/play` smoke test: + - signed-out player can join casual + - signed-out player sees `Sign in to join rated` + - signed-in player can join rated + - queued refresh restores queue state + - matched refresh restores the live match or shows `Return to Match` + - private invite link opens the canonical `/match/:id` route + - a fully occupied invite room resolves to spectate or room-full behavior + +## Beta data reset before launch + +If production beta data becomes noisy, clear it before the public rated beta push so recovery logic is restoring real player state instead of test leftovers. + +### Redis + +Reset stale queue and claim state: + +```text +chess404:matchmaking:tickets +chess404:platform:match-claims +``` + +### Postgres + +Review and clear test-only rows from: + +```sql +guests +finalized_matches +direct_challenges +``` + +Suggested approach: + +1. Remove stale queue tickets and stale match claims first. +2. Delete or archive test guest rows only if you intentionally want a clean guest ladder. +3. Delete abandoned direct challenges that were created for testing and no longer map to a live product scenario. +4. Redeploy the services after cleanup so fresh hosted sessions repopulate state from a known baseline. + +Do not clear production data blindly once real players are using the platform. Snapshot Postgres first, then remove only the stale beta-era rows you intend to reset. diff --git a/PROJECT_STATUS.md b/PROJECT_STATUS.md index 4e4b56e..8abfed9 100644 --- a/PROJECT_STATUS.md +++ b/PROJECT_STATUS.md @@ -256,14 +256,19 @@ Completed: - starting a fresh game now clears old room-bound URL state instead of accidentally reopening the same matched room again - backend now resolves authoritative seat ownership from stored guest ids, not only `white` / `black` placeholder player ids - main app now submits authoritative moves, cards, draw/chat/resign intents using the match seat guest ids when available +- `useMatchTimer` hook extracted from `useMatchEngine`: timer, clock, and abort countdown state are now managed in a dedicated reusable hook +- `useMatchReplay` hook extracted from `useMatchEngine`: board review navigation (`reviewIdx`, `reviewBoard`, `goToSnap`, `reviewFirst/Prev/Next/Last`, `isReviewing`) are now managed in a dedicated reusable hook +- `setup-postgres.ps1` added: one-shot script to create the `chess404` Postgres database and save local credentials to `.pg-creds.env` +- `start-local-postgres.ps1` added: full local stack startup with Postgres backends for platform-service (guests, accounts, archive) and match-service (archive); matchmaking stays on file backend until Redis is available Still missing: - replace the remaining local match authority with server snapshots/events - isolate animation state from gameplay state more cleanly -- split `apps/web/src/App.tsx` into smaller modules - shift focus from gameplay-card migration into platform work: persistence, matchmaking, auth, ratings, reconnects, and replay/history storage - support optimistic UI with rollback from authoritative backend events +- switch `MATCH_CLAIM_STORE_BACKEND` from `memory` to `redis` once a local Redis instance is available +- switch `MATCHMAKING_TICKET_STORE_BACKEND` from `file` to `redis` once a local Redis instance is available ### 5. Product Systems diff --git a/apps/web/app/ClientApp.tsx b/apps/web/app/ClientApp.tsx new file mode 100644 index 0000000..996e9f2 --- /dev/null +++ b/apps/web/app/ClientApp.tsx @@ -0,0 +1,81 @@ +'use client'; + +/** + * ClientApp.tsx + * + * Loads the App shell exclusively on the client side (ssr: false). + * + * Why: App.tsx reads localStorage during state initialisation and calls + * configureMatchServiceRuntime() as a render-time side effect. The server + * generates HTML with empty/null state while the client hydrates with real + * localStorage data, causing React hydration mismatches that surface as + * React error #310 (null dispatcher / invalid hook call). + * + * By skipping SSR for the App boundary we: + * • Eliminate all hydration mismatches (everything initialises fresh on the + * client where localStorage, WebSocket, AudioContext, etc. are available). + * • Keep the root layout as a proper Server Component so fonts, metadata and + * route-level optimisations still work. + * • Show the existing loading skeleton (hostedRuntime === null branch in + * App.tsx) until the JS bundle evaluates – no blank-page flash. + */ + +import dynamic from 'next/dynamic'; +import type React from 'react'; + +// Dynamically import the full App shell, client-side only. +const App = dynamic(() => import('../src/App'), { + ssr: false, + // Render the chess-themed loading skeleton while the bundle loads. + loading: () => ( +