diff options
| author | Peter Stone <thepeterstone@gmail.com> | 2026-03-05 19:02:24 +0000 |
|---|---|---|
| committer | Peter Stone <thepeterstone@gmail.com> | 2026-03-05 19:02:24 +0000 |
| commit | 9e790e35708f834abe1a09af52e43742e164cb63 (patch) | |
| tree | 926f9b323f80dfdd3c030f98b7abebf9f02501d1 /web/app.js | |
| parent | ab93297426353d70ec7c877c710a049b664e4fd0 (diff) | |
web: add Accept/Reject for READY tasks, Start Next button
- READY state task cards show Accept + Reject buttons
- Accept POSTs to /api/tasks/{id}/accept (→ COMPLETED)
- Reject POSTs to /api/tasks/{id}/reject (→ PENDING)
- "Start Next" button in toolbar POSTs to /api/scripts/start-next-task
- CSS for .btn-accept and .btn-reject
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Diffstat (limited to 'web/app.js')
| -rw-r--r-- | web/app.js | 114 |
1 files changed, 113 insertions, 1 deletions
@@ -71,7 +71,7 @@ function createTaskCard(task) { // Footer: action buttons based on state const RESTART_STATES = new Set(['FAILED', 'TIMED_OUT', 'CANCELLED']); - if (task.state === 'PENDING' || task.state === 'RUNNING' || RESTART_STATES.has(task.state)) { + if (task.state === 'PENDING' || task.state === 'RUNNING' || task.state === 'READY' || RESTART_STATES.has(task.state)) { const footer = document.createElement('div'); footer.className = 'task-card-footer'; @@ -93,6 +93,23 @@ function createTaskCard(task) { handleCancel(task.id, btn, footer); }); footer.appendChild(btn); + } else if (task.state === 'READY') { + const acceptBtn = document.createElement('button'); + acceptBtn.className = 'btn-accept'; + acceptBtn.textContent = 'Accept'; + acceptBtn.addEventListener('click', (e) => { + e.stopPropagation(); + handleAccept(task.id, acceptBtn, footer); + }); + const rejectBtn = document.createElement('button'); + rejectBtn.className = 'btn-reject'; + rejectBtn.textContent = 'Reject'; + rejectBtn.addEventListener('click', (e) => { + e.stopPropagation(); + handleReject(task.id, rejectBtn, footer); + }); + footer.appendChild(acceptBtn); + footer.appendChild(rejectBtn); } else if (RESTART_STATES.has(task.state)) { const btn = document.createElement('button'); btn.className = 'btn-restart'; @@ -333,6 +350,97 @@ async function handleRestart(taskId, btn, footer) { } } +// ── Accept / Reject actions ──────────────────────────────────────────────────── + +async function acceptTask(taskId) { + const res = await fetch(`${API_BASE}/api/tasks/${taskId}/accept`, { method: 'POST' }); + if (!res.ok) { + let msg = `HTTP ${res.status}`; + try { const body = await res.json(); msg = body.error || msg; } catch {} + throw new Error(msg); + } + return res.json(); +} + +async function rejectTask(taskId) { + const res = await fetch(`${API_BASE}/api/tasks/${taskId}/reject`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ comment: '' }), + }); + if (!res.ok) { + let msg = `HTTP ${res.status}`; + try { const body = await res.json(); msg = body.error || msg; } catch {} + throw new Error(msg); + } + return res.json(); +} + +async function handleAccept(taskId, btn, footer) { + btn.disabled = true; + btn.textContent = 'Accepting…'; + const prev = footer.querySelector('.task-error'); + if (prev) prev.remove(); + + try { + await acceptTask(taskId); + await poll(); + } catch (err) { + btn.disabled = false; + btn.textContent = 'Accept'; + const errEl = document.createElement('span'); + errEl.className = 'task-error'; + errEl.textContent = `Failed: ${err.message}`; + footer.appendChild(errEl); + } +} + +async function handleReject(taskId, btn, footer) { + btn.disabled = true; + btn.textContent = 'Rejecting…'; + const prev = footer.querySelector('.task-error'); + if (prev) prev.remove(); + + try { + await rejectTask(taskId); + await poll(); + } catch (err) { + btn.disabled = false; + btn.textContent = 'Reject'; + const errEl = document.createElement('span'); + errEl.className = 'task-error'; + errEl.textContent = `Failed: ${err.message}`; + footer.appendChild(errEl); + } +} + +// ── Start-next-task ───────────────────────────────────────────────────────────── + +async function startNextTask() { + const res = await fetch(`${API_BASE}/api/scripts/start-next-task`, { method: 'POST' }); + if (!res.ok) { + let msg = `HTTP ${res.status}`; + try { const body = await res.json(); msg = body.error || msg; } catch {} + throw new Error(msg); + } + return res.json(); +} + +async function handleStartNextTask(btn) { + btn.disabled = true; + btn.textContent = 'Starting…'; + try { + const result = await startNextTask(); + const output = (result.output || '').trim(); + btn.textContent = output || 'No task to start'; + setTimeout(() => { btn.textContent = 'Start Next'; btn.disabled = false; }, 3000); + if (output && output !== 'No task to start.') await poll(); + } catch (err) { + btn.textContent = `Error: ${err.message}`; + setTimeout(() => { btn.textContent = 'Start Next'; btn.disabled = false; }, 3000); + } +} + // ── Delete template ──────────────────────────────────────────────────────────── async function deleteTemplate(id) { @@ -1137,6 +1245,10 @@ document.addEventListener('DOMContentLoaded', () => { await poll(); }); + document.getElementById('btn-start-next').addEventListener('click', function() { + handleStartNextTask(this); + }); + startPolling(); connectWebSocket(); |
