Watch
0
0
Fork
You've already forked hyperhive
0

hive-c0re: hive warnings reach the operator as metrics and alerts, not a banner

The dashboard's top-of-page warning banner is gone. The warning registry
stays, because /health/ready, the swarm status view and the OTEL gauges
read it. What it feeds is now alerted on in Grafana:

- hyperhive.hive.warning{kind,level}: one series per raised warning, so an
  alert names the cause. Raising a warning also logs one `hive warning
  raised` line with the message.
- hyperhive.hive.nix_store.utilization: the used fraction of the nix
  store's filesystem, probed in the gauge callback.
- Alerts: hh-hive-degraded on the existing degraded gauge, hh-hive-warning
  per kind (disk excluded), hh-host-disk-high at 85% and
  hh-host-disk-critical at 95%.
- pending_logins is now a registry warning, refreshed from the container
  rescan, so it reaches the per-kind gauge.

Frontend: server_warnings leaves the state snapshot, and the dashboard,
flow and stats pages keep only the sticky chrome wrapper. The shared
hive-warn and WarnBanner components stay, since other views use them.

A module-eval case checks each alert's metric name against the exporter's
source, so a rename on either side fails the build.
This commit is contained in:
iris 2026-10-21 17:52:53 +02:00
commit 4dbcf6fa80
35 changed files with 468 additions and 322 deletions

View file

@ -564,7 +564,7 @@ hive-side-panel .md td {
Inverted-gutter layout: the dashboard keeps `<body>` full-bleed
and wraps their padded content in a single `.page-content` div that
carries the horizontal gutter. Full-width chrome (the tab strip, the
server-warnings banner, the footer) then spans edge-to-edge for free,
footer) then spans edge-to-edge for free,
with no negative-margin breakout hacks. Only matches on pages that opt
in by adding the wrapper (index.html); FL0W / ST4TS
are full-bleed with their own `.<page>-main` padding. */
@ -573,50 +573,14 @@ hive-side-panel .md td {
}
/* ─── sticky top region ───────────────────────────────────────────────
One sticky container per page holding the warning banner above the
page chrome (tab bar / page header). common.js (ensureStickyTop)
wraps the page's chrome in this on first render and injects the banner
as its first child, so the banner + chrome stack in a single sticky
context — pinned together — instead of two separate `top:0` stickies
colliding (the banner used to overlay the tab bar). */
One sticky container per page holding the page chrome (tab bar / page
header). common.js (initStickyTop) wraps the chrome in this on load. */
.sticky-top {
position: sticky;
top: 0;
z-index: 50;
}
/* ─── server warnings banner ──────────────────────────────────────────
Generic top-of-page strip, injected as the first child of `.sticky-top`
by common.js (renderServerWarnings) on every page. One row per warning;
colour comes from the per-warning `level` (warn = amber, crit = red).
Stickiness lives on the `.sticky-top` wrapper, not here. The bar spans
the full width edge-to-edge; the dashboard is full-bleed at the
body level (their 1.5em gutter lives on an inner `.page-content`
wrapper), so the banner is full-width for free. */
.server-warnings[hidden] {
display: none;
}
.server-warn {
text-align: center;
padding: 0.4em 1em;
font-size: 0.85em;
font-weight: bold;
letter-spacing: 0.02em;
}
/* Tint composited over the opaque elevated-surface grey (not transparent):
the bar is `position: sticky`, so a see-through background would let
page content scroll through behind it. */
.server-warn-warn {
color: var(--amber);
background: color-mix(in srgb, var(--amber) 16%, var(--bg-elev));
border-bottom: 1px solid var(--amber);
}
.server-warn-crit {
color: var(--red);
background: color-mix(in srgb, var(--red) 18%, var(--bg-elev));
border-bottom: 1px solid var(--red);
}
/* ─── stats table ─────────────────────────────────────────────────
Shared right-aligned numeric table. Used by /stats.html (busiest
agents) — moved here from dashboard.css when ST4TS became its own

View file

@ -562,22 +562,12 @@ export function appendLinkified(parent, text, refs) {
}
}
// ─── server warnings banner ──────────────────────────────────────────
// A generic top-of-page banner shown on every page (dashboard + the
// stand-alone FL0W / ST4TS pages). The backend decides what to
// warn about — `/api/state.server_warnings` is a list of
// `{ kind, level, message }` — and this just renders it, coloured by
// `level` (`warn` amber / `crit` red). Adding a new system warning is a
// backend-only change. The bar is injected at the top of <body> so no
// page needs to add markup.
// The sticky top region — one sticky container holding the warning
// banner above the page's chrome (tab bar / page header), so the banner
// stacks with the chrome instead of being overlaid by it (two separate
// `top:0` stickies would otherwise collide). Built once by wrapping the
// page's existing chrome element; pages without a chrome (none today)
// get a banner-only sticky region at the top of <body>.
function ensureStickyTop() {
// ─── sticky top region ───────────────────────────────────────────────
// One sticky container wrapping the page's chrome (tab bar / page
// header), built once by wrapping the existing chrome element. A page
// without a chrome (none today) gets an empty sticky region at the top of
// <body>.
export function initStickyTop() {
let top = document.querySelector(".sticky-top");
if (top) return top;
top = document.createElement("div");
@ -591,51 +581,3 @@ function ensureStickyTop() {
}
return top;
}
function ensureServerWarningsBar() {
let bar = document.getElementById("server-warnings");
if (!bar) {
bar = document.createElement("div");
bar.id = "server-warnings";
bar.className = "server-warnings";
bar.setAttribute("role", "alert");
bar.hidden = true;
ensureStickyTop().prepend(bar);
}
return bar;
}
/// Render a `server_warnings` list (from /api/state) into the banner.
/// Empty / missing → the bar hides itself.
export function renderServerWarnings(warnings) {
const bar = ensureServerWarningsBar();
bar.replaceChildren();
if (!Array.isArray(warnings) || warnings.length === 0) {
bar.hidden = true;
return;
}
for (const w of warnings) {
const row = el("div", {
class:
"server-warn server-warn-" +
(w && w.level === "crit" ? "crit" : "warn"),
});
appendText(row, "⚠ " + ((w && w.message) || ""));
bar.append(row);
}
bar.hidden = false;
}
/// One-shot init for pages that don't otherwise poll /api/state: ensure
/// the bar exists, fetch the snapshot once, render. The dashboard (which
/// already polls /api/state) calls `renderServerWarnings` directly for
/// live updates instead.
export function initServerWarnings() {
ensureServerWarningsBar();
fetch("/api/state")
.then((r) => (r.ok ? r.json() : null))
.then((s) => renderServerWarnings(s && s.server_warnings))
.catch(() => {
/* non-fatal: no banner if the snapshot is unreachable */
});
}

