feat(dashboard): generic server-warnings banner on every page (#1518)
Per operator request: instead of a disk-specific alert, surface a generic
server-warnings banner at the very top of every page, so new system
warnings can be added backend-side with no frontend change.
- hive-c0re `host_stats`: `server_warnings() -> Vec<ServerWarning>`
(`{ kind, level, message }`). The threshold logic lives server-side; the
host disk-pressure check (a `statvfs` probe of `/nix`: ≥85% used → warn,
≥95% → crit) is the first and only producer today. No new deps (libc).
- `/api/state` carries `server_warnings` (replaces the disk-specific
field). Empty when all clear.
- frontend: `renderServerWarnings` / `initServerWarnings` in `common.js`
inject a sticky top-of-<body> banner and render the list, coloured by
`level`. Wired on every page — dashboard (live, via refreshState),
FL0W, L0GS, H0M3. No per-warning frontend code; adding a warning kind
is a pure backend change.
cargo check/clippy/fmt + npm run build green. Closes #1518.
This commit is contained in:
parent
d9d2a52221
commit
5d0f3d060b
10 changed files with 241 additions and 6 deletions
|
|
@ -624,3 +624,55 @@ export const NOTIF = (() => {
|
|||
}
|
||||
return { bind, show, renderControls };
|
||||
})();
|
||||
|
||||
// ─── server warnings banner ──────────────────────────────────────────
|
||||
// A generic top-of-page banner shown on every page (dashboard + the
|
||||
// stand-alone FL0W / L0GS / H0M3 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.
|
||||
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;
|
||||
document.body.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 */ });
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue