feat(alerts): Steward's own confirm dialog, a verdict-first rules list, and a rule page in three sections (#5314, #5315, #5317)
CI / lint (push) Successful in 2s
CI / integration (push) Successful in 55s
CI / unit (push) Failing after 1m25s
CI / publish (push) Skipped

Confirm dialog (#5314):
- Every data-confirm form now asks in one native <dialog> (base.html + static/confirm.js)
  instead of window.confirm.
- The question becomes the heading and what follows it the detail; the act's
  button is named after the form's own button; Cancel has the focus and Esc
  backs out. Optional data-confirm-title/-detail/-action/-dismiss attributes.
- All 27 existing uses work as they are. Cancelling a run says 'Keep running'.

Alerts list (#5315):
- A verdict line (what is firing, how many rules, where notifications go,
  or 'notifies nobody yet' with a link) replaces the empty Active card.
- Each rule's name opens it; rows show 'last fired' and fold fire/clear into
  the condition; History and Disable stay; Delete leaves the list.
- .docker-verdict becomes the shared .page-verdict.

Rule page (#5317):
- Named for the rule (title and crumb), with when it last fired and how many
  changes of state it holds.
- Three sections: what it watches, when it fires (checks side by side,
  seconds echoed as a duration), what happens (where notifications go, no-data,
  the playbook, whose fields stay folded until ticked).
- Delete at the foot, behind the dialog, saying its history goes with it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-07 22:58:38 -04:00
co-authored by Claude Opus 5.5
parent 7cb6cb1fb5
commit 0c66c1e7d3
14 changed files with 401 additions and 131 deletions
+51
View File
@@ -0,0 +1,51 @@
// Steward's own confirm dialog (#5314): a form, or its submit button, with
// data-confirm="Question? What follows." asks in the #confirm-dialog that
// base.html renders before it submits, never through window.confirm.
//
// The prompt is attribute text read through dataset and written with
// textContent, never parsed as HTML or JavaScript, so a name inside it
// cannot run as script (#5099). Optional attributes beside data-confirm:
// data-confirm-title the heading (default: the prompt up to its first "? ")
// data-confirm-detail the line under it (default: the rest of the prompt)
// data-confirm-action the button that goes ahead (default: the submit
// button's own label, or the prompt's first word)
// data-confirm-dismiss the button that backs out (default: "Cancel")
// Cancel has the focus, and Esc backs out.
(function () {
function label(btn, title) {
var own = btn ? (btn.dataset.confirmAction || btn.textContent || btn.value || '').trim() : '';
if (own.length > 2) return own; // "×" and other icons say nothing
return title.split(' ')[0].replace(/\?$/, '');
}
function ask(form, btn) {
var src = btn && btn.dataset.confirm ? btn : form;
var prompt = src.dataset.confirm;
var cut = prompt.indexOf('? ');
var title = src.dataset.confirmTitle || (cut > 0 ? prompt.slice(0, cut + 1) : prompt);
var detail = src.dataset.confirmDetail || (cut > 0 ? prompt.slice(cut + 2) : '');
var d = document.getElementById('confirm-dialog');
d.querySelector('[data-part="title"]').textContent = title;
var p = d.querySelector('[data-part="detail"]');
p.textContent = detail;
p.hidden = !detail;
d.querySelector('[data-part="go"]').textContent = src.dataset.confirmAction || label(btn, title);
d.querySelector('[data-part="back"]').textContent = src.dataset.confirmDismiss || 'Cancel';
d.returnValue = '';
d.onclose = function () {
if (d.returnValue !== 'go') return;
form.dataset.confirmed = '1';
form.requestSubmit(btn && btn.form === form ? btn : undefined);
};
d.showModal();
}
document.addEventListener('submit', function (ev) {
var form = ev.target, btn = ev.submitter;
if (!((btn && btn.dataset.confirm) || form.dataset.confirm)) return;
if (form.dataset.confirmed === '1') { delete form.dataset.confirmed; return; } // the asked-for submit
ev.preventDefault();
ev.stopPropagation(); // before HTMX sees it: an hx-post form waits for the answer too
ask(form, btn);
}, true);
})();