Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
5ff0eba
Trigger Railway redeploy
jraya106 May 14, 2026
c7a5912
Add board arrows and hosted fallback clarity
jraya106 May 15, 2026
69de798
Clarify hosted practice and single-player queue
jraya106 May 15, 2026
cbe91fa
Fix hosted hydration regression
jraya106 May 15, 2026
a212133
Fallback to polling when match WebSocket is missing
jraya106 May 15, 2026
94c1b1f
Polish hosted board labels and fallback sync
jraya106 May 15, 2026
467e5d3
Gate hosted board behind real queue matches
jraya106 May 15, 2026
7eec988
Fallback hosted queue to direct guest sessions
jraya106 May 15, 2026
c6d80e6
Advance quality-first launch platform
jraya106 May 17, 2026
892df0a
Redesign hosted play hub and harden proxies
jraya106 May 17, 2026
8cad9a1
Redesign Chess404 competitive shell
jraya106 May 17, 2026
5a07350
feat(ui): implement sidebar nav, remove raw JSON from replays, add li…
jraya106 May 17, 2026
c3bc8a2
feat(ui): implement Milestone 1 Visual Polish and scaffold Match Layout
jraya106 May 18, 2026
52d42da
refactor: extract useMatchTimer hook and centralize display/URL helpers
jraya106 May 19, 2026
63e950e
feat: add Postgres startup scripts and update project status
jraya106 May 19, 2026
c3424dd
Fix UI layouts and seat enforcement for online matches
jraya106 May 19, 2026
20f51c9
Update DEPLOY_RAILWAY.md with missing MATCH_SERVICE_INTERNAL_URL envi…
jraya106 May 19, 2026
dc6c223
Fix Railway launch routing and service fallbacks
jraya106 May 21, 2026
f10bc23
Polish UI pages, harden match engine, add gateway/platform service im…
jraya106 May 22, 2026
0ebb0b0
Fix watch feed phantom games, join-to-spectate bug, and invite link u…
jraya106 May 22, 2026
9bf6c9f
Fix viewer seat restore on match restore and assign seat for waiting …
jraya106 May 22, 2026
3b288fb
Stabilize hosted match recovery, live feed boards, queue pruning, cla…
jraya106 May 22, 2026
24668ac
fix: pre-launch bug fixes, PlatformContext typing, TLS, disconnect pr…
jraya106 May 23, 2026
7142bfc
fix: rate limiting, Elo matchmaking, HTTP timeouts, CORS, Docker USER…
jraya106 May 23, 2026
46d2a14
fix: crash bugs, data races, security hardening, and test fixes
jraya106 May 26, 2026
6e8ad14
fix: history page snap-back and Docker build failure
jraya106 May 27, 2026
9ed6486
fix: sign-in page, default Play landing, auto-queue, rate limits, pri…
jraya106 May 27, 2026
5a46ee4
fix: pre-launch audit — P0-P2 security, accessibility, performance, a…
jraya106 May 31, 2026
f946ab6
fix: final round — accessibility, lazy loading, DB migrations, chat r…
jraya106 May 31, 2026
0f8eb56
launch hardening
jraya106 Jun 3, 2026
f626301
fix: add id param to Docker cache mounts
jraya106 Jun 3, 2026
8bdfb53
chore: update platform-service + add capabilities endpoint
jraya106 Jun 4, 2026
d861e69
Merge branch 'codex/launch-hardening'
jraya106 Jun 4, 2026
37deeb8
fix: card pool updates, board fixes, state cleanup
jraya106 Jun 4, 2026
ca81548
fix: remove Docker cache mounts for Railway compat
jraya106 Jun 4, 2026
98fd322
fix: fall back to PORT env var in ListenAddr for Railway compat
jraya106 Jun 4, 2026
4883323
chore: frontend updates across pages
jraya106 Jun 4, 2026
14196ef
fix: guard against index-out-of-range in withCORS middleware (#2)
railway-app[bot] Jun 4, 2026
790c405
chore: client app, layout, internal service updates
jraya106 Jun 4, 2026
0dacc67
fix: remove inline arrow wrapper in PlatformContext useMemo
railway-app[bot] Jun 4, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
60 changes: 60 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ coverage
*.exe
*.exe~
.env.local
.pg-creds.env
package-lock.json
client/build/
redis-unstable/
Expand Down
59 changes: 59 additions & 0 deletions DEPLOY_RAILWAY.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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.
7 changes: 6 additions & 1 deletion PROJECT_STATUS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
81 changes: 81 additions & 0 deletions apps/web/app/ClientApp.tsx
Original file line number Diff line number Diff line change
@@ -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: () => (
<div style={{
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
minHeight: '100vh',
background: '#0a0d16',
color: '#ffbe5a',
gap: '16px',
fontFamily: "'Segoe UI', sans-serif",
}}>
<div style={{ fontSize: '28px', fontWeight: 800, letterSpacing: '2px' }}>
♟ CHESS404
</div>
<div style={{
width: '200px',
height: '4px',
borderRadius: '2px',
background: 'rgba(255,190,90,0.15)',
overflow: 'hidden',
}}>
<div style={{
width: '40%',
height: '100%',
borderRadius: '2px',
background: '#ffbe5a',
animation: 'loadingSlide 1.2s ease-in-out infinite',
}} />
</div>
<style>{`
@keyframes loadingSlide {
0% { transform: translateX(-100%); }
50% { transform: translateX(250%); }
100% { transform: translateX(-100%); }
}
`}</style>
</div>
),
});

