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; }