From c8e9377afc0e5bf59ef1ebea947423fee44713bb Mon Sep 17 00:00:00 2001 From: OpSpawn Agent Date: Fri, 6 Feb 2026 09:11:38 +0000 Subject: [PATCH] Add interactive task management UI to dashboard Create/claim/complete tasks and add workstreams directly from the browser. Inline forms, modals, and action buttons all backed by existing REST API. Co-Authored-By: Claude Opus 4.6 --- dashboard.html | 160 ++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 159 insertions(+), 1 deletion(-) diff --git a/dashboard.html b/dashboard.html index 6e9b365..a371b4a 100644 --- a/dashboard.html +++ b/dashboard.html @@ -75,6 +75,33 @@ .empty { color: var(--muted); font-size: 13px; font-style: italic; } + /* Action buttons */ + .btn { background: var(--border); color: var(--text); border: none; padding: 3px 10px; border-radius: 4px; font-size: 12px; cursor: pointer; font-family: inherit; } + .btn:hover { background: var(--muted); } + .btn-accent { background: var(--accent); color: #000; } + .btn-accent:hover { opacity: 0.85; } + .btn-green { background: var(--green); color: #000; } + .btn-green:hover { opacity: 0.85; } + .btn-sm { padding: 2px 6px; font-size: 11px; } + .task-actions { display: flex; gap: 4px; flex-shrink: 0; } + + /* Inline forms */ + .inline-form { display: flex; gap: 6px; padding: 8px 0; align-items: center; } + .inline-form input, .inline-form select { background: var(--bg); color: var(--text); border: 1px solid var(--border); padding: 4px 8px; border-radius: 4px; font-size: 12px; font-family: inherit; } + .inline-form input:focus, .inline-form select:focus { border-color: var(--accent); outline: none; } + .inline-form input[type="text"] { flex: 1; min-width: 120px; } + .add-task-btn { margin-top: 6px; } + + /* Modal overlay */ + .modal-overlay { display: none; position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0,0,0,0.6); z-index: 100; justify-content: center; align-items: center; } + .modal-overlay.show { display: flex; } + .modal { background: var(--surface); border: 1px solid var(--border); border-radius: 8px; padding: 20px; width: 400px; max-width: 90vw; } + .modal h3 { margin-bottom: 12px; font-size: 16px; } + .modal label { display: block; font-size: 13px; color: var(--muted); margin-bottom: 4px; margin-top: 10px; } + .modal input, .modal select { width: 100%; background: var(--bg); color: var(--text); border: 1px solid var(--border); padding: 6px 10px; border-radius: 4px; font-size: 13px; font-family: inherit; } + .modal input:focus, .modal select:focus { border-color: var(--accent); outline: none; } + .modal-actions { display: flex; gap: 8px; margin-top: 16px; justify-content: flex-end; } + .summary-row { display: flex; gap: 24px; padding: 16px 24px; border-bottom: 1px solid var(--border); flex-wrap: wrap; } .summary-stat { text-align: center; } .summary-stat .num { font-size: 28px; font-weight: 700; } @@ -107,10 +134,41 @@
-
Workstreams & Tasks
+
Workstreams & Tasks
Loading...
+ + + + + +
Agents
Loading...
@@ -177,7 +235,19 @@ async function refresh() { ${esc(t.title)} ${t.assigned_to ? `${esc(t.assigned_to)}` : ''} ${t.id} + + ${t.status === 'pending' ? `` : ''} + ${t.status === 'in_progress' ? `` : ''} + `).join('')} +
+ + +
`; }).join(''); } @@ -224,6 +294,12 @@ async function refresh() { }).join(''); } + // Restore open add-task forms after refresh + openAddForms.forEach(wsName => { + const form = document.getElementById('add-form-' + wsName); + if (form) form.style.display = 'flex'; + }); + // Knowledge const kbEl = document.getElementById('knowledge'); if (d.knowledge_topics.length === 0) { @@ -247,6 +323,88 @@ function timeAgo(iso) { return Math.floor(s / 86400) + 'd ago'; } +// State for modals and open forms +let claimWs = '', claimId = ''; +let openAddForms = new Set(); // track which workstream add-task forms are open + +function showAddWorkstream() { + document.getElementById('ws-name').value = ''; + document.getElementById('ws-desc').value = ''; + document.getElementById('ws-priority').value = '2'; + document.getElementById('ws-modal').classList.add('show'); + document.getElementById('ws-name').focus(); +} + +async function createWorkstream() { + const name = document.getElementById('ws-name').value.trim(); + if (!name) return; + await fetch('/api/workstreams', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + name, + description: document.getElementById('ws-desc').value.trim(), + priority: parseInt(document.getElementById('ws-priority').value) + }) + }); + document.getElementById('ws-modal').classList.remove('show'); + refresh(); +} + +function showAddTask(wsName) { + openAddForms.add(wsName); + const form = document.getElementById('add-form-' + wsName); + if (form) { form.style.display = 'flex'; document.getElementById('add-title-' + wsName).focus(); } +} + +async function addTask(wsName) { + const input = document.getElementById('add-title-' + wsName); + const title = input.value.trim(); + if (!title) return; + await fetch('/api/workstreams/' + encodeURIComponent(wsName) + '/tasks', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ title }) + }); + input.value = ''; + openAddForms.delete(wsName); + refresh(); +} + +function hideAddTask(wsName) { + openAddForms.delete(wsName); + const form = document.getElementById('add-form-' + wsName); + if (form) form.style.display = 'none'; +} + +function showClaim(wsName, taskId, title) { + claimWs = wsName; claimId = taskId; + document.getElementById('claim-task-title').textContent = title; + document.getElementById('claim-modal').classList.add('show'); + document.getElementById('claim-agent').focus(); +} + +async function claimTask() { + const agent = document.getElementById('claim-agent').value.trim(); + if (!agent) return; + await fetch('/api/workstreams/' + encodeURIComponent(claimWs) + '/tasks/' + claimId + '/claim', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ agent }) + }); + document.getElementById('claim-modal').classList.remove('show'); + refresh(); +} + +async function completeTask(wsName, taskId) { + await fetch('/api/workstreams/' + encodeURIComponent(wsName) + '/tasks/' + taskId + '/complete', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({}) + }); + refresh(); +} + refresh(); setInterval(refresh, 5000);