diff --git a/web/functions/r2/[[path]].ts b/web/functions/r2/[[path]].ts new file mode 100644 index 0000000..df6f247 --- /dev/null +++ b/web/functions/r2/[[path]].ts @@ -0,0 +1,34 @@ +interface Env { + ACB_BUCKET: R2Bucket; +} + +export const onRequest: PagesFunction = async (context) => { + try { + const url = new URL(context.request.url); + // Strip the leading /r2/ prefix to get the R2 object key + const key = url.pathname.replace(/^\/r2\//, ''); + + if (!key) { + return new Response('Not Found', { status: 404 }); + } + + if (!context.env.ACB_BUCKET) { + return new Response('R2 binding not configured', { status: 503 }); + } + + const object = await context.env.ACB_BUCKET.get(key); + if (!object) { + return new Response('Not Found', { status: 404 }); + } + + const headers = new Headers(); + object.writeHttpMetadata(headers); + headers.set('Cache-Control', 'public, max-age=60'); + headers.set('Access-Control-Allow-Origin', '*'); + + return new Response(object.body, { headers }); + } catch (err: unknown) { + const msg = err instanceof Error ? err.message : String(err); + return new Response(`Error: ${msg}`, { status: 500 }); + } +}; diff --git a/web/src/api-types.ts b/web/src/api-types.ts index aa3add4..54e4dab 100644 --- a/web/src/api-types.ts +++ b/web/src/api-types.ts @@ -243,7 +243,7 @@ export async function registerBot(request: RegisterRequest): Promise { // Evolution data changes every ~10s — bypass SWR, always fetch fresh @@ -500,7 +500,7 @@ export interface EnrichedIndex { entries: EnrichedMatchEntry[]; } -const R2_COMMENTARY_BASE = 'https://r2.aicodebattle.com'; +const R2_COMMENTARY_BASE = '/r2'; export async function fetchCommentary(matchId: string): Promise { try { diff --git a/web/src/components/playlist-carousel.ts b/web/src/components/playlist-carousel.ts index 158c3d6..55f3976 100644 --- a/web/src/components/playlist-carousel.ts +++ b/web/src/components/playlist-carousel.ts @@ -43,7 +43,7 @@ export interface CarouselOptions { const DEFAULT_AUTO_ADVANCE_DELAY = 3000; const METADATA_PANEL_WIDTH = 280; const TRANSITION_MS = 300; -const R2_BASE = 'https://r2.aicodebattle.com'; +const R2_BASE = '/r2'; const B2_FALLBACK = 'https://b2.aicodebattle.com'; const SWIPE_THRESHOLD = 50; // min px to trigger advance const VELOCITY_THRESHOLD = 0.3; // px/ms — fast flick triggers even below threshold diff --git a/web/src/embed.ts b/web/src/embed.ts index 4e84d02..b9ea080 100644 --- a/web/src/embed.ts +++ b/web/src/embed.ts @@ -14,7 +14,7 @@ const PLAYER_COLORS = [ ]; // Configuration -const R2_BASE = 'https://r2.aicodebattle.com'; +const R2_BASE = '/r2'; const B2_BASE = 'https://b2.aicodebattle.com'; const PAGES_BASE = 'https://ai-code-battle.pages.dev'; diff --git a/web/src/lib/ambient.ts b/web/src/lib/ambient.ts index 5a023df..9cdfc36 100644 --- a/web/src/lib/ambient.ts +++ b/web/src/lib/ambient.ts @@ -223,7 +223,7 @@ export function startAmbientPolling(intervalMs = 30_000): void { try { // Fetch evolution live data for generation changes const evoResp = await fetch( - 'https://r2.aicodebattle.com/evolution/live.json', + '/r2/evolution/live.json', ); if (evoResp.ok) { const evoData: LiveJSON = await evoResp.json(); diff --git a/web/src/og-tags.ts b/web/src/og-tags.ts index be93d70..cdb79db 100644 --- a/web/src/og-tags.ts +++ b/web/src/og-tags.ts @@ -68,7 +68,7 @@ export function getBotProfileOGTags(bot: { win_rate: number; evolved?: boolean; }): OGTags { - const cardUrl = `https://r2.aicodebattle.com/cards/${bot.id}.png`; + const cardUrl = `/r2/cards/${bot.id}.png`; return { title: `${bot.name} - Bot Profile`, @@ -89,7 +89,7 @@ export function getReplayOGTags(match: { }): OGTags { const winner = match.participants.find(p => p.won); const winnerName = winner ? winner.name : 'Draw'; - const thumbnailUrl = `https://r2.aicodebattle.com/thumbnails/${match.id}.png`; + const thumbnailUrl = `/r2/thumbnails/${match.id}.png`; return { title: `Match: ${match.participants.map(p => p.name).join(' vs ')}`, diff --git a/web/src/pages/docs-api.ts b/web/src/pages/docs-api.ts index 5bca8ab..c45fae6 100644 --- a/web/src/pages/docs-api.ts +++ b/web/src/pages/docs-api.ts @@ -17,7 +17,7 @@ interface Section { } const PAGES_BASE = 'https://ai-code-battle.pages.dev'; -const R2_BASE = 'https://r2.aicodebattle.com'; +const R2_BASE = '/r2'; const B2_BASE = 'https://b2.aicodebattle.com'; const sections: Section[] = [ @@ -497,7 +497,7 @@ export function renderDocsApiPage(): void {

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

async function fetchReplay(matchId: string): Promise {
   // Try R2 warm cache first
-  const r2Url = \`https://r2.aicodebattle.com/replays/\${matchId}.json.gz\`;
+  const r2Url = \`/r2/replays/\${matchId}.json.gz\`;
   const r2Resp = await fetch(r2Url);
   if (r2Resp.ok) {
     return decompress(await r2Resp.arrayBuffer());
diff --git a/web/wrangler.toml b/web/wrangler.toml
new file mode 100644
index 0000000..2524554
--- /dev/null
+++ b/web/wrangler.toml
@@ -0,0 +1,6 @@
+name = "ai-code-battle"
+pages_build_output_dir = "dist"
+
+[[r2_buckets]]
+binding = "ACB_BUCKET"
+bucket_name = "acb-data"