diff options
Diffstat (limited to 'web')
| -rw-r--r-- | web/app.js | 114 | ||||
| -rw-r--r-- | web/index.html | 1 | ||||
| -rw-r--r-- | web/style.css | 35 | ||||
| -rw-r--r-- | web/test/task-actions.test.mjs | 5 |
4 files changed, 154 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(); diff --git a/web/index.html b/web/index.html index 482b9a9..2bc8222 100644 --- a/web/index.html +++ b/web/index.html @@ -20,6 +20,7 @@ <div data-panel="tasks"> <div class="task-list-toolbar"> <button id="btn-toggle-completed" class="btn-secondary btn-sm"></button> + <button id="btn-start-next" class="btn-secondary btn-sm">Start Next</button> </div> <div class="task-list"> <div id="loading">Loading tasks…</div> diff --git a/web/style.css b/web/style.css index 268f80c..1478b36 100644 --- a/web/style.css +++ b/web/style.css @@ -259,6 +259,41 @@ main { cursor: not-allowed; } +.btn-accept { + font-size: 0.8rem; + font-weight: 600; + padding: 0.35em 0.85em; + border-radius: 0.375rem; + border: none; + cursor: pointer; + background: var(--state-completed); + color: #0f172a; + transition: opacity 0.15s; +} + +.btn-accept:disabled { + opacity: 0.5; + cursor: not-allowed; +} + +.btn-reject { + font-size: 0.8rem; + font-weight: 600; + padding: 0.35em 0.85em; + border-radius: 0.375rem; + border: none; + cursor: pointer; + background: var(--text-muted); + color: #0f172a; + transition: opacity 0.15s; + margin-left: 0.375rem; +} + +.btn-reject:disabled { + opacity: 0.5; + cursor: not-allowed; +} + .task-error { font-size: 0.78rem; color: var(--state-failed); diff --git a/web/test/task-actions.test.mjs b/web/test/task-actions.test.mjs index f2c21c4..2df6523 100644 --- a/web/test/task-actions.test.mjs +++ b/web/test/task-actions.test.mjs @@ -12,6 +12,7 @@ const RESTART_STATES = new Set(['FAILED', 'TIMED_OUT', 'CANCELLED']); function getCardAction(state) { if (state === 'PENDING') return 'run'; if (state === 'RUNNING') return 'cancel'; + if (state === 'READY') return 'approve'; if (RESTART_STATES.has(state)) return 'restart'; return null; } @@ -39,6 +40,10 @@ describe('task card action buttons', () => { assert.equal(getCardAction('CANCELLED'), 'restart'); }); + it('shows approve buttons for READY', () => { + assert.equal(getCardAction('READY'), 'approve'); + }); + it('shows no button for COMPLETED', () => { assert.equal(getCardAction('COMPLETED'), null); }); |
