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/swarm_live.html
Alexander Payne 5f9bbb8435 feat: add task queue with human-in-the-loop approval + work orders + UI bug fixes
Task Queue system:
- New /tasks page with three-column layout (Pending/Active/Completed)
- Full CRUD API at /api/tasks with approve/veto/modify/pause/cancel/retry
- SQLite persistence in task_queue table
- WebSocket live updates via ws_manager
- Create task modal with agent assignment and priority
- Auto-approve rules for low-risk tasks
- HTMX polling for real-time column updates
- HOME TASK buttons now link to task queue with agent pre-selected
- MARKET HIRE buttons link to task queue with agent pre-selected

Work Order system:
- External submission API for agents/users (POST /work-orders/submit)
- Risk scoring and configurable auto-execution thresholds
- Dashboard at /work-orders/queue with approve/reject/execute flow
- Integration with swarm task system for execution

UI & Dashboard bug fixes:
- EVENTS: add startup event so page is never empty
- LEDGER: fix empty filter params in URL
- MISSION CONTROL: LLM backend and model now read from /health
- MISSION CONTROL: agent count fallback to /swarm/agents
- SWARM: HTMX fallback loads initial data if WebSocket is slow
- MEMORY: add edit/delete buttons for personal facts
- UPGRADES: add empty state guidance with links
- BRIEFING: add regenerate button and POST /briefing/regenerate endpoint

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-26 10:27:08 -05:00

457 lines
14 KiB
HTML