View file

@ -17,7 +17,7 @@
body.dashboard-shell {
/* Full-bleed body (no horizontal gutter). The 1.5em side gutter lives
on the inner `.page-content` wrapper (see common.css), so the sticky
chrome bar, the server-warnings banner, and the footer span the full
chrome bar and the footer span the full
width edge-to-edge with no negative-margin breakout. `padding-bottom`
keeps a little breathing room above the viewport edge (overridden to
clear the selection bar via `.has-selection` below). No max-width cap
@ -34,8 +34,7 @@ body.dashboard-shell {
align-items: center;
gap: 0.75em;
/* Stickiness lives on the `.sticky-top` wrapper (common.css) that
common.js wraps this chrome in, alongside the server-warning banner,
so they stack in one sticky context instead of colliding. */
common.js wraps this chrome in. */
background: color-mix(in srgb, var(--bg) 86%, transparent);
-webkit-backdrop-filter: blur(8px) saturate(120%);
backdrop-filter: blur(8px) saturate(120%);

View file

@ -10,17 +10,12 @@
// container list).
import { create as termCreate } from "@hive/shared/terminal.js";
import {
$,
appendLinkified,
openStream,
initServerWarnings,
} from "./common.js";
import { $, appendLinkified, openStream, initStickyTop } from "./common.js";
import { el } from "@hive/shared/dom.js";
import { epochSec } from "./util.js";
(() => {
initServerWarnings();
initStickyTop();
// ─── local containers cache (for compose autocomplete) ──────────────────
// The compose box's @-mention completion suggests known agent names.

View file

@ -5,7 +5,7 @@
// this bundle has no chart lib; per-agent trend charts live on each
// agent's own /stats page. The window selector is a hash-routed
// createTabStrip (#1h / #24h / …), matching the per-agent stats page.
import { $, initServerWarnings } from "./common.js";
import { $, initStickyTop } from "./common.js";
import { createTabStrip } from "@hive/shared/tabs.js";
let hiveStatsWindow = "24h";
@ -173,7 +173,7 @@ async function refreshHiveStats() {
}
}
initServerWarnings();
initStickyTop();
// Window selector → hash-routed createTabStrip (#1h / #24h / …). No panes
// here (the selector swaps data, not DOM); paneFor() returns null for each

View file

@ -11,7 +11,7 @@
// 4G3NTS (containers) domain lives in `./agents.js`.
import { marked } from "marked";
import { $, openStream, renderServerWarnings } from "./common.js";
import { $, openStream, initStickyTop } from "./common.js";
import { el } from "@hive/shared/dom.js";
import { bindAsyncForms } from "@hive/shared/forms.js";
import { createTabStrip } from "@hive/shared/tabs.js";
@ -50,6 +50,8 @@ import { renderLinksMenu } from "./links-menu.js";
window.marked = marked;
(() => {
initStickyTop();
// ─── async forms ────────────────────────────────────────────────────────
// Shared `data-async` submit interceptor (in common.js, shared with the other
// pages' bundles). On success it re-runs refreshState unless the form
@ -135,7 +137,6 @@ window.marked = marked;
// synchronous read (e.g. the compose autocomplete pulls agent
// names from here instead of refetching on every keystroke).
window.__hyperhive_state = s;
renderServerWarnings(s.server_warnings);
// Hive identity: render the swarm/hive name as a headline at the
// top of the 4G3NTS pane + update the page title once the
// server-side display names are known. `hive_name` / `swarm_name`