From ca58c18802c17f57eca37c6ac88f710cab5f51bc Mon Sep 17 00:00:00 2001 From: Andreas Wrede Date: Fri, 8 May 2026 07:00:17 -0400 Subject: [PATCH] eventlog: store structured dicts; filter by user; clock: fix minute hand step - eventlog() now stores {ts, host, level, service, message} dicts instead of strings - WebSocket sends/broadcasts filter event log messages by the user's managed hosts - live.html renders structured log entries with level-coloured spans - Swiss railway clock minute hand now holds until second hand reaches 12, then steps Co-Authored-By: Claude Sonnet 4.6 --- hbd/server/notify.py | 11 +++++++++-- hbd/server/templates/head.html | 2 +- hbd/server/templates/live.html | 27 +++++++++++++++++++++++++-- hbd/server/ws.py | 8 ++++++-- 4 files changed, 41 insertions(+), 7 deletions(-) diff --git a/hbd/server/notify.py b/hbd/server/notify.py index 8b401d2..a44067e 100644 --- a/hbd/server/notify.py +++ b/hbd/server/notify.py @@ -106,11 +106,18 @@ def closelog(): def eventlog(host, lvl, m, service=None): ts = time.time() + msg = { + "ts": ts, + "host": host or None, + "level": lvl, + "service": service, + "message": m, + } + data.msgs.append(msg) s = f"{time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(ts))} {lvl} " if host: s += f"{host} " s += m - data.msgs.append(s) logger.info(s) if logf: try: @@ -118,7 +125,7 @@ def eventlog(host, lvl, m, service=None): logf.flush() except Exception as e: logger.warning("failed to write to logfile: %s", e) - msg_to_websockets("message", s) + msg_to_websockets("message", msg) # --------------------------------------------------------------------------- diff --git a/hbd/server/templates/head.html b/hbd/server/templates/head.html index 4c90623..3167e90 100644 --- a/hbd/server/templates/head.html +++ b/hbd/server/templates/head.html @@ -214,7 +214,7 @@ ctx.restore(); } - hand((m + s / 60) / 60 * Math.PI * 2 - Math.PI / 2, + hand((sFrac >= 58.5 ? m + 1 : m) / 60 * Math.PI * 2 - Math.PI / 2, R * 0.88, -R * 0.12, SIZE * 0.027, '#222'); /* minute */ hand((h + m / 60) / 12 * Math.PI * 2 - Math.PI / 2, R * 0.58, -R * 0.12, SIZE * 0.039, '#222'); /* hour */ diff --git a/hbd/server/templates/live.html b/hbd/server/templates/live.html index 60396b3..a5fb356 100644 --- a/hbd/server/templates/live.html +++ b/hbd/server/templates/live.html @@ -183,11 +183,24 @@ line-height: 1.0; } - #messages div { + #messages .log-entry { padding: 5px 0; border-bottom: 1px solid #f0f0f0; + display: flex; + gap: 0.5em; + align-items: baseline; } + .log-ts { color: #888; white-space: nowrap; } + .log-level { font-weight: bold; min-width: 6em; } + .log-host { font-weight: 600; } + .log-service { color: #888; } + + .log-warning .log-level { color: #b8860b; } + .log-critical .log-level { color: #c00; } + .log-recover .log-level { color: #2a7a2a; } + .log-info .log-level { color: #555; } + /* Modal for connection status messages */ .connection-modal { display: none; @@ -460,7 +473,17 @@ update_table(state.data); } else if (state.type == "message") { var msgs = document.getElementById("messages"); - msgs.insertAdjacentHTML("afterbegin", "
" + state.data + "
"); + var msg = state.data; + var ts_str = new Date(msg.ts * 1000).toLocaleString(); + var lvl = (msg.level || "INFO").toLowerCase(); + var html = '
'; + html += '' + ts_str + ''; + html += '' + (msg.level || "") + ''; + if (msg.host) html += '' + msg.host + ''; + if (msg.service) html += '' + msg.service + ''; + html += '' + msg.message + ''; + html += '
'; + msgs.insertAdjacentHTML("afterbegin", html); } cnt++; }; diff --git a/hbd/server/ws.py b/hbd/server/ws.py index 9117a82..d073479 100644 --- a/hbd/server/ws.py +++ b/hbd/server/ws.py @@ -85,11 +85,13 @@ async def handler(request): except Exception as e: logger.error("Error sending initial hosts: %s", e) - # Send recent messages + # Send recent messages, filtered to hosts this user may see if data.msgs: try: for m in data.msgs: - await ws.send_str(json.dumps({"type": "message", "data": m})) + host_name = m.get("host") if isinstance(m, dict) else None + if not host_name or _user_can_see_host(user, host_name): + await ws.send_str(json.dumps({"type": "message", "data": m})) except Exception as e: logger.error("Error sending initial messages: %s", e) @@ -128,6 +130,8 @@ def broadcast(typ: str, payload) -> bool: host_name: Optional[str] = None if typ in ("host", "plugin"): host_name = payload.get("raw_name") or payload.get("host") or payload.get("name") + elif typ == "message" and isinstance(payload, dict): + host_name = payload.get("host") jmsg = json.dumps({"type": typ, "data": payload})