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) {
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) {

View file

@ -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 {
<section id="fetching-pattern" class="pattern-section">
<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> {
// 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 {
<h3>Cache Behavior</h3>
<ul>
<li><strong>Pages</strong>: ~90 min stale max (deploy cycle)</li>
<li><strong>R2 replays</strong>: immutable, cache forever</li>
<li><strong>R2 live.json</strong>: 10 second max-age</li>
<li><strong>B2</strong>: immutable, cache forever</li>
<li><strong>B2 replays</strong>: immutable, cache forever</li>
<li><strong>B2 live.json</strong>: 10 second max-age</li>
</ul>
<h3>Rate Limits</h3>
@ -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 `

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><strong>Base URLs:</strong></p>
<pre><code>const PAGES = '' // Same origin (Cloudflare Pages)
const R2 = 'https://r2.aicodebattle.com' // Warm replay cache
const B2 = 'https://b2.aicodebattle.com' // Cold archive</code></pre>
const B2 = 'https://b2.aicodebattle.com' // Warm replay cache
const B2 = 'https://b2.aicodebattle.com' // Warm replay cache</code></pre>
</section>
<section>
@ -77,16 +77,16 @@ GET /maps/{map_id}.json # Individual map definition</code></pre>
</section>
<section>
<h2>Replay Data (Cloudflare R2)</h2>
<p>Uploaded in real-time by match workers. R2 is a warm cache for recent replays; B2 is the permanent cold archive.</p>
<h2>Replay Data (Backblaze B2)</h2>
<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>
<pre><code>GET /replays/{match_id}.json.gz</code></pre>
<p>Gzipped replay JSON. Browser handles decompression automatically.</p>
<pre><code># Fetch from R2 (warm cache)
curl https://r2.aicodebattle.com/replays/m_7f3a9b2c.json.gz
<pre><code># 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</code></pre>
<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>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>Replays</td><td>Real-time</td><td>Match worker R2/B2</td></tr>
<tr><td>Match metadata</td><td>Real-time</td><td>Match worker R2/B2</td></tr>
<tr><td>Evolution data</td><td>Every cycle (~15 min)</td><td>Evolver R2 live.json</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 B2/R2</td></tr>
<tr><td>Evolution data</td><td>Every cycle (~15 min)</td><td>Evolver B2 live.json</td></tr>
</table>
</section>
<section>
<h2>Cache Behavior</h2>
<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 (archive):</strong> Same cache headers as R2. Free egress via Cloudflare Bandwidth Alliance.</p>
<p><strong>B2 (replays):</strong> Served with <code>Cache-Control: immutable, max-age=31536000</code> (content-addressed, never changes).</p>
<p><strong>R2 (archive):</strong> Same cache headers as B2.</p>
</section>
<section>
@ -125,10 +125,10 @@ GET /cards/{bot_id}.png</code></pre>
<pre><code>// 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\`)
}</code></pre>
</section>

View file

@ -18,9 +18,9 @@ export function renderDocsReplayFormatPage(): void {
<section>
<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
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</code></pre>

View file

@ -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++;