From 316a34d15d82b6c928bfb1e3670c052d55aa9875 Mon Sep 17 00:00:00 2001 From: Agando Date: Fri, 23 Oct 2026 14:54:27 +0200 Subject: feat(newos4-debug): dashboard triage + thread panels, mobile layout, gate error.php MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Triage card reads admin/docs/newos4-triage-latest.json defensively: missing/unreadable renders as a warning pill, never as zeros (a zero here reads as "nothing is wrong"). Freshness pill goes stale past 26h so a dead collector cannot look healthy. - Maintenance thread merges in-app `feedback` reports (kind=maintenance) with agent replies from data/maintenance/replies.json into one chronological view. - Mobile: <=640px single column, smaller type, .table-scroll wrappers, Type column hidden, max-width clamp. verify-mobile.py: 360/390px, light+dark -> ALL VIEWPORTS OK, page width == viewport (no horizontal overflow). - "Last ingest" now uses max(ingested_at) instead of end($errors): after the prune the array is not chronological and the header contradicted the triage card by 3 days. - Auto-refresh only fires while the tab is visible (was an unconditional 30s reload). - Fixed triage checks parsing: digest ships {exit, findings[]}, not a list of strings — the first render fataled on stripos(array). Verified 15,167 B render, 0 PHP errors. nginx (not in git): error.php added to the admin-gated read regex alongside stats|errors|clusters|dashboard|agent-replies — error.php?hash= was still serving stacks to anonymous callers. All six now answer 302 without a session. --- api/debug/dashboard.php | 175 ++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 163 insertions(+), 12 deletions(-) (limited to 'api/debug/dashboard.php') diff --git a/api/debug/dashboard.php b/api/debug/dashboard.php index 74ba3af..08e6194 100644 --- a/api/debug/dashboard.php +++ b/api/debug/dashboard.php @@ -41,6 +41,41 @@ $topErrors = array_slice($topErrors, 0, 10); ksort($byDay); $recentSessions = array_slice($sessions, -10, 10, true); + +// --- Maintenance layer ------------------------------------------------------- +// Both files are written by the Hermes maintenance job. Read them defensively: a partially +// written JSON must render as "unavailable", never as zero (a zero here would read as +// "nothing is wrong", which is the exact failure this dashboard exists to catch). +$DOCROOT = dirname(__DIR__, 4); +$triageFile = $DOCROOT . '/admin/docs/newos4-triage-latest.json'; +$repliesFile = __DIR__ . '/../../data/maintenance/replies.json'; + +$triage = null; $triageState = 'missing'; +if (is_file($triageFile)) { + $raw = @file_get_contents($triageFile); + $parsed = $raw === false ? null : json_decode($raw, true); + if (is_array($parsed)) { $triage = $parsed; $triageState = 'ok'; } + else { $triageState = 'unreadable'; } +} + +$replies = []; $repliesState = 'missing'; +if (is_file($repliesFile)) { + $raw = @file_get_contents($repliesFile); + $parsed = $raw === false ? null : json_decode($raw, true); + if (is_array($parsed)) { $replies = array_slice($parsed, -5); $repliesState = 'ok'; } + else { $repliesState = 'unreadable'; } +} + +// In-app feedback (kind=maintenance) — the other half of the thread. +$feedback = []; +foreach (glob(__DIR__ . '/../../data/reports/RPT-*.json') ?: [] as $rf) { + $r = json_decode((string)@file_get_contents($rf), true); + if (is_array($r) && ($r['kind'] ?? '') === 'maintenance') { + $feedback[] = $r; + } +} +usort($feedback, fn($a, $b) => strcmp($b['created_at'] ?? '', $a['created_at'] ?? '')); +$feedback = array_slice($feedback, 0, 5); ?> @@ -75,20 +110,42 @@ $recentSessions = array_slice($sessions, -10, 10, true); a { color: var(--accent); text-decoration: none; } a:hover { text-decoration: underline; } .max-w-300 { max-width: 300px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } - + .thread { font-size: 0.85em; line-height: 1.45; } + .thread .msg { border-left: 3px solid var(--border); padding: 4px 0 4px 10px; margin-bottom: 10px; } + .thread .msg.hermes { border-left-color: var(--accent); } + .thread .msg.app { border-left-color: var(--info); } + .thread .who { color: var(--muted); font-size: 0.8em; } + .thread .body { white-space: pre-wrap; word-break: break-word; } + .status-pill { display:inline-block; padding:1px 8px; border-radius:10px; font-size:.75em; } + .status-pill.ok { background:#002a20; color:var(--accent); } + .status-pill.stale { background:#3a2a00; color:var(--warn); } + .status-pill.degraded { background:#3a1010; color:var(--error); } + .table-scroll { overflow-x: auto; -webkit-overflow-scrolling: touch; } + + /* Phones: one column, no horizontal page overflow, smaller type. */ + @media (max-width: 640px) { + body { padding: 10px; font-size: 14px; } + h1 { font-size: 1.1em; } + .subtitle { font-size: 0.75em; line-height: 1.5; } + .grid { grid-template-columns: 1fr; gap: 8px; } + .card { padding: 10px; border-radius: 6px; } + .stat-value { font-size: 1.5em; } + table { font-size: 0.78em; } + th, td { padding: 4px 5px; } + .max-w-300 { max-width: 42vw; } + .hide-sm { display: none; } + }

🔍 newOS4 Debug Dashboard

Server-side error aggregation · · JSON Stats · Clusters · Raw Errors · Health
-
Last sync: UTC
+// Use the newest ingest across all signatures, not the last array element: after the 2026-10-23 +// prune the array order is not chronological, and `end()` showed a 3-day-old timestamp while the +// triage card showed a fresh one — two numbers disagreeing on the same screen. +if ($lastIngest): ?> +
Last ingest: UTC · signatures sorted by occurrence count
@@ -112,6 +169,92 @@ if ($lastSync): ?>
+ +
+

Maintenance triage

+ +
Triage digest — run python3 scripts/newos4-triage.py or check the newos4-maintenance cron.
+ +
+ + + + + + + + + + + +
SignaturesOccurrencesRealNoiseSelf-refNew reportsGenerated
Z
+
+ +
Top real signatures
+ +
×
+ + + +
+
+
+ + +
+ + $f['created_at'] ?? '', 'who' => 'app', 'body' => ($f['title'] ?? '') . (($f['body'] ?? '') !== ($f['title'] ?? '') ? "\n" . ($f['body'] ?? '') : '')]; +} +foreach ($replies as $r) { + $thread[] = ['at' => $r['created_at'] ?? '', 'who' => ($r['author'] ?? 'hermes'), 'body' => $r['body'] ?? '']; +} +usort($thread, fn($a, $b) => strcmp($a['at'], $b['at'])); +$thread = array_slice($thread, -6); +?> +
+

Maintenance thread

+ +
No messages yet. In the app: feedback <message>; agent replies appear here.
+ +
+ +
+
·
+
+
+ +
+
+

By Severity

@@ -148,13 +291,14 @@ if ($lastSync): ?>

Top Errors

+
- + - + @@ -162,6 +306,7 @@ if ($lastSync): ?>
#MessageTypeCount
#MessageTypeCount
No errors recorded
+
@@ -198,7 +343,13 @@ if ($lastSync): ?>
-
Auto-refresh every 30s
- +
Auto-refresh every 30s (paused while the tab is hidden)
+ -- cgit v1.2.3