dashboard: T4LK form — operator sends messages from the browser
This commit is contained in:
parent
07a5d3a778
commit
1333532d3f
1 changed files with 67 additions and 2 deletions
|
|
@ -19,7 +19,9 @@ use axum::{
|
||||||
},
|
},
|
||||||
routing::{get, post},
|
routing::{get, post},
|
||||||
};
|
};
|
||||||
use hive_sh4re::Approval;
|
use axum::extract::Form;
|
||||||
|
use hive_sh4re::{Approval, MANAGER_AGENT, Message};
|
||||||
|
use serde::Deserialize;
|
||||||
use tokio_stream::wrappers::BroadcastStream;
|
use tokio_stream::wrappers::BroadcastStream;
|
||||||
use tokio_stream::{Stream, StreamExt};
|
use tokio_stream::{Stream, StreamExt};
|
||||||
|
|
||||||
|
|
@ -39,6 +41,7 @@ pub async fn serve(port: u16, coord: Arc<Coordinator>) -> Result<()> {
|
||||||
.route("/", get(index))
|
.route("/", get(index))
|
||||||
.route("/approve/{id}", post(post_approve))
|
.route("/approve/{id}", post(post_approve))
|
||||||
.route("/deny/{id}", post(post_deny))
|
.route("/deny/{id}", post(post_deny))
|
||||||
|
.route("/send", post(post_send))
|
||||||
.route("/messages/stream", get(messages_stream))
|
.route("/messages/stream", get(messages_stream))
|
||||||
.with_state(AppState { coord });
|
.with_state(AppState { coord });
|
||||||
let addr = SocketAddr::from(([0, 0, 0, 0], port));
|
let addr = SocketAddr::from(([0, 0, 0, 0], port));
|
||||||
|
|
@ -65,11 +68,35 @@ async fn index(headers: HeaderMap, State(state): State<AppState>) -> Html<String
|
||||||
let approvals_html = render_approvals(&approvals).await;
|
let approvals_html = render_approvals(&approvals).await;
|
||||||
|
|
||||||
Html(format!(
|
Html(format!(
|
||||||
"<!doctype html>\n<html lang=\"en\">\n<head>\n<meta charset=\"utf-8\">\n<title>hyperhive // h1ve-c0re</title>\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",
|
"<!doctype html>\n<html lang=\"en\">\n<head>\n<meta charset=\"utf-8\">\n<title>hyperhive // h1ve-c0re</title>\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",
|
||||||
containers = render_containers(&containers, &hostname),
|
containers = render_containers(&containers, &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(
|
async fn messages_stream(
|
||||||
State(state): State<AppState>,
|
State(state): State<AppState>,
|
||||||
) -> Sse<impl Stream<Item = Result<Event, Infallible>>> {
|
) -> Sse<impl Stream<Item = Result<Event, Infallible>>> {
|
||||||
|
|
@ -159,6 +186,25 @@ async fn render_approvals(approvals: &[Approval]) -> String {
|
||||||
out
|
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
|
/// 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
|
/// (e.g. by a test script's cleanup). Marks them failed so they fall out of
|
||||||
/// `pending` on next render.
|
/// `pending` on next render.
|
||||||
|
|
@ -380,6 +426,25 @@ const STYLE: &str = r#"
|
||||||
.btn:hover { background: rgba(255,255,255,0.05); text-shadow: 0 0 12px currentColor; }
|
.btn:hover { background: rgba(255,255,255,0.05); text-shadow: 0 0 12px currentColor; }
|
||||||
.btn-approve { color: var(--green); border-color: var(--green); }
|
.btn-approve { color: var(--green); border-color: var(--green); }
|
||||||
.btn-deny { color: var(--red); border-color: var(--red); }
|
.btn-deny { color: var(--red); border-color: var(--red); }
|
||||||
|
.btn-talk { color: var(--cyan); border-color: var(--cyan); }
|
||||||
|
.talkform {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.6em;
|
||||||
|
align-items: stretch;
|
||||||
|
margin-top: 0.5em;
|
||||||
|
}
|
||||||
|
.talkform select, .talkform input {
|
||||||
|
font-family: inherit;
|
||||||
|
font-size: 1em;
|
||||||
|
background: var(--bg-elev);
|
||||||
|
color: var(--fg);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
padding: 0.4em 0.6em;
|
||||||
|
}
|
||||||
|
.talkform select { color: var(--amber); }
|
||||||
|
.talkform input { flex: 1; }
|
||||||
|
.talkform input::placeholder { color: var(--muted); }
|
||||||
|
.talkform input:focus, .talkform select:focus { outline: 1px solid var(--purple); }
|
||||||
details { margin-top: 0.5em; }
|
details { margin-top: 0.5em; }
|
||||||
summary {
|
summary {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue