feat: show runtime diagnostics in management summary

This commit is contained in:
Yoilun
2026-04-28 19:03:03 +08:00
parent b1c39d3fa7
commit c81a20b2ea
4 changed files with 92 additions and 4 deletions

View File

@@ -65,6 +65,11 @@ def create_handler(ctx: ManageContext) -> type[BaseHTTPRequestHandler]:
limit = bounded_int(query.get("limit", ["200"])[0], 1, MAX_EVENT_LINES)
self._send_json({"items": load_events(ctx, limit), "limit": limit})
return
if parsed.path == "/api/manage/diagnostics":
query = parse_qs(parsed.query)
limit = bounded_int(query.get("limit", ["50"])[0], 1, MAX_EVENT_LINES)
self._send_json({"items": load_diagnostics(ctx, limit), "limit": limit})
return
self.send_error(HTTPStatus.NOT_FOUND)
def do_PUT(self) -> None:
@@ -234,6 +239,7 @@ def config_payload(ctx: ManageContext) -> dict[str, Any]:
def build_summary(ctx: ManageContext) -> dict[str, Any]:
events = load_events(ctx, MAX_EVENT_LINES)
diagnostics = load_diagnostics(ctx, MAX_EVENT_LINES)
counts: dict[str, int] = {}
last_event_time = ""
latest_alert = ""
@@ -261,22 +267,35 @@ def build_summary(ctx: ManageContext) -> dict[str, Any]:
"violation_count": active_alert_count,
"latest_alert_time": latest_alert,
"events_path": str(event_sink_path(ctx)),
"diagnostics_path": str(diagnostics_path(ctx)),
"diagnostics_count": len(diagnostics),
"latest_zone_counts": latest_zone_counts(diagnostics),
"baseline_ready": latest_baseline_ready(diagnostics),
},
}
def load_events(ctx: ManageContext, limit: int) -> list[dict[str, Any]]:
path = event_sink_path(ctx)
return load_jsonl_tail(path, limit)
def load_diagnostics(ctx: ManageContext, limit: int) -> list[dict[str, Any]]:
path = diagnostics_path(ctx)
return load_jsonl_tail(path, limit)
def load_jsonl_tail(path: Path, limit: int) -> list[dict[str, Any]]:
lines = tail_lines(path, limit)
events: list[dict[str, Any]] = []
items: list[dict[str, Any]] = []
for line in lines:
try:
payload = json.loads(line)
except json.JSONDecodeError:
continue
if isinstance(payload, dict):
events.append(payload)
return events
items.append(payload)
return items
def event_sink_path(ctx: ManageContext, data: dict[str, Any] | None = None) -> Path:
@@ -289,6 +308,32 @@ def event_sink_path(ctx: ManageContext, data: dict[str, Any] | None = None) -> P
return path.resolve()
def diagnostics_path(ctx: ManageContext, data: dict[str, Any] | None = None) -> Path:
if data is None:
data = load_config_document(ctx.config_path)
raw_path = str(data.get("runtime", {}).get("diagnostics_path", "logs/runtime_diagnostics.jsonl"))
path = Path(raw_path).expanduser()
if not path.is_absolute():
path = ctx.project_root / path
return path.resolve()
def latest_zone_counts(diagnostics: list[dict[str, Any]]) -> dict[str, int]:
for item in reversed(diagnostics):
zone_counts = item.get("zone_counts")
if isinstance(zone_counts, dict):
return {str(key): int(value) for key, value in zone_counts.items()}
return {}
def latest_baseline_ready(diagnostics: list[dict[str, Any]]) -> bool:
for item in reversed(diagnostics):
diagnostics_payload = item.get("diagnostics")
if isinstance(diagnostics_payload, dict):
return bool(diagnostics_payload.get("baseline_ready", False))
return False
def tail_lines(path: Path, limit: int) -> list[str]:
if not path.exists():
return []

View File

@@ -77,6 +77,37 @@ class ManageApiTests(unittest.TestCase):
self.assertEqual(summary["metrics"]["event_count"], 2)
self.assertEqual(summary["metrics"]["violation_count"], 1)
def test_summary_reads_runtime_diagnostics(self) -> None:
with tempfile.TemporaryDirectory() as tmpdir:
root = Path(tmpdir)
config_path = root / "config" / "local.toml"
save_config_document(
config_path,
{
"runtime": {"diagnostics_path": "logs/runtime_diagnostics.jsonl"},
"event_sink": {"path": "logs/events.jsonl"},
"layout": {"rows": 1, "cols": 1, "zone_ids": ["r1c1"]},
},
)
diagnostics_path = root / "logs" / "runtime_diagnostics.jsonl"
diagnostics_path.parent.mkdir()
diagnostics_path.write_text(
json.dumps(
{
"ts": "2026-04-28T10:00:00+08:00",
"zone_counts": {"r1c1": 1},
"diagnostics": {"baseline_ready": True},
}
),
encoding="utf-8",
)
summary = build_summary(ManageContext(config_path=config_path, project_root=root))
self.assertEqual(summary["metrics"]["diagnostics_count"], 1)
self.assertEqual(summary["metrics"]["latest_zone_counts"], {"r1c1": 1})
self.assertTrue(summary["metrics"]["baseline_ready"])
if __name__ == "__main__":
unittest.main()

View File

@@ -484,10 +484,18 @@ function renderMetrics() {
const cards = [
["事件总数", metrics.event_count ?? 0],
["违规事件", metrics.violation_count ?? 0],
["诊断帧数", metrics.diagnostics_count ?? 0],
["基线状态", metrics.baseline_ready ? "ready" : "learning"],
["最新报警", metrics.latest_alert_time || "-"],
["事件文件", metrics.events_path || "-"],
];
els.metrics.innerHTML = cards.map(([label, value]) => `<div class="metric"><span>${label}</span><strong>${value}</strong></div>`).join("");
const zoneCounts = metrics.latest_zone_counts || {};
const zoneSummary = Object.keys(zoneCounts).length
? `<div class="metric wide"><span>最新区域状态</span><strong>${Object.entries(zoneCounts)
.map(([zoneId, count]) => `${zoneId}:${count}`)
.join(" ")}</strong></div>`
: "";
els.metrics.innerHTML = cards.map(([label, value]) => `<div class="metric"><span>${label}</span><strong>${value}</strong></div>`).join("") + zoneSummary;
}
function renderEvents() {

View File

@@ -226,6 +226,10 @@ canvas {
font-size: 18px;
}
.metric.wide {
grid-column: 1 / -1;
}
.events-table {
overflow: auto;
}