dashboard + agent UI: surface hive_name + swarm_name in chrome (#701)

Damocles's backend (c41bf1b) landed HYPERHIVE_HIVE_NAME / _SWARM_NAME
env vars and identity.rs accessors. This commit wires them into the two
web surfaces:

hive-c0re/dashboard.rs:
  StateSnapshot gains `hive_name` + `swarm_name` (Option<String>),
  populated from the env vars the c0re NixOS module injects.

hive-ag3nt/web_ui.rs:
  Per-agent StateSnapshot gains the same two fields, populated from
  crate::identity::hive_name() / swarm_name().

Dashboard frontend (index.html + tabs.js):
  A `<p class="banner-thin" id="hive-identity">` sits above the tab
  strip in the chrome. refreshState() populates it with "swarm / hive"
  (or just "hive") when the fields are non-null, and updates
  document.title to "<swarm>/<hive> // h1ve-c0re". No change to
  chrome when both fields are null (backward compat).

Per-agent frontend (index.html + app.js + agent.css):
  A hidden `.agent-hive-label` span under the title row shows the
  swarm/hive label once setHeader() receives non-null names.
  document.title is updated to "<label> // <hive_name>" when hive_name
  is set, enabling browser-tab disambiguation when multiple hives are
  open in parallel. Styled as a small uppercase purple-dim sub-label.
This commit is contained in:
iris 2026-05-31 20:40:12 +02:00 committed by mara
commit e1e5195081
7 changed files with 83 additions and 7 deletions

View file

@ -931,3 +931,19 @@ pre.diff {
font-style: italic;
padding: 1em 0;
}
/* Hive identity row shows "swarm / hive" label beneath the title.
Hidden until JS populates from /api/state (hive_name + swarm_name).
Mirrors the `.banner-thin` style in dashboard.css but kept tight
(no letter-spacing, smaller text) so it reads as a sub-label. */
.agent-hive-row {
/* Omit the default row gap when the row is hidden to avoid a
blank gap between the title row and the state row. */
gap: 0;
}
.agent-hive-label {
font-size: 0.72em;
letter-spacing: 0.1em;
color: var(--purple-dim);
text-transform: uppercase;
}

View file

@ -138,7 +138,7 @@ window.marked = marked;
})();
// ─── state rendering ────────────────────────────────────────────────────
function setHeader(label, qualifiedLabel, dashboardPort) {
function setHeader(label, qualifiedLabel, dashboardPort, hiveName, swarmName) {
const title = $('title');
// Title is just the glowing identity glyph — DASHB04RD, R3BU1LD,
// NEW SESSION live in the overflow `⋯` menu. Glow + uppercase
@ -147,15 +147,26 @@ window.marked = marked;
// the second row's `qualified` chip + the browser tab title so
// the cinematic header reads cleanly at a glance.
title.textContent = `${label}`;
// Document title carries the qualified name so the browser's tab
// bar disambiguates between same-named agents on different hives
// in a federated swarm. Falls back to `${label} // hyperhive` when
// the qualified form is just the short label (single-hive deploys).
// Document title: prefer the human display names when available
// (e.g. "iris // pr1ma") so browser tabs read naturally. Fall back
// to the qualified domain label (e.g. "iris@pr1ma.darkest.space")
// for multi-hive disambiguation when display names are unset, then
// to plain label for single-hive deploys.
const hiveLabel = hiveName || null;
const tab = qualifiedLabel && qualifiedLabel !== label ? qualifiedLabel : label;
document.title = `${tab} // hyperhive`;
document.title = hiveLabel ? `${label} // ${hiveLabel}` : `${tab} // hyperhive`;
const dashUrl = `${location.protocol}//${location.hostname}:${dashboardPort}/`;
dashboardBase = dashUrl;
populateOverflowMenu(label, dashUrl);
// Swarm label in the chrome: show "swarm/hive" when both set, just
// "hive" when only hive is known. Targets the `.agent-hive-label`
// element in the header row if present.
const hiveEl = document.querySelector('.agent-hive-label');
if (hiveEl && (hiveName || swarmName)) {
hiveEl.textContent = swarmName && hiveName
? `${swarmName} / ${hiveName}` : (hiveName || swarmName);
hiveEl.hidden = false;
}
}
// Overflow popover: dashboard back-link + rebuild + new-session +
@ -1044,7 +1055,7 @@ window.marked = marked;
const resp = await fetch('api/state');
if (!resp.ok) throw new Error('http ' + resp.status);
const s = await resp.json();
if (!headerSet) { setHeader(s.label, s.qualified_label, s.dashboard_port); headerSet = true; }
if (!headerSet) { setHeader(s.label, s.qualified_label, s.dashboard_port, s.hive_name, s.swarm_name); headerSet = true; }
currentLabel = s.label;
// Render server-supplied navigation links — see
// docs/web-ui.md::Per-agent page (meta-nav) for the NavLink

View file

@ -25,6 +25,12 @@
<nav class="meta agent-nav" id="meta-links"></nav>
</div>
<!-- Hive identity line: "swarm / hive" label. Hidden until JS
populates it from /api/state (hive_name + swarm_name). -->
<div class="agent-header-row agent-hive-row">
<span class="agent-hive-label" hidden></span>
</div>
<div id="state-row" class="agent-state-row agent-header-row">
<span id="alive-badge" class="status-badge status-loading" title="harness reachability"></span>
<span id="state-badge" class="state-badge state-loading">… booting</span>

View file

@ -14,6 +14,10 @@
button / shared links keep you on the same view. JS owns
the show/hide. -->
<header class="dashboard-chrome">
<!-- Hive identity line: swarm/hive label. Hidden until JS populates
it from /api/state (hive_name + swarm_name). Falls back to
hostname when only the domain is configured. -->
<p class="banner-thin" id="hive-identity" hidden></p>
<nav class="tabbar" id="tabbar" role="tablist">
<a class="tab" id="tab-swarm" href="#swarm" role="tab"
aria-controls="tab-pane-swarm"

View file

@ -3111,6 +3111,23 @@ window.marked = marked;
// docs/web-ui.md::Tab strip for the matrix_gui_enabled rationale.
const matrixTab = $('tab-matrix');
if (matrixTab) matrixTab.hidden = !s.matrix_gui_enabled;
// Hive identity: update the chrome banner + page title once the
// server-side display names are known. `hive_name` / `swarm_name`
// come from HYPERHIVE_HIVE_NAME / HYPERHIVE_SWARM_NAME env vars
// (set by services.hyperhive.{hiveName,swarmName} nix options).
// When unset we fall back gracefully — no chrome change.
const hiveId = $('hive-identity');
if (hiveId) {
const hive = s.hive_name;
const swarm = s.swarm_name;
if (hive || swarm) {
const label = swarm && hive ? `${swarm} / ${hive}`
: hive || swarm;
hiveId.textContent = label;
hiveId.hidden = false;
document.title = label + ' // h1ve-c0re';
}
}
const openDetails = snapshotOpenDetails();
// Sync transients + containers first so renderContainers below
// sees the current derived maps (it reads from

View file

@ -436,6 +436,14 @@ struct StateSnapshot {
/// this directly. See [`docs/web-ui.md::Container row`] for the
/// frontend resolver + which links appear in which conditions.
links: Vec<AgentLink>,
/// Human name of this hive instance (e.g. `"pr1ma"`). Sourced
/// from `HYPERHIVE_HIVE_NAME`; `None` when unset. The frontend
/// uses this for the page `<title>` and header label so browser
/// tabs disambiguate when multiple hives are open in parallel.
hive_name: Option<String>,
/// Human name of the swarm (e.g. `"constellat1on"`). Sourced from
/// `HYPERHIVE_SWARM_NAME`; `None` when unset.
swarm_name: Option<String>,
}
/// One navigation link in the agent page header row. Same JSON
@ -577,6 +585,8 @@ async fn api_state(State(state): State<AppState>) -> axum::Json<StateSnapshot> {
ctx_usage,
cost_usage,
links: agent_links(&state.label, state.gui_vnc_port.is_some()),
hive_name: crate::identity::hive_name(),
swarm_name: crate::identity::swarm_name(),
})
}

