summaryrefslogtreecommitdiff
path: root/web/app.js
diff options
context:
space:
mode:
authorPeter Stone <thepeterstone@gmail.com>2026-03-05 19:02:24 +0000
committerPeter Stone <thepeterstone@gmail.com>2026-03-05 19:02:24 +0000
commit9e790e35708f834abe1a09af52e43742e164cb63 (patch)
tree926f9b323f80dfdd3c030f98b7abebf9f02501d1 /web/app.js
parentab93297426353d70ec7c877c710a049b664e4fd0 (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.js114
1 files changed, 113 insertions, 1 deletions
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();