diff --git a/frontend/src/pages/BotProfilePage.vue b/frontend/src/pages/BotProfilePage.vue index 708d1c8..526b761 100644 --- a/frontend/src/pages/BotProfilePage.vue +++ b/frontend/src/pages/BotProfilePage.vue @@ -298,11 +298,18 @@ async function loadGuide() { guideContent.value = '' guideCopied.value = false try { - const res = await fetch('/docs/BOTFIGHTS.md') + // Fetch the server-rendered /api/docs/prompt, not the static + // /docs/BOTFIGHTS.md file — the static file's {{ARENA_URL}} has no + // choice but to be substituted client-side with window.location.origin, + // which on a proxy-mode instance (ARENA_UPSTREAM_URL set) is this + // node's own local/LAN/Tailscale address, not the real externally- + // reachable arena. /api/docs/prompt is under /api/*, so arena-proxy + // forwards it to the real upstream arena in proxy mode, which resolves + // {{ARENA_URL}} to its own correct origin (see server/src/routes/docs.ts). + const res = await fetch('/api/docs/prompt') let content = await res.text() content = content.replace(/YOUR_BOT_ID/g, regeneratedBotId.value) content = content.replace(/YOUR_BOT_SECRET/g, regeneratedSecret.value) - content = content.replace(/\{\{ARENA_URL\}\}/g, window.location.origin) guideContent.value = content } catch { guideContent.value = '# Failed to load setup guide' diff --git a/frontend/src/pages/JoinBoutPage.vue b/frontend/src/pages/JoinBoutPage.vue index 4ad13d6..6d3d587 100644 --- a/frontend/src/pages/JoinBoutPage.vue +++ b/frontend/src/pages/JoinBoutPage.vue @@ -522,8 +522,21 @@ const setupContentCopied = ref(false) // What DOES need to visibly react to the picker is `modeHint` below, so a // click still produces an immediate, obvious change instead of looking inert. +// IMPORTANT: fetch the server-rendered /api/docs/prompt, NOT the static +// /docs/BOTFIGHTS.md file. The static file is never proxy-aware — on an +// instance running in proxy mode (ARENA_UPSTREAM_URL set), the raw file's +// {{ARENA_URL}} would have to be substituted client-side with +// window.location.origin, which is whatever address the browser happens to +// be on (e.g. this node's own LAN/Tailscale IP) — reachable on that network, +// but not the real, externally-reachable arena, and useless to an external +// bot with no route to that address. /api/docs/prompt is mounted under +// /api/*, so arena-proxy transparently forwards it to the real upstream +// arena in proxy mode, which resolves {{ARENA_URL}} to ITS OWN correct, +// externally-reachable origin — the same substitution already proven +// correct (see server/src/routes/docs.ts). Standalone instances (no +// ARENA_UPSTREAM_URL) get their own correct origin either way. function setupDocPath() { - return '/docs/BOTFIGHTS.md' + return '/api/docs/prompt' } function setupDocName() { @@ -546,9 +559,10 @@ async function toggleSetupContent() { try { const res = await fetch(setupDocPath()) let content = await res.text() + // {{ARENA_URL}} is already resolved server-side (proxy-aware — see + // setupDocPath() above); only the bot-specific placeholders remain. content = content.replace(/YOUR_BOT_ID/g, botId.value) content = content.replace(/YOUR_BOT_SECRET/g, botSecret.value) - content = content.replace(/\{\{ARENA_URL\}\}/g, window.location.origin) setupContent.value = content } catch { setupContent.value = '# Failed to load setup guide' @@ -568,9 +582,10 @@ async function copyFullPrompt() { try { const res = await fetch(setupDocPath()) let content = await res.text() + // {{ARENA_URL}} is already resolved server-side (proxy-aware — see + // setupDocPath() above); only the bot-specific placeholders remain. content = content.replace(/YOUR_BOT_ID/g, botId.value) content = content.replace(/YOUR_BOT_SECRET/g, botSecret.value) - content = content.replace(/\{\{ARENA_URL\}\}/g, window.location.origin) setupContent.value = content } catch { /* fall through with empty content */ } }