/** * Hermes Dashboard - minimal ES module for auto-refresh. * * Updates elapsed-time spans every 10 seconds and refreshes * the worker status and tasks sections on the dashboard. */ function updateElapsedTimes() { const now = Math.floor(Date.now() / 1000); document.querySelectorAll('.elapsed-time[data-started]').forEach(el => { const started = parseInt(el.dataset.started, 10); if (!started) return; const diff = now - started; if (diff < 0) { el.textContent = 'in the future'; } else if (diff < 60) { el.textContent = `${diff}s`; } else if (diff < 3600) { el.textContent = `${Math.floor(diff / 60)}m ${diff % 60}s`; } else { const h = Math.floor(diff / 3600); const m = Math.floor((diff % 3600) / 60); el.textContent = `${h}h ${m}m`; } }); } async function refreshDashboard() { try { const resp = await fetch('/api/workers'); if (!resp.ok) return; const data = await resp.json(); const container = document.getElementById('worker-status'); if (!container) return; if (data.current_task) { const t = data.current_task; let html = `
Current Task: #${t.id} - ${(t.title || '').substring(0, 60)}
`; html += `Worker PID: ${t.worker_pid || 'N/A'}
`; if (t.started_at) { html += `Elapsed:
`; } container.innerHTML = html; } else { container.innerHTML = 'No task currently running
'; } // Refresh process list const procContainer = document.getElementById('process-list'); if (procContainer && data.processes) { if (data.processes.length > 0) { let html = '| PID | Elapsed | Command |
|---|---|---|
| ${proc.pid} | `; html += ``; html += ` | ${escapeHtml(proc.cmdline || '')} |
No opencode workers running
'; } } updateElapsedTimes(); } catch (_err) { // Silently ignore refresh errors } } function escapeHtml(str) { const div = document.createElement('div'); div.textContent = str; return div.innerHTML; } function getCsrfToken() { const meta = document.querySelector('meta[name="csrf-token"]'); return meta ? meta.content : ''; } async function postJson(url, payload) { const resp = await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': getCsrfToken(), }, body: JSON.stringify(payload), }); const data = await resp.json().catch(() => ({})); return { ok: resp.ok, status: resp.status, data }; } async function submitDispatch(ev) { ev.preventDefault(); const form = ev.target; const out = document.getElementById('dispatch-result'); const title = form.title.value.trim(); const body = form.body.value.trim(); const maxRuntime = parseInt(form.max_runtime.value, 10) || 1800; const assignee = (form.assignee.value || 'omo-pm').trim(); if (!title) { out.textContent = 'Error: title is required'; out.style.color = '#f88'; return false; } out.textContent = 'Dispatching...'; out.style.color = '#888'; const { ok, status, data } = await postJson('/api/dispatch', { title, body, max_runtime: maxRuntime, assignee, }); if (ok) { out.textContent = `Dispatched: ${data.task_id} (status=${data.status})`; out.style.color = '#8f8'; form.reset(); setTimeout(() => window.location.reload(), 1500); } else { out.textContent = `Error ${status}: ${data.error || 'unknown'}`; out.style.color = '#f88'; } return false; } function initDispatchForm() { const form = document.getElementById('dispatch-form'); if (!form) return; form.addEventListener('submit', submitDispatch); const tplSelect = document.getElementById('dispatch-template'); if (!tplSelect) return; fetch('/api/dispatch/templates') .then(r => r.json()) .then(data => { const templates = data.templates || []; for (const t of templates) { const opt = document.createElement('option'); opt.value = t.name; opt.textContent = t.name; opt.dataset.body = t.default_body || ''; opt.dataset.description = t.description || ''; tplSelect.appendChild(opt); } tplSelect.addEventListener('change', () => { const sel = tplSelect.selectedOptions[0]; if (sel && sel.dataset.body) { form.body.value = sel.dataset.body; if (!form.title.value) form.title.value = sel.value; } }); }) .catch(() => {}); } // Initial update + 10s interval updateElapsedTimes(); setInterval(updateElapsedTimes, 10000); // Refresh dashboard data every 10s if on the dashboard page if (document.getElementById('worker-status')) { setInterval(refreshDashboard, 10000); } if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', initDispatchForm); } else { initDispatchForm(); }