mirror of
https://github.com/CyberMind-FR/secubox-deb.git
synced 2026-07-29 13:59:40 +00:00
Merge pull request #629 from CyberMind-FR/feature/628-hub-health-monitor-page-vital-common-ser
hub: Health Monitor page (vital + common services, live) (closes #628)
This commit is contained in:
commit
206157047e
|
|
@ -1,3 +1,11 @@
|
|||
secubox-hub (1.4.5-1~bookworm1) bookworm; urgency=medium
|
||||
|
||||
* feat(health): Health Monitor page (/health/) — live status of vital +
|
||||
common services from public/health-batch, grouped + severity-sorted,
|
||||
auto-refresh, dark hybrid-skin. Sidebar menu entry (closes #628).
|
||||
|
||||
-- Gerald Kerma <devel@cybermind.fr> Tue, 17 Jun 2026 10:00:00 +0200
|
||||
|
||||
secubox-hub (1.4.4-1~bookworm1) bookworm; urgency=medium
|
||||
|
||||
* fix(perf): dashboard/services cache never warmed under the aggregator,
|
||||
|
|
|
|||
9
packages/secubox-hub/menu.d/05-health.json
Normal file
9
packages/secubox-hub/menu.d/05-health.json
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
{
|
||||
"id": "health",
|
||||
"name": "Health Monitor",
|
||||
"category": "root",
|
||||
"icon": "💚",
|
||||
"path": "/health/",
|
||||
"order": 5,
|
||||
"description": "Live status of vital and common SecuBox services"
|
||||
}
|
||||
51
packages/secubox-hub/www/health/health.css
Normal file
51
packages/secubox-hub/www/health/health.css
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
/* SPDX-License-Identifier: LicenseRef-CMSD-1.0
|
||||
SecuBox-Deb :: Hub Health Monitor — dark hybrid-skin styles */
|
||||
:root {
|
||||
--gmb-h: 48px; --sp-s: 8px; --sp-m: 16px; --sp-l: 24px; --sp-xl: 40px; --r-card: 14px;
|
||||
--bg0: var(--bg-dark, #0A0E14); --bg1: var(--surface-dark, #141A24);
|
||||
--bd: var(--border-dark, #2A3444); --tx: var(--text-dark, #E8E6E0); --mut: var(--muted-dark, #8A9AA8);
|
||||
}
|
||||
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
body { font-family: var(--font-body, 'Space Grotesk', system-ui, sans-serif); font-size: 14px;
|
||||
line-height: 1.6; background: var(--bg0); color: var(--tx); min-height: 100vh; }
|
||||
.main { margin-left: 220px; padding: calc(var(--gmb-h) + var(--sp-l)) var(--sp-l) var(--sp-xl); max-width: 1280px; }
|
||||
@media (max-width: 768px) { .main { margin-left: 0; padding: calc(var(--gmb-h) + var(--sp-m)) var(--sp-m) var(--sp-l); } }
|
||||
|
||||
header.page { display: flex; justify-content: space-between; align-items: baseline; gap: var(--sp-m);
|
||||
margin-bottom: var(--sp-l); padding-bottom: var(--sp-m); border-bottom: 1px solid var(--bd); }
|
||||
header.page h1 { font-size: 24px; font-weight: 600; letter-spacing: -0.4px; color: var(--root-light, #148C66); }
|
||||
header.page .ver { font-family: var(--font-mono, monospace); font-size: 12px; color: var(--mut); margin-left: var(--sp-s); }
|
||||
.actions { display: flex; gap: var(--sp-m); align-items: center; }
|
||||
.updated { font-family: var(--font-mono, monospace); font-size: 11px; color: var(--mut); }
|
||||
.btn { background: var(--bg1); color: var(--tx); border: 1px solid var(--bd); border-radius: 6px;
|
||||
padding: 6px 12px; cursor: pointer; font: inherit; font-size: 13px; }
|
||||
.btn:hover { border-color: var(--root-light, #148C66); }
|
||||
h2 { font-size: 16px; font-weight: 600; margin: var(--sp-xl) 0 var(--sp-m); }
|
||||
.count { font-family: var(--font-mono, monospace); font-size: 12px; color: var(--mut); font-weight: 400; }
|
||||
.banner { padding: var(--sp-m); border-radius: 8px; margin-bottom: var(--sp-m); font-size: 13px; }
|
||||
.banner.info { background: var(--bg1); color: var(--mut); border: 1px solid var(--bd); }
|
||||
.banner.err { background: rgba(192,64,64,.12); color: #E8845A; border: 1px solid #803018; }
|
||||
|
||||
/* Summary */
|
||||
.summary { display: flex; gap: var(--sp-m); flex-wrap: wrap; }
|
||||
.sum { flex: 1; min-width: 120px; background: var(--bg1); border: 1px solid var(--bd);
|
||||
border-radius: var(--r-card); padding: var(--sp-m); text-align: center; }
|
||||
.sum b { display: block; font-size: 2rem; font-weight: 700; font-family: var(--font-mono, monospace); line-height: 1; }
|
||||
.sum span { font-size: 12px; color: var(--mut); }
|
||||
.sum.ok b { color: #2ecc8f; } .sum.warn b { color: #f0b94c; } .sum.err b { color: #ff7a6b; } .sum.total b { color: var(--tx); }
|
||||
.sum.ok { border-left: 3px solid #0A5840; } .sum.warn { border-left: 3px solid #9A6010; } .sum.err { border-left: 3px solid #803018; }
|
||||
|
||||
/* Service grid */
|
||||
.svc-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); gap: var(--sp-s); }
|
||||
.svc { display: flex; align-items: center; gap: var(--sp-s); background: var(--bg1); border: 1px solid var(--bd);
|
||||
border-radius: 8px; padding: 10px 12px; min-width: 0; }
|
||||
.svc .led { width: 9px; height: 9px; border-radius: 50%; flex: none; box-shadow: 0 0 6px currentColor; }
|
||||
.svc.ok .led { background: #2ecc8f; color: #2ecc8f; }
|
||||
.svc.warn .led, .svc.unknown .led { background: #f0b94c; color: #f0b94c; }
|
||||
.svc.error .led { background: #ff7a6b; color: #ff7a6b; animation: pulse 1.2s infinite; }
|
||||
@keyframes pulse { 50% { opacity: .4; } }
|
||||
.svc.error { border-left: 3px solid #803018; }
|
||||
.svc-name { font-weight: 600; font-size: 13px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
||||
.svc-msg { margin-left: auto; font-size: 11px; color: var(--mut); font-family: var(--font-mono, monospace);
|
||||
white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 45%; }
|
||||
.empty { color: var(--mut); font-size: 13px; padding: var(--sp-m); }
|
||||
89
packages/secubox-hub/www/health/health.js
Normal file
89
packages/secubox-hub/www/health/health.js
Normal file
|
|
@ -0,0 +1,89 @@
|
|||
// SPDX-License-Identifier: LicenseRef-CMSD-1.0
|
||||
// Copyright (c) 2026 CyberMind — Gérald Kerma <devel@cybermind.fr>
|
||||
// SecuBox-Deb :: Hub Health Monitor — CyberMind https://cybermind.fr
|
||||
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
const BATCH = '/api/v1/hub/public/health-batch';
|
||||
const INFO = '/api/v1/hub/public/info';
|
||||
const REFRESH_MS = 15000;
|
||||
|
||||
// Vital services — the security/serving spine; everything else is "common".
|
||||
const VITAL = ['waf', 'crowdsec', 'mitmproxy', 'haproxy', 'aggregator', 'hub',
|
||||
'system', 'vortex-dns', 'dns-guard', 'certs', 'wireguard', 'soc',
|
||||
'metrics', 'core'];
|
||||
const VITAL_SET = new Set(VITAL);
|
||||
|
||||
const $ = (id) => document.getElementById(id);
|
||||
const esc = (s) => String(s == null ? '' : s)
|
||||
.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
||||
|
||||
async function getJSON(u) {
|
||||
const r = await fetch(u, { headers: { 'Accept': 'application/json' }, cache: 'no-store' });
|
||||
if (!r.ok) throw new Error('HTTP ' + r.status);
|
||||
return r.json();
|
||||
}
|
||||
|
||||
function chip(id, st) {
|
||||
const status = (st && st.status) || 'unknown';
|
||||
const msg = (st && st.msg) || '';
|
||||
return `<div class="svc ${status}" title="${esc(id)}: ${esc(msg)}">
|
||||
<span class="led"></span>
|
||||
<span class="svc-name">${esc(id)}</span>
|
||||
<span class="svc-msg">${esc(msg)}</span>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
function render(modules) {
|
||||
const ids = Object.keys(modules).sort();
|
||||
let ok = 0, warn = 0, err = 0;
|
||||
ids.forEach((id) => {
|
||||
const s = (modules[id] || {}).status;
|
||||
if (s === 'ok') ok++; else if (s === 'error') err++; else warn++;
|
||||
});
|
||||
|
||||
$('summary').innerHTML =
|
||||
`<div class="sum ok"><b>${ok}</b><span>healthy</span></div>` +
|
||||
`<div class="sum warn"><b>${warn}</b><span>degraded</span></div>` +
|
||||
`<div class="sum err"><b>${err}</b><span>down</span></div>` +
|
||||
`<div class="sum total"><b>${ids.length}</b><span>services</span></div>`;
|
||||
|
||||
const vital = ids.filter((id) => VITAL_SET.has(id));
|
||||
const common = ids.filter((id) => !VITAL_SET.has(id));
|
||||
// Sort each: errors first, then warn, then ok, so problems surface.
|
||||
const rank = (id) => ({ error: 0, warn: 1, unknown: 1, ok: 2 }[(modules[id] || {}).status] ?? 1);
|
||||
const bySeverity = (a, b) => rank(a) - rank(b) || a.localeCompare(b);
|
||||
|
||||
$('vital').innerHTML = vital.sort(bySeverity).map((id) => chip(id, modules[id])).join('')
|
||||
|| '<div class="empty">no vital services reported</div>';
|
||||
$('common').innerHTML = common.sort(bySeverity).map((id) => chip(id, modules[id])).join('')
|
||||
|| '<div class="empty">none</div>';
|
||||
$('vitalCount').textContent = '(' + vital.length + ')';
|
||||
$('commonCount').textContent = '(' + common.length + ')';
|
||||
}
|
||||
|
||||
async function load() {
|
||||
try {
|
||||
const batch = await getJSON(BATCH);
|
||||
// health-batch returns {modules: {id: {status,msg}}, count: N}
|
||||
const modules = (batch && batch.modules) || batch || {};
|
||||
render(modules);
|
||||
$('updated').textContent = 'updated ' + new Date().toLocaleTimeString();
|
||||
$('loading').hidden = true; $('error').hidden = true; $('content').hidden = false;
|
||||
getJSON(INFO).then((i) => {
|
||||
if (i && i.hostname) $('ver').textContent = esc(i.hostname);
|
||||
}).catch(() => {});
|
||||
} catch (e) {
|
||||
$('error').hidden = false;
|
||||
$('error').textContent = 'Could not load health: ' + e.message;
|
||||
$('loading').hidden = true;
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
$('refresh').addEventListener('click', load);
|
||||
load();
|
||||
setInterval(load, REFRESH_MS);
|
||||
});
|
||||
})();
|
||||
54
packages/secubox-hub/www/health/index.html
Normal file
54
packages/secubox-hub/www/health/index.html
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
<!DOCTYPE html>
|
||||
<!--
|
||||
SecuBox · Health Monitor — live status of vital + common services.
|
||||
Consumes /api/v1/hub/public/health-batch. Dark hybrid-skin via /shared/sidebar.js.
|
||||
-->
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta http-equiv="Cache-Control" content="no-store, no-cache, must-revalidate">
|
||||
<title>SecuBox · Health Monitor</title>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500;700&display=swap" rel="stylesheet">
|
||||
<link rel="stylesheet" href="/shared/design-tokens.css">
|
||||
<link rel="stylesheet" href="/shared/sidebar.css">
|
||||
<link rel="stylesheet" href="health.css">
|
||||
</head>
|
||||
<body class="module-root">
|
||||
<nav class="sidebar" id="sidebar"></nav>
|
||||
<main class="main">
|
||||
<header class="page">
|
||||
<h1>💚 Health Monitor <span class="ver" id="ver">v1.0</span></h1>
|
||||
<div class="actions">
|
||||
<span class="updated" id="updated">—</span>
|
||||
<button class="btn" id="refresh" type="button">↻ Refresh</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div id="error" class="banner err" hidden></div>
|
||||
<div id="loading" class="banner info">Loading service health…</div>
|
||||
|
||||
<div id="content" hidden>
|
||||
<!-- Summary strip -->
|
||||
<section class="summary" id="summary"></section>
|
||||
|
||||
<!-- Vital services -->
|
||||
<section>
|
||||
<h2>Vital services <span class="count" id="vitalCount"></span></h2>
|
||||
<div class="svc-grid" id="vital"></div>
|
||||
</section>
|
||||
|
||||
<!-- Common services -->
|
||||
<section>
|
||||
<h2>Common services <span class="count" id="commonCount"></span></h2>
|
||||
<div class="svc-grid" id="common"></div>
|
||||
</section>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<script src="/shared/sidebar.js"></script>
|
||||
<script src="health.js" defer></script>
|
||||
</body>
|
||||
</html>
|
||||
Loading…
Reference in New Issue
Block a user