{% extends "base.html" %}
{% block title %}{{ page_title }}{% endblock %}
{% block extra_styles %}
<style>
.swarm-container {
max-width: 1200px;
margin: 0 auto;
}
.swarm-header-row {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16px;
}
.swarm-title {
font-size: 1.3rem;
font-weight: 700;
color: var(--text-bright);
letter-spacing: 0.08em;
}
.swarm-log-box {
height: 200px;
overflow-y: auto;
-webkit-overflow-scrolling: touch;
background: rgba(24, 10, 45, 0.6);
padding: 12px;
border-radius: var(--radius-md);
border: 1px solid var(--border);
font-family: var(--font);
font-size: 12px;
}
@media (max-width: 768px) {
.swarm-title { font-size: 1rem; }
.swarm-log-box { height: 160px; font-size: 11px; }
}
/* Activity Feed Styles */
.activity-feed-panel {
margin-bottom: 16px;
}
.activity-feed {
max-height: 300px;
overflow-y: auto;
background: rgba(24, 10, 45, 0.6);
padding: 12px;
border-radius: var(--radius-md);
border: 1px solid var(--border);
}
.activity-item {
display: flex;
align-items: flex-start;
gap: 10px;
padding: 8px 0;
border-bottom: 1px solid rgba(255,255,255,0.05);
animation: fadeIn 0.3s ease;
}
.activity-item:last-child {
border-bottom: none;
}
@keyframes fadeIn {
from { opacity: 0; transform: translateY(-5px); }
to { opacity: 1; transform: translateY(0); }
}
.activity-icon {
font-size: 16px;
flex-shrink: 0;
width: 24px;
text-align: center;
}
.activity-content {
flex: 1;
min-width: 0;
}
.activity-label {
font-weight: 600;
color: var(--text-bright);
font-size: 12px;
}
.activity-desc {
color: var(--text-dim);
font-size: 11px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.activity-meta {
display: flex;
gap: 8px;
font-size: 10px;
color: var(--text-dim);
margin-top: 2px;
}
.activity-time {
font-family: var(--font);
color: var(--amber);
}
.activity-source {
opacity: 0.7;
}
.activity-empty {
color: var(--text-dim);
font-size: 12px;
text-align: center;
padding: 20px;
}
.activity-badge {
display: inline-block;
width: 8px;
height: 8px;
background: #28a745;
border-radius: 50%;
margin-left: 8px;
animation: pulse 2s infinite;
}
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.5; }
}
</style>
{% endblock %}
{% block content %}
<div class="swarm-container py-3">
<div class="swarm-header-row">
<div class="swarm-title">LIVE SWARM</div>
<span class="badge badge-success" id="connection-status">Connecting...</span>
</div>
<div class="grid grid-3" style="margin-bottom: 16px;">
<div class="stat">
<div class="stat-value" id="stat-agents">-</div>
<div class="stat-label">Total Agents</div>
</div>
<div class="stat">
<div class="stat-value" id="stat-active">-</div>
<div class="stat-label">Active</div>
</div>
<div class="stat">
<div class="stat-value" id="stat-tasks">-</div>
<div class="stat-label">Active Tasks</div>
</div>
</div>
<div class="grid grid-2" style="margin-bottom: 16px;">
<div class="card mc-panel">
<div class="card-header mc-panel-header">// AGENTS</div>
<div class="card-body" id="agents-list">
<p style="color: var(--text-dim); font-size: 12px;">Loading agents...</p>
</div>
</div>
<div class="card mc-panel">
<div class="card-header mc-panel-header">// ACTIVE AUCTIONS</div>
<div class="card-body" id="auctions-list">
<p style="color: var(--text-dim); font-size: 12px;">Loading auctions...</p>
</div>
</div>
</div>
<!-- Activity Feed Panel -->
<div class="card mc-panel activity-feed-panel">
<div class="card-header mc-panel-header">
// LIVE ACTIVITY FEED
<span class="activity-badge" id="activity-badge"></span>
</div>
<div class="card-body p-0">
<div class="activity-feed" id="activity-feed">
<div class="activity-empty">Waiting for events...</div>
</div>
</div>
</div>
<div class="card mc-panel">
<div class="card-header mc-panel-header">// SWARM LOG</div>
<div class="card-body p-0">
<div class="swarm-log-box" id="swarm-log">
<div style="color: var(--text-dim);">Waiting for updates...</div>
</div>
</div>
</div>
</div>
<script>
let ws = null;
let reconnectInterval = 1000;
const maxReconnectInterval = 30000;
function connect() {
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
ws = new WebSocket(protocol + '//' + window.location.host + '/swarm/live');
ws.onopen = function() {
console.log('WebSocket connected');
document.getElementById('connection-status').textContent = 'Live';
document.getElementById('connection-status').className = 'badge badge-success';
reconnectInterval = 1000;
addLog('Connected to swarm', 'success');
};
ws.onmessage = function(event) {
var message = JSON.parse(event.data);
handleMessage(message);
};
ws.onclose = function() {
console.log('WebSocket disconnected');
document.getElementById('connection-status').textContent = 'Reconnecting...';
document.getElementById('connection-status').className = 'badge badge-warning';
addLog('Disconnected, reconnecting...', 'warning');
setTimeout(connect, reconnectInterval);
reconnectInterval = Math.min(reconnectInterval * 2, maxReconnectInterval);
};
ws.onerror = function(error) {
console.error('WebSocket error:', error);
addLog('Connection error', 'error');
};
}
function handleMessage(message) {
// Handle activity feed events (from event_log broadcaster)
if (message.type === 'event' && message.payload) {
addActivityEvent(message.payload);
// Also add to log
var evt = message.payload;
var logMsg = evt.event_type + ': ' + (evt.source || '');
addLog(logMsg, 'info');
return;
}
if (message.type === 'initial_state' || message.type === 'state_update') {
var data = message.data;
document.getElementById('stat-agents').textContent = data.agents.total;
document.getElementById('stat-active').textContent = data.agents.active;
document.getElementById('stat-tasks').textContent = data.tasks.active;
updateAgentsList(data.agents.list);
updateAuctionsList(data.auctions.list);
return;
}
var evt = message.event || message.type || '';
var data = message.data || message;
if (evt === 'agent_joined') {
addLog('Agent joined: ' + (data.name || data.agent_id || ''), 'success');
refreshStats();
} else if (evt === 'agent_left') {
addLog('Agent left: ' + (data.name || data.agent_id || ''), 'warning');
refreshStats();
} else if (evt === 'task_posted') {
addLog('Task posted: ' + (data.description || data.task_id || '').slice(0, 60), 'info');
refreshStats();
} else if (evt === 'bid_submitted') {
addLog('Bid: ' + (data.agent_id || '').slice(0, 8) + ' bid ' + (data.bid_sats || '?') + ' sats', 'info');
} else if (evt === 'task_assigned') {
addLog('Task assigned to ' + (data.agent_id || '').slice(0, 8), 'success');
refreshStats();
} else if (evt === 'task_completed') {
addLog('Task completed by ' + (data.agent_id || '').slice(0, 8), 'success');
refreshStats();
}
}
// Activity Feed Functions
const EVENT_ICONS = {
'task.created': '📝',
'task.bidding': '⏳',
'task.assigned': '👤',
'task.started': '▶️',
'task.completed': '✅',
'task.failed': '❌',
'agent.joined': '🟢',
'agent.left': '🔴',
'bid.submitted': '💰',
'auction.closed': '🏁',
'tool.called': '🔧',
'system.error': '⚠️',
};
const EVENT_LABELS = {
'task.created': 'New task',
'task.assigned': 'Task assigned',
'task.completed': 'Task completed',
'task.failed': 'Task failed',
'agent.joined': 'Agent joined',
'agent.left': 'Agent left',
'bid.submitted': 'Bid submitted',
};
function addActivityEvent(evt) {
var container = document.getElementById('activity-feed');
// Remove empty message if present
var empty = container.querySelector('.activity-empty');
if (empty) empty.remove();
// Create activity item
var item = document.createElement('div');
item.className = 'activity-item';
var icon = EVENT_ICONS[evt.event_type] || '•';
var label = EVENT_LABELS[evt.event_type] || evt.event_type;
var time = evt.timestamp ? evt.timestamp.split('T')[1].slice(0, 8) : '--:--:--';
// Build description from data
var desc = '';
if (evt.data) {
try {
var data = typeof evt.data === 'string' ? JSON.parse(evt.data) : evt.data;
if (data.description) desc = data.description.slice(0, 50);
else if (data.reason) desc = data.reason.slice(0, 50);
} catch(e) {}
}
item.innerHTML = `
<div class="activity-icon">${icon}</div>
<div class="activity-content">
<div class="activity-label">${label}</div>
${desc ? `<div class="activity-desc">${desc}</div>` : ''}
<div class="activity-meta">
<span class="activity-time">${time}</span>
<span class="activity-source">${evt.source || 'system'}</span>
</div>
</div>
`;
// Add to top
container.insertBefore(item, container.firstChild);
// Keep only last 50 items
while (container.children.length > 50) {
container.removeChild(container.lastChild);
}
// Update badge
var badge = document.getElementById('activity-badge');
if (badge) {
badge.style.background = '#28a745';
setTimeout(() => {
badge.style.background = '';
}, 500);
}
}
function refreshStats() {
fetch('/swarm').then(function(r) { return r.json(); }).then(function(data) {
document.getElementById('stat-agents').textContent = data.agents || 0;
document.getElementById('stat-active').textContent = data.agents_busy || 0;
document.getElementById('stat-tasks').textContent = (data.tasks_pending || 0) + (data.tasks_running || 0);
}).catch(function() {});
}
function _t(el, text) { el.textContent = text; return el; }
function _el(tag, cls) { var e = document.createElement(tag); if (cls) e.className = cls; return e; }
function updateAgentsList(agents) {
var container = document.getElementById('agents-list');
container.innerHTML = '';
if (!agents || agents.length === 0) {
var p = _el('p'); p.style.color = 'var(--text-dim)';
_t(p, 'No agents registered');
container.appendChild(p);
return;
}
agents.forEach(function(agent) {
var card = _el('div', 'agent-card');
var avatar = _el('div', 'agent-avatar');
_t(avatar, (agent.name || '?').charAt(0).toUpperCase());
var info = _el('div', 'agent-info');
var name = _el('div', 'agent-name');
_t(name, agent.name || '');
var desc = _el('div', 'agent-meta');
_t(desc, agent.description || 'No description');
var meta = _el('div', 'agent-meta');
var badge = _el('span', 'badge badge-' + (agent.status === 'active' ? 'success' : agent.status === 'busy' ? 'warning' : 'danger'));
_t(badge, agent.status || '');
var stats = _el('span');
stats.style.marginLeft = '6px';
_t(stats, (agent.min_bid || 0) + ' sats min | ' + (agent.tasks_completed || 0) + ' tasks | ' + (agent.total_earned || 0) + ' earned');
meta.appendChild(badge);
meta.appendChild(stats);
info.appendChild(name);
info.appendChild(desc);
info.appendChild(meta);
card.appendChild(avatar);
card.appendChild(info);
container.appendChild(card);
});
}
function updateAuctionsList(auctions) {
var container = document.getElementById('auctions-list');
container.innerHTML = '';
if (!auctions || auctions.length === 0) {
var p = _el('p'); p.style.color = 'var(--text-dim)';
_t(p, 'No active auctions');
container.appendChild(p);
return;
}
auctions.forEach(function(auction) {
var card = _el('div', 'agent-card');
var info = _el('div', 'agent-info');
var name = _el('div', 'agent-name');
_t(name, 'Task ' + String(auction.task_id || '').slice(0, 8));
var meta = _el('div', 'agent-meta');
_t(meta, Math.round(auction.time_remaining || 0) + 's remaining | ' + (auction.bid_count || 0) + ' bids');
info.appendChild(name);
info.appendChild(meta);
card.appendChild(info);
container.appendChild(card);
});
}
function addLog(message, type) {
type = type || 'info';
var log = document.getElementById('swarm-log');
var timestamp = new Date().toLocaleTimeString();
var color = type === 'error' ? 'var(--red)' : type === 'warning' ? 'var(--amber)' : type === 'success' ? 'var(--green)' : 'var(--text-dim)';
var entry = document.createElement('div');
entry.style.marginBottom = '4px';
var tsSpan = _el('span');
tsSpan.style.color = 'var(--text-dim)';
_t(tsSpan, '[' + timestamp + '] ');
var msgSpan = _el('span');
msgSpan.style.color = color;
_t(msgSpan, message);
entry.appendChild(tsSpan);
entry.appendChild(msgSpan);
log.appendChild(entry);
log.scrollTop = log.scrollHeight;
}
connect();
// HTMX fallback: load initial data via REST if WebSocket is slow
setTimeout(function() {
if (document.getElementById('stat-agents').textContent === '-') {
refreshStats();
fetch('/swarm/agents').then(function(r) { return r.json(); }).then(function(data) {
if (data.agents && data.agents.length > 0) {
updateAgentsList(data.agents);
}
}).catch(function() {});
}
}, 2000);
</script>
{% endblock %}