This repository has been archived on 2026-03-24. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
Timmy-time-dashboard/src/dashboard/templates/mission_control.html
Claude 17059bc0ea feat: add Grok (xAI) as opt-in premium backend with monetization
- Add GrokBackend class in src/timmy/backends.py with full sync/async
  support, health checks, usage stats, and cost estimation in sats
- Add consult_grok tool to Timmy's toolkit for proactive Grok queries
- Extend cascade router with Grok provider type for failover chain
- Add Grok Mode toggle card to Mission Control dashboard (HTMX live)
- Add "Ask Grok" button on chat input for direct Grok queries
- Add /grok/* routes: status, toggle, chat, stats endpoints
- Integrate Lightning invoice generation for Grok usage monetization
- Add GROK_ENABLED, XAI_API_KEY, GROK_DEFAULT_MODEL, GROK_MAX_SATS_PER_QUERY,
  GROK_FREE config settings via pydantic-settings
- Update .env.example and docker-compose.yml with Grok env vars
- Add 21 tests covering backend, tools, and route endpoints (all green)

Local-first ethos preserved: Grok is premium augmentation only,
disabled by default, and Lightning-payable when enabled.

https://claude.ai/code/session_01FygwN8wS8J6WGZ8FPb7XGV
2026-02-27 01:12:51 +00:00

416 lines
16 KiB
HTML

{% extends "base.html" %}
{% block title %}Mission Control — Timmy Time{% endblock %}
{% block content %}
<div class="card">
<div class="card-header">
<h2 class="card-title">🎛️ Mission Control</h2>
<div>
<span class="badge badge-success" id="system-status">Loading...</span>
</div>
</div>
<!-- Sovereignty Score -->
<div style="margin-bottom: 24px;">
<div style="display: flex; align-items: center; gap: 16px; margin-bottom: 12px;">
<div style="font-size: 3rem; font-weight: 700;" id="sov-score">-</div>
<div>
<div style="font-weight: 600;">Sovereignty Score</div>
<div style="font-size: 0.875rem; color: var(--text-muted);" id="sov-label">Calculating...</div>
</div>
</div>
<div style="background: var(--bg-tertiary); height: 8px; border-radius: 4px; overflow: hidden;">
<div id="sov-bar" style="background: var(--success); height: 100%; width: 0%; transition: width 0.5s;"></div>
</div>
</div>
<!-- Dependency Grid -->
<h3 style="margin-bottom: 12px;">Dependencies</h3>
<div class="grid grid-2" id="dependency-grid" style="margin-bottom: 24px;">
<p style="color: var(--text-muted);">Loading...</p>
</div>
<!-- Recommendations -->
<h3 style="margin-bottom: 12px;">Recommendations</h3>
<div id="recommendations" style="margin-bottom: 24px;">
<p style="color: var(--text-muted);">Loading...</p>
</div>
<!-- System Metrics -->
<h3 style="margin-bottom: 12px;">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" style="margin-top: 24px;">
<div class="card-header">
<h2 class="card-title">Grok Mode</h2>
<div>
<span class="badge" id="grok-badge" style="background: #666;">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 style="border: 2px solid #666; border-radius: 12px; padding: 16px;
background: var(--bg-secondary);">
<div style="display: flex; justify-content: space-between; align-items: center;">
<div>
<div style="font-weight: 700; font-size: 1.1rem; color: #666;">
GROK MODE: LOADING...
</div>
<div style="font-size: 0.8rem; color: var(--text-muted); margin-top: 4px;">
xAI frontier reasoning augmentation
</div>
</div>
<button hx-post="/grok/toggle"
hx-target="#grok-toggle-card"
hx-swap="outerHTML"
style="background: #666; color: #000; border: none;
border-radius: 8px; padding: 8px 20px; cursor: pointer;
font-weight: 700; font-family: inherit;">
ACTIVATE
</button>
</div>
</div>
</div>
<div class="grid grid-3" style="margin-top: 12px;">
<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" style="margin-top: 24px;">
<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 style="margin-top: 16px;">
<div id="heartbeat-log" style="height: 100px; overflow-y: auto; background: var(--bg-tertiary); padding: 12px; border-radius: 8px; font-family: monospace; font-size: 0.75rem;">
<div style="color: var(--text-muted);">Waiting for heartbeat...</div>
</div>
</div>
</div>
<!-- Chat History -->
<div class="card" style="margin-top: 24px;">
<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" style="max-height: 300px; overflow-y: auto;">
<p style="color: var(--text-muted);">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
document.getElementById('sov-score').textContent = data.overall_score.toFixed(1);
document.getElementById('sov-score').style.color = data.overall_score >= 9 ? 'var(--success)' :
data.overall_score >= 7 ? 'var(--warning)' : 'var(--danger)';
document.getElementById('sov-bar').style.width = (data.overall_score * 10) + '%';
document.getElementById('sov-bar').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}${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(dep => {
const card = document.createElement('div');
card.className = 'card';
card.style.padding = '12px';
const statusColor = dep.status === 'healthy' ? 'var(--success)' :
dep.status === 'degraded' ? 'var(--warning)' : 'var(--danger)';
const scoreColor = dep.sovereignty_score >= 9 ? 'var(--success)' :
dep.sovereignty_score >= 7 ? 'var(--warning)' : 'var(--danger)';
card.innerHTML = `
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px;">
<strong>${dep.name}</strong>
<span class="badge" style="background: ${statusColor};">${dep.status}</span>
</div>
<div style="font-size: 0.875rem; color: var(--text-muted); margin-bottom: 8px;">
${dep.details.error || dep.details.note || 'Operating normally'}
</div>
<div style="font-size: 0.75rem; color: ${scoreColor};">
Sovereignty: ${dep.sovereignty_score}/10
</div>
`;
grid.appendChild(card);
});
// Update recommendations
const recs = document.getElementById('recommendations');
if (data.recommendations && data.recommendations.length > 0) {
recs.innerHTML = '<ul>' + data.recommendations.map(r => `<li>${r}</li>`).join('') + '</ul>';
} else {
recs.innerHTML = '<p style="color: var(--text-muted);">No recommendations — system optimal</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
const uptime = data.uptime_seconds;
let 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;
// LLM backend and model from /health response
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;
// Fallback: if /swarm returns 0, try /swarm/agents for a direct count
if (agentCount === 0) {
try {
const agentResp = await fetch('/swarm/agents');
const 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) {
// /serve may not be running — default to 0 instead of '-'
document.getElementById('metric-earned').textContent = '0';
}
}
// Heartbeat simulation
let tickCount = 0;
function updateHeartbeat() {
tickCount++;
const now = new Date().toLocaleTimeString();
document.getElementById('hb-tick').textContent = now;
document.getElementById('heartbeat-status').textContent = 'Active';
document.getElementById('heartbeat-status').className = 'badge badge-success';
const log = document.getElementById('heartbeat-log');
const entry = document.createElement('div');
entry.style.marginBottom = '2px';
entry.innerHTML = `<span style="color: var(--text-muted);">[${now}]</span> <span style="color: var(--success);">✓</span> Tick ${tickCount}`;
log.appendChild(entry);
log.scrollTop = log.scrollHeight;
// Keep only last 50 entries
while (log.children.length > 50) {
log.removeChild(log.firstChild);
}
}
// Load chat history
async function loadChatHistory() {
const container = document.getElementById('chat-history');
container.innerHTML = '<p style="color: var(--text-muted);">Loading...</p>';
try {
// Try to load from the message log endpoint if available
const response = await fetch('/dashboard/messages');
const messages = await response.json();
if (messages.length === 0) {
container.innerHTML = '<p style="color: var(--text-muted);">No messages yet</p>';
return;
}
container.innerHTML = '';
messages.slice(-20).forEach(msg => {
const div = document.createElement('div');
div.style.marginBottom = '12px';
div.style.padding = '8px';
div.style.background = msg.role === 'user' ? 'var(--bg-tertiary)' : 'transparent';
div.style.borderRadius = '4px';
const role = document.createElement('strong');
role.textContent = msg.role === 'user' ? 'You: ' : 'Timmy: ';
role.style.color = msg.role === 'user' ? 'var(--accent)' : 'var(--success)';
const content = document.createElement('span');
content.textContent = msg.content;
div.appendChild(role);
div.appendChild(content);
container.appendChild(div);
});
} catch (error) {
// Fallback: show placeholder
container.innerHTML = `
<div style="color: var(--text-muted); text-align: center; padding: 20px;">
<p>Chat history persistence coming soon</p>
<p style="font-size: 0.875rem;">Messages are currently in-memory only</p>
</div>
`;
}
}
// Load Grok stats
async function loadGrokStats() {
try {
const response = await fetch('/grok/status');
const 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;
}
const badge = document.getElementById('grok-badge');
if (data.active) {
badge.textContent = 'ACTIVE';
badge.style.background = '#00ff88';
badge.style.color = '#000';
} else {
badge.textContent = 'STANDBY';
badge.style.background = '#666';
badge.style.color = '#fff';
}
} catch (error) {
// Grok endpoint may not respond — silent fallback
}
}
// Initial load
loadSovereignty();
loadHealth();
loadSwarmStats();
loadLightningStats();
loadGrokStats();
loadChatHistory();
// Periodic updates
setInterval(loadSovereignty, 30000); // Every 30s
setInterval(loadHealth, 10000); // Every 10s
setInterval(loadSwarmStats, 5000); // Every 5s
setInterval(updateHeartbeat, 5000); // Heartbeat every 5s
setInterval(loadGrokStats, 10000); // Grok stats every 10s
</script>
{% endblock %}