From 3796882d22fe073da5317fc9051f25744cf4e390 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Wed, 10 Dec 2025 15:32:29 +0000 Subject: [PATCH] webchat: improve logging and static serving --- .../Clawdis/Resources/WebChat/bootstrap.js | 32 +++++++++++++---- .../Resources/WebChat/webchat.bundle.js | 20 +++++++---- src/webchat/server.test.ts | 18 ++++++++++ src/webchat/server.ts | 36 +++++++++++++------ 4 files changed, 81 insertions(+), 25 deletions(-) diff --git a/apps/macos/Sources/Clawdis/Resources/WebChat/bootstrap.js b/apps/macos/Sources/Clawdis/Resources/WebChat/bootstrap.js index 3daef912d..b97788463 100644 --- a/apps/macos/Sources/Clawdis/Resources/WebChat/bootstrap.js +++ b/apps/macos/Sources/Clawdis/Resources/WebChat/bootstrap.js @@ -50,6 +50,7 @@ class GatewaySocket { this.ws = ws; ws.onopen = () => { + logStatus(`ws: open -> sending hello (${this.url})`); const hello = { type: "hello", minProtocol: 1, @@ -65,9 +66,15 @@ class GatewaySocket { ws.send(JSON.stringify(hello)); }; - ws.onerror = (err) => reject(err); + ws.onerror = (err) => { + logStatus(`ws: error ${formatError(err)}`); + reject(err); + }; ws.onclose = (ev) => { + logStatus( + `ws: close code=${ev.code} reason=${ev.reason || "n/a"} clean=${ev.wasClean}`, + ); if (this.pending.size > 0) { for (const [, p] of this.pending) p.reject(new Error("gateway closed")); @@ -84,6 +91,9 @@ class GatewaySocket { return; } if (msg.type === "hello-ok") { + logStatus( + `ws: hello-ok presence=${msg?.snapshot?.presence?.length ?? 0} healthOk=${msg?.snapshot?.health?.ok ?? "n/a"}`, + ); this.handlers.set("snapshot", msg.snapshot); resolve(msg); return; @@ -267,14 +277,22 @@ const startChat = async () => { const params = new URLSearchParams(window.location.search); const sessionKey = params.get("session") || "main"; const wsUrl = (() => { - const u = new URL(window.location.href); - u.protocol = u.protocol.replace("http", "ws"); - u.port = params.get("gatewayPort") || "18789"; - u.pathname = "/"; - u.search = ""; + const loc = new URL(window.location.href); + const requestedPort = Number.parseInt( + params.get("gatewayPort") ?? "", + 10, + ); + const gatewayPort = + Number.isInteger(requestedPort) && + requestedPort > 0 && + requestedPort <= 65_535 + ? requestedPort + : 18_789; + const gatewayHost = params.get("gatewayHost") || loc.hostname || "127.0.0.1"; + const u = new URL(`ws://${gatewayHost}:${gatewayPort}/`); return u.toString(); })(); - logStatus("boot: connecting gateway"); + logStatus(`boot: connecting gateway (${wsUrl})`); const gateway = new GatewaySocket(wsUrl); const hello = await gateway.connect(); const healthOkRef = { current: Boolean(hello?.snapshot?.health?.ok ?? true) }; diff --git a/apps/macos/Sources/Clawdis/Resources/WebChat/webchat.bundle.js b/apps/macos/Sources/Clawdis/Resources/WebChat/webchat.bundle.js index 9e736d118..63ca20c54 100644 --- a/apps/macos/Sources/Clawdis/Resources/WebChat/webchat.bundle.js +++ b/apps/macos/Sources/Clawdis/Resources/WebChat/webchat.bundle.js @@ -196394,6 +196394,7 @@ var GatewaySocket = class { const ws = new WebSocket(this.url); this.ws = ws; ws.onopen = () => { + logStatus(`ws: open -> sending hello (${this.url})`); const hello = { type: "hello", minProtocol: 1, @@ -196408,8 +196409,12 @@ var GatewaySocket = class { }; ws.send(JSON.stringify(hello)); }; - ws.onerror = (err) => reject(err); + ws.onerror = (err) => { + logStatus(`ws: error ${formatError(err)}`); + reject(err); + }; ws.onclose = (ev) => { + logStatus(`ws: close code=${ev.code} reason=${ev.reason || "n/a"} clean=${ev.wasClean}`); if (this.pending.size > 0) { for (const [, p$3] of this.pending) p$3.reject(new Error("gateway closed")); this.pending.clear(); @@ -196424,6 +196429,7 @@ var GatewaySocket = class { return; } if (msg.type === "hello-ok") { + logStatus(`ws: hello-ok presence=${msg?.snapshot?.presence?.length ?? 0} healthOk=${msg?.snapshot?.health?.ok ?? "n/a"}`); this.handlers.set("snapshot", msg.snapshot); resolve(msg); return; @@ -196592,14 +196598,14 @@ const startChat = async () => { const params = new URLSearchParams(window.location.search); const sessionKey = params.get("session") || "main"; const wsUrl = (() => { - const u$4 = new URL(window.location.href); - u$4.protocol = u$4.protocol.replace("http", "ws"); - u$4.port = params.get("gatewayPort") || "18789"; - u$4.pathname = "/"; - u$4.search = ""; + const loc = new URL(window.location.href); + const requestedPort = Number.parseInt(params.get("gatewayPort") ?? "", 10); + const gatewayPort = Number.isInteger(requestedPort) && requestedPort > 0 && requestedPort <= 65535 ? requestedPort : 18789; + const gatewayHost = params.get("gatewayHost") || loc.hostname || "127.0.0.1"; + const u$4 = new URL(`ws://${gatewayHost}:${gatewayPort}/`); return u$4.toString(); })(); - logStatus("boot: connecting gateway"); + logStatus(`boot: connecting gateway (${wsUrl})`); const gateway = new GatewaySocket(wsUrl); const hello = await gateway.connect(); const healthOkRef = { current: Boolean(hello?.snapshot?.health?.ok ?? true) }; diff --git a/src/webchat/server.test.ts b/src/webchat/server.test.ts index b55af2c5c..cf1a7bea2 100644 --- a/src/webchat/server.test.ts +++ b/src/webchat/server.test.ts @@ -30,6 +30,11 @@ const fetchText = (url: string) => .on("error", reject); }); +const fetchHeaders = (url: string) => + new Promise((resolve, reject) => { + http.get(url, (res) => resolve(res.headers)).on("error", reject); + }); + describe("webchat server (static only)", () => { test("serves index.html over loopback", { timeout: 8000 }, async () => { const port = await getFreePort(); @@ -41,4 +46,17 @@ describe("webchat server (static only)", () => { await stopWebChatServer(); } }); + + test("serves bundled JS with module-friendly content type", async () => { + const port = await getFreePort(); + await startWebChatServer(port); + try { + const headers = await fetchHeaders( + `http://127.0.0.1:${port}/webchat.bundle.js`, + ); + expect(headers["content-type"]).toContain("application/javascript"); + } finally { + await stopWebChatServer(); + } + }); }); diff --git a/src/webchat/server.ts b/src/webchat/server.ts index 81ff95538..47f744f43 100644 --- a/src/webchat/server.ts +++ b/src/webchat/server.ts @@ -38,6 +38,28 @@ function notFound(res: http.ServerResponse) { res.end("Not Found"); } +function contentTypeForExt(ext: string) { + switch (ext) { + case ".html": + return "text/html"; + case ".js": + return "application/javascript"; + case ".css": + return "text/css"; + case ".json": + case ".map": + return "application/json"; + case ".svg": + return "image/svg+xml"; + case ".png": + return "image/png"; + case ".ico": + return "image/x-icon"; + default: + return "application/octet-stream"; + } +} + export async function startWebChatServer( port = WEBCHAT_DEFAULT_PORT, ): Promise { @@ -67,15 +89,7 @@ export async function startWebChatServer( } const data = fs.readFileSync(filePath); const ext = path.extname(filePath).toLowerCase(); - const type = - ext === ".html" - ? "text/html" - : ext === ".js" - ? "application/javascript" - : ext === ".css" - ? "text/css" - : "application/octet-stream"; - res.setHeader("Content-Type", type); + res.setHeader("Content-Type", contentTypeForExt(ext)); res.end(data); return; } @@ -93,7 +107,8 @@ export async function startWebChatServer( const filePath = path.join(root, relPath); if (filePath.startsWith(root) && fs.existsSync(filePath)) { const data = fs.readFileSync(filePath); - res.setHeader("Content-Type", "application/octet-stream"); + const ext = path.extname(filePath).toLowerCase(); + res.setHeader("Content-Type", contentTypeForExt(ext)); res.end(data); return; } @@ -121,7 +136,6 @@ export async function startWebChatServer( } state = { server, port }; - logDebug(`webchat server listening on 127.0.0.1:${port}`); return state; }