From 036bf6c3c11d354f5abcc9a6272abd5e260deb2e Mon Sep 17 00:00:00 2001 From: iris Date: Fri, 29 May 2026 16:40:44 +0200 Subject: [PATCH] dashboard: cancel-X on queued rebuild_queue rows (closes #575) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Backend route POST /api/rebuild-queue/{id}/cancel already exists (refuses Running / terminal entries with {cancelled: false}). This adds the operator-facing affordance: - small circular X button on the right edge of each row whose state === 'queued'. Running / done / failed rows don't render it, so the operator never clicks a button that the backend would refuse. - uses the same data-async + data-confirm pattern as the reminder cancel form — global submit handler does POST + spinner + error toast for free. - successful cancel flips the row queued -> cancelled via the live RebuildQueueChanged snapshot, so the button disappears on the next paint without an explicit refresh. CSS keeps it quiet by default (muted border, transparent background) and lights red on hover / focus, matching the .btn-deny family without claiming a full button-width slot that would push the row layout around. --- frontend/packages/dashboard/src/dashboard.css | 38 +++++++++++++++++++ frontend/packages/dashboard/src/tabs.js | 28 ++++++++++++++ 2 files changed, 66 insertions(+) diff --git a/frontend/packages/dashboard/src/dashboard.css b/frontend/packages/dashboard/src/dashboard.css index 0c3231a9..0bc94eab 100644 --- a/frontend/packages/dashboard/src/dashboard.css +++ b/frontend/packages/dashboard/src/dashboard.css @@ -830,6 +830,44 @@ code { font-size: 0.8em; white-space: pre-wrap; } + +/* #575: cancel-X for queued rebuild_queue entries. Quiet by default + (sits at the far right via margin-left: auto), lights red on hover. + Renders only when `state === 'queued'` per renderQueueEntry's + guard — Running / terminal rows don't get the affordance. Mirrors + the side-panel-close glyph shape (✗) without the full B6N-style + `btn-deny` width so it stays unobtrusive in a list row. */ +.rqe-cancel { + margin-left: auto; +} +.rqe-cancel-btn { + background: transparent; + border: 1px solid var(--purple-dim); + color: var(--muted); + border-radius: 999px; + width: 1.6em; + height: 1.6em; + font-size: 0.85em; + line-height: 1; + padding: 0; + cursor: pointer; + display: inline-flex; + align-items: center; + justify-content: center; + transition: color 0.15s ease, border-color 0.15s ease, box-shadow 0.15s ease; +} +.rqe-cancel-btn:hover:not(:disabled), +.rqe-cancel-btn:focus-visible { + color: var(--red); + border-color: var(--red); + box-shadow: 0 0 8px -2px var(--red); + outline: none; +} +.rqe-cancel-btn:disabled { + opacity: 0.5; + cursor: default; +} + .history-note { margin-left: 1.8em; margin-top: 0.2em; diff --git a/frontend/packages/dashboard/src/tabs.js b/frontend/packages/dashboard/src/tabs.js index aa46c5d2..149ef514 100644 --- a/frontend/packages/dashboard/src/tabs.js +++ b/frontend/packages/dashboard/src/tabs.js @@ -1839,6 +1839,34 @@ window.marked = marked; if (entry.error) { li.append(el('pre', { class: 'rqe-error', title: entry.error }, truncate(entry.error, 200))); } + // #575: cancel-X for queued entries. Backend + // `POST /api/rebuild-queue/{id}/cancel` is already implemented + // (refuses Running / terminal entries); we surface it only on + // `queued` rows here so the operator never sees a dead button. + // Uses the same `data-async` form + `data-confirm` pattern as the + // reminder cancel, so the global async-form handler takes care + // of POST + spinner + error toast. A successful cancel flips + // `state` from `queued` → `cancelled` via the live + // RebuildQueueChanged snapshot and the row re-renders without + // this button. + if (entry.state === 'queued') { + const cancelForm = el('form', { + method: 'POST', + action: '/api/rebuild-queue/' + entry.id + '/cancel', + class: 'inline rqe-cancel', + 'data-async': '', + 'data-confirm': + `cancel ${entry.kind} for \`${entry.agent}\` (queue id ${entry.id})? ` + + `the row drops from the queue and never runs. running / done / failed entries can't be cancelled this way.`, + }); + cancelForm.append(el('button', { + type: 'submit', + class: 'rqe-cancel-btn', + title: 'cancel this queued ' + entry.kind, + 'aria-label': 'cancel queued ' + entry.kind + ' for ' + entry.agent, + }, '✗')); + li.append(cancelForm); + } return li; }