dashboard: cancel-X on queued rebuild_queue rows (closes #575)

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.
This commit is contained in:
iris 2026-05-29 16:40:44 +02:00 committed by Mara
commit 036bf6c3c1
2 changed files with 66 additions and 0 deletions

View file

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

View file

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