extremely claude-assisted go game based on atproto! working on cleaning up and giving a more unique design, still has a bit of a slop vibe to it.

Update homepage to fetch games from Constellation instead of database

- Add fetchAllGamesFromConstellation() to query all game records
- Remove database dependency from homepage
- Filter games by time windows after fetching from Constellation
- Maintain same filtering logic (12h for active, 7d for completed)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>

+109 -12
+68 -2
src/lib/atproto-client.ts
··· 683 683 } 684 684 685 685 /** 686 - * Fetch all games from Constellation by querying all records 687 - * Note: This fetches from multiple known players or uses a game index 686 + * Fetch all game records from Constellation's record index 687 + */ 688 + export async function fetchAllGamesFromConstellation(): Promise<GameWithMetadata[]> { 689 + const games: Array<{ uri: string; rkey: string; value: GameRecord; creatorDid: string }> = []; 690 + let cursor: string | undefined; 691 + 692 + try { 693 + // Use Constellation's record listing API 694 + do { 695 + const params = new URLSearchParams({ 696 + collection: 'boo.sky.go.game', 697 + limit: '100', 698 + }); 699 + if (cursor) params.set('cursor', cursor); 700 + 701 + const res = await fetch( 702 + `${CONSTELLATION_BASE}/blue.microcosm.records.listRecords?${params}`, 703 + { headers: { Accept: 'application/json' } } 704 + ); 705 + 706 + if (!res.ok) { 707 + console.error('Constellation listRecords failed:', res.status); 708 + break; 709 + } 710 + 711 + const body = await res.json(); 712 + 713 + for (const rec of body.records || []) { 714 + if (rec.uri && rec.value) { 715 + const uriParts = rec.uri.split('/'); 716 + const did = uriParts[2]; // Extract DID from at://did/collection/rkey 717 + const rkey = uriParts[4]; // Extract rkey 718 + 719 + games.push({ 720 + uri: rec.uri, 721 + rkey, 722 + value: rec.value as GameRecord, 723 + creatorDid: did, 724 + }); 725 + } 726 + } 727 + 728 + cursor = body.cursor; 729 + } while (cursor); 730 + } catch (err) { 731 + console.error('Failed to fetch games from Constellation:', err); 732 + } 733 + 734 + // Calculate metadata for all games (in batches to avoid overwhelming the API) 735 + const BATCH_SIZE = 20; 736 + const gamesWithMetadata: GameWithMetadata[] = []; 737 + 738 + for (let i = 0; i < games.length; i += BATCH_SIZE) { 739 + const batch = games.slice(i, i + BATCH_SIZE); 740 + const batchResults = await Promise.all( 741 + batch.map(({ uri, value, creatorDid, rkey }) => 742 + calculateGameMetadata(uri, value, creatorDid, rkey) 743 + ) 744 + ); 745 + gamesWithMetadata.push(...batchResults); 746 + } 747 + 748 + return gamesWithMetadata; 749 + } 750 + 751 + /** 752 + * Fetch all games from Constellation by querying multiple players' games 753 + * Fallback method if Constellation's record listing is unavailable 688 754 */ 689 755 export async function fetchAllGames(knownPlayerDids: string[] = []): Promise<GameWithMetadata[]> { 690 756 const gameMap = new Map<string, { uri: string; rkey: string; value: GameRecord; creatorDid: string }>();
+41 -10
src/routes/+page.server.ts
··· 1 1 import type { PageServerLoad } from './$types'; 2 2 import { getSession } from '$lib/server/auth'; 3 - import { getDb } from '$lib/server/db'; 3 + import { fetchAllGamesFromConstellation } from '$lib/atproto-client'; 4 4 import { gameTitle } from '$lib/game-titles'; 5 5 6 6 export const load: PageServerLoad = async (event) => { 7 7 const session = await getSession(event); 8 - const db = getDb(); 8 + 9 + // Calculate timestamps for filtering 10 + const twelveHoursAgo = new Date(Date.now() - 12 * 60 * 60 * 1000).toISOString(); 11 + const sevenDaysAgo = new Date(Date.now() - 7 * 24 * 60 * 60 * 1000).toISOString(); 12 + 13 + // Fetch all games from Constellation 14 + const allGames = await fetchAllGamesFromConstellation(); 15 + 16 + // Filter active/waiting games (updated in last 12 hours) 17 + const activeGames = allGames.filter( 18 + (game) => 19 + (game.status === 'active' || game.status === 'waiting') && 20 + game.updatedAt >= twelveHoursAgo 21 + ); 22 + 23 + // Filter completed games (from last 7 days) 24 + const completedGames = allGames.filter( 25 + (game) => game.status === 'completed' && game.updatedAt >= sevenDaysAgo 26 + ); 27 + 28 + // Combine and sort by updated time 29 + const recentActiveGames = activeGames 30 + .sort((a, b) => new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime()) 31 + .slice(0, 100); 32 + 33 + const recentCompletedGames = completedGames 34 + .sort((a, b) => new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime()) 35 + .slice(0, 50); 9 36 10 - // Fetch all games from index (filtering/sorting done client-side) 11 - const games = await db 12 - .selectFrom('games') 13 - .select(['rkey', 'id', 'player_one', 'player_two', 'board_size', 'status', 'created_at', 'updated_at', 'last_action_type', 'action_count']) 14 - .orderBy('updated_at', 'desc') 15 - .limit(100) 16 - .execute(); 37 + const games = [...recentActiveGames, ...recentCompletedGames]; 17 38 39 + // Map to expected format with titles 18 40 const gamesWithTitles = games.map((game) => ({ 19 - ...game, 41 + rkey: game.rkey, 42 + id: game.uri, 43 + player_one: game.playerOne, 44 + player_two: game.playerTwo || null, 45 + board_size: game.boardSize, 46 + status: game.status, 47 + created_at: game.createdAt, 48 + updated_at: game.updatedAt, 49 + last_action_type: game.lastActionType, 50 + action_count: game.actionCount, 20 51 title: gameTitle(game.rkey), 21 52 })); 22 53