From ecfc9251ecd49128fbe26def3deb8fcc63ee1f1b Mon Sep 17 00:00:00 2001 From: Scott Hadfield Date: Sun, 29 Mar 2026 08:56:16 -0700 Subject: [PATCH] Add homepage game discovery and live feed Redesign homepage with rich game cards showing player handles, status, move count, and relative time. User's games fetched from Constellation and deduplicated (challenger's record only. Split into active and completed sections. Live feed connects to Jetstream with 2-hour historical cursor to show active games across the network, visible to all visitors. Add standalone firehose logger script (npm run firehose) that writes all game and challenge events to data/firehose.jsonl for future stats. EOF ) --- .gitignore | 3 + package.json | 3 +- scripts/firehose-logger.ts | 70 +++++++++ src/lib/components/GameCard.svelte | 90 +++++++++++ src/lib/microcosm.ts | 11 ++ src/routes/+page.svelte | 233 +++++++++++++++++++++++------ 6 files changed, 360 insertions(+), 50 deletions(-) create mode 100644 scripts/firehose-logger.ts create mode 100644 src/lib/components/GameCard.svelte diff --git a/.gitignore b/.gitignore index 3b462cb..51f349b 100644 --- a/.gitignore +++ b/.gitignore @@ -21,3 +21,6 @@ Thumbs.db # Vite vite.config.js.timestamp-* vite.config.ts.timestamp-* + +# Firehose logs +/data diff --git a/package.json b/package.json index e9b2e27..ba5dd05 100644 --- a/package.json +++ b/package.json @@ -11,7 +11,8 @@ "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch", "test": "vitest run", - "test:watch": "vitest" + "test:watch": "vitest", + "firehose": "npx tsx scripts/firehose-logger.ts" }, "devDependencies": { "@sveltejs/adapter-static": "^3.0.10", diff --git a/scripts/firehose-logger.ts b/scripts/firehose-logger.ts new file mode 100644 index 0000000..4a772aa --- /dev/null +++ b/scripts/firehose-logger.ts @@ -0,0 +1,70 @@ +import { createWriteStream } from 'fs'; +import { join, dirname } from 'path'; +import { fileURLToPath } from 'url'; +import { mkdir } from 'fs/promises'; + +const JETSTREAM_URL = 'wss://jetstream2.us-east.bsky.network/subscribe'; +const COLLECTIONS = ['blue.checkmate.game', 'blue.checkmate.challenge']; + +const __dirname = dirname(fileURLToPath(import.meta.url)); +const logDir = join(__dirname, '..', 'data'); +const logFile = join(logDir, 'firehose.jsonl'); + +await mkdir(logDir, { recursive: true }); +const stream = createWriteStream(logFile, { flags: 'a' }); + +let cursor: number | null = null; +let reconnectAttempts = 0; + +function connect() { + const url = new URL(JETSTREAM_URL); + for (const c of COLLECTIONS) { + url.searchParams.append('wantedCollections', c); + } + if (cursor) url.searchParams.set('cursor', String(cursor)); + + console.log(`[firehose] connecting...${cursor ? ` (cursor: ${cursor})` : ''}`); + const ws = new WebSocket(url.toString()); + + ws.onopen = () => { + console.log('[firehose] connected'); + reconnectAttempts = 0; + }; + + ws.onmessage = (event) => { + const data = JSON.parse(String(event.data)); + if (data.time_us) cursor = data.time_us; + if (data.kind !== 'commit') return; + + const line = JSON.stringify({ + time: new Date().toISOString(), + time_us: data.time_us, + did: data.did, + operation: data.commit.operation, + collection: data.commit.collection, + rkey: data.commit.rkey, + record: data.commit.record ?? null, + }); + + stream.write(line + '\n'); + console.log( + `[firehose] ${data.commit.operation} ${data.commit.collection} ${data.did.slice(0, 24)}...` + ); + }; + + ws.onclose = (event: CloseEvent) => { + console.log(`[firehose] disconnected: ${event.code} ${event.reason}`); + const delay = Math.min(1000 * Math.pow(2, reconnectAttempts), 30_000); + reconnectAttempts++; + console.log(`[firehose] reconnecting in ${delay}ms...`); + setTimeout(connect, delay); + }; + + ws.onerror = () => { + ws.close(); + }; +} + +console.log(`[firehose] logging to ${logFile}`); +console.log(`[firehose] watching: ${COLLECTIONS.join(', ')}`); +connect(); diff --git a/src/lib/components/GameCard.svelte b/src/lib/components/GameCard.svelte new file mode 100644 index 0000000..f203ded --- /dev/null +++ b/src/lib/components/GameCard.svelte @@ -0,0 +1,90 @@ + + + +
+
+ {whiteHandle} + vs + {blackHandle} +
+ {#if record.status === 'active'} + Live + {:else if record.status === 'waiting'} + Waiting + {/if} +
+
+ {statusText()} + {timeAgo(timestamp ?? record.createdAt)} +
+
diff --git a/src/lib/microcosm.ts b/src/lib/microcosm.ts index a5cf303..5a7219e 100644 --- a/src/lib/microcosm.ts +++ b/src/lib/microcosm.ts @@ -33,3 +33,14 @@ export async function resolveIdentity(identifier: string): Promise(); + +export async function resolveHandle(did: string): Promise { + const cached = handleCache.get(did); + if (cached) return cached; + const profile = await resolveIdentity(did); + const handle = profile?.handle ?? did; + handleCache.set(did, handle); + return handle; +} diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte index f44d831..5e0fb88 100644 --- a/src/routes/+page.svelte +++ b/src/routes/+page.svelte @@ -1,47 +1,144 @@ -
-
+
+

checkmate.blue

Chess on the Atmosphere

-
- - {#if auth.isLoggedIn} -
-

- Signed in as {auth.handle} -

-
+ {#if auth.isLoggedIn} + -
- - {#if loadingGames} -

Loading games...

- {:else if games.length > 0} -
+ + {#if auth.isLoggedIn} + {#if loadingUserGames} +

Loading your games...

+ {:else} + {#if activeUserGames.length > 0} +
+

Your Active Games

+
+ {#each activeUserGames as game} + + {/each} +
+
+ {/if} + + {#if completedUserGames.length > 0} +
+

Completed

+
+ {#each completedUserGames.slice(0, 5) as game} + + {/each} +
+
+ {/if} + + {#if userGames.length === 0} +

+ No games yet. Start one! +

+ {/if} + {/if} {/if} + +
+
+

Live on the Atmosphere

+ {#if liveConnected} + + {/if} +
+ {#if liveGames.length > 0} +
+ {#each liveGames.slice(0, 20) as game} + + {/each} +
+ {:else} +

+ {liveConnected ? 'No active games right now. Be the first!' : 'Connecting to live feed...'} +

+ {/if} +
-- 2.51.2