feat(web): route R2 assets through Pages Function instead of r2.aicodebattle.com
The aicodebattle.com domain was never registered. Replace all hardcoded https://r2.aicodebattle.com references with the /r2/ relative path, served by a Cloudflare Pages Function that reads from the acb-data R2 bucket via binding. Adds web/functions/r2/[[path]].ts and web/wrangler.toml. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
8652e77655
commit
ae0f072f80
8 changed files with 49 additions and 9 deletions
34
web/functions/r2/[[path]].ts
Normal file
34
web/functions/r2/[[path]].ts
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
interface Env {
|
||||
ACB_BUCKET: R2Bucket;
|
||||
}
|
||||
|
||||
export const onRequest: PagesFunction<Env> = 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 });
|
||||
}
|
||||
};
|
||||
|
|
@ -243,7 +243,7 @@ export async function registerBot(request: RegisterRequest): Promise<RegisterRes
|
|||
|
||||
// R2_BASE_URL is the Cloudflare R2 bucket custom domain for live data.
|
||||
// The evolver writes live.json here every cycle with Cache-Control: max-age=10.
|
||||
const R2_BASE_URL = 'https://r2.aicodebattle.com';
|
||||
const R2_BASE_URL = '/r2';
|
||||
|
||||
export async function fetchEvolutionData(): Promise<EvolutionLiveData> {
|
||||
// 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<EnrichedCommentary | null> {
|
||||
try {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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 ')}`,
|
||||
|
|
|
|||
|
|
@ -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 {
|
|||
<p>For replays and match metadata, always try R2 first and fall back to B2:</p>
|
||||
<pre><code>async function fetchReplay(matchId: string): Promise<Replay> {
|
||||
// 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());
|
||||
|
|
|
|||
6
web/wrangler.toml
Normal file
6
web/wrangler.toml
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
name = "ai-code-battle"
|
||||
pages_build_output_dir = "dist"
|
||||
|
||||
[[r2_buckets]]
|
||||
binding = "ACB_BUCKET"
|
||||
bucket_name = "acb-data"
|
||||
Loading…
Add table
Reference in a new issue