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
|
||||
differently in the frontend: `Container` → same-origin path
|
||||
(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 /
|
||||
label / url strings never reach `innerHTML` (XSS-safe by
|
||||
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`).
|
||||
Optional tiles are hidden until `home.js` confirms their availability:
|
||||
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
|
||||
`state.forge_present` and fills the href from `state.forge_public_url` (the
|
||||
gateway-served public URL when `services.hyperhive.forge.behindGateway=true`)
|
||||
or falls back to the direct `:3000` port. Operators without matrix or forge
|
||||
enabled never see dead links. `home.js` also fills the swarm/hive identity
|
||||
the dashboard's M4TR1X tab); Forge is hidden until `home.js` confirms both
|
||||
`state.forge_present` **and** `state.forge_public_url` (sourced from
|
||||
`services.hyperhive.forge.publicUrl`, which defaults to the gateway vhost URL
|
||||
when `behindGateway=true` and is `null` otherwise) and fills the href from the
|
||||
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
|
||||
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.
|
||||
|
|
@ -835,7 +838,9 @@ fetch entirely.
|
|||
`docs/gateway.md::Per-agent unix-socket upstream`).
|
||||
Gateway-off (legacy / local dev): base URL is
|
||||
`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
|
||||
link + favicon fetch, so the whole row routes through the gateway
|
||||
as a unit.
|
||||
|
|
|
|||
|
|
@ -1230,10 +1230,13 @@ window.marked = marked;
|
|||
const metaLinks = $('meta-links');
|
||||
if (metaLinks && Array.isArray(s.links)) {
|
||||
metaLinks.replaceChildren();
|
||||
// Prefer forge_public_url from state (set when the gateway serves
|
||||
// forge at its sub-domain); fall back to :3000 for local-dev.
|
||||
const forgeBase = s.forge_public_url || `http://${window.location.hostname}:3000`;
|
||||
// s.forge_public_url (set from services.hyperhive.forge.publicUrl)
|
||||
// or falsy — never guessed from "<hostname>:3000". A forge-kind
|
||||
// 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) => {
|
||||
if (lnk.kind === 'forge' && !forgeBase) return;
|
||||
const href = lnk.kind === 'forge' ? forgeBase + (lnk.url || '')
|
||||
: lnk.kind === 'external' ? (lnk.url || '')
|
||||
: /* container */ (lnk.url || '');
|
||||
|
|
|
|||
|
|
@ -242,12 +242,10 @@ export function renderApprovals() {
|
|||
}
|
||||
// forge link base — only when the hive-forge container is up.
|
||||
const fs = window.__hyperhive_state;
|
||||
const hostname = (fs && fs.hostname) || window.location.hostname;
|
||||
// Prefer state.forge_public_url (set when forge.behindGateway=true,
|
||||
// e.g. "https://forge.pr1ma.darkest.space") over the direct :3000 port.
|
||||
const forgeBase = (fs && fs.forge_present)
|
||||
? (fs.forge_public_url || `http://${hostname}:3000`)
|
||||
: null;
|
||||
// state.forge_public_url (set from services.hyperhive.forge.publicUrl)
|
||||
// or null — never guessed from "<hostname>:3000". The PR-link builder
|
||||
// below already gates on forgeBase being truthy.
|
||||
const forgeBase = (fs && fs.forge_present && fs.forge_public_url) || null;
|
||||
|
||||
const ul = el('ul', { class: 'approvals' });
|
||||
for (const a of pending) {
|
||||
|
|
|
|||
|
|
@ -30,14 +30,16 @@ async function init() {
|
|||
if (tile) tile.hidden = false;
|
||||
}
|
||||
|
||||
// Forge tile: reveal + point at the live forge only when the
|
||||
// hive-forge container is up. Prefer the gateway-served public URL
|
||||
// (set when forge.behindGateway=true), fall back to the direct :3000
|
||||
// port — same precedence the dashboard uses for forge links.
|
||||
if (state.forge_present) {
|
||||
// Forge tile: reveal only when the hive-forge container is up AND
|
||||
// the operator has stated a public URL for it
|
||||
// (services.hyperhive.forge.publicUrl, surfaced as
|
||||
// state.forge_public_url) — hidden rather than guessed from
|
||||
// `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');
|
||||
if (tile) {
|
||||
tile.href = state.forge_public_url || `http://${location.hostname}:3000`;
|
||||
tile.href = state.forge_public_url;
|
||||
tile.hidden = false;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -414,6 +414,9 @@ function buildContainerLi(c, node, opts) {
|
|||
// -- nav strip -----------------------------------------------
|
||||
if (Array.isArray(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 || '')
|
||||
: lnk.kind === 'external' ? (lnk.url || '')
|
||||
: /* container */ containerBase + '/' + (lnk.url || '');
|
||||
|
|
@ -637,10 +640,12 @@ export function renderContainers(s) {
|
|||
// loopback port while the socket marker is absent). See
|
||||
// `docs/web-ui.md::Container row` + `docs/gateway.md::Vhost map`.
|
||||
const gatewayLinks = !!(s && s.gateway_enabled);
|
||||
// Forge public URL: prefer state.forge_public_url (set by the NixOS
|
||||
// module when forge.behindGateway=true), fall back to
|
||||
// "<hostname>:3000" for gateway-off / local-dev deploys.
|
||||
const forgeBase = (s && s.forge_public_url) || `http://${hostname}:3000`;
|
||||
// Forge public URL: state.forge_public_url (set by the NixOS module
|
||||
// from services.hyperhive.forge.publicUrl) or null — never guessed
|
||||
// from "<hostname>:3000". `null` propagates down to
|
||||
// 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 tree = buildAgentTree(containers);
|
||||
// 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`]
|
||||
/// for the frontend resolver + which links appear in which conditions.
|
||||
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
|
||||
/// `HIVE_FORGE_PUBLIC_URL`; `None` when `forge.behindGateway=false`
|
||||
/// or the env var is absent. The frontend uses this to build forge
|
||||
/// nav-strip links instead of hardcoding `<hostname>:3000`.
|
||||
/// `HIVE_FORGE_PUBLIC_URL` (set from `services.hyperhive.forge.
|
||||
/// publicUrl`); `None` when unset. The frontend uses this to build
|
||||
/// forge nav-strip links, and **hides** the forge link entirely
|
||||
/// when absent rather than guessing `<hostname>:3000`.
|
||||
forge_public_url: Option<String>,
|
||||
/// Human name of this hive instance (e.g. `"pr1ma"`). Sourced
|
||||
/// 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
|
||||
/// fallback for the env being unset. See `docs/gateway.md::Vhost map`.
|
||||
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
|
||||
/// `HIVE_FORGE_PUBLIC_URL` env var, which the c0re NixOS module
|
||||
/// sets when `forge.behindGateway = true`. `None` when absent —
|
||||
/// the frontend falls back to `http://<hostname>:3000`.
|
||||
/// sets from `services.hyperhive.forge.publicUrl` (defaults to
|
||||
/// 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>,
|
||||
/// Human name of this single-host hive instance (e.g. `"pr1ma"`).
|
||||
/// 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.
|
||||
HIVE_GATEWAY_ENABLED = "1";
|
||||
}
|
||||
// lib.optionalAttrs config.services.hyperhive.forge.behindGateway {
|
||||
# Public URL of the forge vhost served by hive-gateway. The
|
||||
# dashboard uses this to build browser-facing forge links
|
||||
# instead of hardcoding `<hostname>:3000`, which breaks when
|
||||
# the operator accesses the dashboard through the gateway
|
||||
# (forge sub-domain has no port; direct port URL would be
|
||||
# wrong). Absent when `behindGateway = false` — dashboard
|
||||
# falls back to `<hostname>:3000`.
|
||||
HIVE_FORGE_PUBLIC_URL = "https://${config.services.hyperhive.forge.domain}";
|
||||
// lib.optionalAttrs (config.services.hyperhive.forge.publicUrl != null) {
|
||||
# Public URL of the forge, for the dashboard to build browser-facing
|
||||
# forge links from instead of guessing `<hostname>:3000` (which
|
||||
# breaks the moment the operator's browser hostname isn't the forge
|
||||
# host, e.g. through the gateway or a reverse proxy). Sourced from
|
||||
# `services.hyperhive.forge.publicUrl`, which itself defaults to the
|
||||
# gateway vhost URL when `behindGateway = true` and `null` otherwise
|
||||
# — see that option's doc for the "hide, don't guess" rationale.
|
||||
# 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
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
type = lib.types.package;
|
||||
default = pkgs.forgejo;
|
||||
|
|
|
|||
Loading…
Reference in a new issue