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() } })