interface ClientAppProps {
runtimeConfig?: {
matchServiceHttpBase?: string;
matchServiceWsBase?: string;
};
children?: React.ReactNode;
}

export default function ClientApp({ runtimeConfig, children }: ClientAppProps) {
return <App runtimeConfig={runtimeConfig}>{children}</App>;
}
75 changes: 75 additions & 0 deletions apps/web/app/ShellLayout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
'use client';

import React from 'react';
import { usePathname, useRouter } from 'next/navigation';
import AppShell, { ShellNavGroup, ShellNavItem } from '../src/components/layout/AppShell';
import {
AdminIcon,
CardsIcon,
CommunityIcon,
FriendsIcon,
HistoryIcon,
InboxIcon,
PlayIcon,
ProfileIcon,
StatusIcon,
WatchIcon,
} from '../src/components/layout/icons';

const primaryItems: ShellNavItem[] = [
{ key: '/play', label: 'Play', icon: <PlayIcon /> },
{ key: '/watch', label: 'Watch', icon: <WatchIcon /> },
];

const utilityGroups: ShellNavGroup[] = [
{
label: 'Social',
items: [
{ key: '/rankings', label: 'Rankings', icon: <ProfileIcon /> },
{ key: '/profiles', label: 'Profiles', icon: <ProfileIcon /> },
{ key: '/community', label: 'Community', icon: <CommunityIcon /> },
],
},
{
label: 'Library',
items: [
{ key: '/cards', label: 'Card Index', icon: <CardsIcon /> },
{ key: '/history', label: 'History', icon: <HistoryIcon /> },
],
},
];

export function ShellLayout({ children }: { children: React.ReactNode }) {
const router = useRouter();
const pathname = usePathname();
const [accountOpen, setAccountOpen] = React.useState(false);

// In a real implementation, pageMeta would be dynamic based on pathname
const pageMeta = {
title: pathname === '/' ? 'Home' : pathname.slice(1).charAt(0).toUpperCase() + pathname.slice(2),
};

const handleNavigate = (key: string) => {
router.push(key);
};

return (
<AppShell
brandTitle="Chess404"
brandSubtitle="Arcane Chess Platform"
pageMeta={pageMeta}
primaryItems={primaryItems}
utilityGroups={utilityGroups}
accountLabel="Account"
activeKey={pathname}
onNavigate={handleNavigate}
onOpenAccount={() => setAccountOpen(true)}
>
{children}
{/*
In full implementation, the Auth/Account modal would be rendered here
when accountOpen is true
*/}
</AppShell>
);
}
12 changes: 12 additions & 0 deletions apps/web/app/account/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
'use client';
import React from 'react';
import { usePlatform } from '../../src/contexts/PlatformContext';

export default function Route() {
const platform = usePlatform();
React.useEffect(() => {
platform.setActivePage('Account');
}, [platform.setActivePage]);
return null;
}

12 changes: 12 additions & 0 deletions apps/web/app/admin/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
'use client';
import React from 'react';
import { usePlatform } from '../../src/contexts/PlatformContext';

export default function Route() {
const platform = usePlatform();
React.useEffect(() => {
platform.setActivePage('Admin');
}, [platform.setActivePage]);
return null;
}

Loading
Loading