diff --git a/dashboard/lib/api.ts b/dashboard/lib/api.ts index 113c497..07bd813 100644 --- a/dashboard/lib/api.ts +++ b/dashboard/lib/api.ts @@ -4,7 +4,6 @@ */ const ENV_API_BASE = process.env.NEXT_PUBLIC_API_URL ?? 'http://localhost:3000/api'; -const WS_BASE = process.env.NEXT_PUBLIC_WS_URL ?? 'ws://localhost:3000/ws'; /** Resolve API base URL: localStorage override → env var → localhost default. */ export function getApiBase(): string { @@ -45,6 +44,23 @@ export function getServerBase(): string { return getApiBase().replace(/\/api$/, ''); } +/** + * WebSocket URL. Prefers an explicit `NEXT_PUBLIC_WS_URL`, otherwise derives it + * from the resolved server origin (`http(s)` → `ws(s)`, `/ws` path) so production + * never falls back to `ws://localhost:3000` when only the API URL is configured. + */ +export function getWsBase(): string { + if (process.env.NEXT_PUBLIC_WS_URL) { + return process.env.NEXT_PUBLIC_WS_URL; + } + // Normalize a trailing `/api` or `/api/` (and any stray trailing slash) so the + // derived path is exactly `/ws` — the server's upgrade handler rejects anything else. + const base = getServerBase() + .replace(/\/api\/?$/, '') + .replace(/\/+$/, ''); + return `${base.replace(/^http/, 'ws')}/ws`; +} + // --- Health --- export interface HealthResult { @@ -385,7 +401,7 @@ export type EventHandler = (event: { event: string; data: unknown }) => void; export function connectWebSocket(onEvent: EventHandler): WebSocket | null { const token = typeof window !== 'undefined' ? localStorage.getItem('textrawl_token') : null; - const url = WS_BASE; + const url = getWsBase(); try { // Auth via subprotocol to avoid exposing token in URL/logs