(int)($e['count'] ?? 1), $errors));
$lastIngest = $errors ? max(array_column($errors, 'ingested_at')) : null;
$totalSessions = count($sessions);
$byType = [];
$bySeverity = [];
$byCategory = [];
$byModule = [];
$byDay = [];
$topErrors = [];
foreach ($errors as $e) {
$t = $e['type'] ?? 'unknown';
$byType[$t] = ($byType[$t] ?? 0) + ($e['count'] ?? 1);
$s = $e['severity'] ?? 'error';
$bySeverity[$s] = ($bySeverity[$s] ?? 0) + ($e['count'] ?? 1);
$m = $e['module'] ?? 'unknown';
$byModule[$m] = ($byModule[$m] ?? 0) + ($e['count'] ?? 1);
$c = $e['category'] ?? 'runtime';
$byCategory[$c] = ($byCategory[$c] ?? 0) + ($e['count'] ?? 1);
$day = substr($e['ingested_at'] ?? $e['last_seen'], 0, 10);
$byDay[$day] = ($byDay[$day] ?? 0) + ($e['count'] ?? 1);
$hash = $e['hash'] ?? '';
if ($hash) {
if (!isset($topErrors[$hash])) {
$topErrors[$hash] = ['hash' => $hash, 'message' => $e['message'] ?? '', 'count' => 0, 'type' => $t];
}
$topErrors[$hash]['count'] += $e['count'] ?? 1;
}
}
usort($topErrors, fn($a, $b) => $b['count'] - $a['count']);
$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);
?>
newOS4 Debug Dashboard
🔍 newOS4 Debug Dashboard
Last ingest: = htmlspecialchars(substr($lastIngest, 0, 19)) ?> UTC · signatures sorted by occurrence count
Total Occurrences
= $totalOccurrences ?>
= $uniqueErrors ?> unique signatures
last ingest: = $lastIngest ? htmlspecialchars($lastIngest) : '—' ?>
Sessions
= $totalSessions ?>
Error Types
= count($byType) ?>
Modules Affected
= count($byModule) ?>
Maintenance triage = htmlspecialchars($triLabel) ?>
Triage digest = htmlspecialchars($triageState) ?> — run python3 scripts/newos4-triage.py or check the newos4-maintenance cron.
Top real signatures
= (int)($t['count'] ?? 0) ?>× = htmlspecialchars($t['type'] ?? '?') ?> = htmlspecialchars(mb_substr($t['message'] ?? '', 0, 110)) ?>
= htmlspecialchars($c) ?>
$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 = htmlspecialchars($repliesState) ?>
No messages yet. In the app: feedback <message>; agent replies appear here.
= $m['who'] === 'app' ? 'app (feedback)' : 'maintenance agent' ?> · = htmlspecialchars(substr($m['at'], 0, 19)) ?>
= htmlspecialchars(mb_substr($m['body'], 0, 600)) ?>
By Severity
'error', 'error' => 'error', 'warning' => 'warn'] as $sev => $cls): ?>
| = $sev ?> | = $bySeverity[$sev] ?? 0 ?> |
By Category
0): ?>
| = $cat ?> | = $byCategory[$cat] ?? 0 ?> |
By Module
$count):
if ($i++ > 5) break;
?>
| = htmlspecialchars($mod) ?> | = $count ?> |
Daily Trend (last 14 days)
| Day | Errors | |
$count):
$pct = round($count / $maxDay * 100, 0);
?>
| = $day ?> |
= $count ?> |
|
Recent Sessions
| Session | Version | Errors | Last Seen |
$s): ?>
| = htmlspecialchars($sid) ?> |
= htmlspecialchars($s['version'] ?? '?') ?> |
= $s['error_count'] ?? 0 ?> |
= htmlspecialchars(substr($s['last_seen'] ?? '', 0, 16)) ?> |
Auto-refresh every 30s (paused while the tab is hidden)