View file

@ -248,6 +248,16 @@ struct StateSnapshot {
/// local-dev deploys keep working. See `docs/gateway.md::Vhost
/// map`.
gateway_enabled: bool,
/// Human name of this single-host hive instance (e.g. `"pr1ma"`).
/// Sourced from `HYPERHIVE_HIVE_NAME` env var, set by the c0re
/// NixOS module from `services.hyperhive.hiveName`. `None` when
/// the option is unset — chrome falls back to `hostname`.
hive_name: Option<String>,
/// Human name of the wider swarm this hive belongs to (e.g.
/// `"constellat1on"`). Sourced from `HYPERHIVE_SWARM_NAME` env
/// var, set from `services.hyperhive.swarmName`. `None` when
/// unset — chrome omits the swarm segment of the breadcrumb.
swarm_name: Option<String>,
}
/// `OpQuestion` + computed `question_refs` / `answer_refs`. Built
@ -454,6 +464,8 @@ async fn api_state(headers: HeaderMap, State(state): State<AppState>) -> axum::J
let s = v.to_string_lossy().to_ascii_lowercase();
matches!(s.as_str(), "1" | "true" | "yes")
}),
hive_name: std::env::var("HYPERHIVE_HIVE_NAME").ok().filter(|s| !s.is_empty()),
swarm_name: std::env::var("HYPERHIVE_SWARM_NAME").ok().filter(|s| !s.is_empty()),
})
}