From 74031e1e2aab77881c8e1eddb5f1766b47dfcfdc Mon Sep 17 00:00:00 2001 From: wesleysimplicio <6108320+wesleysimplicio@users.noreply.github.com> Date: Sun, 17 May 2026 11:36:29 -0700 Subject: [PATCH] fix(dashboard): respect HERMES_BASE_PATH in WebSocket URLs (#25547) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When the dashboard is reverse-proxied under a path prefix (`X-Forwarded-Prefix: /dashboard`), the SPA already routes its `/api/...` REST traffic through `HERMES_BASE_PATH` via `web/src/lib/api.ts`. Three WebSocket URLs constructed elsewhere were still hardcoded to root `/api/...` and so opened `wss://host/api/...` instead of `wss://host/dashboard/api/...`, forcing operators to forward selected root API/WS paths through the reverse proxy as a workaround (see issue #25547). Add `HERMES_BASE_PATH` between `host` and `/api/...` in the three constructed WebSocket URLs: - `web/src/pages/ChatPage.tsx` — PTY WebSocket - `web/src/components/ChatSidebar.tsx` — events subscriber - `web/src/lib/gatewayClient.ts` — JSON-RPC gateway WebSocket When the dashboard is served at root, `HERMES_BASE_PATH === """ and the URLs are bit-for-bit identical to before. Under a prefix, the WebSocket connections now go through the same proxy path the REST calls already use. Note: bundled dashboard plugins (kanban, hermes-achievements) embed `"/api/plugins/..."` in their compiled `dist/index.js` and remain out of scope here — those need source-side fixes per plugin. Fixes #25547. --- web/src/components/ChatSidebar.tsx | 3 ++- web/src/lib/gatewayClient.ts | 4 +++- web/src/pages/ChatPage.tsx | 3 ++- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/web/src/components/ChatSidebar.tsx b/web/src/components/ChatSidebar.tsx index 38f1cf80abd..c311673fafc 100644 --- a/web/src/components/ChatSidebar.tsx +++ b/web/src/components/ChatSidebar.tsx @@ -30,6 +30,7 @@ import { Card } from "@/components/ui/card"; import { ModelPickerDialog } from "@/components/ModelPickerDialog"; import { ToolCall, type ToolEntry } from "@/components/ToolCall"; import { GatewayClient, type ConnectionState } from "@/lib/gatewayClient"; +import { HERMES_BASE_PATH } from "@/lib/api"; import { cn } from "@/lib/utils"; import { AlertCircle, ChevronDown, RefreshCw } from "lucide-react"; @@ -160,7 +161,7 @@ export function ChatSidebar({ channel, className }: ChatSidebarProps) { const proto = window.location.protocol === "https:" ? "wss:" : "ws:"; const qs = new URLSearchParams({ token, channel }); const ws = new WebSocket( - `${proto}//${window.location.host}/api/events?${qs.toString()}`, + `${proto}//${window.location.host}${HERMES_BASE_PATH}/api/events?${qs.toString()}`, ); // `unmounting` suppresses the banner during cleanup — `ws.close()` diff --git a/web/src/lib/gatewayClient.ts b/web/src/lib/gatewayClient.ts index fa58841ce18..9092ef2d32d 100644 --- a/web/src/lib/gatewayClient.ts +++ b/web/src/lib/gatewayClient.ts @@ -13,6 +13,8 @@ * await gw.request("prompt.submit", { session_id, text: "hi" }) */ +import { HERMES_BASE_PATH } from "@/lib/api"; + export type GatewayEventName = | "gateway.ready" | "session.info" @@ -117,7 +119,7 @@ export class GatewayClient { const scheme = location.protocol === "https:" ? "wss:" : "ws:"; const ws = new WebSocket( - `${scheme}//${location.host}/api/ws?token=${encodeURIComponent(resolved)}`, + `${scheme}//${location.host}${HERMES_BASE_PATH}/api/ws?token=${encodeURIComponent(resolved)}`, ); this.ws = ws; diff --git a/web/src/pages/ChatPage.tsx b/web/src/pages/ChatPage.tsx index 6fd32fa43fc..3e3c2e3268b 100644 --- a/web/src/pages/ChatPage.tsx +++ b/web/src/pages/ChatPage.tsx @@ -24,6 +24,7 @@ import { Terminal } from "@xterm/xterm"; import "@xterm/xterm/css/xterm.css"; import { Button } from "@nous-research/ui/ui/components/button"; import { Typography } from "@/components/NouiTypography"; +import { HERMES_BASE_PATH } from "@/lib/api"; import { cn } from "@/lib/utils"; import { Copy, PanelRight, X } from "lucide-react"; import { useCallback, useEffect, useMemo, useRef, useState } from "react"; @@ -44,7 +45,7 @@ function buildWsUrl( const proto = window.location.protocol === "https:" ? "wss:" : "ws:"; const qs = new URLSearchParams({ token, channel }); if (resume) qs.set("resume", resume); - return `${proto}//${window.location.host}/api/pty?${qs.toString()}`; + return `${proto}//${window.location.host}${HERMES_BASE_PATH}/api/pty?${qs.toString()}`; } // Channel id ties this chat tab's PTY child (publisher) to its sidebar