feat(ui): wire WebSocket live feed into HTMX dashboard

- Fix swarm_live.html WebSocket URL from /swarm/ws to /swarm/live
  (matching the actual endpoint in swarm_ws.py)
- Update handleMessage() to process individual swarm events
  (agent_joined, task_posted, bid_submitted, task_assigned, etc.)
  in addition to bulk state snapshots
- Add refreshStats() helper that fetches /swarm REST endpoint to
  update stat counters after each event
- Add GET /swarm/live page route to render the swarm_live.html template
- Add SWARM and MOBILE navigation links to base.html header
  (fixes UX-01: /mobile route not in desktop nav)
This commit is contained in:
Manus AI
2026-02-21 13:43:42 -05:00
parent ee45a16267
commit ccfe2717ed
3 changed files with 46 additions and 7 deletions

View File

@@ -24,6 +24,15 @@ async def swarm_status():
return coordinator.status()
@router.get("/live", response_class=HTMLResponse)
async def swarm_live_page(request: Request):
"""Render the live swarm dashboard page."""
return templates.TemplateResponse(
"swarm_live.html",
{"request": request, "page_title": "Swarm Live"},
)
@router.get("/agents")
async def list_swarm_agents():
"""List all registered swarm agents."""

View File

@@ -21,6 +21,8 @@
<span class="mc-subtitle">MISSION CONTROL</span>
</div>
<div class="mc-header-right">
<a href="/swarm/live" class="mc-test-link">SWARM</a>
<a href="/mobile" class="mc-test-link">MOBILE</a>
<a href="/mobile-test" class="mc-test-link">TEST</a>
<span class="mc-time" id="clock"></span>
</div>

View File

@@ -56,7 +56,7 @@ const maxReconnectInterval = 30000;
function connect() {
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
ws = new WebSocket(`${protocol}//${window.location.host}/swarm/ws`);
ws = new WebSocket(`${protocol}//${window.location.host}/swarm/live`);
ws.onopen = function() {
console.log('WebSocket connected');
@@ -88,20 +88,48 @@ function connect() {
}
function handleMessage(message) {
// Handle structured state snapshots (initial_state / state_update)
if (message.type === 'initial_state' || message.type === 'state_update') {
const data = message.data;
// Update stats
document.getElementById('stat-agents').textContent = data.agents.total;
document.getElementById('stat-active').textContent = data.agents.active;
document.getElementById('stat-tasks').textContent = data.tasks.active;
// Update agents list
updateAgentsList(data.agents.list);
// Update auctions list
updateAuctionsList(data.auctions.list);
return;
}
// Handle individual swarm events broadcast by ws_manager
const evt = message.event || message.type || '';
const 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();
}
}
function refreshStats() {
// Fetch current swarm status via REST and update the stat counters
fetch('/swarm').then(r => r.json()).then(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(() => {});
}
// Safe text setter — avoids XSS when inserting user/server data into DOM