operator input: per-agent /send form (dashboard T4LK removed)

This commit is contained in:
müde 2026-05-15 15:28:17 +02:00
parent 3c493934da
commit 409263f1c9
8 changed files with 142 additions and 52 deletions

View file

@ -107,5 +107,15 @@ fn dispatch(req: &AgentRequest, agent: &str, broker: &Broker) -> AgentResponse {
message: format!("{e:#}"),
},
},
AgentRequest::OperatorMsg { body } => match broker.send(&Message {
from: "operator".to_owned(),
to: agent.to_owned(),
body: body.clone(),
}) {
Ok(()) => AgentResponse::Ok,
Err(e) => AgentResponse::Err {
message: format!("{e:#}"),
},
},
}
}

View file

@ -20,7 +20,7 @@ use axum::{
},
routing::{get, post},
};
use hive_sh4re::{Approval, MANAGER_AGENT, Message};
use hive_sh4re::Approval;
use serde::Deserialize;
use tokio_stream::wrappers::BroadcastStream;
use tokio_stream::{Stream, StreamExt};
@ -44,7 +44,6 @@ pub async fn serve(port: u16, coord: Arc<Coordinator>) -> Result<()> {
.route("/destroy/{name}", post(post_destroy))
.route("/rebuild/{name}", post(post_rebuild))
.route("/request-spawn", post(post_request_spawn))
.route("/send", post(post_send))
.route("/messages/stream", get(messages_stream))
.with_state(AppState { coord });
let addr = SocketAddr::from(([0, 0, 0, 0], port));
@ -83,35 +82,11 @@ async fn index(headers: HeaderMap, State(state): State<AppState>) -> Html<String
};
Html(format!(
"<!doctype html>\n<html lang=\"en\">\n<head>\n<meta charset=\"utf-8\">\n<title>hyperhive // h1ve-c0re</title>\n{refresh}\n{STYLE}\n</head>\n<body>\n{BANNER}\n{containers}\n{talk}\n{approvals_html}\n{MSG_FLOW}\n{FOOTER}\n{MSG_FLOW_JS}\n</body>\n</html>\n",
"<!doctype html>\n<html lang=\"en\">\n<head>\n<meta charset=\"utf-8\">\n<title>hyperhive // h1ve-c0re</title>\n{refresh}\n{STYLE}\n</head>\n<body>\n{BANNER}\n{containers}\n{approvals_html}\n{MSG_FLOW}\n{FOOTER}\n{MSG_FLOW_JS}\n</body>\n</html>\n",
containers = render_containers(&containers, &transient, current_rev.as_deref(), &hostname),
talk = render_talk(&containers),
))
}
#[derive(Deserialize)]
struct SendForm {
to: String,
body: String,
}
async fn post_send(State(state): State<AppState>, Form(form): Form<SendForm>) -> Response {
let to = form.to.trim().to_owned();
let body = form.body.trim().to_owned();
if to.is_empty() || body.is_empty() {
return error_response("send: `to` and `body` required");
}
let msg = Message {
from: "operator".into(),
to,
body,
};
match state.coord.broker.send(&msg) {
Ok(()) => Redirect::to("/").into_response(),
Err(e) => error_response(&format!("send failed: {e:#}")),
}
}
async fn messages_stream(
State(state): State<AppState>,
) -> Sse<impl Stream<Item = Result<Event, Infallible>>> {
@ -296,25 +271,6 @@ async fn render_approvals(approvals: &[Approval]) -> String {
out
}
fn render_talk(containers: &[String]) -> String {
let mut options = String::new();
let _ = writeln!(
options,
"<option value=\"{MANAGER_AGENT}\">manager (hm1nd)</option>",
);
for container in containers {
if container == MANAGER_NAME {
continue;
}
if let Some(name) = container.strip_prefix(AGENT_PREFIX) {
let _ = writeln!(options, "<option value=\"{name}\">{name}</option>");
}
}
format!(
"<h2>◆ T4LK ◆</h2>\n<div class=\"divider\">══════════════════════════════════════════════════════════════</div>\n<form method=\"POST\" action=\"/send\" class=\"talkform\">\n <select name=\"to\" required>{options}</select>\n <input name=\"body\" placeholder=\"message body...\" required autocomplete=\"off\">\n <button type=\"submit\" class=\"btn btn-talk\">◆ S3ND</button>\n</form>\n<p class=\"meta\">sends as <code>from: operator</code>. Replies stream into the message panel below.</p>\n"
)
}
/// Filter out approvals whose agent state dir was wiped out from under us
/// (e.g. by a test script's cleanup). Marks them failed so they fall out of
/// `pending` on next render.

View file

@ -81,6 +81,16 @@ async fn dispatch(req: &ManagerRequest, coord: &Coordinator) -> ManagerResponse
message: format!("{e:#}"),
},
},
ManagerRequest::OperatorMsg { body } => match coord.broker.send(&Message {
from: "operator".to_owned(),
to: MANAGER_AGENT.to_owned(),
body: body.clone(),
}) {
Ok(()) => ManagerResponse::Ok,
Err(e) => ManagerResponse::Err {
message: format!("{e:#}"),
},
},
ManagerRequest::Status => match coord.broker.count_pending(MANAGER_AGENT) {
Ok(unread) => ManagerResponse::Status { unread },
Err(e) => ManagerResponse::Err {