diff --git a/hbd/server/http.py b/hbd/server/http.py index 4bb12e4..4ce5070 100644 --- a/hbd/server/http.py +++ b/hbd/server/http.py @@ -154,6 +154,25 @@ async def start( lst = [h.jsons() for h in hosts] return web.json_response(json.loads("[" + ",".join(lst) + "]")) + async def api_alert_summary(request): + """GET /api/0/alert_summary — counts of ok/warning/critical hosts visible to caller.""" + user, err = _require_auth(request) + if err: + return err + from .threshold import AlertLevel + critical = warning = ok = 0 + for host in hbdclass.Host.hosts.values(): + if not _can_operate_host(user, host): + continue + levels = {s.level for s in host.alert_states.values()} + if AlertLevel.CRITICAL in levels: + critical += 1 + elif AlertLevel.WARNING in levels: + warning += 1 + else: + ok += 1 + return web.json_response({"critical": critical, "warning": warning, "ok": ok}) + async def api_messages(request): lst = data.msgs[-30:] return web.json_response(lst) @@ -893,6 +912,7 @@ async def start( web.get("/api/0/users/{username}/avatar", api_user_avatar), # Hosts web.get("/api/0/hosts", api_hosts), + web.get("/api/0/alert_summary", api_alert_summary), web.get("/api/0/messages", api_messages), web.get("/api/0/hosts/{hostname}/plugins", api_host_plugins), web.get("/api/0/hosts/{hostname}/plugins/{plugin_name}", api_host_plugin_detail), diff --git a/hbd/server/templates/head.html b/hbd/server/templates/head.html index a3233cf..4c90623 100644 --- a/hbd/server/templates/head.html +++ b/hbd/server/templates/head.html @@ -126,11 +126,17 @@ } /* Swiss railway clock — nav */ - .nav-clock { + .nav-pie { flex-shrink: 0; line-height: 0; margin-left: auto; padding: 4px 4px 4px 0; + } + #alert-pie { display: block; cursor: default; } + .nav-clock { + flex-shrink: 0; + line-height: 0; + padding: 4px 4px 4px 0; cursor: pointer; } #swiss-clock { display: block; } diff --git a/hbd/server/templates/nav.html b/hbd/server/templates/nav.html index 88983df..26d82a0 100644 --- a/hbd/server/templates/nav.html +++ b/hbd/server/templates/nav.html @@ -11,6 +11,9 @@ {% endif %} About + @@ -42,4 +45,52 @@ }); } })(); + + function drawAlertPie(critical, warning, ok) { + var canvas = document.getElementById('alert-pie'); + if (!canvas) return; + var ctx = canvas.getContext('2d'); + var SIZE = canvas.width; + var R = SIZE / 2; + ctx.clearRect(0, 0, SIZE, SIZE); + var total = critical + warning + ok; + if (total === 0) { + ctx.beginPath(); + ctx.arc(R, R, R - 1, 0, Math.PI * 2); + ctx.fillStyle = '#ccc'; + ctx.fill(); + return; + } + var slices = [ + { value: critical, color: '#e53935' }, + { value: warning, color: '#ffb300' }, + { value: ok, color: '#43a047' } + ]; + var start = -Math.PI / 2; + slices.forEach(function(s) { + if (s.value === 0) return; + var sweep = (s.value / total) * Math.PI * 2; + ctx.beginPath(); + ctx.moveTo(R, R); + ctx.arc(R, R, R - 1, start, start + sweep); + ctx.closePath(); + ctx.fillStyle = s.color; + ctx.fill(); + start += sweep; + }); + } + + function updateAlertPie() { + fetch('/api/0/alert_summary').then(function(r) { + if (!r.ok) return; + return r.json(); + }).then(function(d) { + if (d) drawAlertPie(d.critical || 0, d.warning || 0, d.ok || 0); + }).catch(function() {}); + } + + document.addEventListener('DOMContentLoaded', function() { + updateAlertPie(); + setInterval(updateAlertPie, 30000); + });