Voice UI broken when served behind hero_router — multiple path resolution issues #10

Closed
opened 2026-04-15 14:51:46 +00:00 by salmaelsoly · 5 comments
Member

Summary

The Voice UI (hero_voice_ui) does not function when accessed through hero_router (e.g. at /hero_voice/ui/).
This affects both standalone access via hero_router and embedding inside hero_os as an iframe.

Issues Found

1. app.js fails to load (404)

The HTML loads app.js with a relative path. When the page is served at /hero_voice/ui (hero_router strips the trailing slash via 308 redirect), the browser resolves app.js to /hero_voice/app.js instead of /hero_voice/ui/app.js.

  • File: crates/hero_voice_ui/static/index.html line 992
  • Result: Entire app is non-functional — no JS loads

2. RPC calls go to wrong endpoint

app.js hardcodes const RPC_URL = '/rpc' as an absolute path. When behind hero_router, this hits the router's own management RPC at /rpc instead of /hero_voice/rpc.

  • File: crates/hero_voice_ui/static/app.js line 10
  • Result: All RPC calls (create topic, list topics, record, etc.) fail silently

3. Health check polling returns 404

The connection status widget in index.html calls fetch('health') which resolves to /hero_voice/health instead of /hero_voice/ui/health.

  • File: crates/hero_voice_ui/static/index.html (inline script at bottom)
  • Result: Status dot always shows disconnected, 404 errors every 5 seconds in DevTools

How to Reproduce

  1. Start hero_proc, hero_osis, hero_router, hero_os, hero_voice
  2. Open http://127.0.0.1:9988/hero_os/ui/
  3. Click the Voice island
  4. Open DevTools Console — see 404 errors for app.js, health
  5. Click + to create a topic — nothing happens
## Summary The Voice UI (`hero_voice_ui`) does not function when accessed through hero_router (e.g. at `/hero_voice/ui/`). This affects both standalone access via hero_router and embedding inside hero_os as an iframe. ## Issues Found ### 1. `app.js` fails to load (404) The HTML loads `app.js` with a relative path. When the page is served at `/hero_voice/ui` (hero_router strips the trailing slash via 308 redirect), the browser resolves `app.js` to `/hero_voice/app.js` instead of `/hero_voice/ui/app.js`. - **File:** `crates/hero_voice_ui/static/index.html` line 992 - **Result:** Entire app is non-functional — no JS loads ### 2. RPC calls go to wrong endpoint `app.js` hardcodes `const RPC_URL = '/rpc'` as an absolute path. When behind hero_router, this hits the router's own management RPC at `/rpc` instead of `/hero_voice/rpc`. - **File:** `crates/hero_voice_ui/static/app.js` line 10 - **Result:** All RPC calls (create topic, list topics, record, etc.) fail silently ### 3. Health check polling returns 404 The connection status widget in `index.html` calls `fetch('health')` which resolves to `/hero_voice/health` instead of `/hero_voice/ui/health`. - **File:** `crates/hero_voice_ui/static/index.html` (inline script at bottom) - **Result:** Status dot always shows disconnected, 404 errors every 5 seconds in DevTools ## How to Reproduce 1. Start `hero_proc`, `hero_osis`, `hero_router`, `hero_os`, `hero_voice` 2. Open `http://127.0.0.1:9988/hero_os/ui/` 3. Click the Voice island 4. Open DevTools Console — see 404 errors for `app.js`, `health` 5. Click **+** to create a topic — nothing happens
Author
Member

Same path resolution issue also affects the WebSocket endpoint. app.js:1174 hardcodes new WebSocket("${protocol}//${window.location.host}/ws") which hits hero_router instead of /hero_voice/ui/ws.

This causes recording to fail silently — the WebSocket errors out immediately, triggering stopRecording() and an InvalidStateError: Cannot close a closed AudioContext.

Same path resolution issue also affects the WebSocket endpoint. `app.js:1174` hardcodes `new WebSocket("${protocol}//${window.location.host}/ws")` which hits hero_router instead of `/hero_voice/ui/ws`. This causes recording to fail silently — the WebSocket errors out immediately, triggering `stopRecording()` and an `InvalidStateError: Cannot close a closed AudioContext`.
Author
Member

Implementation Spec for Issue #10

Objective

Make the hero_voice_ui frontend work correctly both when accessed directly and when served behind hero_router at any prefix path (e.g., /hero_voice/ui/). All URL references (script loading, RPC calls, health checks, WebSocket connections, file URLs) must resolve correctly regardless of the base path, using the X-Forwarded-Prefix header that hero_router injects.

