<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>aidaemon dashboard</title>
<style>
:root {
--bg: #fff; --bg2: #f5f5f7; --fg: #1d1d1f; --fg2: #6e6e73;
--border: #d2d2d7; --accent: #0071e3; --err: #ff3b30;
--card-bg: #fff; --card-shadow: 0 1px 3px rgba(0,0,0,.08);
--radius: 8px; --font: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
}
@media (prefers-color-scheme: dark) {
:root {
--bg: #1c1c1e; --bg2: #2c2c2e; --fg: #f5f5f7; --fg2: #98989d;
--border: #38383a; --card-bg: #2c2c2e; --card-shadow: 0 1px 3px rgba(0,0,0,.3);
}
}
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
body { font-family: var(--font); background: var(--bg); color: var(--fg); line-height: 1.5; padding: 1rem; max-width: 960px; margin: 0 auto; }
h1 { font-size: 1.25rem; font-weight: 600; margin-bottom: 1rem; }
h2 { font-size: .9rem; font-weight: 600; text-transform: uppercase; letter-spacing: .04em; color: var(--fg2); margin-bottom: .5rem; }
.cards { display: grid; gap: 1rem; grid-template-columns: 1fr; }
@media (min-width: 600px) { .cards { grid-template-columns: 1fr 1fr; } }
.card { background: var(--card-bg); border: 1px solid var(--border); border-radius: var(--radius); padding: 1rem; box-shadow: var(--card-shadow); }
.card.wide { grid-column: 1 / -1; }
table { width: 100%; border-collapse: collapse; font-size: .85rem; }
th { text-align: left; font-weight: 500; color: var(--fg2); border-bottom: 1px solid var(--border); padding: .35rem .5rem; }
td { padding: .35rem .5rem; border-bottom: 1px solid var(--border); }
tr:last-child td { border-bottom: none; }
.mono { font-family: "SF Mono", "Fira Code", "Consolas", monospace; font-size: .8rem; }
.status-grid { display: grid; grid-template-columns: auto 1fr; gap: .25rem .75rem; font-size: .9rem; }
.status-grid dt { color: var(--fg2); }
.badge { display: inline-block; font-size: .75rem; padding: .1rem .4rem; border-radius: 4px; }
.badge-ok { background: #34c75920; color: #30d158; }
.badge-paused { background: #ff9f0a20; color: #ff9f0a; }
.badge-warn { background: #ffcc0022; color: #b45309; }
.badge-err { background: #ff3b3022; color: #d70015; }
.empty { color: var(--fg2); font-style: italic; font-size: .85rem; }
#auth-overlay { position: fixed; inset: 0; background: var(--bg); display: flex; align-items: center; justify-content: center; z-index: 100; }
#auth-box { text-align: center; }
#auth-box input { font-family: var(--font); font-size: .9rem; padding: .5rem .75rem; border: 1px solid var(--border); border-radius: var(--radius); background: var(--bg2); color: var(--fg); width: 300px; margin: .5rem 0; }
#auth-box button { font-family: var(--font); font-size: .9rem; padding: .5rem 1.25rem; border: none; border-radius: var(--radius); background: var(--accent); color: #fff; cursor: pointer; }
#auth-box .err { color: var(--err); font-size: .85rem; margin-top: .25rem; }
.refresh-info { font-size: .75rem; color: var(--fg2); margin-top: 1rem; text-align: center; }
.truncate { max-width: 200px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
</style>
</head>
<body>
<div id="auth-overlay">
<div id="auth-box">
<h1><a href="/" style="color:inherit;text-decoration:none">aidaemon</a></h1>
<p style="color:var(--fg2);margin-bottom:.5rem">Enter dashboard token</p>
<div><input id="token-input" type="password" placeholder="Bearer token" autocomplete="off"></div>
<div><button onclick="submitToken()">Connect</button></div>
<div class="err" id="auth-err"></div>
</div>
</div>
<div id="app" style="display:none">
<h1><a href="/" style="color:inherit;text-decoration:none">aidaemon</a> <span style="font-weight:400;color:var(--fg2);font-size:.85rem" id="version"></span></h1>
<div class="cards">
<div class="card" id="status-card">
<h2>Status</h2>
<dl class="status-grid" id="status-dl"></dl>
</div>
<div class="card" id="tasks-card">
<h2>Scheduled Tasks</h2>
<div id="tasks-body"></div>
</div>
<div class="card wide" id="maintenance-card">
<h2>Maintenance Jobs</h2>
<div id="maintenance-body"></div>
</div>
<div class="card wide" id="queues-card">
<h2>Queue Health</h2>
<div id="queues-body"></div>
</div>
<div class="card wide" id="llm-latency-card">
<h2>LLM Latency (last 24h)</h2>
<div id="llm-latency-body"></div>
</div>
<div class="card wide" id="policy-card">
<h2>Policy Metrics</h2>
<div id="policy-body"></div>
</div>
<div class="card wide" id="writes-card">
<h2>Write Consistency</h2>
<div id="writes-body"></div>
</div>
<div class="card wide" id="usage-card">
<h2>Token Usage (7 days)</h2>
<div id="usage-body"></div>
</div>
<div class="card wide" id="sessions-card">
<h2>Recent Sessions</h2>
<div id="sessions-body"></div>
</div>
</div>
<div class="refresh-info">Auto-refresh every 30s · <a href="#" onclick="fetchAll();return false" style="color:var(--accent)">refresh now</a></div>
</div>
<script>
let TOKEN = sessionStorage.getItem('aidaemon_token') || '';
function submitToken() {
TOKEN = document.getElementById('token-input').value.trim();
if (!TOKEN) return;
sessionStorage.setItem('aidaemon_token', TOKEN);
tryConnect();
}
document.getElementById('token-input').addEventListener('keydown', e => {
if (e.key === 'Enter') submitToken();
});
async function api(path) {
const r = await fetch(path, { headers: { 'Authorization': 'Bearer ' + TOKEN } });
if (r.status === 401) { showAuth('Invalid or expired token'); throw new Error('401'); }
return r.json();
}
function showAuth(msg) {
document.getElementById('auth-overlay').style.display = 'flex';
document.getElementById('app').style.display = 'none';
if (msg) document.getElementById('auth-err').textContent = msg;
}
function showApp() {
document.getElementById('auth-overlay').style.display = 'none';
document.getElementById('app').style.display = '';
}
async function tryConnect() {
try {
await api('/api/status');
showApp();
fetchAll();
} catch { showAuth('Could not connect'); }
}
function esc(s) { const d = document.createElement('div'); d.textContent = s; return d.innerHTML; }
function fmtDuration(secs) {
const d = Math.floor(secs / 86400), h = Math.floor((secs % 86400) / 3600), m = Math.floor((secs % 3600) / 60);
if (d > 0) return d + 'd ' + h + 'h';
if (h > 0) return h + 'h ' + m + 'm';
return m + 'm';
}
function fmtNum(n) { return n != null ? n.toLocaleString() : '-'; }
function fmtTs(ts) { return ts ? ts.slice(0, 16).replace('T', ' ') : '-'; }
async function fetchStatus() {
try {
const d = await api('/api/status');
document.getElementById('version').textContent = 'v' + esc(d.version);
let html = '';
const fallbackModels = Array.isArray(d.models.fallback) && d.models.fallback.length
? d.models.fallback.join(', ')
: '-';
const items = [
['Provider', d.provider],
['Default', d.models.default || d.models.primary],
['Fallbacks', fallbackModels],
['Primary', d.models.primary],
['Fast', d.models.fast],
['Smart', d.models.smart],
['Uptime', fmtDuration(d.uptime_secs)],
];
if (d.daily_token_budget) items.push(['Daily Budget', fmtNum(d.daily_token_budget) + ' tokens']);
for (const [k, v] of items) html += '<dt>' + esc(k) + '</dt><dd>' + esc(String(v)) + '</dd>';
document.getElementById('status-dl').innerHTML = html;
} catch {}
}
async function fetchUsage() {
try {
const rows = await api('/api/usage?days=7');
if (!rows.length) { document.getElementById('usage-body').innerHTML = '<p class="empty">No usage data</p>'; return; }
let html = '<table><tr><th>Date</th><th>Model</th><th>Input</th><th>Output</th><th>Requests</th></tr>';
for (const r of rows) html += '<tr><td class="mono">' + esc(r.day) + '</td><td class="truncate">' + esc(r.model) + '</td><td>' + fmtNum(r.input_tokens) + '</td><td>' + fmtNum(r.output_tokens) + '</td><td>' + fmtNum(r.request_count) + '</td></tr>';
html += '</table>';
document.getElementById('usage-body').innerHTML = html;
} catch {}
}
async function fetchSessions() {
try {
const rows = await api('/api/sessions?limit=20');
if (!rows.length) { document.getElementById('sessions-body').innerHTML = '<p class="empty">No sessions</p>'; return; }
let html = '<table><tr><th>Session</th><th>Messages</th><th>First</th><th>Last Activity</th></tr>';
for (const r of rows) html += '<tr><td class="mono truncate">' + esc(r.session_id) + '</td><td>' + fmtNum(r.message_count) + '</td><td class="mono">' + esc((r.first_message||'').slice(0,16)) + '</td><td class="mono">' + esc((r.last_activity||'').slice(0,16)) + '</td></tr>';
html += '</table>';
document.getElementById('sessions-body').innerHTML = html;
} catch {}
}
async function fetchTasks() {
try {
const rows = await api('/api/tasks');
if (!rows.length) { document.getElementById('tasks-body').innerHTML = '<p class="empty">No schedules</p>'; return; }
let html = '<table><tr><th>Goal</th><th>Schedule</th><th>Goal Status</th><th>Schedule</th><th>Next Run</th></tr>';
for (const r of rows) {
const scheduleText = r.original_schedule || r.cron_expr || '';
const schedBadge = r.is_paused ? '<span class="badge badge-paused">paused</span>' : '<span class="badge badge-ok">active</span>';
html += '<tr>'
+ '<td class="truncate">' + esc(r.goal_description || r.goal_id || '-') + '</td>'
+ '<td class="mono truncate">' + esc(scheduleText) + '</td>'
+ '<td class="mono">' + esc(r.goal_status || '-') + '</td>'
+ '<td>' + schedBadge + '</td>'
+ '<td class="mono">' + esc((r.next_run_at||'-').slice(0,16)) + '</td>'
+ '</tr>';
}
html += '</table>';
document.getElementById('tasks-body').innerHTML = html;
} catch {}
}
async function fetchMaintenanceJobs() {
try {
const payload = await api('/api/heartbeat/jobs');
const rows = payload.maintenance_jobs || [];
if (!rows.length) {
document.getElementById('maintenance-body').innerHTML = '<p class="empty">No maintenance telemetry available</p>';
return;
}
let html = '<table><tr><th>Job</th><th>Last Run</th><th>Last Success</th><th>Failures</th><th>Last Error</th></tr>';
for (const r of rows) {
html += '<tr>'
+ '<td class="mono">' + esc(r.name) + '</td>'
+ '<td class="mono">' + esc(fmtTs(r.last_run_at)) + '</td>'
+ '<td class="mono">' + esc(fmtTs(r.last_success_at)) + '</td>'
+ '<td>' + fmtNum(r.consecutive_failures) + '</td>'
+ '<td class="truncate">' + esc(r.last_error || '-') + '</td>'
+ '</tr>';
}
html += '</table>';
document.getElementById('maintenance-body').innerHTML = html;
} catch {}
}
async function fetchPolicyMetrics() {
try {
const d = await api('/api/policy/metrics');
const rows = [
['Tool exposure samples', fmtNum(d.tool_exposure_samples)],
['Avg tools before filter', (d.avg_tools_before_filter || 0).toFixed(2)],
['Avg tools after filter', (d.avg_tools_after_filter || 0).toFixed(2)],
['Ambiguity detected', fmtNum(d.ambiguity_detected_total)],
['Uncertainty clarifications', fmtNum(d.uncertainty_clarify_total)],
['Current uncertainty threshold', (d.uncertainty_threshold || 0).toFixed(2)],
['Context refreshes', fmtNum(d.context_refresh_total)],
['Escalations', fmtNum(d.escalation_total)],
['Fallback expansions', fmtNum(d.fallback_expansion_total)],
['Route reason: clarification_required', fmtNum(d.orchestration_route_clarification_required_total)],
['Route reason: tools_required', fmtNum(d.orchestration_route_tools_required_total)],
['Route reason: short_correction_direct_reply', fmtNum(d.orchestration_route_short_correction_direct_reply_total)],
['Route reason: acknowledgment_direct_reply', fmtNum(d.orchestration_route_acknowledgment_direct_reply_total)],
['Route reason: default_continue', fmtNum(d.orchestration_route_default_continue_total)],
['Tool schema contract rejections', fmtNum(d.tool_schema_contract_rejections_total)],
['Route drift alerts', fmtNum(d.route_drift_alert_total)],
['Route fail-safe activations', fmtNum(d.route_drift_failsafe_activation_total)],
['Route fail-safe active turns', fmtNum(d.route_failsafe_active_turn_total)],
];
let html = '<table><tr><th>Metric</th><th>Value</th></tr>';
for (const [k, v] of rows) html += '<tr><td>' + esc(k) + '</td><td class="mono">' + esc(String(v)) + '</td></tr>';
html += '</table>';
document.getElementById('policy-body').innerHTML = html;
} catch {
document.getElementById('policy-body').innerHTML = '<p class="empty">Policy metrics unavailable</p>';
}
}
async function fetchLlmLatency() {
try {
const d = await api('/api/llm/latency?hours=24');
if (d.error || !d.total_calls) {
document.getElementById('llm-latency-body').innerHTML = '<p class="empty">No LLM calls recorded yet</p>';
return;
}
const rows = [
['Total calls', fmtNum(d.total_calls)],
['Avg latency', fmtNum(d.avg_latency_ms) + ' ms'],
['p50 latency', fmtNum(d.p50_latency_ms) + ' ms'],
['p95 latency', fmtNum(d.p95_latency_ms) + ' ms'],
['Max latency', fmtNum(d.max_latency_ms) + ' ms'],
['Calls with fallback', fmtNum(d.fell_back_count)],
['Avg input tokens', fmtNum(d.avg_input_tokens)],
['Avg output tokens', fmtNum(d.avg_output_tokens)],
];
let html = '<table><tr><th>Metric</th><th>Value</th></tr>';
for (const [k, v] of rows) html += '<tr><td>' + esc(k) + '</td><td class="mono">' + esc(String(v)) + '</td></tr>';
html += '</table>';
document.getElementById('llm-latency-body').innerHTML = html;
} catch {
document.getElementById('llm-latency-body').innerHTML = '<p class="empty">LLM latency unavailable</p>';
}
}
async function fetchQueues() {
try {
const payload = await api('/api/queues');
const rows = payload.queues || [];
if (!rows.length) {
document.getElementById('queues-body').innerHTML = '<p class="empty">Queue telemetry unavailable</p>';
return;
}
let html = '<table><tr><th>Queue</th><th>Depth</th><th>Saturation</th><th>High-Water</th><th>Dropped</th><th>Failed</th><th>Overload Events</th><th>Status</th></tr>';
for (const r of rows) {
const saturation = ((r.saturation || 0) * 100).toFixed(0) + '%';
const depth = String(r.current_depth) + ' / ' + String(r.capacity);
let status = '<span class="badge badge-ok">normal</span>';
if (r.overload_active) status = '<span class="badge badge-err">overload</span>';
else if (r.current_depth >= r.warn_depth) status = '<span class="badge badge-warn">warning</span>';
html += '<tr>'
+ '<td class="mono">' + esc(r.name) + '</td>'
+ '<td class="mono">' + esc(depth) + '</td>'
+ '<td>' + esc(saturation) + '</td>'
+ '<td>' + fmtNum(r.high_watermark) + '</td>'
+ '<td>' + fmtNum(r.dropped_total) + '</td>'
+ '<td>' + fmtNum(r.failed_total) + '</td>'
+ '<td>' + fmtNum(r.overload_events_total) + '</td>'
+ '<td>' + status + '</td>'
+ '</tr>';
}
html += '</table>';
document.getElementById('queues-body').innerHTML = html;
} catch {
document.getElementById('queues-body').innerHTML = '<p class="empty">Queue telemetry unavailable</p>';
}
}
async function fetchWriteConsistency() {
try {
const d = await api('/api/writes/consistency');
if (d.error) {
document.getElementById('writes-body').innerHTML = '<p class="empty">' + esc(d.error) + '</p>';
return;
}
const gate = d.gate || {};
const gateBadge = gate.passed
? '<span class="badge badge-ok">PASS</span>'
: '<span class="badge badge-warn">WARN</span>';
const thresholds = gate.thresholds || {};
let html = '<table><tr><th>Metric</th><th>Value</th></tr>';
html += '<tr><td>Generated At</td><td class="mono">' + esc(fmtTs(d.generated_at)) + '</td></tr>';
html += '<tr><td>Consistency Gate</td><td>' + gateBadge + '</td></tr>';
html += '<tr><td>Event Rows</td><td class="mono">' + fmtNum(d.conversation_event_rows) + '</td></tr>';
html += '<tr><td>Missing Message IDs</td><td class="mono">' + fmtNum(d.missing_message_id_events) + '</td></tr>';
html += '<tr><td>Global Delta</td><td class="mono">' + fmtNum(d.global_delta) + '</td></tr>';
html += '<tr><td>Sessions with Drift</td><td class="mono">' + fmtNum(d.session_mismatch_count) + '</td></tr>';
html += '<tr><td>Stale Task Starts</td><td class="mono">' + fmtNum(d.stale_task_starts) + '</td></tr>';
if (thresholds.max_abs_global_delta != null) {
html += '<tr><td>Gate Thresholds</td><td class="mono">delta<=' + fmtNum(thresholds.max_abs_global_delta)
+ ', mismatches<=' + fmtNum(thresholds.max_session_mismatch_count)
+ ', stale<=' + fmtNum(thresholds.max_stale_task_starts)
+ ', missing_ids<=' + fmtNum(thresholds.max_missing_message_id_events) + '</td></tr>';
}
html += '</table>';
if (gate.reasons && gate.reasons.length) {
html += '<div style="margin-top:.5rem"></div>';
html += '<table><tr><th>Guardrail Warnings</th></tr>';
for (const reason of gate.reasons) {
html += '<tr><td>' + esc(reason) + '</td></tr>';
}
html += '</table>';
}
const drifts = d.top_session_drifts || [];
if (drifts.length) {
html += '<div style="margin-top:.75rem"></div>';
html += '<table><tr><th>Session</th><th>Messages</th><th>Events</th><th>Delta</th></tr>';
for (const r of drifts) {
html += '<tr>'
+ '<td class="mono truncate">' + esc(r.session_id) + '</td>'
+ '<td>' + fmtNum(r.message_rows) + '</td>'
+ '<td>' + fmtNum(r.event_rows) + '</td>'
+ '<td class="mono">' + fmtNum(r.delta) + '</td>'
+ '</tr>';
}
html += '</table>';
}
document.getElementById('writes-body').innerHTML = html;
} catch {
document.getElementById('writes-body').innerHTML = '<p class="empty">Write consistency report unavailable</p>';
}
}
function fetchAll() { fetchStatus(); fetchUsage(); fetchSessions(); fetchTasks(); fetchMaintenanceJobs(); fetchQueues(); fetchLlmLatency(); fetchPolicyMetrics(); fetchWriteConsistency(); }
if (TOKEN) { tryConnect(); } else { showAuth(); }
setInterval(fetchAll, 30000);
</script>
</body>
</html>