forked from Rockachopa/Timmy-time-dashboard
447 lines
16 KiB
HTML
447 lines
16 KiB
HTML
{% extends "base.html" %}
|
|
|
|
{% block title %}System Overview — Timmy Time{% endblock %}
|
|
|
|
{% block content %}
|
|
<div class="card">
|
|
<div class="card-header">
|
|
<h2 class="card-title">System Overview</h2>
|
|
<div>
|
|
<span class="badge badge-success" id="system-status">Loading...</span>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Sovereignty Score -->
|
|
<div class="sov-section">
|
|
<div class="sov-header">
|
|
<div class="sov-score-value" id="sov-score">-</div>
|
|
<div>
|
|
<div class="sov-score-label">Sovereignty Score</div>
|
|
<div class="sov-score-detail" id="sov-label">Calculating...</div>
|
|
</div>
|
|
</div>
|
|
<div class="sov-bar-track">
|
|
<div id="sov-bar" class="sov-bar-fill"></div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Dependency Grid -->
|
|
<h3 class="mc-section-heading">Dependencies</h3>
|
|
<div class="grid grid-2 mc-section-gap" id="dependency-grid">
|
|
<p class="chat-history-placeholder">Loading...</p>
|
|
</div>
|
|
|
|
<!-- Recommendations -->
|
|
<h3 class="mc-section-heading">Recommendations</h3>
|
|
<div id="recommendations" class="mc-section-gap">
|
|
<p class="chat-history-placeholder">Loading...</p>
|
|
</div>
|
|
|
|
<!-- System Metrics -->
|
|
<h3 class="mc-section-heading">System Metrics</h3>
|
|
<div class="grid grid-4" id="metrics-grid">
|
|
<div class="stat">
|
|
<div class="stat-value" id="metric-uptime">-</div>
|
|
<div class="stat-label">Uptime</div>
|
|
</div>
|
|
<div class="stat">
|
|
<div class="stat-value" id="metric-agents">-</div>
|
|
<div class="stat-label">Agents</div>
|
|
</div>
|
|
<div class="stat">
|
|
<div class="stat-value" id="metric-tasks">-</div>
|
|
<div class="stat-label">Tasks</div>
|
|
</div>
|
|
<div class="stat">
|
|
<div class="stat-value" id="metric-earned">-</div>
|
|
<div class="stat-label">Sats Earned</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Grok Mode Toggle -->
|
|
<div class="card mc-card-spaced">
|
|
<div class="card-header">
|
|
<h2 class="card-title">Grok Mode</h2>
|
|
<div>
|
|
<span class="badge grok-badge-standby" id="grok-badge">STANDBY</span>
|
|
</div>
|
|
</div>
|
|
<div id="grok-toggle-card"
|
|
hx-get="/grok/status"
|
|
hx-trigger="load"
|
|
hx-target="#grok-toggle-card"
|
|
hx-swap="innerHTML">
|
|
<div class="grok-toggle-box">
|
|
<div class="grok-toggle-row">
|
|
<div>
|
|
<div class="grok-toggle-title">
|
|
GROK MODE: LOADING...
|
|
</div>
|
|
<div class="grok-toggle-desc">
|
|
xAI frontier reasoning augmentation
|
|
</div>
|
|
</div>
|
|
<button hx-post="/grok/toggle"
|
|
hx-target="#grok-toggle-card"
|
|
hx-swap="outerHTML"
|
|
class="grok-activate-btn">
|
|
ACTIVATE
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="grid grid-3 mc-section-heading">
|
|
<div class="stat">
|
|
<div class="stat-value" id="grok-requests">0</div>
|
|
<div class="stat-label">Grok Queries</div>
|
|
</div>
|
|
<div class="stat">
|
|
<div class="stat-value" id="grok-tokens">0</div>
|
|
<div class="stat-label">Tokens Used</div>
|
|
</div>
|
|
<div class="stat">
|
|
<div class="stat-value" id="grok-cost">0</div>
|
|
<div class="stat-label">Est. Cost (sats)</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Heartbeat Monitor -->
|
|
<div class="card mc-card-spaced">
|
|
<div class="card-header">
|
|
<h2 class="card-title">Heartbeat Monitor</h2>
|
|
<div>
|
|
<span class="badge" id="heartbeat-status">Checking...</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="grid grid-3">
|
|
<div class="stat">
|
|
<div class="stat-value" id="hb-tick">-</div>
|
|
<div class="stat-label">Last Tick</div>
|
|
</div>
|
|
<div class="stat">
|
|
<div class="stat-value" id="hb-backend">-</div>
|
|
<div class="stat-label">LLM Backend</div>
|
|
</div>
|
|
<div class="stat">
|
|
<div class="stat-value" id="hb-model">-</div>
|
|
<div class="stat-label">Model</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="hb-log-wrap">
|
|
<div id="heartbeat-log" class="hb-log">
|
|
<div class="hb-log-placeholder">Waiting for heartbeat...</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Chat History -->
|
|
<div class="card mc-card-spaced">
|
|
<div class="card-header">
|
|
<h2 class="card-title">Chat History</h2>
|
|
<div>
|
|
<button class="btn btn-sm" onclick="loadChatHistory()">Refresh</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div id="chat-history" class="chat-history-container">
|
|
<p class="chat-history-placeholder">Loading chat history...</p>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
// Load sovereignty status
|
|
async function loadSovereignty() {
|
|
try {
|
|
const response = await fetch('/health/sovereignty');
|
|
const data = await response.json();
|
|
|
|
// Update score
|
|
var scoreEl = document.getElementById('sov-score');
|
|
scoreEl.textContent = data.overall_score.toFixed(1);
|
|
scoreEl.style.color = data.overall_score >= 9 ? 'var(--success)' :
|
|
data.overall_score >= 7 ? 'var(--warning)' : 'var(--danger)';
|
|
var barEl = document.getElementById('sov-bar');
|
|
barEl.style.width = (data.overall_score * 10) + '%';
|
|
barEl.style.background = data.overall_score >= 9 ? 'var(--success)' :
|
|
data.overall_score >= 7 ? 'var(--warning)' : 'var(--danger)';
|
|
|
|
// Update label
|
|
let label = 'Poor';
|
|
if (data.overall_score >= 9) label = 'Excellent';
|
|
else if (data.overall_score >= 8) label = 'Good';
|
|
else if (data.overall_score >= 6) label = 'Fair';
|
|
document.getElementById('sov-label').textContent = label + ' \u2014 ' + data.dependencies.length + ' dependencies checked';
|
|
|
|
// Update system status
|
|
const systemStatus = document.getElementById('system-status');
|
|
if (data.overall_score >= 9) {
|
|
systemStatus.textContent = 'Sovereign';
|
|
systemStatus.className = 'badge badge-success';
|
|
} else if (data.overall_score >= 7) {
|
|
systemStatus.textContent = 'Operational';
|
|
systemStatus.className = 'badge badge-warning';
|
|
} else {
|
|
systemStatus.textContent = 'Degraded';
|
|
systemStatus.className = 'badge badge-danger';
|
|
}
|
|
|
|
// Update dependency grid
|
|
const grid = document.getElementById('dependency-grid');
|
|
grid.innerHTML = '';
|
|
data.dependencies.forEach(function(dep) {
|
|
var card = document.createElement('div');
|
|
card.className = 'card';
|
|
card.style.padding = '12px';
|
|
|
|
var statusColor = dep.status === 'healthy' ? 'var(--success)' :
|
|
dep.status === 'degraded' ? 'var(--warning)' : 'var(--danger)';
|
|
var scoreColor = dep.sovereignty_score >= 9 ? 'var(--success)' :
|
|
dep.sovereignty_score >= 7 ? 'var(--warning)' : 'var(--danger)';
|
|
|
|
var header = document.createElement('div');
|
|
header.className = 'dep-card-header';
|
|
|
|
var name = document.createElement('strong');
|
|
name.textContent = dep.name;
|
|
|
|
var badge = document.createElement('span');
|
|
badge.className = 'badge';
|
|
badge.style.background = statusColor;
|
|
badge.textContent = dep.status;
|
|
|
|
header.appendChild(name);
|
|
header.appendChild(badge);
|
|
|
|
var details = document.createElement('div');
|
|
details.className = 'dep-card-details';
|
|
details.textContent = dep.details.error || dep.details.note || 'Operating normally';
|
|
|
|
var score = document.createElement('div');
|
|
score.className = 'dep-card-score';
|
|
score.style.color = scoreColor;
|
|
score.textContent = 'Sovereignty: ' + dep.sovereignty_score + '/10';
|
|
|
|
card.appendChild(header);
|
|
card.appendChild(details);
|
|
card.appendChild(score);
|
|
grid.appendChild(card);
|
|
});
|
|
|
|
// Update recommendations securely
|
|
const recs = document.getElementById('recommendations');
|
|
recs.innerHTML = '';
|
|
if (data.recommendations && data.recommendations.length > 0) {
|
|
var ul = document.createElement('ul');
|
|
data.recommendations.forEach(function(r) {
|
|
var li = document.createElement('li');
|
|
li.textContent = r;
|
|
ul.appendChild(li);
|
|
});
|
|
recs.appendChild(ul);
|
|
} else {
|
|
var p = document.createElement('p');
|
|
p.className = 'chat-history-placeholder';
|
|
p.textContent = 'No recommendations \u2014 system optimal';
|
|
recs.appendChild(p);
|
|
}
|
|
|
|
} catch (error) {
|
|
console.error('Failed to load sovereignty:', error);
|
|
document.getElementById('system-status').textContent = 'Error';
|
|
document.getElementById('system-status').className = 'badge badge-danger';
|
|
}
|
|
}
|
|
|
|
// Load basic health
|
|
async function loadHealth() {
|
|
try {
|
|
const response = await fetch('/health');
|
|
const data = await response.json();
|
|
|
|
// Format uptime
|
|
var uptime = data.uptime_seconds;
|
|
var uptimeStr;
|
|
if (uptime < 60) uptimeStr = Math.floor(uptime) + 's';
|
|
else if (uptime < 3600) uptimeStr = Math.floor(uptime / 60) + 'm';
|
|
else uptimeStr = Math.floor(uptime / 3600) + 'h ' + Math.floor((uptime % 3600) / 60) + 'm';
|
|
|
|
document.getElementById('metric-uptime').textContent = uptimeStr;
|
|
|
|
if (data.llm_backend) {
|
|
document.getElementById('hb-backend').textContent = data.llm_backend;
|
|
}
|
|
if (data.llm_model) {
|
|
document.getElementById('hb-model').textContent = data.llm_model;
|
|
}
|
|
|
|
} catch (error) {
|
|
console.error('Failed to load health:', error);
|
|
}
|
|
}
|
|
|
|
// Load swarm stats
|
|
async function loadSwarmStats() {
|
|
try {
|
|
const response = await fetch('/swarm');
|
|
const data = await response.json();
|
|
|
|
var agentCount = data.agents || 0;
|
|
if (agentCount === 0) {
|
|
try {
|
|
var agentResp = await fetch('/swarm/agents');
|
|
var agentData = await agentResp.json();
|
|
if (Array.isArray(agentData.agents)) {
|
|
agentCount = agentData.agents.length;
|
|
}
|
|
} catch (e) { /* ignore fallback failure */ }
|
|
}
|
|
document.getElementById('metric-agents').textContent = agentCount;
|
|
document.getElementById('metric-tasks').textContent =
|
|
(data.tasks_pending || 0) + (data.tasks_running || 0);
|
|
|
|
} catch (error) {
|
|
console.error('Failed to load swarm stats:', error);
|
|
}
|
|
}
|
|
|
|
// Load Lightning stats
|
|
async function loadLightningStats() {
|
|
try {
|
|
const response = await fetch('/serve/status');
|
|
const data = await response.json();
|
|
document.getElementById('metric-earned').textContent = data.total_earned_sats || 0;
|
|
} catch (error) {
|
|
document.getElementById('metric-earned').textContent = '0';
|
|
}
|
|
}
|
|
|
|
// Heartbeat simulation
|
|
var tickCount = 0;
|
|
function updateHeartbeat() {
|
|
tickCount++;
|
|
var now = new Date().toLocaleTimeString();
|
|
document.getElementById('hb-tick').textContent = now;
|
|
document.getElementById('heartbeat-status').textContent = 'Active';
|
|
document.getElementById('heartbeat-status').className = 'badge badge-success';
|
|
|
|
var log = document.getElementById('heartbeat-log');
|
|
var entry = document.createElement('div');
|
|
entry.className = 'hb-entry';
|
|
var ts = document.createElement('span');
|
|
ts.className = 'hb-entry-ts';
|
|
ts.textContent = '[' + now + ']';
|
|
var check = document.createElement('span');
|
|
check.className = 'hb-entry-ok';
|
|
check.textContent = ' \u2713 ';
|
|
entry.appendChild(ts);
|
|
entry.appendChild(check);
|
|
entry.appendChild(document.createTextNode('Tick ' + tickCount));
|
|
log.appendChild(entry);
|
|
log.scrollTop = log.scrollHeight;
|
|
|
|
while (log.children.length > 50) {
|
|
log.removeChild(log.firstChild);
|
|
}
|
|
}
|
|
|
|
// Load chat history
|
|
async function loadChatHistory() {
|
|
var container = document.getElementById('chat-history');
|
|
container.innerHTML = '';
|
|
var loadingP = document.createElement('p');
|
|
loadingP.className = 'chat-history-placeholder';
|
|
loadingP.textContent = 'Loading...';
|
|
container.appendChild(loadingP);
|
|
|
|
try {
|
|
var response = await fetch('/dashboard/messages');
|
|
var messages = await response.json();
|
|
|
|
if (messages.length === 0) {
|
|
container.innerHTML = '';
|
|
var emptyP = document.createElement('p');
|
|
emptyP.className = 'chat-history-placeholder';
|
|
emptyP.textContent = 'No messages yet';
|
|
container.appendChild(emptyP);
|
|
return;
|
|
}
|
|
|
|
container.innerHTML = '';
|
|
messages.slice(-20).forEach(function(msg) {
|
|
var div = document.createElement('div');
|
|
div.className = msg.role === 'user' ? 'chat-history-msg chat-history-msg--user' : 'chat-history-msg';
|
|
|
|
var role = document.createElement('strong');
|
|
role.textContent = msg.role === 'user' ? 'You: ' : 'Timmy: ';
|
|
role.style.color = msg.role === 'user' ? 'var(--accent)' : 'var(--success)';
|
|
|
|
var content = document.createElement('span');
|
|
content.textContent = msg.content;
|
|
|
|
div.appendChild(role);
|
|
div.appendChild(content);
|
|
container.appendChild(div);
|
|
});
|
|
|
|
} catch (error) {
|
|
container.innerHTML = '';
|
|
var fallback = document.createElement('div');
|
|
fallback.className = 'chat-history-fallback';
|
|
var p1 = document.createElement('p');
|
|
p1.textContent = 'Chat history persistence coming soon';
|
|
var p2 = document.createElement('p');
|
|
p2.className = 'chat-history-fallback-sub';
|
|
p2.textContent = 'Messages are currently in-memory only';
|
|
fallback.appendChild(p1);
|
|
fallback.appendChild(p2);
|
|
container.appendChild(fallback);
|
|
}
|
|
}
|
|
|
|
// Load Grok stats
|
|
async function loadGrokStats() {
|
|
try {
|
|
var response = await fetch('/grok/status');
|
|
var data = await response.json();
|
|
|
|
if (data.stats) {
|
|
document.getElementById('grok-requests').textContent = data.stats.total_requests || 0;
|
|
document.getElementById('grok-tokens').textContent =
|
|
(data.stats.total_prompt_tokens || 0) + (data.stats.total_completion_tokens || 0);
|
|
document.getElementById('grok-cost').textContent = data.stats.estimated_cost_sats || 0;
|
|
}
|
|
|
|
var badge = document.getElementById('grok-badge');
|
|
if (data.active) {
|
|
badge.textContent = 'ACTIVE';
|
|
badge.className = 'badge grok-badge-active';
|
|
} else {
|
|
badge.textContent = 'STANDBY';
|
|
badge.className = 'badge grok-badge-standby';
|
|
}
|
|
} catch (error) {
|
|
// Grok endpoint may not respond — silent fallback
|
|
}
|
|
}
|
|
|
|
// Initial load
|
|
loadSovereignty();
|
|
loadHealth();
|
|
loadSwarmStats();
|
|
loadLightningStats();
|
|
loadGrokStats();
|
|
loadChatHistory();
|
|
|
|
// Periodic updates
|
|
setInterval(loadSovereignty, 30000);
|
|
setInterval(loadHealth, 10000);
|
|
setInterval(loadSwarmStats, 5000);
|
|
setInterval(updateHeartbeat, 5000);
|
|
setInterval(loadGrokStats, 10000);
|
|
</script>
|
|
{% endblock %}
|