Requirements

  • The HTML page must load app.js using a path that works at any mount point.
  • RPC calls from JavaScript must target the correct /rpc endpoint relative to the UI's mount path, not the absolute root.
  • The health check polling must resolve health relative to the UI's actual served path.
  • The WebSocket connection must use the correct path (not hardcoded /ws).
  • File URLs for audio and transforms (e.g., /files/audio/...) must be prefixed correctly.
  • A /rpc route must be added to the UI's Axum router since it is currently missing entirely.
  • The solution must NOT hardcode any prefix -- it must derive the base path dynamically from X-Forwarded-Prefix or from the browser's current page URL.
  • When no prefix is present (direct access), behavior must remain unchanged.

Files to Modify

File Description
crates/hero_voice_ui/src/main.rs Add /rpc proxy route. Enhance middleware to store X-Forwarded-Prefix as BasePath extension. Modify serve_static to inject <meta name="base-path"> into index.html when prefix is present.
crates/hero_voice_ui/static/app.js Derive RPC_URL dynamically from base-path meta tag. Fix WebSocket URL to use BASE prefix. Fix /files/audio/... and /files/transforms/... URLs.
crates/hero_voice_ui/static/index.html Change <script src="app.js"> to <script src="./app.js">. Fix inline health-check script to use base-path-derived URLs.

Implementation Plan

Step 1: Add /rpc proxy route and BasePath extension to the Axum server

Files: crates/hero_voice_ui/src/main.rs

  • Define a BasePath newtype struct
  • Modify hero_context_middleware to extract X-Forwarded-Prefix and insert BasePath extension
  • Add rpc_proxy_handler function using existing proxy_to_socket
  • Add .route("/rpc", post(rpc_proxy_handler)) to the router
  • Modify serve_static to inject <meta name="base-path"> tag into index.html when BasePath is present
    Dependencies: none

Step 2: Fix JavaScript to derive all URLs from the base path

Files: crates/hero_voice_ui/static/app.js

  • Add BASE constant that reads from <meta name="base-path"> tag with fallback
  • Replace hardcoded RPC_URL = '/rpc' with RPC_URL = BASE + '/rpc'
  • Fix WebSocket URL to use BASE + '/ws'
  • Fix audio file URLs to use BASE + '/files/audio/...'
  • Fix transform file URLs to use BASE + '/files/transforms/...'
    Dependencies: none (works independently, reads meta tag at runtime)

Step 3: Fix index.html script tag and inline health-check script

Files: crates/hero_voice_ui/static/index.html

  • Change <script src="app.js"> to <script src="./app.js">
  • Fix inline connection status script to derive BASE from meta tag
  • Prefix fetch('health') with BASE + '/health'
  • Prefix fetch('rpc', ...) with BASE + '/rpc'
    Dependencies: none (works independently, reads meta tag at runtime)

Acceptance Criteria

  • A /rpc route exists on the hero_voice_ui Axum server that proxies JSON-RPC requests to the backend socket
  • When accessed directly (no reverse proxy), the UI works exactly as before
  • When accessed through hero_router at /hero_voice/ui/, app.js loads successfully (no 404)
  • RPC calls from JavaScript target {prefix}/rpc and reach the UI's proxy
  • Health-check polling calls {prefix}/health and returns 200
  • WebSocket connection opens to {prefix}/ws
  • Audio and transform file URLs are prefixed correctly
  • Connection status dot shows green when both UI and backend are reachable through hero_router
  • <meta name="base-path"> is injected only when X-Forwarded-Prefix is present
  • /hero-bootstrap-bridge.css link remains absolute (served by hero_router at root)

Notes

  1. The missing /rpc route is a pre-existing bug -- proxy_to_socket exists but was never mounted as an HTTP route.
  2. The hero-bootstrap-bridge.css link on line 19 uses an absolute path intentionally -- it is served by hero_router at the root level.
  3. The meta tag injection only modifies the HTTP response body at runtime; the embedded asset itself is not modified.
