Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 18 additions & 2 deletions dashboard/lib/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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`;
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

// --- Health ---

export interface HealthResult {
Expand Down Expand Up @@ -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
Expand Down
Loading