initial: hermes-dashboard snapshot (post overview + git integration)
This commit is contained in:
176
static/js/main.js
Normal file
176
static/js/main.js
Normal file
@@ -0,0 +1,176 @@
|
||||
/**
|
||||
* 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();
|
||||
}
|
||||
Reference in New Issue
Block a user