From 06056c08c8c49b0007202223e849de81b2e44c7d Mon Sep 17 00:00:00 2001 From: Jan Date: Mon, 6 Apr 2026 08:40:38 +0200 Subject: [PATCH] Surface control audit telemetry --- docs/API.md | 8 +++++++ internal/control/ui.html | 48 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 56 insertions(+) diff --git a/docs/API.md b/docs/API.md index ce1538b..c000124 100644 --- a/docs/API.md +++ b/docs/API.md @@ -15,6 +15,8 @@ Health check. {"ok": true} ``` +`controlAudit` mirrors the control plane's HTTP reject counters (405/415/413/400) so runtime telemetry can spot abusive clients and the UI can keep ops aware of guardrail hits. + `engine.state` spiegelt jetzt die Runtime-State-Maschine wider (idle, arming, prebuffering, running, degraded, muted, faulted, stopping) und bietet eine erste beobachtbare Basis für Fault-Transitions. @@ -98,6 +100,12 @@ Live engine and driver telemetry. Only populated when TX is active. "underrunStreak": 0, "maxUnderrunStreak": 0, "effectiveSampleRateHz": 2280000 + }, + "controlAudit": { + "methodNotAllowed": 0, + "unsupportedMediaType": 0, + "bodyTooLarge": 0, + "unexpectedBody": 0 } } ``` diff --git a/internal/control/ui.html b/internal/control/ui.html index eb211cd..38f1170 100644 --- a/internal/control/ui.html +++ b/internal/control/ui.html @@ -1193,6 +1193,19 @@ input.input-error { + + +

Shortcuts

@@ -1974,6 +1987,7 @@ function render() { updateText('info-live', engine.state ? `${String(engine.state).toUpperCase()} / ${state.server.runtimeOk ? 'runtime ok' : 'runtime pending'}` : (state.server.configOk ? 'config only' : '--')); updateHealth(engine, driver, audioStream); + updateControlAudit(runtime.controlAudit); updateFaultHistory(engine); updateTransitionHistory(); updateResetHint(engine); @@ -2248,6 +2262,40 @@ function updateHealth(engine, driver, audioStream) { } + +function updateControlAudit(audit) { + const entries = [ + { key: 'methodNotAllowed', id: 'audit-methodNotAllowed' }, + { key: 'unsupportedMediaType', id: 'audit-unsupportedMediaType' }, + { key: 'bodyTooLarge', id: 'audit-bodyTooLarge' }, + { key: 'unexpectedBody', id: 'audit-unexpectedBody' }, + ]; + let total = 0; + let hasData = false; + entries.forEach(({ key, id }) => { + const raw = audit && typeof audit[key] !== 'undefined' ? Number(audit[key]) : NaN; + const value = Number.isFinite(raw) ? raw : null; + if (value != null) { + hasData = true; + total += value; + } + setAuditValue(id, value); + }); + setAuditValue('audit-total', hasData ? total : null); +} + +function setAuditValue(id, count) { + const el = $(id); + if (!el) return; + if (count == null) { + el.textContent = '--'; + el.className = 'val'; + return; + } + el.textContent = String(count); + el.className = 'val ' + (count > 0 ? 'warn' : 'good'); +} + function updateFaultHistory(engine) { const container = $('fault-history'); if (!container) return;