Files
hermes-dashboard/static/js/main.js

177 lines
5.3 KiB
JavaScript

/**
* 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 = `<p><strong>Current Task:</strong> #${t.id} - ${(t.title || '').substring(0, 60)}</p>`;
html += `<p><strong>Worker PID:</strong> ${t.worker_pid || 'N/A'}</p>`;
if (t.started_at) {
html += `<p><strong>Elapsed:</strong> <span class="elapsed-time" data-started="${t.started_at}"></span></p>`;
}
container.innerHTML = html;
} else {
container.innerHTML = '<p class="text-muted">No task currently running</p>';
}
// Refresh process list
const procContainer = document.getElementById('process-list');
if (procContainer && data.processes) {
if (data.processes.length > 0) {
let html = '<table><thead><tr><th>PID</th><th>Elapsed</th><th>Command</th></tr></thead><tbody>';
for (const proc of data.processes) {
html += `<tr><td>${proc.pid}</td>`;
html += `<td><span class="elapsed-time" data-started="${proc.create_time || 0}"></span></td>`;
html += `<td class="text-small">${escapeHtml(proc.cmdline || '')}</td></tr>`;
}
html += '</tbody></table>';
procContainer.innerHTML = html;
} else {
procContainer.innerHTML = '<p class="text-muted">No opencode workers running</p>';
}
}
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();
}