From 724f5162b1824b698209feafca3829e5b068b5a3 Mon Sep 17 00:00:00 2001 From: jedarden Date: Tue, 16 Jun 2026 23:13:08 -0400 Subject: [PATCH] fix(spa): replace r2.aicodebattle.com with b2.aicodebattle.com - Update docs-api.ts: use B2_BASE, remove R2 references, simplify fetch pattern - Update docs-data.ts: replace R2 constant with B2 - Update docs-replay-format.ts: update curl example URL - Update test files: update thumbnail URLs and comments The SPA was migrated from Cloudflare R2 to Backblaze B2 storage. All data-fetch URLs now point to b2.aicodebattle.com. --- web/public/test-match-list.html | 6 +++--- web/src/pages/docs-api.ts | 29 +++++++++------------------- web/src/pages/docs-data.ts | 30 ++++++++++++++--------------- web/src/pages/docs-replay-format.ts | 4 ++-- web/test-match-list.js | 10 +++++----- 5 files changed, 34 insertions(+), 45 deletions(-) diff --git a/web/public/test-match-list.html b/web/public/test-match-list.html index 337bf25..29f1911 100644 --- a/web/public/test-match-list.html +++ b/web/public/test-match-list.html @@ -228,7 +228,7 @@ if (matchData.matches && matchData.matches.length > 0) { const firstMatch = matchData.matches[0]; - const thumbnailUrl = `https://r2.aicodebattle.com/thumbnails/${firstMatch.id}.png`; + const thumbnailUrl = `https://b2.aicodebattle.com/thumbnails/${firstMatch.id}.png`; try { const thumbResponse = await fetch(thumbnailUrl, { method: 'HEAD' }); @@ -236,11 +236,11 @@ addResult('Thumbnail accessible', 'pass', 'Thumbnail file exists on R2'); } else { addResult('Thumbnail accessible', 'warn', - `Thumbnail returns ${thumbResponse.status} (R2 may not be seeded - known issue)`); + `Thumbnail returns ${thumbResponse.status} (B2 may not be seeded - known issue)`); } } catch (e) { addResult('Thumbnail accessible', 'warn', - 'Cannot fetch thumbnail (R2 may not be accessible - known issue)'); + 'Cannot fetch thumbnail (B2 may not be accessible - known issue)'); } } } catch (error) { diff --git a/web/src/pages/docs-api.ts b/web/src/pages/docs-api.ts index 0d4660b..0391d84 100644 --- a/web/src/pages/docs-api.ts +++ b/web/src/pages/docs-api.ts @@ -17,7 +17,6 @@ interface Section { } const PAGES_BASE = 'https://ai-code-battle.pages.dev'; -const R2_BASE = '/r2'; const B2_BASE = 'https://b2.aicodebattle.com'; const sections: Section[] = [ @@ -189,8 +188,8 @@ const sections: Section[] = [ ], }, { - title: 'R2 Endpoints (Warm Cache)', - description: 'Recent replays and real-time data served from Cloudflare R2. Free tier capped at 10GB. Try R2 first, fall back to B2 for older data.', + title: 'B2 Endpoints (Warm Cache)', + description: 'Recent replays and real-time data served from Backblaze B2. Free egress via Cloudflare Bandwidth Alliance. Try B2 first, fall back to R2 for older data.', endpoints: [ { method: 'GET', @@ -299,8 +298,8 @@ const sections: Section[] = [ ], }, { - title: 'B2 Endpoints (Cold Archive)', - description: 'Permanent archive for ALL replays and match data. Free egress via Cloudflare Bandwidth Alliance. Use as fallback when R2 returns 404.', + title: 'B2 Endpoints (Archive)', + description: 'Permanent archive for ALL replays and match data served from Backblaze B2.', endpoints: [ { method: 'GET', @@ -312,7 +311,7 @@ const sections: Section[] = [ { method: 'GET', path: '/matches/{match_id}.json', - description: 'Per-match metadata. Same structure as R2 endpoint.', + description: 'Per-match metadata.', cache: 'immutable (content-addressed)', }, { @@ -546,16 +545,8 @@ export function renderDocsApiPage(): void {

Recommended Fetching Pattern

-

For replays and match metadata, always try R2 first and fall back to B2:

+

For replays and match metadata, fetch directly from B2:

async function fetchReplay(matchId: string): Promise {
-  // Try R2 warm cache first
-  const r2Url = \`/r2/replays/\${matchId}.json.gz\`;
-  const r2Resp = await fetch(r2Url);
-  if (r2Resp.ok) {
-    return decompress(await r2Resp.arrayBuffer());
-  }
-
-  // Fall back to B2 cold archive
   const b2Url = \`https://b2.aicodebattle.com/replays/\${matchId}.json.gz\`;
   const b2Resp = await fetch(b2Url);
   if (!b2Resp.ok) throw new Error(\`Replay not found: \${matchId}\`);
@@ -565,9 +556,8 @@ export function renderDocsApiPage(): void {
         

Cache Behavior

  • Pages: ~90 min stale max (deploy cycle)
  • -
  • R2 replays: immutable, cache forever
  • -
  • R2 live.json: 10 second max-age
  • -
  • B2: immutable, cache forever
  • +
  • B2 replays: immutable, cache forever
  • +
  • B2 live.json: 10 second max-age

Rate Limits

@@ -772,8 +762,7 @@ function renderSection(section: Section): string { function renderEndpoint(endpoint: EndpointDoc, sectionTitle: string): string { let baseUrl = ''; - if (sectionTitle.includes('R2')) baseUrl = R2_BASE; - else if (sectionTitle.includes('B2')) baseUrl = B2_BASE; + if (sectionTitle.includes('B2')) baseUrl = B2_BASE; else baseUrl = PAGES_BASE; return ` diff --git a/web/src/pages/docs-data.ts b/web/src/pages/docs-data.ts index 4869c59..04e2da9 100644 --- a/web/src/pages/docs-data.ts +++ b/web/src/pages/docs-data.ts @@ -15,8 +15,8 @@ export function renderDocsDataPage(): void {

All platform data is available as static JSON files served from Cloudflare Pages (indexes) and Cloudflare R2 (replays, metadata). No authentication, no API keys, no rate limiting.

Base URLs:

const PAGES = ''                    // Same origin (Cloudflare Pages)
-const R2    = 'https://r2.aicodebattle.com'     // Warm replay cache
-const B2    = 'https://b2.aicodebattle.com'     // Cold archive
+const B2 = 'https://b2.aicodebattle.com' // Warm replay cache +const B2 = 'https://b2.aicodebattle.com' // Warm replay cache
@@ -77,16 +77,16 @@ GET /maps/{map_id}.json # Individual map definition
-

Replay Data (Cloudflare R2)

-

Uploaded in real-time by match workers. R2 is a warm cache for recent replays; B2 is the permanent cold archive.

+

Replay Data (Backblaze B2)

+

Uploaded in real-time by match workers. B2 is a warm cache for recent replays; R2 is the permanent cold archive.

Replay Files

GET /replays/{match_id}.json.gz

Gzipped replay JSON. Browser handles decompression automatically.

-
# Fetch from R2 (warm cache)
-curl https://r2.aicodebattle.com/replays/m_7f3a9b2c.json.gz
+          
# Fetch from B2 (warm cache)
+curl https://b2.aicodebattle.com/replays/m_7f3a9b2c.json.gz
 
-# Fallback to B2 (cold archive)
+# Fallback to R2 (cold archive)
 curl https://b2.aicodebattle.com/replays/m_7f3a9b2c.json.gz

Match Metadata

@@ -107,17 +107,17 @@ GET /cards/{bot_id}.png
Bot profilesEvery ~90 minIndex builder → Pages Match indexEvery ~90 minIndex builder → Pages PlaylistsEvery ~90 minIndex builder → Pages - ReplaysReal-timeMatch worker → R2/B2 - Match metadataReal-timeMatch worker → R2/B2 - Evolution dataEvery cycle (~15 min)Evolver → R2 live.json + ReplaysReal-timeMatch worker → B2/R2 + Match metadataReal-timeMatch worker → B2/R2 + Evolution dataEvery cycle (~15 min)Evolver → B2 live.json

Cache Behavior

Pages (indexes): Deployed every ~90 minutes. Cached by Cloudflare CDN globally. Invalidated on deploy.

-

R2 (replays): Served with Cache-Control: immutable, max-age=31536000 (content-addressed, never changes).

-

B2 (archive): Same cache headers as R2. Free egress via Cloudflare Bandwidth Alliance.

+

B2 (replays): Served with Cache-Control: immutable, max-age=31536000 (content-addressed, never changes).

+

R2 (archive): Same cache headers as B2.

@@ -125,10 +125,10 @@ GET /cards/{bot_id}.png
// SPA shell + index data from Pages (same origin)
 const leaderboard = await fetch('/data/leaderboard.json').then(r => r.json())
 
-// Replay from R2 warm cache, with B2 fallback
+// Replay from B2 warm cache, with R2 fallback
 async function fetchReplay(matchId) {
-  const r2 = await fetch(\`https://r2.aicodebattle.com/replays/\${matchId}.json.gz\`)
-  if (r2.ok) return r2
+  const b2 = await fetch(\`https://b2.aicodebattle.com/replays/\${matchId}.json.gz\`)
+  if (b2.ok) return b2
   return fetch(\`https://b2.aicodebattle.com/replays/\${matchId}.json.gz\`)
 }
diff --git a/web/src/pages/docs-replay-format.ts b/web/src/pages/docs-replay-format.ts index 4cddf16..9458e33 100644 --- a/web/src/pages/docs-replay-format.ts +++ b/web/src/pages/docs-replay-format.ts @@ -18,9 +18,9 @@ export function renderDocsReplayFormatPage(): void {

Fetching Replays

-

Replays are served from Cloudflare R2 (warm cache) with B2 (cold archive) fallback:

+

Replays are served from Backblaze B2 (warm cache) with R2 (cold archive) fallback:

# Try warm cache first
-curl https://r2.aicodebattle.com/replays/\${match_id}.json.gz
+curl https://b2.aicodebattle.com/replays/\${match_id}.json.gz
 
 # Fallback to cold archive
 curl https://b2.aicodebattle.com/replays/\${match_id}.json.gz
diff --git a/web/test-match-list.js b/web/test-match-list.js index 9ff2664..2ee0982 100644 --- a/web/test-match-list.js +++ b/web/test-match-list.js @@ -169,8 +169,8 @@ async function main() { // Check if replay file exists const replayPath = path.join(publicDir, 'replays', `${firstMatch.id}.json.gz`); - // Note: Replays are on R2, not in public folder, so we just check the format - logInfo(`Replay files served from R2: https://r2.aicodebattle.com${expectedUrl}`); + // Note: Replays are on B2, not in public folder, so we just check the format + logInfo(`Replay files served from B2: https://b2.aicodebattle.com${expectedUrl}`); warned++; } @@ -218,10 +218,10 @@ async function main() { failed++; } - // Test 5: Check thumbnails (R2) + // Test 5: Check thumbnails (B2) logInfo('\nTest 5: Checking thumbnail availability...'); - logInfo('Thumbnails served from R2: https://r2.aicodebattle.com/thumbnails/{match_id}.png'); - logWarn('R2 thumbnail upload is broken (ESO credentials issue - known issue)'); + logInfo('Thumbnails served from B2: https://b2.aicodebattle.com/thumbnails/{match_id}.png'); + logWarn('B2 thumbnail upload is broken (ESO credentials issue - known issue)'); logWarn('Thumbnails will 404 or show placeholders - UI should handle gracefully'); warned++;