diff --git a/hive-c0re/assets/app.js b/hive-c0re/assets/app.js
index 5c97489..c367693 100644
--- a/hive-c0re/assets/app.js
+++ b/hive-c0re/assets/app.js
@@ -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 = '◐'; }
@@ -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) {
diff --git a/hive-c0re/src/actions.rs b/hive-c0re/src/actions.rs
index 1b54efc..00318fd 100644
--- a/hive-c0re/src/actions.rs
+++ b/hive-c0re/src/actions.rs
@@ -178,17 +178,17 @@ pub async fn destroy(coord: &Coordinator, name: &str, purge: bool) -> Result<()>
Ok(())
}
-pub fn deny(coord: &Coordinator, id: i64) -> Result<()> {
+pub fn deny(coord: &Coordinator, id: i64, note: Option<&str>) -> Result<()> {
let approval = coord.approvals.get(id)?;
- coord.approvals.mark_denied(id)?;
- tracing::info!(%id, "approval denied");
+ coord.approvals.mark_denied(id, note)?;
+ tracing::info!(%id, note, "approval denied");
if let Some(a) = approval {
coord.notify_manager(&HelperEvent::ApprovalResolved {
id: a.id,
agent: a.agent,
commit_ref: a.commit_ref,
status: ApprovalStatus::Denied,
- note: None,
+ note: note.map(String::from),
});
}
Ok(())
diff --git a/hive-c0re/src/approvals.rs b/hive-c0re/src/approvals.rs
index d3e659b..fbc06ab 100644
--- a/hive-c0re/src/approvals.rs
+++ b/hive-c0re/src/approvals.rs
@@ -142,12 +142,12 @@ impl Approvals {
})
}
- pub fn mark_denied(&self, id: i64) -> Result<()> {
+ pub fn mark_denied(&self, id: i64, note: Option<&str>) -> Result<()> {
let conn = self.conn.lock().unwrap();
let affected = conn.execute(
- "UPDATE approvals SET status = 'denied', resolved_at = ?1
- WHERE id = ?2 AND status = 'pending'",
- params![now_unix(), id],
+ "UPDATE approvals SET status = 'denied', resolved_at = ?1, note = ?2
+ WHERE id = ?3 AND status = 'pending'",
+ params![now_unix(), note, id],
)?;
if affected == 0 {
bail!("approval {id} not pending");
diff --git a/hive-c0re/src/dashboard.rs b/hive-c0re/src/dashboard.rs
index 46657a7..ceaafa1 100644
--- a/hive-c0re/src/dashboard.rs
+++ b/hive-c0re/src/dashboard.rs
@@ -410,8 +410,23 @@ async fn post_approve(State(state): State, AxumPath(id): AxumPath
}
}
-async fn post_deny(State(state): State, AxumPath(id): AxumPath) -> Response {
- match actions::deny(&state.coord, id) {
+#[derive(Deserialize, Default)]
+struct DenyForm {
+ #[serde(default)]
+ note: Option,
+}
+
+async fn post_deny(
+ State(state): State,
+ AxumPath(id): AxumPath,
+ Form(form): Form,
+) -> Response {
+ let note = form
+ .note
+ .as_deref()
+ .map(str::trim)
+ .filter(|s| !s.is_empty());
+ match actions::deny(&state.coord, id, note) {
Ok(()) => Redirect::to("/").into_response(),
Err(e) => error_response(&format!("deny {id} failed: {e:#}")),
}
diff --git a/hive-c0re/src/server.rs b/hive-c0re/src/server.rs
index 9ce4df1..80f3551 100644
--- a/hive-c0re/src/server.rs
+++ b/hive-c0re/src/server.rs
@@ -144,7 +144,7 @@ async fn dispatch(req: &HostRequest, coord: Arc) -> HostResponse {
HostResponse::success()
}
HostRequest::Deny { id } => {
- actions::deny(&coord, *id)?;
+ actions::deny(&coord, *id, None)?;
HostResponse::success()
}
})