forked from Rockachopa/Timmy-time-dashboard
366 lines
12 KiB
HTML
366 lines
12 KiB
HTML
{% extends "base.html" %}
|
|
|
|
{% block title %}{{ page_title }}{% endblock %}
|
|
|
|
{% block extra_styles %}{% 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) {}
|
|
}
|
|
|
|
// Build DOM safely using createElement and textContent
|
|
var iconDiv = document.createElement('div');
|
|
iconDiv.className = 'activity-icon';
|
|
iconDiv.textContent = icon;
|
|
|
|
var contentDiv = document.createElement('div');
|
|
contentDiv.className = 'activity-content';
|
|
|
|
var labelDiv = document.createElement('div');
|
|
labelDiv.className = 'activity-label';
|
|
labelDiv.textContent = label;
|
|
contentDiv.appendChild(labelDiv);
|
|
|
|
if (desc) {
|
|
var descDiv = document.createElement('div');
|
|
descDiv.className = 'activity-desc';
|
|
descDiv.textContent = desc;
|
|
contentDiv.appendChild(descDiv);
|
|
}
|
|
|
|
var metaDiv = document.createElement('div');
|
|
metaDiv.className = 'activity-meta';
|
|
|
|
var timeSpan = document.createElement('span');
|
|
timeSpan.className = 'activity-time';
|
|
timeSpan.textContent = time;
|
|
|
|
var sourceSpan = document.createElement('span');
|
|
sourceSpan.className = 'activity-source';
|
|
sourceSpan.textContent = evt.source || 'system';
|
|
|
|
metaDiv.appendChild(timeSpan);
|
|
metaDiv.appendChild(sourceSpan);
|
|
contentDiv.appendChild(metaDiv);
|
|
|
|
item.appendChild(iconDiv);
|
|
item.appendChild(contentDiv);
|
|
|
|
// 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 %}
|