1
0

[claude] Real-time monitoring dashboard for all agent systems (#862) (#1319)

This commit is contained in:
2026-03-24 02:07:38 +00:00
parent 715ad82726
commit 6bb5e7e1a6
6 changed files with 967 additions and 0 deletions

View File

@@ -0,0 +1,429 @@
{% extends "base.html" %}
{% block title %}Monitoring — Timmy Time{% endblock %}
{% block content %}
<!-- Page header -->
<div class="card">
<div class="card-header">
<h2 class="card-title">Real-Time Monitoring</h2>
<div class="d-flex align-items-center gap-2">
<span class="badge" id="mon-overall-badge">Loading...</span>
<span class="mon-last-updated" id="mon-last-updated"></span>
</div>
</div>
<!-- Uptime stat bar -->
<div class="grid grid-4">
<div class="stat">
<div class="stat-value" id="mon-uptime"></div>
<div class="stat-label">Uptime</div>
</div>
<div class="stat">
<div class="stat-value" id="mon-agents-count"></div>
<div class="stat-label">Agents</div>
</div>
<div class="stat">
<div class="stat-value" id="mon-alerts-count">0</div>
<div class="stat-label">Alerts</div>
</div>
<div class="stat">
<div class="stat-value" id="mon-ollama-badge"></div>
<div class="stat-label">LLM Backend</div>
</div>
</div>
</div>
<!-- Alerts panel (conditionally shown) -->
<div class="card mc-card-spaced" id="mon-alerts-card" style="display:none">
<div class="card-header">
<h2 class="card-title">Alerts</h2>
<span class="badge badge-danger" id="mon-alerts-badge">0</span>
</div>
<div id="mon-alerts-list"></div>
</div>
<!-- Agent Status -->
<div class="card mc-card-spaced">
<div class="card-header">
<h2 class="card-title">Agent Status</h2>
</div>
<div id="mon-agents-list">
<p class="chat-history-placeholder">Loading agents...</p>
</div>
</div>
<!-- System Resources + Economy row -->
<div class="grid grid-2 mc-card-spaced mc-section-gap">
<!-- System Resources -->
<div class="card">
<div class="card-header">
<h2 class="card-title">System Resources</h2>
</div>
<div class="grid grid-2">
<div class="stat">
<div class="stat-value" id="mon-cpu"></div>
<div class="stat-label">CPU</div>
</div>
<div class="stat">
<div class="stat-value" id="mon-ram"></div>
<div class="stat-label">RAM</div>
</div>
<div class="stat">
<div class="stat-value" id="mon-disk"></div>
<div class="stat-label">Disk</div>
</div>
<div class="stat">
<div class="stat-value" id="mon-models-loaded"></div>
<div class="stat-label">Models Loaded</div>
</div>
</div>
<!-- Resource bars -->
<div class="mon-resource-bars" id="mon-resource-bars">
<div class="mon-bar-row">
<span class="mon-bar-label">RAM</span>
<div class="mon-bar-track">
<div class="mon-bar-fill" id="mon-ram-bar" style="width:0%"></div>
</div>
<span class="mon-bar-pct" id="mon-ram-pct"></span>
</div>
<div class="mon-bar-row">
<span class="mon-bar-label">Disk</span>
<div class="mon-bar-track">
<div class="mon-bar-fill" id="mon-disk-bar" style="width:0%"></div>
</div>
<span class="mon-bar-pct" id="mon-disk-pct"></span>
</div>
<div class="mon-bar-row" id="mon-cpu-bar-row">
<span class="mon-bar-label">CPU</span>
<div class="mon-bar-track">
<div class="mon-bar-fill" id="mon-cpu-bar" style="width:0%"></div>
</div>
<span class="mon-bar-pct" id="mon-cpu-pct"></span>
</div>
</div>
</div>
<!-- Economy -->
<div class="card">
<div class="card-header">
<h2 class="card-title">Economy</h2>
</div>
<div class="grid grid-2">
<div class="stat">
<div class="stat-value" id="mon-balance"></div>
<div class="stat-label">Balance (sats)</div>
</div>
<div class="stat">
<div class="stat-value" id="mon-earned"></div>
<div class="stat-label">Earned</div>
</div>
<div class="stat">
<div class="stat-value" id="mon-spent"></div>
<div class="stat-label">Spent</div>
</div>
<div class="stat">
<div class="stat-value" id="mon-injections"></div>
<div class="stat-label">Injections</div>
</div>
</div>
<div class="grid grid-2 mc-section-heading">
<div class="stat">
<div class="stat-value" id="mon-tx-count"></div>
<div class="stat-label">Transactions</div>
</div>
<div class="stat">
<div class="stat-value" id="mon-auction"></div>
<div class="stat-label">Auction</div>
</div>
</div>
</div>
</div>
<!-- Stream Health + Content Pipeline row -->
<div class="grid grid-2 mc-card-spaced mc-section-gap">
<!-- Stream Health -->
<div class="card">
<div class="card-header">
<h2 class="card-title">Stream Health</h2>
<span class="badge" id="mon-stream-badge">Offline</span>
</div>
<div class="grid grid-2">
<div class="stat">
<div class="stat-value" id="mon-viewers"></div>
<div class="stat-label">Viewers</div>
</div>
<div class="stat">
<div class="stat-value" id="mon-bitrate"></div>
<div class="stat-label">Bitrate (kbps)</div>
</div>
<div class="stat">
<div class="stat-value" id="mon-stream-uptime"></div>
<div class="stat-label">Stream Uptime</div>
</div>
<div class="stat">
<div class="stat-value mon-stream-title" id="mon-stream-title"></div>
<div class="stat-label">Title</div>
</div>
</div>
</div>
<!-- Content Pipeline -->
<div class="card">
<div class="card-header">
<h2 class="card-title">Content Pipeline</h2>
<span class="badge" id="mon-pipeline-badge"></span>
</div>
<div class="grid grid-2">
<div class="stat">
<div class="stat-value" id="mon-highlights"></div>
<div class="stat-label">Highlights</div>
</div>
<div class="stat">
<div class="stat-value" id="mon-clips"></div>
<div class="stat-label">Clips</div>
</div>
</div>
<div class="mon-last-episode" id="mon-last-episode-wrap" style="display:none">
<span class="mon-bar-label">Last episode: </span>
<span id="mon-last-episode"></span>
</div>
</div>
</div>
<script>
// -----------------------------------------------------------------------
// Utility
// -----------------------------------------------------------------------
function _pct(val) {
if (val === null || val === undefined) return '—';
return val.toFixed(0) + '%';
}
function _barColor(pct) {
if (pct >= 90) return 'var(--red)';
if (pct >= 75) return 'var(--amber)';
return 'var(--green)';
}
function _setBar(barId, pct) {
var bar = document.getElementById(barId);
if (!bar) return;
var w = Math.min(100, Math.max(0, pct || 0));
bar.style.width = w + '%';
bar.style.background = _barColor(w);
}
function _uptime(secs) {
if (!secs && secs !== 0) return '—';
secs = Math.floor(secs);
if (secs < 60) return secs + 's';
if (secs < 3600) return Math.floor(secs / 60) + 'm';
var h = Math.floor(secs / 3600);
var m = Math.floor((secs % 3600) / 60);
return h + 'h ' + m + 'm';
}
function _setText(id, val) {
var el = document.getElementById(id);
if (el) el.textContent = (val !== null && val !== undefined) ? val : '—';
}
// -----------------------------------------------------------------------
// Render helpers
// -----------------------------------------------------------------------
function renderAgents(agents) {
var container = document.getElementById('mon-agents-list');
if (!agents || agents.length === 0) {
container.innerHTML = '';
var p = document.createElement('p');
p.className = 'chat-history-placeholder';
p.textContent = 'No agents configured';
container.appendChild(p);
return;
}
container.innerHTML = '';
agents.forEach(function(a) {
var row = document.createElement('div');
row.className = 'mon-agent-row';
var dot = document.createElement('span');
dot.className = 'mon-agent-dot';
dot.style.background = a.status === 'running' ? 'var(--green)' :
a.status === 'idle' ? 'var(--amber)' : 'var(--red)';
var name = document.createElement('span');
name.className = 'mon-agent-name';
name.textContent = a.name;
var model = document.createElement('span');
model.className = 'mon-agent-model';
model.textContent = a.model;
var status = document.createElement('span');
status.className = 'mon-agent-status';
status.textContent = a.status || '—';
var action = document.createElement('span');
action.className = 'mon-agent-action';
action.textContent = a.last_action || '—';
row.appendChild(dot);
row.appendChild(name);
row.appendChild(model);
row.appendChild(status);
row.appendChild(action);
container.appendChild(row);
});
}
function renderAlerts(alerts) {
var card = document.getElementById('mon-alerts-card');
var list = document.getElementById('mon-alerts-list');
var badge = document.getElementById('mon-alerts-badge');
var countEl = document.getElementById('mon-alerts-count');
badge.textContent = alerts.length;
countEl.textContent = alerts.length;
if (alerts.length === 0) {
card.style.display = 'none';
return;
}
card.style.display = '';
list.innerHTML = '';
alerts.forEach(function(a) {
var item = document.createElement('div');
item.className = 'mon-alert-item mon-alert-' + (a.level || 'warning');
var title = document.createElement('strong');
title.textContent = a.title;
var detail = document.createElement('span');
detail.className = 'mon-alert-detail';
detail.textContent = ' — ' + (a.detail || '');
item.appendChild(title);
item.appendChild(detail);
list.appendChild(item);
});
}
function renderResources(r) {
_setText('mon-cpu', r.cpu_percent !== null ? r.cpu_percent.toFixed(0) + '%' : '—');
_setText('mon-ram',
r.ram_available_gb !== null
? r.ram_available_gb.toFixed(1) + ' GB free'
: '—'
);
_setText('mon-disk',
r.disk_free_gb !== null
? r.disk_free_gb.toFixed(1) + ' GB free'
: '—'
);
_setText('mon-models-loaded', r.loaded_models ? r.loaded_models.length : '—');
if (r.ram_percent !== null) {
_setBar('mon-ram-bar', r.ram_percent);
_setText('mon-ram-pct', _pct(r.ram_percent));
}
if (r.disk_percent !== null) {
_setBar('mon-disk-bar', r.disk_percent);
_setText('mon-disk-pct', _pct(r.disk_percent));
}
if (r.cpu_percent !== null) {
_setBar('mon-cpu-bar', r.cpu_percent);
_setText('mon-cpu-pct', _pct(r.cpu_percent));
}
var ollamaBadge = document.getElementById('mon-ollama-badge');
ollamaBadge.textContent = r.ollama_reachable ? 'Online' : 'Offline';
ollamaBadge.style.color = r.ollama_reachable ? 'var(--green)' : 'var(--red)';
}
function renderEconomy(e) {
_setText('mon-balance', e.balance_sats);
_setText('mon-earned', e.earned_sats);
_setText('mon-spent', e.spent_sats);
_setText('mon-injections', e.injection_count);
_setText('mon-tx-count', e.tx_count);
_setText('mon-auction', e.auction_active ? 'Active' : 'None');
}
function renderStream(s) {
var badge = document.getElementById('mon-stream-badge');
if (s.live) {
badge.textContent = 'LIVE';
badge.className = 'badge badge-success';
} else {
badge.textContent = 'Offline';
badge.className = 'badge badge-danger';
}
_setText('mon-viewers', s.viewer_count);
_setText('mon-bitrate', s.bitrate_kbps);
_setText('mon-stream-uptime', _uptime(s.uptime_seconds));
_setText('mon-stream-title', s.title || '—');
}
function renderPipeline(p) {
var badge = document.getElementById('mon-pipeline-badge');
badge.textContent = p.pipeline_healthy ? 'Healthy' : 'Degraded';
badge.className = p.pipeline_healthy ? 'badge badge-success' : 'badge badge-warning';
_setText('mon-highlights', p.highlight_count);
_setText('mon-clips', p.clip_count);
if (p.last_episode) {
var wrap = document.getElementById('mon-last-episode-wrap');
wrap.style.display = '';
_setText('mon-last-episode', p.last_episode);
}
}
// -----------------------------------------------------------------------
// Poll /monitoring/status
// -----------------------------------------------------------------------
async function pollMonitoring() {
try {
var resp = await fetch('/monitoring/status');
if (!resp.ok) throw new Error('HTTP ' + resp.status);
var data = await resp.json();
// Overall badge
var overall = document.getElementById('mon-overall-badge');
var alertCount = (data.alerts || []).length;
if (alertCount === 0) {
overall.textContent = 'All Systems Nominal';
overall.className = 'badge badge-success';
} else {
var critical = (data.alerts || []).filter(function(a) { return a.level === 'critical'; });
overall.textContent = critical.length > 0 ? 'Critical Issues' : 'Warnings';
overall.className = critical.length > 0 ? 'badge badge-danger' : 'badge badge-warning';
}
// Uptime
_setText('mon-uptime', _uptime(data.uptime_seconds));
_setText('mon-agents-count', (data.agents || []).length);
// Last updated
var updEl = document.getElementById('mon-last-updated');
if (updEl) updEl.textContent = 'Updated ' + new Date().toLocaleTimeString();
// Panels
renderAgents(data.agents || []);
renderAlerts(data.alerts || []);
if (data.resources) renderResources(data.resources);
if (data.economy) renderEconomy(data.economy);
if (data.stream) renderStream(data.stream);
if (data.pipeline) renderPipeline(data.pipeline);
} catch (err) {
console.error('Monitoring poll failed:', err);
var overall = document.getElementById('mon-overall-badge');
overall.textContent = 'Poll Error';
overall.className = 'badge badge-danger';
}
}
// Start immediately, then every 10 s
pollMonitoring();
setInterval(pollMonitoring, 10000);
</script>
{% endblock %}