From 9e790e35708f834abe1a09af52e43742e164cb63 Mon Sep 17 00:00:00 2001 From: Peter Stone Date: Thu, 5 Mar 2026 19:02:24 +0000 Subject: web: add Accept/Reject for READY tasks, Start Next button MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- web/app.js | 114 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 113 insertions(+), 1 deletion(-) (limited to 'web/app.js') diff --git a/web/app.js b/web/app.js index 6d2a029..578d9a8 100644 --- a/web/app.js +++ b/web/app.js @@ -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(); -- cgit v1.2.3