diff --git a/pyproject.toml b/pyproject.toml index 9ee6cd5..e9dba5c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -12,6 +12,7 @@ license = { text = "MIT" } dependencies = [ "fastapi>=0.115.0", "uvicorn>=0.30.0", + "websockets>=12.0", "httpx>=0.27.0", "sqlalchemy>=2.0.0", "psycopg[binary]>=3.2.0", diff --git a/requirements.txt b/requirements.txt index 0b254b0..50a9808 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,6 @@ fastapi>=0.115.0 uvicorn>=0.30.0 +websockets>=12.0 httpx>=0.27.0 sqlalchemy>=2.0.0 psycopg[binary]>=3.2.0 diff --git a/web/src/components/WebTerminal.tsx b/web/src/components/WebTerminal.tsx index dea8f67..5781705 100644 --- a/web/src/components/WebTerminal.tsx +++ b/web/src/components/WebTerminal.tsx @@ -113,10 +113,14 @@ export function WebTerminal({ wsUrl, title, onStatus, onReady }: Props) { ws.onerror = () => { onStatusRef.current?.("error", "websocket_error"); term.writeln("\r\n\x1b[31m[websocket error]\x1b[0m"); + term.writeln("\x1b[33mHint: ensure API has package 'websockets' and is restarted; Vite UI should reach ws://127.0.0.1:8890\x1b[0m"); }; - ws.onclose = () => { - onStatusRef.current?.("closed", "websocket_closed"); + ws.onclose = (ev) => { + onStatusRef.current?.("closed", `websocket_closed:${ev.code}`); + if (!ev.wasClean) { + term.writeln(`\r\n\x1b[33m[websocket closed code=${ev.code}]\x1b[0m`); + } }; const dataDisposable = term.onData((data) => { diff --git a/web/src/services/api.ts b/web/src/services/api.ts index a2cdeaf..96b4415 100644 --- a/web/src/services/api.ts +++ b/web/src/services/api.ts @@ -352,7 +352,19 @@ export const closeWebcrtSession = (sessionId: string) => export const webcrtWsUrl = (sessionId: string): string => { const proto = window.location.protocol === "https:" ? "wss:" : "ws:"; - return `${proto}//${window.location.host}/v1/webcrt/sessions/${encodeURIComponent(sessionId)}/ws`; + const path = `/v1/webcrt/sessions/${encodeURIComponent(sessionId)}/ws`; + // Optional override, e.g. ws://127.0.0.1:8890 + const override = String((import.meta as ImportMeta & { env?: Record }).env?.VITE_NETX_WS_BASE || "").trim(); + if (override) { + return `${override.replace(/\/$/, "")}${path}`; + } + // Vite/preview: HTTP is proxied, but WS proxy is often flaky — hit API directly. + const port = window.location.port; + if (port === "5173" || port === "4173") { + const apiHost = window.location.hostname === "localhost" ? "127.0.0.1" : window.location.hostname; + return `${proto}//${apiHost}:8890${path}`; + } + return `${proto}//${window.location.host}${path}`; }; export const fetchCliMeta = () => apiGet("/v1/cli/meta"); diff --git a/web/vite.config.ts b/web/vite.config.ts index 7e7d7f6..5c8c270 100644 --- a/web/vite.config.ts +++ b/web/vite.config.ts @@ -11,6 +11,8 @@ export default defineConfig({ target: "http://127.0.0.1:8890", changeOrigin: true, ws: true, + // Keep Origin aligned with upstream for WS handshake through proxy. + rewriteWsOrigin: true, }, }, },