/** * Backend URL resolution. * * On a local dev machine (vite dev server on :5173) the backend runs on a * different port, so we default to ``http://localhost:8000``. * * On a deployed environment (Hugging Face Space, Docker, etc.) the backend * and frontend share the same origin, so we default to ``window.location.origin``. * * Priority (first match wins): * 1. Explicit user setting (``localStorage.homepilot_backend_url``) — * ignored when it points at a local dev host but the page is served * from a remote host (stale leftover from an earlier session). * 2. Build-time env (``VITE_API_URL``) * 3. Current origin when served from a non-dev host * 4. ``http://localhost:8000`` fallback * * Always returns a value with no trailing slash. */ const LOCAL_HOSTS = new Set(["localhost", "127.0.0.1", "0.0.0.0", "::1"]); // Vite dev ports used across the repo. The Makefile's `make start` target // launches vite with --port 3000; earlier tooling used Vite's defaults // 5173/5174. All of them must route API traffic to the backend at :8000 // rather than to the Vite origin (which would SPA-fallback every request // to index.html and break JSON.parse). const DEV_VITE_PORTS = new Set(["3000", "5173", "5174"]); function isLocalHost(hostname) { return LOCAL_HOSTS.has(hostname); } function parsedHostname(url) { try { return new URL(url).hostname; } catch { return null; } } /** Best default when nothing else is configured — based on current location. */ export function getDefaultBackendUrl() { if (typeof window === "undefined") { return "http://localhost:8000"; } const env = import.meta.env?.VITE_API_URL?.trim(); if (env) return stripTrailingSlash(env); const { hostname, origin, port } = window.location; // On the Vite dev server the backend is a separate process on :8000. if (isLocalHost(hostname) && DEV_VITE_PORTS.has(port)) { return "http://localhost:8000"; } // Everywhere else — deployed Spaces, Docker, preview builds — the frontend // is served by the backend itself, so same-origin is always correct. return stripTrailingSlash(origin); } /** * Resolve the user-configured or default backend URL. Use this anywhere the * old hardcoded fallback ``'http://localhost:8000'`` was written. * * Robustness: if the stored value points at a localhost backend but the * page itself is served from a remote host (common after upgrades), we * ignore the stale setting and fall through to the current origin. This * fixes the "Failed to fetch" storm users hit after first deploy. */ export function resolveBackendUrl(override) { if (typeof override === "string" && override.trim()) { return stripTrailingSlash(override.trim()); } if (typeof window !== "undefined") { try { const stored = window.localStorage.getItem("homepilot_backend_url"); if (stored && stored.trim()) { const clean = stripTrailingSlash(stored.trim()); const pageIsRemote = !isLocalHost(window.location.hostname); const storedUrl = (() => { try { return new URL(clean); } catch { return null; } })(); const storedHost = storedUrl?.hostname ?? null; const storedPort = storedUrl?.port ?? ""; const storedIsLocal = storedHost !== null && isLocalHost(storedHost); if (pageIsRemote && storedIsLocal) { // Stale setting from a previous local dev session. Ignore it and // let getDefaultBackendUrl() take over (same origin). return getDefaultBackendUrl(); } // Stale setting pointing at a Vite DEV port on localhost (3000 / 5173 // / 5174). The frontend origin serves the SPA — POSTing JSON there // SPA-falls-back to index.html and the caller sees a 404, which the // voice-call path reads as "feature unavailable" and flips to the // chat-REST fallback. Redirect to :8000 (the default backend port) // so the stored preference doesn't silently break voice-call // session creation. if (storedIsLocal && DEV_VITE_PORTS.has(storedPort)) { return "http://localhost:8000"; } return clean; } } catch { /* localStorage unavailable — fall through to default */ } } return getDefaultBackendUrl(); } function stripTrailingSlash(url) { return url.replace(/\/+$/, ""); }