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.
This commit is contained in:
jedarden 2026-06-16 23:13:08 -04:00
parent faf8770dee
commit 724f5162b1
5 changed files with 34 additions and 45 deletions

View file

@ -228,7 +228,7 @@
if (matchData.matches && matchData.matches.length > 0) { if (matchData.matches && matchData.matches.length > 0) {
const firstMatch = matchData.matches[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 { try {
const thumbResponse = await fetch(thumbnailUrl, { method: 'HEAD' }); const thumbResponse = await fetch(thumbnailUrl, { method: 'HEAD' });
@ -236,11 +236,11 @@
addResult('Thumbnail accessible', 'pass', 'Thumbnail file exists on R2'); addResult('Thumbnail accessible', 'pass', 'Thumbnail file exists on R2');
} else { } else {
addResult('Thumbnail accessible', 'warn', 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) { } catch (e) {
addResult('Thumbnail accessible', 'warn', 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) { } catch (error) {

View file

@ -17,7 +17,6 @@ interface Section {
} }
const PAGES_BASE = 'https://ai-code-battle.pages.dev'; const PAGES_BASE = 'https://ai-code-battle.pages.dev';
const R2_BASE = '/r2';
const B2_BASE = 'https://b2.aicodebattle.com'; const B2_BASE = 'https://b2.aicodebattle.com';
const sections: Section[] = [ const sections: Section[] = [
@ -189,8 +188,8 @@ const sections: Section[] = [
], ],
}, },
{ {
title: 'R2 Endpoints (Warm Cache)', title: 'B2 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.', 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: [ endpoints: [
{ {
method: 'GET', method: 'GET',
@ -299,8 +298,8 @@ const sections: Section[] = [
], ],
}, },
{ {
title: 'B2 Endpoints (Cold Archive)', title: 'B2 Endpoints (Archive)',
description: 'Permanent archive for ALL replays and match data. Free egress via Cloudflare Bandwidth Alliance. Use as fallback when R2 returns 404.', description: 'Permanent archive for ALL replays and match data served from Backblaze B2.',
endpoints: [ endpoints: [
{ {
method: 'GET', method: 'GET',
@ -312,7 +311,7 @@ const sections: Section[] = [
{ {
method: 'GET', method: 'GET',
path: '/matches/{match_id}.json', path: '/matches/{match_id}.json',
description: 'Per-match metadata. Same structure as R2 endpoint.', description: 'Per-match metadata.',
cache: 'immutable (content-addressed)', cache: 'immutable (content-addressed)',
}, },
{ {
@ -546,16 +545,8 @@ export function renderDocsApiPage(): void {
<section id="fetching-pattern" class="pattern-section"> <section id="fetching-pattern" class="pattern-section">
<h2>Recommended Fetching Pattern</h2> <h2>Recommended Fetching Pattern</h2>
<p>For replays and match metadata, always try R2 first and fall back to B2:</p> <p>For replays and match metadata, fetch directly from B2:</p>
<pre><code>async function fetchReplay(matchId: string): Promise<Replay> { <pre><code>async function fetchReplay(matchId: string): Promise<Replay> {
// 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 b2Url = \`https://b2.aicodebattle.com/replays/\${matchId}.json.gz\`;
const b2Resp = await fetch(b2Url); const b2Resp = await fetch(b2Url);
if (!b2Resp.ok) throw new Error(\`Replay not found: \${matchId}\`); if (!b2Resp.ok) throw new Error(\`Replay not found: \${matchId}\`);
@ -565,9 +556,8 @@ export function renderDocsApiPage(): void {
<h3>Cache Behavior</h3> <h3>Cache Behavior</h3>
<ul> <ul>
<li><strong>Pages</strong>: ~90 min stale max (deploy cycle)</li> <li><strong>Pages</strong>: ~90 min stale max (deploy cycle)</li>
<li><strong>R2 replays</strong>: immutable, cache forever</li> <li><strong>B2 replays</strong>: immutable, cache forever</li>
<li><strong>R2 live.json</strong>: 10 second max-age</li> <li><strong>B2 live.json</strong>: 10 second max-age</li>
<li><strong>B2</strong>: immutable, cache forever</li>
</ul> </ul>
<h3>Rate Limits</h3> <h3>Rate Limits</h3>
@ -772,8 +762,7 @@ function renderSection(section: Section): string {
function renderEndpoint(endpoint: EndpointDoc, sectionTitle: string): string { function renderEndpoint(endpoint: EndpointDoc, sectionTitle: string): string {
let baseUrl = ''; let baseUrl = '';
if (sectionTitle.includes('R2')) baseUrl = R2_BASE; if (sectionTitle.includes('B2')) baseUrl = B2_BASE;
else if (sectionTitle.includes('B2')) baseUrl = B2_BASE;
else baseUrl = PAGES_BASE; else baseUrl = PAGES_BASE;
return ` return `

View file

@ -15,8 +15,8 @@ export function renderDocsDataPage(): void {
<p>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.</p> <p>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.</p>
<p><strong>Base URLs:</strong></p> <p><strong>Base URLs:</strong></p>
<pre><code>const PAGES = '' // Same origin (Cloudflare Pages) <pre><code>const PAGES = '' // Same origin (Cloudflare Pages)
const R2 = 'https://r2.aicodebattle.com' // Warm replay cache const B2 = 'https://b2.aicodebattle.com' // Warm replay cache
const B2 = 'https://b2.aicodebattle.com' // Cold archive</code></pre> const B2 = 'https://b2.aicodebattle.com' // Warm replay cache</code></pre>
</section> </section>
<section> <section>
@ -77,16 +77,16 @@ GET /maps/{map_id}.json # Individual map definition</code></pre>
</section> </section>
<section> <section>
<h2>Replay Data (Cloudflare R2)</h2> <h2>Replay Data (Backblaze B2)</h2>
<p>Uploaded in real-time by match workers. R2 is a warm cache for recent replays; B2 is the permanent cold archive.</p> <p>Uploaded in real-time by match workers. B2 is a warm cache for recent replays; R2 is the permanent cold archive.</p>
<h3>Replay Files</h3> <h3>Replay Files</h3>
<pre><code>GET /replays/{match_id}.json.gz</code></pre> <pre><code>GET /replays/{match_id}.json.gz</code></pre>
<p>Gzipped replay JSON. Browser handles decompression automatically.</p> <p>Gzipped replay JSON. Browser handles decompression automatically.</p>
<pre><code># Fetch from R2 (warm cache) <pre><code># Fetch from B2 (warm cache)
curl https://r2.aicodebattle.com/replays/m_7f3a9b2c.json.gz 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</code></pre> curl https://b2.aicodebattle.com/replays/m_7f3a9b2c.json.gz</code></pre>
<h3>Match Metadata</h3> <h3>Match Metadata</h3>
@ -107,17 +107,17 @@ GET /cards/{bot_id}.png</code></pre>
<tr><td>Bot profiles</td><td>Every ~90 min</td><td>Index builder Pages</td></tr> <tr><td>Bot profiles</td><td>Every ~90 min</td><td>Index builder Pages</td></tr>
<tr><td>Match index</td><td>Every ~90 min</td><td>Index builder Pages</td></tr> <tr><td>Match index</td><td>Every ~90 min</td><td>Index builder Pages</td></tr>
<tr><td>Playlists</td><td>Every ~90 min</td><td>Index builder Pages</td></tr> <tr><td>Playlists</td><td>Every ~90 min</td><td>Index builder Pages</td></tr>
<tr><td>Replays</td><td>Real-time</td><td>Match worker R2/B2</td></tr> <tr><td>Replays</td><td>Real-time</td><td>Match worker B2/R2</td></tr>
<tr><td>Match metadata</td><td>Real-time</td><td>Match worker R2/B2</td></tr> <tr><td>Match metadata</td><td>Real-time</td><td>Match worker B2/R2</td></tr>
<tr><td>Evolution data</td><td>Every cycle (~15 min)</td><td>Evolver R2 live.json</td></tr> <tr><td>Evolution data</td><td>Every cycle (~15 min)</td><td>Evolver B2 live.json</td></tr>
</table> </table>
</section> </section>
<section> <section>
<h2>Cache Behavior</h2> <h2>Cache Behavior</h2>
<p><strong>Pages (indexes):</strong> Deployed every ~90 minutes. Cached by Cloudflare CDN globally. Invalidated on deploy.</p> <p><strong>Pages (indexes):</strong> Deployed every ~90 minutes. Cached by Cloudflare CDN globally. Invalidated on deploy.</p>
<p><strong>R2 (replays):</strong> Served with <code>Cache-Control: immutable, max-age=31536000</code> (content-addressed, never changes).</p> <p><strong>B2 (replays):</strong> Served with <code>Cache-Control: immutable, max-age=31536000</code> (content-addressed, never changes).</p>
<p><strong>B2 (archive):</strong> Same cache headers as R2. Free egress via Cloudflare Bandwidth Alliance.</p> <p><strong>R2 (archive):</strong> Same cache headers as B2.</p>
</section> </section>
<section> <section>
@ -125,10 +125,10 @@ GET /cards/{bot_id}.png</code></pre>
<pre><code>// SPA shell + index data from Pages (same origin) <pre><code>// SPA shell + index data from Pages (same origin)
const leaderboard = await fetch('/data/leaderboard.json').then(r => r.json()) 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) { async function fetchReplay(matchId) {
const r2 = await fetch(\`https://r2.aicodebattle.com/replays/\${matchId}.json.gz\`) const b2 = await fetch(\`https://b2.aicodebattle.com/replays/\${matchId}.json.gz\`)
if (r2.ok) return r2 if (b2.ok) return b2
return fetch(\`https://b2.aicodebattle.com/replays/\${matchId}.json.gz\`) return fetch(\`https://b2.aicodebattle.com/replays/\${matchId}.json.gz\`)
}</code></pre> }</code></pre>
</section> </section>

View file

@ -18,9 +18,9 @@ export function renderDocsReplayFormatPage(): void {
<section> <section>
<h2>Fetching Replays</h2> <h2>Fetching Replays</h2>
<p>Replays are served from Cloudflare R2 (warm cache) with B2 (cold archive) fallback:</p> <p>Replays are served from Backblaze B2 (warm cache) with R2 (cold archive) fallback:</p>
<pre><code># Try warm cache first <pre><code># 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 # Fallback to cold archive
curl https://b2.aicodebattle.com/replays/\${match_id}.json.gz</code></pre> curl https://b2.aicodebattle.com/replays/\${match_id}.json.gz</code></pre>

View file

@ -169,8 +169,8 @@ async function main() {
// Check if replay file exists // Check if replay file exists
const replayPath = path.join(publicDir, 'replays', `${firstMatch.id}.json.gz`); 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 // Note: Replays are on B2, not in public folder, so we just check the format
logInfo(`Replay files served from R2: https://r2.aicodebattle.com${expectedUrl}`); logInfo(`Replay files served from B2: https://b2.aicodebattle.com${expectedUrl}`);
warned++; warned++;
} }
@ -218,10 +218,10 @@ async function main() {
failed++; failed++;
} }
// Test 5: Check thumbnails (R2) // Test 5: Check thumbnails (B2)
logInfo('\nTest 5: Checking thumbnail availability...'); logInfo('\nTest 5: Checking thumbnail availability...');
logInfo('Thumbnails served from R2: https://r2.aicodebattle.com/thumbnails/{match_id}.png'); logInfo('Thumbnails served from B2: https://b2.aicodebattle.com/thumbnails/{match_id}.png');
logWarn('R2 thumbnail upload is broken (ESO credentials issue - known issue)'); logWarn('B2 thumbnail upload is broken (ESO credentials issue - known issue)');
logWarn('Thumbnails will 404 or show placeholders - UI should handle gracefully'); logWarn('Thumbnails will 404 or show placeholders - UI should handle gracefully');
warned++; warned++;