dashboard: hide forge links instead of guessing <hostname>:3000
Adds services.hyperhive.forge.publicUrl (defaults to the gateway vhost URL when behindGateway=true, null otherwise). HIVE_FORGE_PUBLIC_URL is now sourced from it instead of hardcoding https://${forge.domain} whenever behindGateway is on. The 4 frontend call sites that built a forge link from state.forge_public_url now hide the link when that's absent, rather than guessing http://<browser-hostname>:3000 — a guess that's only correct by accident once the operator isn't on plain localhost. Fixes the dashboard H0M3 tile, per-agent-row forge links + agent menu, the approval-queue PR link, and the per-agent page's own meta-nav forge link (found during this pass, same defect, not in the original 3-site inventory). Docs + doc-comments updated to match.
This commit is contained in:
parent
68560215bd
commit
3512e4b019
10 changed files with 96 additions and 42 deletions
|
|
@ -35,7 +35,10 @@ through. Three flex columns:
|
||||||
Each `NavLink.kind` resolves
|
Each `NavLink.kind` resolves
|
||||||
differently in the frontend: `Container` → same-origin path
|
differently in the frontend: `Container` → same-origin path
|
||||||
(the agent page is itself container-local); `Forge` →
|
(the agent page is itself container-local); `Forge` →
|
||||||
`http://<host>:3000<url>`; `External` → already absolute.
|
`state.forge_public_url + url` (sourced from
|
||||||
|
`services.hyperhive.forge.publicUrl`), and the link is omitted
|
||||||
|
entirely when that's unset — never guessed from `<host>:3000`;
|
||||||
|
`External` → already absolute.
|
||||||
All anchors are built via `el()` — agent-declared icon /
|
All anchors are built via `el()` — agent-declared icon /
|
||||||
label / url strings never reach `innerHTML` (XSS-safe by
|
label / url strings never reach `innerHTML` (XSS-safe by
|
||||||
construction).
|
construction).
|
||||||
|
|
|
||||||
|
|
@ -728,11 +728,14 @@ is a pure portal with no tab-bar or SSE subscriptions. Typography + colours
|
||||||
inherit from the shared theme (Catppuccin Mocha via `common.css` + `theme.css`).
|
inherit from the shared theme (Catppuccin Mocha via `common.css` + `theme.css`).
|
||||||
Optional tiles are hidden until `home.js` confirms their availability:
|
Optional tiles are hidden until `home.js` confirms their availability:
|
||||||
Matrix is hidden until `home.js` confirms `matrix_gui_enabled` (same gating as
|
Matrix is hidden until `home.js` confirms `matrix_gui_enabled` (same gating as
|
||||||
the dashboard's M4TR1X tab); Forge is hidden until `home.js` confirms
|
the dashboard's M4TR1X tab); Forge is hidden until `home.js` confirms both
|
||||||
`state.forge_present` and fills the href from `state.forge_public_url` (the
|
`state.forge_present` **and** `state.forge_public_url` (sourced from
|
||||||
gateway-served public URL when `services.hyperhive.forge.behindGateway=true`)
|
`services.hyperhive.forge.publicUrl`, which defaults to the gateway vhost URL
|
||||||
or falls back to the direct `:3000` port. Operators without matrix or forge
|
when `behindGateway=true` and is `null` otherwise) and fills the href from the
|
||||||
enabled never see dead links. `home.js` also fills the swarm/hive identity
|
latter — never guessed from the operator's browser hostname + a container
|
||||||
|
port, which is only right by accident off plain localhost. Operators without
|
||||||
|
matrix or forge enabled — or with forge on but no public URL configured —
|
||||||
|
never see a dead or wrong link. `home.js` also fills the swarm/hive identity
|
||||||
line at the top. Dashboard is now served at `/dashboard.html` (route swap
|
line at the top. Dashboard is now served at `/dashboard.html` (route swap
|
||||||
completed in #1464 step 2); the home page at `/` replaces the old dashboard
|
completed in #1464 step 2); the home page at `/` replaces the old dashboard
|
||||||
root. All dashboard sub-pages include a `← Home` back-link for navigation.
|
root. All dashboard sub-pages include a `← Home` back-link for navigation.
|
||||||
|
|
@ -835,7 +838,9 @@ fetch entirely.
|
||||||
`docs/gateway.md::Per-agent unix-socket upstream`).
|
`docs/gateway.md::Per-agent unix-socket upstream`).
|
||||||
Gateway-off (legacy / local dev): base URL is
|
Gateway-off (legacy / local dev): base URL is
|
||||||
`http://<host>:<container.port>` (direct TCP fallback). Forge
|
`http://<host>:<container.port>` (direct TCP fallback). Forge
|
||||||
links resolve against `http://<host>:3000`, external links are
|
links resolve against `state.forge_public_url` (sourced from
|
||||||
|
`services.hyperhive.forge.publicUrl`) and are omitted entirely when
|
||||||
|
that's unset — never guessed from `<host>:3000`. External links are
|
||||||
already absolute. The same base URL drives the primary agent-name
|
already absolute. The same base URL drives the primary agent-name
|
||||||
link + favicon fetch, so the whole row routes through the gateway
|
link + favicon fetch, so the whole row routes through the gateway
|
||||||
as a unit.
|
as a unit.
|
||||||
|
|
|
||||||
|
|
@ -1230,10 +1230,13 @@ window.marked = marked;
|
||||||
const metaLinks = $('meta-links');
|
const metaLinks = $('meta-links');
|
||||||
if (metaLinks && Array.isArray(s.links)) {
|
if (metaLinks && Array.isArray(s.links)) {
|
||||||
metaLinks.replaceChildren();
|
metaLinks.replaceChildren();
|
||||||
// Prefer forge_public_url from state (set when the gateway serves
|
// s.forge_public_url (set from services.hyperhive.forge.publicUrl)
|
||||||
// forge at its sub-domain); fall back to :3000 for local-dev.
|
// or falsy — never guessed from "<hostname>:3000". A forge-kind
|
||||||
const forgeBase = s.forge_public_url || `http://${window.location.hostname}:3000`;
|
// link is skipped entirely below when there's no public URL to
|
||||||
|
// point it at.
|
||||||
|
const forgeBase = s.forge_public_url || null;
|
||||||
s.links.forEach((lnk, i) => {
|
s.links.forEach((lnk, i) => {
|
||||||
|
if (lnk.kind === 'forge' && !forgeBase) return;
|
||||||
const href = lnk.kind === 'forge' ? forgeBase + (lnk.url || '')
|
const href = lnk.kind === 'forge' ? forgeBase + (lnk.url || '')
|
||||||
: lnk.kind === 'external' ? (lnk.url || '')
|
: lnk.kind === 'external' ? (lnk.url || '')
|
||||||
: /* container */ (lnk.url || '');
|
: /* container */ (lnk.url || '');
|
||||||
|
|
|
||||||
|
|
@ -242,12 +242,10 @@ export function renderApprovals() {
|
||||||
}
|
}
|
||||||
// forge link base — only when the hive-forge container is up.
|
// forge link base — only when the hive-forge container is up.
|
||||||
const fs = window.__hyperhive_state;
|
const fs = window.__hyperhive_state;
|
||||||
const hostname = (fs && fs.hostname) || window.location.hostname;
|
// state.forge_public_url (set from services.hyperhive.forge.publicUrl)
|
||||||
// Prefer state.forge_public_url (set when forge.behindGateway=true,
|
// or null — never guessed from "<hostname>:3000". The PR-link builder
|
||||||
// e.g. "https://forge.pr1ma.darkest.space") over the direct :3000 port.
|
// below already gates on forgeBase being truthy.
|
||||||
const forgeBase = (fs && fs.forge_present)
|
const forgeBase = (fs && fs.forge_present && fs.forge_public_url) || null;
|
||||||
? (fs.forge_public_url || `http://${hostname}:3000`)
|
|
||||||
: null;
|
|
||||||
|
|
||||||
const ul = el('ul', { class: 'approvals' });
|
const ul = el('ul', { class: 'approvals' });
|
||||||
for (const a of pending) {
|
for (const a of pending) {
|
||||||
|
|
|
||||||
|
|
@ -30,14 +30,16 @@ async function init() {
|
||||||
if (tile) tile.hidden = false;
|
if (tile) tile.hidden = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Forge tile: reveal + point at the live forge only when the
|
// Forge tile: reveal only when the hive-forge container is up AND
|
||||||
// hive-forge container is up. Prefer the gateway-served public URL
|
// the operator has stated a public URL for it
|
||||||
// (set when forge.behindGateway=true), fall back to the direct :3000
|
// (services.hyperhive.forge.publicUrl, surfaced as
|
||||||
// port — same precedence the dashboard uses for forge links.
|
// state.forge_public_url) — hidden rather than guessed from
|
||||||
if (state.forge_present) {
|
// `location.hostname` + the direct :3000 port, which is only
|
||||||
|
// correct by accident on deployments that aren't plain localhost.
|
||||||
|
if (state.forge_present && state.forge_public_url) {
|
||||||
const tile = $('home-tile-forge');
|
const tile = $('home-tile-forge');
|
||||||
if (tile) {
|
if (tile) {
|
||||||
tile.href = state.forge_public_url || `http://${location.hostname}:3000`;
|
tile.href = state.forge_public_url;
|
||||||
tile.hidden = false;
|
tile.hidden = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -414,6 +414,9 @@ function buildContainerLi(c, node, opts) {
|
||||||
// -- nav strip -----------------------------------------------
|
// -- nav strip -----------------------------------------------
|
||||||
if (Array.isArray(ds.links)) {
|
if (Array.isArray(ds.links)) {
|
||||||
for (const lnk of ds.links) {
|
for (const lnk of ds.links) {
|
||||||
|
// No public forge URL to link to — hide rather than build
|
||||||
|
// a broken/guessed href (see forgeBase above).
|
||||||
|
if (lnk.kind === 'forge' && !forgeBase) continue;
|
||||||
const href = lnk.kind === 'forge' ? forgeBase + (lnk.url || '')
|
const href = lnk.kind === 'forge' ? forgeBase + (lnk.url || '')
|
||||||
: lnk.kind === 'external' ? (lnk.url || '')
|
: lnk.kind === 'external' ? (lnk.url || '')
|
||||||
: /* container */ containerBase + '/' + (lnk.url || '');
|
: /* container */ containerBase + '/' + (lnk.url || '');
|
||||||
|
|
@ -637,10 +640,12 @@ export function renderContainers(s) {
|
||||||
// loopback port while the socket marker is absent). See
|
// loopback port while the socket marker is absent). See
|
||||||
// `docs/web-ui.md::Container row` + `docs/gateway.md::Vhost map`.
|
// `docs/web-ui.md::Container row` + `docs/gateway.md::Vhost map`.
|
||||||
const gatewayLinks = !!(s && s.gateway_enabled);
|
const gatewayLinks = !!(s && s.gateway_enabled);
|
||||||
// Forge public URL: prefer state.forge_public_url (set by the NixOS
|
// Forge public URL: state.forge_public_url (set by the NixOS module
|
||||||
// module when forge.behindGateway=true), fall back to
|
// from services.hyperhive.forge.publicUrl) or null — never guessed
|
||||||
// "<hostname>:3000" for gateway-off / local-dev deploys.
|
// from "<hostname>:3000". `null` propagates down to
|
||||||
const forgeBase = (s && s.forge_public_url) || `http://${hostname}:3000`;
|
// buildContainerLi/buildAgentMenu, which already hide forge links
|
||||||
|
// rather than emit a possibly-wrong one.
|
||||||
|
const forgeBase = (s && s.forge_public_url) || null;
|
||||||
const ul = existingUl ?? el('ul', { class: 'containers' });
|
const ul = existingUl ?? el('ul', { class: 'containers' });
|
||||||
const tree = buildAgentTree(containers);
|
const tree = buildAgentTree(containers);
|
||||||
// In-flight rebuild / meta-update / destroy ops per agent name —
|
// In-flight rebuild / meta-update / destroy ops per agent name —
|
||||||
|
|
|
||||||
|
|
@ -153,11 +153,12 @@ pub(super) struct StateSnapshot {
|
||||||
/// — single source of truth. See [`docs/web-ui/dashboard.md::Container row`]
|
/// — single source of truth. See [`docs/web-ui/dashboard.md::Container row`]
|
||||||
/// for the frontend resolver + which links appear in which conditions.
|
/// for the frontend resolver + which links appear in which conditions.
|
||||||
links: Vec<AgentLink>,
|
links: Vec<AgentLink>,
|
||||||
/// Public URL of the forge served by hive-gateway (e.g.
|
/// Public URL of the forge (e.g.
|
||||||
/// `"https://forge.pr1ma.darkest.space"`). Sourced from
|
/// `"https://forge.pr1ma.darkest.space"`). Sourced from
|
||||||
/// `HIVE_FORGE_PUBLIC_URL`; `None` when `forge.behindGateway=false`
|
/// `HIVE_FORGE_PUBLIC_URL` (set from `services.hyperhive.forge.
|
||||||
/// or the env var is absent. The frontend uses this to build forge
|
/// publicUrl`); `None` when unset. The frontend uses this to build
|
||||||
/// nav-strip links instead of hardcoding `<hostname>:3000`.
|
/// forge nav-strip links, and **hides** the forge link entirely
|
||||||
|
/// when absent rather than guessing `<hostname>:3000`.
|
||||||
forge_public_url: Option<String>,
|
forge_public_url: Option<String>,
|
||||||
/// Human name of this hive instance (e.g. `"pr1ma"`). Sourced
|
/// Human name of this hive instance (e.g. `"pr1ma"`). Sourced
|
||||||
/// from `HYPERHIVE_HIVE_NAME`; `None` when unset. The frontend
|
/// from `HYPERHIVE_HIVE_NAME`; `None` when unset. The frontend
|
||||||
|
|
|
||||||
|
|
@ -107,11 +107,15 @@ pub(super) struct StateSnapshot {
|
||||||
/// `http://<hostname>:<port>/` TCP links) is retained as a defensive
|
/// `http://<hostname>:<port>/` TCP links) is retained as a defensive
|
||||||
/// fallback for the env being unset. See `docs/gateway.md::Vhost map`.
|
/// fallback for the env being unset. See `docs/gateway.md::Vhost map`.
|
||||||
gateway_enabled: bool,
|
gateway_enabled: bool,
|
||||||
/// Public URL of the forge vhost served by hive-gateway (e.g.
|
/// Public URL of the forge (e.g.
|
||||||
/// `"https://forge.pr1ma.darkest.space"`). Sourced from the
|
/// `"https://forge.pr1ma.darkest.space"`). Sourced from the
|
||||||
/// `HIVE_FORGE_PUBLIC_URL` env var, which the c0re NixOS module
|
/// `HIVE_FORGE_PUBLIC_URL` env var, which the c0re NixOS module
|
||||||
/// sets when `forge.behindGateway = true`. `None` when absent —
|
/// sets from `services.hyperhive.forge.publicUrl` (defaults to
|
||||||
/// the frontend falls back to `http://<hostname>:3000`.
|
/// the gateway vhost URL when `forge.behindGateway = true`,
|
||||||
|
/// `null` otherwise). `None` when absent — the frontend **hides**
|
||||||
|
/// forge links rather than guessing `http://<hostname>:3000`,
|
||||||
|
/// which is only right by accident on deployments that aren't
|
||||||
|
/// plain localhost.
|
||||||
forge_public_url: Option<String>,
|
forge_public_url: Option<String>,
|
||||||
/// Human name of this single-host hive instance (e.g. `"pr1ma"`).
|
/// Human name of this single-host hive instance (e.g. `"pr1ma"`).
|
||||||
/// Sourced from `HYPERHIVE_HIVE_NAME` env var, set by the c0re
|
/// Sourced from `HYPERHIVE_HIVE_NAME` env var, set by the c0re
|
||||||
|
|
|
||||||
|
|
@ -127,15 +127,17 @@ in
|
||||||
# dashboard doesn't need to learn the gateway is unconditional.
|
# dashboard doesn't need to learn the gateway is unconditional.
|
||||||
HIVE_GATEWAY_ENABLED = "1";
|
HIVE_GATEWAY_ENABLED = "1";
|
||||||
}
|
}
|
||||||
// lib.optionalAttrs config.services.hyperhive.forge.behindGateway {
|
// lib.optionalAttrs (config.services.hyperhive.forge.publicUrl != null) {
|
||||||
# Public URL of the forge vhost served by hive-gateway. The
|
# Public URL of the forge, for the dashboard to build browser-facing
|
||||||
# dashboard uses this to build browser-facing forge links
|
# forge links from instead of guessing `<hostname>:3000` (which
|
||||||
# instead of hardcoding `<hostname>:3000`, which breaks when
|
# breaks the moment the operator's browser hostname isn't the forge
|
||||||
# the operator accesses the dashboard through the gateway
|
# host, e.g. through the gateway or a reverse proxy). Sourced from
|
||||||
# (forge sub-domain has no port; direct port URL would be
|
# `services.hyperhive.forge.publicUrl`, which itself defaults to the
|
||||||
# wrong). Absent when `behindGateway = false` — dashboard
|
# gateway vhost URL when `behindGateway = true` and `null` otherwise
|
||||||
# falls back to `<hostname>:3000`.
|
# — see that option's doc for the "hide, don't guess" rationale.
|
||||||
HIVE_FORGE_PUBLIC_URL = "https://${config.services.hyperhive.forge.domain}";
|
# Absent here whenever `publicUrl` is `null`; the dashboard hides
|
||||||
|
# forge links rather than emitting one it can't justify.
|
||||||
|
HIVE_FORGE_PUBLIC_URL = config.services.hyperhive.forge.publicUrl;
|
||||||
}
|
}
|
||||||
//
|
//
|
||||||
lib.optionalAttrs
|
lib.optionalAttrs
|
||||||
|
|
|
||||||
|
|
@ -130,6 +130,37 @@ in
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
|
|
||||||
|
publicUrl = lib.mkOption {
|
||||||
|
type = lib.types.nullOr lib.types.str;
|
||||||
|
default =
|
||||||
|
if config.services.hyperhive.enable && cfg.behindGateway then "https://${cfg.domain}" else null;
|
||||||
|
defaultText = lib.literalExpression ''
|
||||||
|
if behindGateway then "https://''${domain}" else null
|
||||||
|
'';
|
||||||
|
example = "https://forge.example.com";
|
||||||
|
description = ''
|
||||||
|
Browser-facing forge URL the dashboard uses to build clickable
|
||||||
|
forge links (the H0M3 Forge tile, per-agent-row forge links,
|
||||||
|
the approval-queue's "review PR on forge" link) — sourced into
|
||||||
|
every agent container + hive-c0re as `HIVE_FORGE_PUBLIC_URL`.
|
||||||
|
|
||||||
|
Defaults to `https://''${cfg.domain}` when `behindGateway =
|
||||||
|
true` (the gateway vhost is genuinely reachable at that URL)
|
||||||
|
and `null` otherwise. When `null`, the dashboard **hides**
|
||||||
|
forge links rather than guessing one — see
|
||||||
|
`docs/web-ui/dashboard.md::H0M3 page` for the rationale (a
|
||||||
|
link built from the operator's own browser hostname + a
|
||||||
|
container port is only an accident away from wrong on any
|
||||||
|
deployment that isn't plain localhost).
|
||||||
|
|
||||||
|
**Set this explicitly if `behindGateway = false`** and the
|
||||||
|
forge is still reachable at a stable URL you want linked from
|
||||||
|
the dashboard (e.g. `http://<lan-host>:''${toString cfg.httpPort}`
|
||||||
|
for an all-LAN deployment) — leaving it unset there means the
|
||||||
|
dashboard's forge links are simply absent, not broken.
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
package = lib.mkOption {
|
package = lib.mkOption {
|
||||||
type = lib.types.package;
|
type = lib.types.package;
|
||||||
default = pkgs.forgejo;
|
default = pkgs.forgejo;
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue