UI: stuck on "Connecting…" when embedded under a path prefix (Hero OS iframe) #92

Closed
opened 2026-04-16 14:20:43 +00:00 by rawan · 3 comments
Member

Summary

When the Compute dashboard is opened from inside the Hero OS shell, the sidebar
shows a yellow "Connecting…" indicator and the Component Versions list never
loads, even though hero_compute_server, hero_compute_ui, my_hypervisor,
and hero_proc are all running and healthy.

Hitting the UI directly at http://127.0.0.1:9001/status works and returns a
fully populated, healthy JSON response.

Environment

  • hero_compute: latest development
  • hero_os: embeds the island via <iframe src="/hero_compute/ui/">
    (see hero_os/crates/hero_os_app/src/island_content.rs:422)
  • hero_router: routes /hero_compute/ui/*hero_compute/ui.sock, does
    not strip the prefix, injects X-Forwarded-Prefix: /hero_compute/ui

Root cause

crates/hero_compute_ui/static/js/dashboard.js fetches status/RPC with
absolute paths:

  • dashboard.js:2296fetch("/status") (startup check)
  • dashboard.js:2365fetch("/status") (health poll every 2 s)
  • any other fetch("/rpc"), fetch("/console/…"), fetch("/explorer/rpc") etc.

When the iframe is loaded at /hero_compute/ui/, the browser resolves
/status to http://<router>:9988/status, which does not exist on the
router → 404 → setServerStatus(false) → sidebar is stuck on "Connecting…"
forever.

Works when accessed directly on :9001 because there is no prefix.

Reproduce

  1. hero_proc up with hero_compute_server + hero_compute_ui registered
  2. Open Hero OS, click the Compute island
  3. Observe sidebar: yellow dot, "Connecting…", spinner on Component Versions
  4. Open devtools → Network: requests to /status return 404 from hero_router
  5. Compare with direct http://127.0.0.1:9001/ — works fine

Fix options

Quick (recommended for now): change absolute fetches to relative in
dashboard.js:

  • fetch("/status")fetch("./status")
  • fetch("/rpc")fetch("./rpc")
  • audit the rest of the file for any other fetch("/…") / new WebSocket("/…")
    / new EventSource("/…") and make them prefix-relative

Proper: adopt the standard hero_web_prefix pattern used by other
hero_*_ui crates:

  1. UI server reads X-Forwarded-Prefix from the request
  2. Template injects it as a JS constant, e.g.
    <script>window.HERO_PREFIX = "{{ prefix }}";</script>
  3. dashboard.js builds URLs with a helper:
    const url = (p) => (window.HERO_PREFIX || "") + p;
  4. Works in both direct (:9001, prefix="") and proxied
    (/hero_compute/ui/, prefix=/hero_compute/ui) modes

Acceptance

  • Compute island loads cleanly inside Hero OS: solid green dot,
    Component Versions populated, Recent Activity live
  • Direct access on :9001 still works
  • No absolute /status, /rpc, /console/…, /explorer/rpc fetches
    remain in dashboard.js

References

  • hero_os/crates/hero_os_app/src/island_content.rs:422 — iframe src
  • hero_router/crates/hero_router/src/server/routes.rs (~L874, ~L1091) —
    forward-path and X-Forwarded-Prefix injection
  • hero_compute/crates/hero_compute_ui/static/js/dashboard.js:2296,2365 — bug
  • hero_web_prefix skill — the standard fix pattern
    image
## Summary When the Compute dashboard is opened from inside the Hero OS shell, the sidebar shows a yellow "Connecting…" indicator and the Component Versions list never loads, even though `hero_compute_server`, `hero_compute_ui`, `my_hypervisor`, and `hero_proc` are all running and healthy. Hitting the UI directly at `http://127.0.0.1:9001/status` works and returns a fully populated, healthy JSON response. ## Environment - hero_compute: latest `development` - hero_os: embeds the island via `<iframe src="/hero_compute/ui/">` (see `hero_os/crates/hero_os_app/src/island_content.rs:422`) - hero_router: routes `/hero_compute/ui/*` → `hero_compute/ui.sock`, does **not** strip the prefix, injects `X-Forwarded-Prefix: /hero_compute/ui` ## Root cause `crates/hero_compute_ui/static/js/dashboard.js` fetches status/RPC with **absolute** paths: - `dashboard.js:2296` — `fetch("/status")` (startup check) - `dashboard.js:2365` — `fetch("/status")` (health poll every 2 s) - any other `fetch("/rpc")`, `fetch("/console/…")`, `fetch("/explorer/rpc")` etc. When the iframe is loaded at `/hero_compute/ui/`, the browser resolves `/status` to `http://<router>:9988/status`, which does not exist on the router → 404 → `setServerStatus(false)` → sidebar is stuck on "Connecting…" forever. Works when accessed directly on `:9001` because there is no prefix. ## Reproduce 1. `hero_proc` up with `hero_compute_server` + `hero_compute_ui` registered 2. Open Hero OS, click the **Compute** island 3. Observe sidebar: yellow dot, "Connecting…", spinner on Component Versions 4. Open devtools → Network: requests to `/status` return 404 from hero_router 5. Compare with direct `http://127.0.0.1:9001/` — works fine ## Fix options **Quick (recommended for now):** change absolute fetches to relative in `dashboard.js`: - `fetch("/status")` → `fetch("./status")` - `fetch("/rpc")` → `fetch("./rpc")` - audit the rest of the file for any other `fetch("/…")` / `new WebSocket("/…")` / `new EventSource("/…")` and make them prefix-relative **Proper:** adopt the standard `hero_web_prefix` pattern used by other hero_*_ui crates: 1. UI server reads `X-Forwarded-Prefix` from the request 2. Template injects it as a JS constant, e.g. `<script>window.HERO_PREFIX = "{{ prefix }}";</script>` 3. `dashboard.js` builds URLs with a helper: `const url = (p) => (window.HERO_PREFIX || "") + p;` 4. Works in both direct (`:9001`, prefix=`""`) and proxied (`/hero_compute/ui/`, prefix=`/hero_compute/ui`) modes ## Acceptance - [ ] Compute island loads cleanly inside Hero OS: solid green dot, Component Versions populated, Recent Activity live - [ ] Direct access on `:9001` still works - [ ] No absolute `/status`, `/rpc`, `/console/…`, `/explorer/rpc` fetches remain in `dashboard.js` ## References - `hero_os/crates/hero_os_app/src/island_content.rs:422` — iframe src - `hero_router/crates/hero_router/src/server/routes.rs` (~L874, ~L1091) — forward-path and `X-Forwarded-Prefix` injection - `hero_compute/crates/hero_compute_ui/static/js/dashboard.js:2296,2365` — bug - `hero_web_prefix` skill — the standard fix pattern ![image](/attachments/c0aed196-ac0e-4d7d-9b8c-bc6964c52d1b)
Author
Member

Implementation Spec for Issue #92

Objective

Make the Hero Compute UI work correctly when embedded under a reverse-proxy path prefix (e.g. /hero_compute/ui/ in Hero OS), by adopting the standard hero_web_prefix pattern: an Axum middleware reads X-Forwarded-Prefix per request, injects it as base_path into every Askama template, the base template exposes it via a <meta name="base-path"> tag and a window.HERO_PREFIX JS constant, and both HTML templates and dashboard.js build every same-origin URL with that prefix. Direct access on :9001 continues to work because the header is absent and base_path renders as the empty string.

Requirements

  • Backend reads X-Forwarded-Prefix per request (no env var, no restart needed).
  • Every Askama template (base.html, index.html, nodes.html, vms.html, admin.html, explorer.html, settings.html, partials/navbar.html, partials/sidebar.html) has a base_path field available.
  • Every absolute same-origin URL in HTML (href="/...", src="/...") is prefixed with {{ base_path }}.
  • dashboard.js uses a HERO_PREFIX constant (read from the meta tag or window.HERO_PREFIX) on every fetch(...), new WebSocket(...), window.location.href = "/...", and inline-generated <a href="/..."> HTML.
  • Direct access on :9001 still works (prefix resolves to "").
  • No remaining absolute /status, /rpc, /explorer/rpc, /console/..., /api/..., /openrpc.json, /explorer-openrpc.json, /css/..., /js/..., /fonts/..., /favicon.svg, /settings, /vms, /nodes, /admin, /explorer, / anchor or resource references in files shipped by the UI crate.

Files to Modify/Create

  • crates/hero_compute_ui/src/server.rs
    • Add BasePath(String) extension type and base_path_middleware reading x-forwarded-prefix.
    • Attach middleware to the router (.layer(axum::middleware::from_fn(base_path_middleware))).
    • Add a base_path: String field to every #[derive(Template)] struct (IndexTemplate, NodesTemplate, VmsTemplate, AdminTemplate, ExplorerTemplate, SettingsTemplate).
    • Each page handler extracts Extension(BasePath(bp)) and passes it into the template.
    • The admin_handler and explorer_handler worker-mode redirect must use format!("{}/", base_path) so it still works behind a prefix.
  • crates/hero_compute_ui/templates/base.html
    • Add <meta name="base-path" content="{{ base_path }}"> inside <head>.
    • Prefix every href/src on <link>, <script>, <link rel="icon"> with {{ base_path }}.
    • Add <script>window.HERO_PREFIX = "{{ base_path }}";</script> before loading dashboard.js (next to the existing _uiMode injection).
  • crates/hero_compute_ui/templates/partials/navbar.html
    • Prefix every href="/..." (brand link, /settings, /openrpc.json) with {{ base_path }}.
  • crates/hero_compute_ui/templates/partials/sidebar.html
    • Prefix every href="/..." (/, /nodes, /vms, /admin, /explorer, /settings) with {{ base_path }}.
  • crates/hero_compute_ui/templates/index.html
    • Replace absolute <a href="/vms">, <a href="/nodes">, <a href="/admin">, <a href="/explorer"> (both static markup and JS-generated HTML) with {{ base_path }}-prefixed or HERO_PREFIX-prefixed equivalents.
  • crates/hero_compute_ui/templates/nodes.html
    • Same treatment for the JS-generated <a href="/vms"> (use HERO_PREFIX in JS-string construction).
  • crates/hero_compute_ui/static/js/dashboard.js
    • Add HERO_PREFIX constant near the top (read from window.HERO_PREFIX or the <meta name="base-path"> tag as a fallback).
    • Rewrite every absolute same-origin fetch / WebSocket / location mutation to prepend HERO_PREFIX. Specific call sites (approximate line numbers):
      • L6: fetch("/api/config")
      • L120: rpc() uses "/rpc"
      • L121: explorerRpc() uses "/explorer/rpc"
      • L502: window.location.href = "/settings"
      • L1154: fetch("/api/console/destroy/" + ...)
      • L2035: "/explorer-openrpc.json" / "/openrpc.json"
      • L2126: "/explorer/rpc" / "/rpc"
      • L2296: fetch("/status") (startup check)
      • L2365: fetch("/status") (health poll)
      • L2791: consolePath = '/console/' + ...
      • L2979: fetch("/api/console/destroy/..." )
      • L3072: fetch("/api/console/sessions")
      • L3095: background WS url building "/console/" + ...
    • Leave window.location.replace(window.location.pathname) as-is.
    • Leave cross-origin remote-console URL (http://<nodeIp>:<uiPort>/vms?...) as-is — it targets a different node entirely.

Implementation Plan

Step 1: Add base_path middleware and extension in the server

Files: crates/hero_compute_ui/src/server.rs

  • Add a BasePath(String) newtype and an async base_path_middleware that reads the lowercase x-forwarded-prefix header, trims trailing /, and inserts the value into request extensions.
  • Attach the middleware to the top-level router with .layer(axum::middleware::from_fn(base_path_middleware)).
    Dependencies: none

Step 2: Thread base_path into every Askama template struct and page handler

Files: crates/hero_compute_ui/src/server.rs

  • Add base_path: String to each of IndexTemplate, NodesTemplate, VmsTemplate, AdminTemplate, ExplorerTemplate, SettingsTemplate.
  • In each handler, add an Extension(BasePath(base_path)): Extension<BasePath> extractor and pass base_path into the template.
  • In admin_handler and explorer_handler worker-mode redirect, redirect to format!("{}/", base_path) instead of "/".
    Dependencies: Step 1

Step 3: Update base.html — meta tag, HERO_PREFIX script, and static-asset URLs

Files: crates/hero_compute_ui/templates/base.html

  • In <head>, add <meta name="base-path" content="{{ base_path }}"> just after the viewport meta.
  • Prefix every same-origin <link>/<script> href/src with {{ base_path }} (bootstrap, unpoly, dashboard css/js, xterm, favicon). Leave the external CDN bootstrap-icons link alone.
  • Next to the existing _uiMode inline script, add <script>window.HERO_PREFIX = "{{ base_path }}";</script>.
    Dependencies: Step 2

Step 4: Update the shared partials (navbar and sidebar)

Files: crates/hero_compute_ui/templates/partials/navbar.html, crates/hero_compute_ui/templates/partials/sidebar.html

  • Prefix every absolute href="/..." in both partials with {{ base_path }}. Askama includes render in the parent's context, so base_path is already in scope once Step 2 lands.
    Dependencies: Step 2

Step 5: Update page-level templates (index.html, nodes.html)

Files: crates/hero_compute_ui/templates/index.html, crates/hero_compute_ui/templates/nodes.html

  • Static <a href="/..."> in index.html → prefix with {{ base_path }}.
  • JS-generated HTML strings inside <script> blocks that contain "/vms", "/admin", "/explorer" → prefix with HERO_PREFIX.
  • Same JS treatment for the anchor at line ~385 of nodes.html.
    Dependencies: Step 3 (HERO_PREFIX must exist before the scripts run — it does, since base.html injects it above dashboard.js).

Step 6: Introduce HERO_PREFIX in dashboard.js and rewrite every absolute same-origin URL

Files: crates/hero_compute_ui/static/js/dashboard.js

  • Near the top, add a HERO_PREFIX constant read from window.HERO_PREFIX or the <meta name="base-path"> tag as a fallback.
  • Rewrite each call site listed in "Files to Modify/Create" to prepend HERO_PREFIX.
  • For the console preflight+WebSocket pair, prefixing consolePath at its single point of definition is sufficient — do not also prepend HERO_PREFIX at the WS call site.
  • For the background WS URL builder at L3095, insert HERO_PREFIX before /console/.
  • Leave location.pathname-based reloads and the cross-origin remote-console URL untouched.
    Dependencies: Step 3 (so window.HERO_PREFIX is defined before dashboard.js loads).

Step 7: Final audit

  • Grep for remaining absolute same-origin paths:
    grep -nE 'fetch\("/|new WebSocket\("/|new EventSource\("/' crates/hero_compute_ui/static/js/dashboard.js
    grep -nE 'href="/|src="/|action="/' crates/hero_compute_ui/templates/
    
  • Expected residual: only the external CDN links in base.html, the cross-origin node URL builder in dashboard.js, and window.location.replace(window.location.pathname).
    Dependencies: Steps 3–6.

Acceptance Criteria

  • Direct access on :9001: dashboard loads, solid green dot, Component Versions populated, Recent Activity live, /status and /rpc fire unmodified.
  • Embedded at /hero_compute/ui/ via hero_router (prefix preserved, X-Forwarded-Prefix injected): dashboard inside the Hero OS iframe loads cleanly, solid green dot, Component Versions populated, Recent Activity live, VM console opens and streams.
  • grep -nE 'fetch\("/|new WebSocket\("/|new EventSource\("/' crates/hero_compute_ui/static/js/dashboard.js returns nothing.
  • grep -nE 'href="/|src="/|action="/' crates/hero_compute_ui/templates/ returns only external CDN URLs.
  • Navigating between pages (Dashboard → Nodes → VMs → Admin → API Explorer → Settings) via the sidebar works in both direct and proxied modes.
  • Deploying a VM, opening its console, minimizing it to the floating indicator, and restoring it all work in both modes.

Notes

  • Template context inheritance: Askama {% include "partials/..." %} renders with the parent's context, so once every top-level template has base_path, partials can use {{ base_path }} with no extra wiring.
  • Header case: Axum HeaderMap::get is case-insensitive for lowercase names. Use "x-forwarded-prefix".
  • Trim trailing slash: The middleware strips any trailing / so HERO_PREFIX + "/status" never produces //status.
  • Empty prefix fallthrough: When the header is absent, base_path = "", so {{ base_path }}/css/... renders as /css/... (unchanged) and HERO_PREFIX + "/status" evaluates to /status.
  • Script load order: window.HERO_PREFIX is set before dashboard.js runs because the inline script sits just above <script src=".../dashboard.js"> in base.html. The meta-tag fallback inside dashboard.js is defensive.
  • WebSocket URLs: Prefix consolePath at its single definition; the downstream WS call already uses it, so no double-prefix.
  • Cross-origin remote console link: http://<nodeIp>:<uiPort>/vms?console=... points to another node on its own port (no proxy), so it must NOT receive the prefix.
  • worker-mode redirects: Use Redirect::to(&format!("{}/", base_path)) so workers land on the island home inside Hero OS rather than escaping the iframe.
  • No new dependencies: axum + askama already in Cargo.toml; no additions needed.
  • hero_router already does the right thing: strips the prefix and injects X-Forwarded-Prefix. No proxy-side changes needed.
## Implementation Spec for Issue #92 ### Objective Make the Hero Compute UI work correctly when embedded under a reverse-proxy path prefix (e.g. `/hero_compute/ui/` in Hero OS), by adopting the standard `hero_web_prefix` pattern: an Axum middleware reads `X-Forwarded-Prefix` per request, injects it as `base_path` into every Askama template, the base template exposes it via a `<meta name="base-path">` tag and a `window.HERO_PREFIX` JS constant, and both HTML templates and `dashboard.js` build every same-origin URL with that prefix. Direct access on `:9001` continues to work because the header is absent and `base_path` renders as the empty string. ### Requirements - Backend reads `X-Forwarded-Prefix` per request (no env var, no restart needed). - Every Askama template (`base.html`, `index.html`, `nodes.html`, `vms.html`, `admin.html`, `explorer.html`, `settings.html`, `partials/navbar.html`, `partials/sidebar.html`) has a `base_path` field available. - Every absolute same-origin URL in HTML (`href="/..."`, `src="/..."`) is prefixed with `{{ base_path }}`. - `dashboard.js` uses a `HERO_PREFIX` constant (read from the meta tag or `window.HERO_PREFIX`) on every `fetch(...)`, `new WebSocket(...)`, `window.location.href = "/..."`, and inline-generated `<a href="/...">` HTML. - Direct access on `:9001` still works (prefix resolves to `""`). - No remaining absolute `/status`, `/rpc`, `/explorer/rpc`, `/console/...`, `/api/...`, `/openrpc.json`, `/explorer-openrpc.json`, `/css/...`, `/js/...`, `/fonts/...`, `/favicon.svg`, `/settings`, `/vms`, `/nodes`, `/admin`, `/explorer`, `/` anchor or resource references in files shipped by the UI crate. ### Files to Modify/Create - `crates/hero_compute_ui/src/server.rs` - Add `BasePath(String)` extension type and `base_path_middleware` reading `x-forwarded-prefix`. - Attach middleware to the router (`.layer(axum::middleware::from_fn(base_path_middleware))`). - Add a `base_path: String` field to every `#[derive(Template)]` struct (`IndexTemplate`, `NodesTemplate`, `VmsTemplate`, `AdminTemplate`, `ExplorerTemplate`, `SettingsTemplate`). - Each page handler extracts `Extension(BasePath(bp))` and passes it into the template. - The `admin_handler` and `explorer_handler` worker-mode redirect must use `format!("{}/", base_path)` so it still works behind a prefix. - `crates/hero_compute_ui/templates/base.html` - Add `<meta name="base-path" content="{{ base_path }}">` inside `<head>`. - Prefix every `href`/`src` on `<link>`, `<script>`, `<link rel="icon">` with `{{ base_path }}`. - Add `<script>window.HERO_PREFIX = "{{ base_path }}";</script>` before loading `dashboard.js` (next to the existing `_uiMode` injection). - `crates/hero_compute_ui/templates/partials/navbar.html` - Prefix every `href="/..."` (brand link, `/settings`, `/openrpc.json`) with `{{ base_path }}`. - `crates/hero_compute_ui/templates/partials/sidebar.html` - Prefix every `href="/..."` (`/`, `/nodes`, `/vms`, `/admin`, `/explorer`, `/settings`) with `{{ base_path }}`. - `crates/hero_compute_ui/templates/index.html` - Replace absolute `<a href="/vms">`, `<a href="/nodes">`, `<a href="/admin">`, `<a href="/explorer">` (both static markup and JS-generated HTML) with `{{ base_path }}`-prefixed or `HERO_PREFIX`-prefixed equivalents. - `crates/hero_compute_ui/templates/nodes.html` - Same treatment for the JS-generated `<a href="/vms">` (use `HERO_PREFIX` in JS-string construction). - `crates/hero_compute_ui/static/js/dashboard.js` - Add `HERO_PREFIX` constant near the top (read from `window.HERO_PREFIX` or the `<meta name="base-path">` tag as a fallback). - Rewrite every absolute same-origin fetch / WebSocket / location mutation to prepend `HERO_PREFIX`. Specific call sites (approximate line numbers): - L6: `fetch("/api/config")` - L120: `rpc()` uses `"/rpc"` - L121: `explorerRpc()` uses `"/explorer/rpc"` - L502: `window.location.href = "/settings"` - L1154: `fetch("/api/console/destroy/" + ...)` - L2035: `"/explorer-openrpc.json"` / `"/openrpc.json"` - L2126: `"/explorer/rpc"` / `"/rpc"` - L2296: `fetch("/status")` (startup check) - L2365: `fetch("/status")` (health poll) - L2791: `consolePath = '/console/' + ...` - L2979: `fetch("/api/console/destroy/..." )` - L3072: `fetch("/api/console/sessions")` - L3095: background WS url building `"/console/" + ...` - Leave `window.location.replace(window.location.pathname)` as-is. - Leave cross-origin remote-console URL (`http://<nodeIp>:<uiPort>/vms?...`) as-is — it targets a different node entirely. ### Implementation Plan #### Step 1: Add base_path middleware and extension in the server Files: `crates/hero_compute_ui/src/server.rs` - Add a `BasePath(String)` newtype and an async `base_path_middleware` that reads the lowercase `x-forwarded-prefix` header, trims trailing `/`, and inserts the value into request extensions. - Attach the middleware to the top-level router with `.layer(axum::middleware::from_fn(base_path_middleware))`. Dependencies: none #### Step 2: Thread base_path into every Askama template struct and page handler Files: `crates/hero_compute_ui/src/server.rs` - Add `base_path: String` to each of `IndexTemplate`, `NodesTemplate`, `VmsTemplate`, `AdminTemplate`, `ExplorerTemplate`, `SettingsTemplate`. - In each handler, add an `Extension(BasePath(base_path)): Extension<BasePath>` extractor and pass `base_path` into the template. - In `admin_handler` and `explorer_handler` worker-mode redirect, redirect to `format!("{}/", base_path)` instead of `"/"`. Dependencies: Step 1 #### Step 3: Update base.html — meta tag, HERO_PREFIX script, and static-asset URLs Files: `crates/hero_compute_ui/templates/base.html` - In `<head>`, add `<meta name="base-path" content="{{ base_path }}">` just after the viewport meta. - Prefix every same-origin `<link>`/`<script>` `href`/`src` with `{{ base_path }}` (bootstrap, unpoly, dashboard css/js, xterm, favicon). Leave the external CDN bootstrap-icons link alone. - Next to the existing `_uiMode` inline script, add `<script>window.HERO_PREFIX = "{{ base_path }}";</script>`. Dependencies: Step 2 #### Step 4: Update the shared partials (navbar and sidebar) Files: `crates/hero_compute_ui/templates/partials/navbar.html`, `crates/hero_compute_ui/templates/partials/sidebar.html` - Prefix every absolute `href="/..."` in both partials with `{{ base_path }}`. Askama includes render in the parent's context, so `base_path` is already in scope once Step 2 lands. Dependencies: Step 2 #### Step 5: Update page-level templates (index.html, nodes.html) Files: `crates/hero_compute_ui/templates/index.html`, `crates/hero_compute_ui/templates/nodes.html` - Static `<a href="/...">` in `index.html` → prefix with `{{ base_path }}`. - JS-generated HTML strings inside `<script>` blocks that contain `"/vms"`, `"/admin"`, `"/explorer"` → prefix with `HERO_PREFIX`. - Same JS treatment for the anchor at line ~385 of `nodes.html`. Dependencies: Step 3 (HERO_PREFIX must exist before the scripts run — it does, since base.html injects it above dashboard.js). #### Step 6: Introduce HERO_PREFIX in dashboard.js and rewrite every absolute same-origin URL Files: `crates/hero_compute_ui/static/js/dashboard.js` - Near the top, add a `HERO_PREFIX` constant read from `window.HERO_PREFIX` or the `<meta name="base-path">` tag as a fallback. - Rewrite each call site listed in "Files to Modify/Create" to prepend `HERO_PREFIX`. - For the console preflight+WebSocket pair, prefixing `consolePath` at its single point of definition is sufficient — do not also prepend `HERO_PREFIX` at the WS call site. - For the background WS URL builder at L3095, insert `HERO_PREFIX` before `/console/`. - Leave `location.pathname`-based reloads and the cross-origin remote-console URL untouched. Dependencies: Step 3 (so `window.HERO_PREFIX` is defined before dashboard.js loads). #### Step 7: Final audit - Grep for remaining absolute same-origin paths: ``` grep -nE 'fetch\("/|new WebSocket\("/|new EventSource\("/' crates/hero_compute_ui/static/js/dashboard.js grep -nE 'href="/|src="/|action="/' crates/hero_compute_ui/templates/ ``` - Expected residual: only the external CDN links in `base.html`, the cross-origin node URL builder in dashboard.js, and `window.location.replace(window.location.pathname)`. Dependencies: Steps 3–6. ### Acceptance Criteria - [ ] Direct access on `:9001`: dashboard loads, solid green dot, Component Versions populated, Recent Activity live, `/status` and `/rpc` fire unmodified. - [ ] Embedded at `/hero_compute/ui/` via `hero_router` (prefix preserved, `X-Forwarded-Prefix` injected): dashboard inside the Hero OS iframe loads cleanly, solid green dot, Component Versions populated, Recent Activity live, VM console opens and streams. - [ ] `grep -nE 'fetch\("/|new WebSocket\("/|new EventSource\("/' crates/hero_compute_ui/static/js/dashboard.js` returns nothing. - [ ] `grep -nE 'href="/|src="/|action="/' crates/hero_compute_ui/templates/` returns only external CDN URLs. - [ ] Navigating between pages (Dashboard → Nodes → VMs → Admin → API Explorer → Settings) via the sidebar works in both direct and proxied modes. - [ ] Deploying a VM, opening its console, minimizing it to the floating indicator, and restoring it all work in both modes. ### Notes - **Template context inheritance**: Askama `{% include "partials/..." %}` renders with the parent's context, so once every top-level template has `base_path`, partials can use `{{ base_path }}` with no extra wiring. - **Header case**: Axum `HeaderMap::get` is case-insensitive for lowercase names. Use `"x-forwarded-prefix"`. - **Trim trailing slash**: The middleware strips any trailing `/` so `HERO_PREFIX + "/status"` never produces `//status`. - **Empty prefix fallthrough**: When the header is absent, `base_path = ""`, so `{{ base_path }}/css/...` renders as `/css/...` (unchanged) and `HERO_PREFIX + "/status"` evaluates to `/status`. - **Script load order**: `window.HERO_PREFIX` is set before `dashboard.js` runs because the inline script sits just above `<script src=".../dashboard.js">` in `base.html`. The meta-tag fallback inside dashboard.js is defensive. - **WebSocket URLs**: Prefix `consolePath` at its single definition; the downstream WS call already uses it, so no double-prefix. - **Cross-origin remote console link**: `http://<nodeIp>:<uiPort>/vms?console=...` points to another node on its own port (no proxy), so it must NOT receive the prefix. - **worker-mode redirects**: Use `Redirect::to(&format!("{}/", base_path))` so workers land on the island home inside Hero OS rather than escaping the iframe. - **No new dependencies**: axum + askama already in `Cargo.toml`; no additions needed. - **hero_router already does the right thing**: strips the prefix and injects `X-Forwarded-Prefix`. No proxy-side changes needed.
rawan self-assigned this 2026-04-19 08:44:56 +00:00
Author
Member

Test Results

Command: cargo test -p hero_compute_ui

  • Total: 1
  • Passed: 1
  • Failed: 0
  • Ignored: 0

All tests passed. One unit test executed in the hero_compute_ui crate binary (server::tests::test_normalize_socket_path). Final result line: test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out.

Workspace check

cargo check --workspace: PASS - the entire workspace compiles cleanly, including hero_compute, hero_compute_sdk, hero_compute_ui, hero_compute_server, hero_compute_explorer, and hero_compute_examples. No warnings or errors reported; build finished in ~8.6s.

No failures to report.

## Test Results **Command:** `cargo test -p hero_compute_ui` - Total: 1 - Passed: 1 - Failed: 0 - Ignored: 0 All tests passed. One unit test executed in the `hero_compute_ui` crate binary (`server::tests::test_normalize_socket_path`). Final result line: `test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out`. ### Workspace check `cargo check --workspace`: PASS - the entire workspace compiles cleanly, including `hero_compute`, `hero_compute_sdk`, `hero_compute_ui`, `hero_compute_server`, `hero_compute_explorer`, and `hero_compute_examples`. No warnings or errors reported; build finished in ~8.6s. No failures to report.
Author
Member

Implementation Summary

The hero_web_prefix pattern has been applied to hero_compute_ui so the dashboard works identically when served directly on :9001 and when embedded under a reverse-proxy path prefix such as /hero_compute/ui/.

Changes

crates/hero_compute_ui/src/server.rs

  • Added a BasePath(String) newtype and an axum middleware (base_path_middleware) that reads the X-Forwarded-Prefix header on every request, trims any trailing /, and stores it in request extensions. Falls back to the empty string when the header is absent.
  • Attached the middleware to the top-level router with .layer(axum::middleware::from_fn(base_path_middleware)).
  • Added a base_path: String field to each Askama template struct: IndexTemplate, NodesTemplate, VmsTemplate, AdminTemplate, ExplorerTemplate, SettingsTemplate.
  • Every page handler now extracts Extension(BasePath(base_path)): Extension<BasePath> and threads the value into the rendered template.
  • Worker-mode redirects in admin_handler and explorer_handler now use Redirect::to(&format!("{}/", base_path)) so the user stays inside the iframe when running under a prefix.

crates/hero_compute_ui/templates/base.html

  • Added <meta name="base-path" content="{{ base_path }}"> in <head>.
  • Prefixed every same-origin <link rel="icon|stylesheet"> and <script src=...> with {{ base_path }} (bootstrap, unpoly, dashboard, xterm, favicon). The external CDN bootstrap-icons link is left untouched.
  • Added <script>window.HERO_PREFIX = "{{ base_path }}";</script> immediately above the existing _uiMode / dashboard.js scripts, guaranteeing the constant is defined before dashboard.js runs.

crates/hero_compute_ui/templates/partials/navbar.html and partials/sidebar.html

  • Prefixed every absolute href="/..." (brand link, /, /nodes, /vms, /admin, /explorer, /settings, /openrpc.json) with {{ base_path }}.

crates/hero_compute_ui/templates/index.html

  • Prefixed the three static <a href="/..."> anchors with {{ base_path }}.
  • Updated five JS-generated anchor strings inside <script> blocks to concatenate HERO_PREFIX + "/..." instead of "/...".

crates/hero_compute_ui/templates/nodes.html

  • Updated the JS-generated anchor in the empty-state message to use HERO_PREFIX + "/vms".

crates/hero_compute_ui/static/js/dashboard.js

  • Added a HERO_PREFIX constant near the top of the file, sourced from window.HERO_PREFIX with a <meta name="base-path"> fallback.
  • Prefixed every absolute same-origin URL: /api/config, /rpc, /explorer/rpc, /settings, /status (startup + health poll), /openrpc.json, /explorer-openrpc.json, /api/console/destroy/..., /api/console/sessions, /console/... (primary preflight+WebSocket pair and the background WS builder), and two inline <a href="/settings"> strings used in SSH-keys empty-state and toast HTML.
  • Prefixed consolePath at its single definition, so both the downstream fetch(...) preflight and new WebSocket(...) upgrade pick it up automatically with no risk of double-prefixing.
  • Left window.location.replace(window.location.pathname) as-is because pathname already carries the prefix.
  • Left the cross-origin remote-console URL builder (http://<nodeIp>:<uiPort>/vms?console=...) as-is because it targets another node on its own port, where no reverse proxy is involved.

Audit

grep -nE 'fetch\("/|new WebSocket\("/|new EventSource\("/' \
  crates/hero_compute_ui/static/js/dashboard.js
  → No matches

grep -rnE 'href="/|src="/|action="/' \
  crates/hero_compute_ui/templates/
  → No matches

The only remaining absolute paths in the code shipped by this crate are:

  • External CDN URLs in base.html (cdn.jsdelivr.net/...bootstrap-icons...).
  • The cross-origin remote-console URL builder in dashboard.js.
  • window.location.replace(window.location.pathname) (path, not URL).

Test Results

cargo test -p hero_compute_ui

  • Total: 1
  • Passed: 1
  • Failed: 0
  • Ignored: 0
  • test result: ok. 1 passed; 0 failed

cargo check --workspace — PASS. All crates (hero_compute, hero_compute_sdk, hero_compute_ui, hero_compute_server, hero_compute_explorer, hero_compute_examples) compile cleanly.

Acceptance Criteria

  • Direct access on :9001 still works (header absent → base_path resolves to "", all URLs render unchanged).
  • No absolute fetch("/..."), new WebSocket("/..."), or new EventSource("/...") calls remain in dashboard.js.
  • No absolute href="/...", src="/...", or action="/..." remain in any template under crates/hero_compute_ui/templates/.
  • Manual verification inside the Hero OS iframe (solid green dot, Component Versions populated, Recent Activity live, console opens and streams) — to be confirmed by the reviewer.

Notes

  • No new dependencies were introduced; axum and askama were already in Cargo.toml.
  • hero_router already strips the prefix and injects X-Forwarded-Prefix, so no proxy-side changes were needed.
  • The workspace carries a pre-existing, unrelated uncommitted fix to crates/hero_compute_server/src/cloud/constants.rs (literal merge-conflict markers in the committed file); that file was not touched by this change set. Per the issue author's instruction, no commits were created — the changes are ready in the working tree for review.
## Implementation Summary The `hero_web_prefix` pattern has been applied to `hero_compute_ui` so the dashboard works identically when served directly on `:9001` and when embedded under a reverse-proxy path prefix such as `/hero_compute/ui/`. ### Changes **`crates/hero_compute_ui/src/server.rs`** - Added a `BasePath(String)` newtype and an axum middleware (`base_path_middleware`) that reads the `X-Forwarded-Prefix` header on every request, trims any trailing `/`, and stores it in request extensions. Falls back to the empty string when the header is absent. - Attached the middleware to the top-level router with `.layer(axum::middleware::from_fn(base_path_middleware))`. - Added a `base_path: String` field to each Askama template struct: `IndexTemplate`, `NodesTemplate`, `VmsTemplate`, `AdminTemplate`, `ExplorerTemplate`, `SettingsTemplate`. - Every page handler now extracts `Extension(BasePath(base_path)): Extension<BasePath>` and threads the value into the rendered template. - Worker-mode redirects in `admin_handler` and `explorer_handler` now use `Redirect::to(&format!("{}/", base_path))` so the user stays inside the iframe when running under a prefix. **`crates/hero_compute_ui/templates/base.html`** - Added `<meta name="base-path" content="{{ base_path }}">` in `<head>`. - Prefixed every same-origin `<link rel="icon|stylesheet">` and `<script src=...>` with `{{ base_path }}` (bootstrap, unpoly, dashboard, xterm, favicon). The external CDN `bootstrap-icons` link is left untouched. - Added `<script>window.HERO_PREFIX = "{{ base_path }}";</script>` immediately above the existing `_uiMode` / `dashboard.js` scripts, guaranteeing the constant is defined before dashboard.js runs. **`crates/hero_compute_ui/templates/partials/navbar.html`** and **`partials/sidebar.html`** - Prefixed every absolute `href="/..."` (brand link, `/`, `/nodes`, `/vms`, `/admin`, `/explorer`, `/settings`, `/openrpc.json`) with `{{ base_path }}`. **`crates/hero_compute_ui/templates/index.html`** - Prefixed the three static `<a href="/...">` anchors with `{{ base_path }}`. - Updated five JS-generated anchor strings inside `<script>` blocks to concatenate `HERO_PREFIX + "/..."` instead of `"/..."`. **`crates/hero_compute_ui/templates/nodes.html`** - Updated the JS-generated anchor in the empty-state message to use `HERO_PREFIX + "/vms"`. **`crates/hero_compute_ui/static/js/dashboard.js`** - Added a `HERO_PREFIX` constant near the top of the file, sourced from `window.HERO_PREFIX` with a `<meta name="base-path">` fallback. - Prefixed every absolute same-origin URL: `/api/config`, `/rpc`, `/explorer/rpc`, `/settings`, `/status` (startup + health poll), `/openrpc.json`, `/explorer-openrpc.json`, `/api/console/destroy/...`, `/api/console/sessions`, `/console/...` (primary preflight+WebSocket pair and the background WS builder), and two inline `<a href="/settings">` strings used in SSH-keys empty-state and toast HTML. - Prefixed `consolePath` at its single definition, so both the downstream `fetch(...)` preflight and `new WebSocket(...)` upgrade pick it up automatically with no risk of double-prefixing. - Left `window.location.replace(window.location.pathname)` as-is because `pathname` already carries the prefix. - Left the cross-origin remote-console URL builder (`http://<nodeIp>:<uiPort>/vms?console=...`) as-is because it targets another node on its own port, where no reverse proxy is involved. ### Audit ``` grep -nE 'fetch\("/|new WebSocket\("/|new EventSource\("/' \ crates/hero_compute_ui/static/js/dashboard.js → No matches grep -rnE 'href="/|src="/|action="/' \ crates/hero_compute_ui/templates/ → No matches ``` The only remaining absolute paths in the code shipped by this crate are: - External CDN URLs in `base.html` (`cdn.jsdelivr.net/...bootstrap-icons...`). - The cross-origin remote-console URL builder in `dashboard.js`. - `window.location.replace(window.location.pathname)` (path, not URL). ### Test Results `cargo test -p hero_compute_ui` - Total: 1 - Passed: 1 - Failed: 0 - Ignored: 0 - `test result: ok. 1 passed; 0 failed` `cargo check --workspace` — PASS. All crates (`hero_compute`, `hero_compute_sdk`, `hero_compute_ui`, `hero_compute_server`, `hero_compute_explorer`, `hero_compute_examples`) compile cleanly. ### Acceptance Criteria - [x] Direct access on `:9001` still works (header absent → `base_path` resolves to `""`, all URLs render unchanged). - [x] No absolute `fetch("/...")`, `new WebSocket("/...")`, or `new EventSource("/...")` calls remain in `dashboard.js`. - [x] No absolute `href="/..."`, `src="/..."`, or `action="/..."` remain in any template under `crates/hero_compute_ui/templates/`. - [ ] Manual verification inside the Hero OS iframe (solid green dot, Component Versions populated, Recent Activity live, console opens and streams) — to be confirmed by the reviewer. ### Notes - No new dependencies were introduced; axum and askama were already in `Cargo.toml`. - `hero_router` already strips the prefix and injects `X-Forwarded-Prefix`, so no proxy-side changes were needed. - The workspace carries a pre-existing, unrelated uncommitted fix to `crates/hero_compute_server/src/cloud/constants.rs` (literal merge-conflict markers in the committed file); that file was not touched by this change set. Per the issue author's instruction, no commits were created — the changes are ready in the working tree for review.
rawan closed this issue 2026-04-20 09:19:09 +00:00
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_compute#92
No description provided.