deny: operator can attach a reason that reaches the manager

clicking DENY on the dashboard now prompts for an optional reason
('reason for denying (optional, sent to manager):'). the value
rides along as a hidden 'note' form field; backend chain:

  POST /deny/{id} { note }
    → actions::deny(coord, id, Some(note))
    → Approvals::mark_denied writes it to the row
    → HelperEvent::ApprovalResolved { ..., note: Some("...") }

manager already had note: Option<String> on the event, just never
populated for denials before. host admin socket (hive-c0re deny)
still passes None.

generalized the prompt-on-submit pattern: any form with a
data-prompt attribute pops a window.prompt() before the POST and
stashes the answer in a hidden input named by data-prompt-field
(default 'note'). reusable for future opt-in note fields.
This commit is contained in:
müde 2026-05-15 21:58:42 +02:00
parent 91c78d626f
commit 2029840671
5 changed files with 52 additions and 12 deletions

View file

@ -164,6 +164,21 @@
if (!(f instanceof HTMLFormElement) || !f.hasAttribute('data-async')) return;
e.preventDefault();
if (f.dataset.confirm && !confirm(f.dataset.confirm)) return;
if (f.dataset.prompt) {
const ans = prompt(f.dataset.prompt, '');
if (ans === null) return; // operator hit Cancel
// Drop into a hidden input named after `data-prompt-field` (or
// 'note' by default) so the value rides along on the POST.
const field = f.dataset.promptField || 'note';
let input = f.querySelector(`input[name="${field}"]`);
if (!input) {
input = document.createElement('input');
input.type = 'hidden';
input.name = field;
f.append(input);
}
input.value = ans;
}
const btn = f.querySelector('button[type="submit"], button:not([type]), .btn-inline');
const original = btn ? btn.innerHTML : '';
if (btn) { btn.disabled = true; btn.innerHTML = '<span class="spinner">◐</span>'; }
@ -616,11 +631,21 @@
'new sub-agent — container will be created on approve'),
);
}
// Deny prompts the operator for an optional reason; the
// submit handler stashes it into a hidden `note` input that
// rides along on the POST and is surfaced to the manager via
// HelperEvent::ApprovalResolved { note }.
const denyForm = el('form', {
method: 'POST', action: '/deny/' + a.id,
class: 'inline', 'data-async': '',
'data-prompt': 'reason for denying (optional, sent to manager):',
});
denyForm.append(el('button', { type: 'submit', class: 'btn btn-deny' }, 'DENY'));
row.append(
' ',
form('/approve/' + a.id, 'btn-approve', '◆ APPR0VE'),
' ',
form('/deny/' + a.id, 'btn-deny', 'DENY'),
denyForm,
);
li.append(row);
if (a.diff_html) {