Compare commits

..
3 changed files with 11 additions and 71 deletions

View file

@ -4,7 +4,9 @@
//! shape they want (HTTP redirect vs JSON).
use anyhow::Result;
use hive_sh4re::{ApprovalStatus, HelperEvent, MANAGER_AGENT, Message, SYSTEM_SENDER};
use hive_sh4re::{
ApprovalStatus, HelperEvent, MANAGER_AGENT, Message, SYSTEM_SENDER,
};
use crate::coordinator::Coordinator;
use crate::lifecycle;

View file

@ -9,7 +9,6 @@ use std::path::Path;
use std::sync::Arc;
use anyhow::{Context, Result};
use axum::extract::Form;
use axum::{
Router,
extract::{Path as AxumPath, State},
@ -20,8 +19,7 @@ use axum::{
},
routing::{get, post},
};
use hive_sh4re::{Approval, MANAGER_AGENT, Message};
use serde::Deserialize;
use hive_sh4re::Approval;
use tokio_stream::wrappers::BroadcastStream;
use tokio_stream::{Stream, StreamExt};
@ -41,7 +39,6 @@ pub async fn serve(port: u16, coord: Arc<Coordinator>) -> Result<()> {
.route("/", get(index))
.route("/approve/{id}", post(post_approve))
.route("/deny/{id}", post(post_deny))
.route("/send", post(post_send))
.route("/messages/stream", get(messages_stream))
.with_state(AppState { coord });
let addr = SocketAddr::from(([0, 0, 0, 0], port));
@ -68,35 +65,11 @@ async fn index(headers: HeaderMap, State(state): State<AppState>) -> Html<String
let approvals_html = render_approvals(&approvals).await;
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{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{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, &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>>> {
@ -186,25 +159,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.
@ -215,7 +169,9 @@ fn gc_orphans(coord: &Coordinator, approvals: Vec<Approval>) -> Vec<Approval> {
if Coordinator::agent_proposed_dir(&a.agent).exists() {
true
} else {
let _ = coord.approvals.mark_failed(a.id, "agent state dir missing");
let _ = coord
.approvals
.mark_failed(a.id, "agent state dir missing");
tracing::info!(id = a.id, agent = %a.agent, "auto-failed orphan approval");
false
}
@ -424,25 +380,6 @@ const STYLE: &str = r#"
.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-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; }
summary {
cursor: pointer;

View file

@ -296,8 +296,9 @@ fn set_resource_limits(container: &str) -> Result<()> {
let dir = format!("/run/systemd/system/container@{container}.service.d");
std::fs::create_dir_all(&dir).with_context(|| format!("create {dir}"))?;
let path = format!("{dir}/hyperhive-limits.conf");
let content =
format!("[Service]\nMemoryMax={DEFAULT_MEMORY_MAX}\nCPUQuota={DEFAULT_CPU_QUOTA}\n",);
let content = format!(
"[Service]\nMemoryMax={DEFAULT_MEMORY_MAX}\nCPUQuota={DEFAULT_CPU_QUOTA}\n",
);
std::fs::write(&path, content).with_context(|| format!("write {path}"))?;
tracing::info!(
%path,