From fbc41d42a692a9adf22048bf924d8f48047acf24 Mon Sep 17 00:00:00 2001 From: iris Date: Sun, 31 May 2026 16:56:50 +0200 Subject: [PATCH] dashboard: build same-origin /agent// links when gateway is up (closes #842) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When `services.hyperhive.gateway.enable` is on (default), the c0re NixOS module now sets `HIVE_GATEWAY_ENABLED=1` on the service env. `/api/state` exposes the flag as `gateway_enabled`; the SW4RM tab's container-row renderer flips three link sites (primary agent-name link, favicon fetch, `container`-kind nav-strip links) from the legacy `http://:/` direct TCP shape to same-origin `/agent//` URLs — the gateway proxies them to the per-agent harness via `agent-ports.json` or `agent-sockets.json` (#784 / #815). Gateway-off deploys keep the direct TCP fallback so local-dev / operator opt-out keeps working. `forge`-kind nav-strip links still resolve against `:3000` (separate sub-domain transition, tracked by `forge.behindGateway`); `external`-kind links are already absolute. Mirrors the `HIVE_MATRIX_GUI_ENABLED` env→snapshot-flag pattern. Docs updated: `docs/web-ui.md::Container row` + new `docs/gateway.md::Dashboard link shape` section. --- docs/gateway.md | 16 ++++++++++++++++ docs/web-ui.md | 14 ++++++++++++-- frontend/packages/dashboard/src/tabs.js | 19 +++++++++++++++++-- hive-c0re/src/dashboard.rs | 18 ++++++++++++++++++ nix/modules/hive-c0re.nix | 9 +++++++++ 5 files changed, 72 insertions(+), 4 deletions(-) diff --git a/docs/gateway.md b/docs/gateway.md index 618d5017..edc372fa 100644 --- a/docs/gateway.md +++ b/docs/gateway.md @@ -104,6 +104,22 @@ appear in `agent-ports.json` (the legacy TCP map) and the gateway falls back to TCP for them. Step 4 of #784 will drop the TCP map + the harness's TCP bind once every agent's flipped. +## Dashboard link shape (gateway vs direct) + +When the gateway is in front, the SW4RM tab builds per-agent links +as same-origin `/agent//…` URLs instead of the legacy direct +`http://:/` TCP shape. The signal comes from +`StateSnapshot.gateway_enabled`, sourced from the +`HIVE_GATEWAY_ENABLED` env the c0re NixOS module sets when +`services.hyperhive.gateway.enable = true`. Three render sites +flip together: the primary agent-name link, the favicon fetch +(`/icon`), and the nav-strip `container`-kind links from +`/api/agent//links`. `forge`-kind nav-strip links still +resolve against `http://:3000` (separate sub-domain transition +tracked by `forge.behindGateway`); `external`-kind links are +already absolute. See `docs/web-ui.md::Container row` for the +frontend-side derivation. + ## Sequencing history - #15 v0 (per-agent routing, #740) — first sub-app behind the gateway, JSON port table from c0re. diff --git a/docs/web-ui.md b/docs/web-ui.md index c2fbe770..441e0c0f 100644 --- a/docs/web-ui.md +++ b/docs/web-ui.md @@ -514,8 +514,18 @@ fetch entirely. `GET /api/agent/{name}/links`, a same-origin passthrough proxy that forwards the agent's own link list; the agent backend is the single source of truth. The frontend resolves each - `AgentLink.kind` (`container` → `http://host:`, - `forge` → `http://host:3000`, `external` → already absolute). + `AgentLink.kind` against a per-agent base URL that depends on + whether hive-gateway is in front (`StateSnapshot.gateway_enabled`, + sourced from the `HIVE_GATEWAY_ENABLED` env the c0re NixOS + module sets when `services.hyperhive.gateway.enable = true`). + Gateway-on (default): `container` → `/agent//` (same + origin, gateway proxies to the per-agent harness — TCP or + unix-domain per #784). Gateway-off (legacy / local dev): + `container` → `http://:/` (direct TCP + fallback). Forge links resolve against `http://:3000`, + external links are already absolute. The same flag drives the + primary agent-name link + favicon fetch (`/icon`), so the + whole row routes through the gateway as a unit. **When the container is stopped** (`ContainerView.running = false`), the host clears live-only fields before emitting the state, so the dashboard never renders stale data: the badge chain is diff --git a/frontend/packages/dashboard/src/tabs.js b/frontend/packages/dashboard/src/tabs.js index 2b86d4a2..1fce37e5 100644 --- a/frontend/packages/dashboard/src/tabs.js +++ b/frontend/packages/dashboard/src/tabs.js @@ -471,6 +471,13 @@ window.marked = marked; } const hostname = (s && s.hostname) || window.location.hostname; + // When hive-gateway is in front of the dashboard, build same-origin + // `/agent//` URLs instead of the direct `http://:/` + // TCP fallback — the gateway proxies the prefix to the per-agent + // harness (TCP via `agent-ports.json` or unix-domain via + // `agent-sockets.json` per #784 / #815). See + // `docs/web-ui.md::Container row` + `docs/gateway.md::Vhost map`. + const gatewayLinks = !!(s && s.gateway_enabled); const ul = el('ul', { class: 'containers' }); const tree = buildAgentTree(containers); // In-flight rebuild / meta-update / destroy ops per agent name — @@ -480,7 +487,9 @@ window.marked = marked; const inFlight = inFlightOpsByAgent(); for (const node of tree) { const c = node.container; - const url = `http://${hostname}:${c.port}/`; + const url = gatewayLinks + ? `/agent/${encodeURIComponent(c.name)}/` + : `http://${hostname}:${c.port}/`; // Pending-state derivation + queued-vs-running split — see // docs/web-ui.md::Container row for the transient → in-flight // queue priority order and the opRunning rationale. @@ -572,7 +581,13 @@ window.marked = marked; const navStrip = el('span', { class: 'nav-strip' }); head.append(navStrip); const forgeBase = `http://${hostname}:3000`; - const containerBase = `http://${hostname}:${c.port}`; + // Container nav-strip links resolve against the same base as the + // primary `name` link — gateway-prefixed when the gateway is in + // front, direct TCP otherwise. Strips the trailing slash so + // `lnk.url` starting with `/` concatenates cleanly. + const containerBase = gatewayLinks + ? `/agent/${encodeURIComponent(c.name)}` + : `http://${hostname}:${c.port}`; if (c.running) { fetch(`/api/agent/${encodeURIComponent(c.name)}/links`) .then((r) => (r.ok ? r.json() : [])) diff --git a/hive-c0re/src/dashboard.rs b/hive-c0re/src/dashboard.rs index dedc2ce3..f73c1e2a 100644 --- a/hive-c0re/src/dashboard.rs +++ b/hive-c0re/src/dashboard.rs @@ -236,6 +236,17 @@ struct StateSnapshot { /// chrome so the `M4TR1X →` tab doesn't flash when the GUI is off /// (#607, #634). matrix_gui_enabled: bool, + /// Whether `hive-gateway` is in front of this dashboard. Sourced + /// from `HIVE_GATEWAY_ENABLED` env var (set by the c0re NixOS + /// module when `services.hyperhive.gateway.enable` is on). When + /// true the dashboard frontend builds same-origin + /// `/agent//` links to the per-agent web UI (the gateway + /// proxies them via `agent-ports.json` + `agent-sockets.json`); + /// when false it falls back to direct + /// `http://:/` TCP links so gateway-off / + /// local-dev deploys keep working. See + /// `docs/gateway.md::Vhost map`. + gateway_enabled: bool, } /// `OpQuestion` + computed `question_refs` / `answer_refs`. Built @@ -435,6 +446,13 @@ async fn api_state(headers: HeaderMap, State(state): State) -> axum::J let s = v.to_string_lossy().to_ascii_lowercase(); matches!(s.as_str(), "1" | "true" | "yes") }), + gateway_enabled: std::env::var_os("HIVE_GATEWAY_ENABLED").is_some_and(|v| { + // Same truthy-string parse as `matrix_gui_enabled`; the + // env var is set by the c0re NixOS module to the literal + // "1" when `services.hyperhive.gateway.enable` is on. + let s = v.to_string_lossy().to_ascii_lowercase(); + matches!(s.as_str(), "1" | "true" | "yes") + }), }) } diff --git a/nix/modules/hive-c0re.nix b/nix/modules/hive-c0re.nix index dc7cc055..dd9a047d 100644 --- a/nix/modules/hive-c0re.nix +++ b/nix/modules/hive-c0re.nix @@ -304,6 +304,15 @@ in # dashboard chrome whether the GUI is reachable so iris's # `M4TR1X →` tab doesn't show when the GUI is off. HIVE_MATRIX_GUI_ENABLED = "1"; + } + // lib.optionalAttrs config.services.hyperhive.gateway.enable { + # Availability flag for `/api/state.gateway_enabled`. When + # true the dashboard builds same-origin `/agent//` links + # to the per-agent web UI (gateway proxies them); when false + # it falls back to direct `http://:/` TCP + # links (legacy / gateway-disabled ops). See + # `docs/gateway.md::Vhost map` for the routing shape. + HIVE_GATEWAY_ENABLED = "1"; }; # Matrix GUI static serving lives entirely on the hive-gateway # nginx since #609 — when gateway is off the operator opts out