## Implementation Spec for Issue #10 ### Objective Make the hero_voice_ui frontend work correctly both when accessed directly and when served behind hero_router at any prefix path (e.g., `/hero_voice/ui/`). All URL references (script loading, RPC calls, health checks, WebSocket connections, file URLs) must resolve correctly regardless of the base path, using the `X-Forwarded-Prefix` header that hero_router injects. ### Requirements - The HTML page must load `app.js` using a path that works at any mount point. - RPC calls from JavaScript must target the correct `/rpc` endpoint relative to the UI's mount path, not the absolute root. - The health check polling must resolve `health` relative to the UI's actual served path. - The WebSocket connection must use the correct path (not hardcoded `/ws`). - File URLs for audio and transforms (e.g., `/files/audio/...`) must be prefixed correctly. - A `/rpc` route must be added to the UI's Axum router since it is currently missing entirely. - The solution must NOT hardcode any prefix -- it must derive the base path dynamically from `X-Forwarded-Prefix` or from the browser's current page URL. - When no prefix is present (direct access), behavior must remain unchanged. ### Files to Modify | File | Description | |------|-------------| | `crates/hero_voice_ui/src/main.rs` | Add `/rpc` proxy route. Enhance middleware to store `X-Forwarded-Prefix` as BasePath extension. Modify `serve_static` to inject `<meta name="base-path">` into `index.html` when prefix is present. | | `crates/hero_voice_ui/static/app.js` | Derive `RPC_URL` dynamically from base-path meta tag. Fix WebSocket URL to use `BASE` prefix. Fix `/files/audio/...` and `/files/transforms/...` URLs. | | `crates/hero_voice_ui/static/index.html` | Change `<script src="app.js">` to `<script src="./app.js">`. Fix inline health-check script to use base-path-derived URLs. | ### Implementation Plan #### Step 1: Add `/rpc` proxy route and `BasePath` extension to the Axum server Files: `crates/hero_voice_ui/src/main.rs` - Define a `BasePath` newtype struct - Modify `hero_context_middleware` to extract `X-Forwarded-Prefix` and insert `BasePath` extension - Add `rpc_proxy_handler` function using existing `proxy_to_socket` - Add `.route("/rpc", post(rpc_proxy_handler))` to the router - Modify `serve_static` to inject `<meta name="base-path">` tag into `index.html` when BasePath is present Dependencies: none #### Step 2: Fix JavaScript to derive all URLs from the base path Files: `crates/hero_voice_ui/static/app.js` - Add `BASE` constant that reads from `<meta name="base-path">` tag with fallback - Replace hardcoded `RPC_URL = '/rpc'` with `RPC_URL = BASE + '/rpc'` - Fix WebSocket URL to use `BASE + '/ws'` - Fix audio file URLs to use `BASE + '/files/audio/...'` - Fix transform file URLs to use `BASE + '/files/transforms/...'` Dependencies: none (works independently, reads meta tag at runtime) #### Step 3: Fix index.html script tag and inline health-check script Files: `crates/hero_voice_ui/static/index.html` - Change `<script src="app.js">` to `<script src="./app.js">` - Fix inline connection status script to derive `BASE` from meta tag - Prefix `fetch('health')` with `BASE + '/health'` - Prefix `fetch('rpc', ...)` with `BASE + '/rpc'` Dependencies: none (works independently, reads meta tag at runtime) ### Acceptance Criteria - [ ] A `/rpc` route exists on the hero_voice_ui Axum server that proxies JSON-RPC requests to the backend socket - [ ] When accessed directly (no reverse proxy), the UI works exactly as before - [ ] When accessed through hero_router at `/hero_voice/ui/`, `app.js` loads successfully (no 404) - [ ] RPC calls from JavaScript target `{prefix}/rpc` and reach the UI's proxy - [ ] Health-check polling calls `{prefix}/health` and returns 200 - [ ] WebSocket connection opens to `{prefix}/ws` - [ ] Audio and transform file URLs are prefixed correctly - [ ] Connection status dot shows green when both UI and backend are reachable through hero_router - [ ] `<meta name="base-path">` is injected only when `X-Forwarded-Prefix` is present - [ ] `/hero-bootstrap-bridge.css` link remains absolute (served by hero_router at root) ### Notes 1. The missing `/rpc` route is a pre-existing bug -- `proxy_to_socket` exists but was never mounted as an HTTP route. 2. The `hero-bootstrap-bridge.css` link on line 19 uses an absolute path intentionally -- it is served by hero_router at the root level. 3. The meta tag injection only modifies the HTTP response body at runtime; the embedded asset itself is not modified.
Author
Member

Test Results

Build: pass
Tests: Total: 13, Passed: 13, Failed: 0

All 13 unit tests passed across 7 test binaries and 3 doc-test suites (3 doc-tests ignored). Build completed with warnings only (unused imports, dead code, deprecated methods, unused mut variables) -- no errors.

Test breakdown:

  • hero_voice: 13 tests (audio processing, wav encoding, conversion quality, voice/topic/folder CRUD)
  • hero_voice_app: 0 tests
  • hero_voice_examples: 0 tests
  • hero_voice_sdk: 0 tests
  • hero_voice_server: 0 tests
  • hero_voice_ui: 0 tests
  • Doc-tests: 3 ignored

Branch: development
Commit: 8cb9ec5

## Test Results **Build**: pass **Tests**: Total: 13, Passed: 13, Failed: 0 All 13 unit tests passed across 7 test binaries and 3 doc-test suites (3 doc-tests ignored). Build completed with warnings only (unused imports, dead code, deprecated methods, unused mut variables) -- no errors. Test breakdown: - hero_voice: 13 tests (audio processing, wav encoding, conversion quality, voice/topic/folder CRUD) - hero_voice_app: 0 tests - hero_voice_examples: 0 tests - hero_voice_sdk: 0 tests - hero_voice_server: 0 tests - hero_voice_ui: 0 tests - Doc-tests: 3 ignored Branch: development Commit: 8cb9ec5
Author
Member

Implementation Summary

Changes Made

crates/hero_voice_ui/src/main.rs

  • Added BasePath newtype struct to carry the URL prefix through request extensions
  • Modified hero_context_middleware to extract X-Forwarded-Prefix header, trim trailing slashes, and insert BasePath into request extensions
  • Added rpc_handler function that proxies JSON-RPC requests to the backend via proxy_to_socket
  • Added /rpc route to the Axum router (this was completely missing)
  • Rewrote serve_static to accept the full request, extract BasePath, and inject a <meta name="base-path"> tag into index.html when a prefix is present

crates/hero_voice_ui/static/app.js

  • Added BASE constant that reads from <meta name="base-path"> tag (injected by server when behind proxy), with fallback to deriving from window.location.pathname
  • Changed RPC_URL from hardcoded /rpc to BASE + '/rpc'
  • Prefixed audio file URLs with BASE (/files/audio/... -> BASE + '/files/audio/...')
  • Prefixed transform file URLs with BASE (/files/transforms/... -> BASE + '/files/transforms/...')
  • Fixed WebSocket URL from /ws to BASE + '/ws'

crates/hero_voice_ui/static/index.html

  • Changed <script src="app.js"> to <script src="./app.js"> for explicit relative resolution
  • Added B base-path variable to the inline connection status script (reads from <meta name="base-path"> tag)
  • Changed fetch('health') to fetch(B+'/health') and fetch('rpc',...) to fetch(B+'/rpc',...) in the polling function

Test Results

  • Build: pass (warnings only)
  • Tests: 13 passed, 0 failed

Notes

  • The /rpc route was completely absent from the UI server. While it may have appeared to work previously because hero_router proxied /rpc directly to the backend server socket, this meant the UI could not proxy RPC requests independently. The new route uses the existing proxy_to_socket function.
  • The <meta name="base-path"> tag is only injected when X-Forwarded-Prefix is present. When accessed directly (no reverse proxy), no tag is injected and BASE evaluates to empty string, preserving existing behavior.
  • The /hero-bootstrap-bridge.css link remains absolute as intended (served by hero_router at root).
## Implementation Summary ### Changes Made **crates/hero_voice_ui/src/main.rs** - Added `BasePath` newtype struct to carry the URL prefix through request extensions - Modified `hero_context_middleware` to extract `X-Forwarded-Prefix` header, trim trailing slashes, and insert `BasePath` into request extensions - Added `rpc_handler` function that proxies JSON-RPC requests to the backend via `proxy_to_socket` - Added `/rpc` route to the Axum router (this was completely missing) - Rewrote `serve_static` to accept the full request, extract `BasePath`, and inject a `<meta name="base-path">` tag into `index.html` when a prefix is present **crates/hero_voice_ui/static/app.js** - Added `BASE` constant that reads from `<meta name="base-path">` tag (injected by server when behind proxy), with fallback to deriving from `window.location.pathname` - Changed `RPC_URL` from hardcoded `/rpc` to `BASE + '/rpc'` - Prefixed audio file URLs with `BASE` (`/files/audio/...` -> `BASE + '/files/audio/...'`) - Prefixed transform file URLs with `BASE` (`/files/transforms/...` -> `BASE + '/files/transforms/...'`) - Fixed WebSocket URL from `/ws` to `BASE + '/ws'` **crates/hero_voice_ui/static/index.html** - Changed `<script src="app.js">` to `<script src="./app.js">` for explicit relative resolution - Added `B` base-path variable to the inline connection status script (reads from `<meta name="base-path">` tag) - Changed `fetch('health')` to `fetch(B+'/health')` and `fetch('rpc',...)` to `fetch(B+'/rpc',...)` in the polling function ### Test Results - Build: pass (warnings only) - Tests: 13 passed, 0 failed ### Notes - The `/rpc` route was completely absent from the UI server. While it may have appeared to work previously because hero_router proxied `/rpc` directly to the backend server socket, this meant the UI could not proxy RPC requests independently. The new route uses the existing `proxy_to_socket` function. - The `<meta name="base-path">` tag is only injected when `X-Forwarded-Prefix` is present. When accessed directly (no reverse proxy), no tag is injected and `BASE` evaluates to empty string, preserving existing behavior. - The `/hero-bootstrap-bridge.css` link remains absolute as intended (served by hero_router at root).
Author
Member

Pull request opened: #14

This PR implements the changes discussed in this issue.

Pull request opened: https://forge.ourworld.tf/lhumina_code/hero_voice/pulls/14 This PR implements the changes discussed in this issue.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
lhumina_code/hero_voice#10
No description provided.