75 lines
2.6 KiB
Rust
75 lines
2.6 KiB
Rust
//! Per-container HTTP UI. Phase 6 minimum — a status page on a host port.
|
|
//! Containers share the host's network namespace (privateNetwork = false), so
|
|
//! each instance must bind a distinct port. `HIVE_PORT` is set per agent by
|
|
//! `hive-c0re`'s generated per-agent flake (deterministic from agent name).
|
|
|
|
use std::net::SocketAddr;
|
|
|
|
use anyhow::{Context, Result};
|
|
use axum::{Router, extract::State, response::Html, routing::get};
|
|
|
|
#[derive(Clone)]
|
|
struct AppState {
|
|
label: String,
|
|
}
|
|
|
|
pub async fn serve(label: String, port: u16) -> Result<()> {
|
|
let state = AppState { label };
|
|
let app = Router::new().route("/", get(index)).with_state(state);
|
|
let addr = SocketAddr::from(([0, 0, 0, 0], port));
|
|
let listener = tokio::net::TcpListener::bind(addr)
|
|
.await
|
|
.with_context(|| format!("bind web UI on port {port}"))?;
|
|
tracing::info!(%port, "web UI listening");
|
|
axum::serve(listener, app).await?;
|
|
Ok(())
|
|
}
|
|
|
|
async fn index(State(state): State<AppState>) -> Html<String> {
|
|
Html(format!(
|
|
"<!doctype html>\n<html lang=\"en\">\n<head>\n<meta charset=\"utf-8\">\n<title>{label} // hyperhive</title>\n{STYLE}\n</head>\n<body>\n<pre class=\"banner\">░▒▓█▓▒░ {label} ░▒▓█▓▒░ hyperhive ag3nt ░▒▓█▓▒░</pre>\n<h2>◆ {label} ◆</h2>\n<div class=\"divider\">══════════════════════════════════════════════════════════════</div>\n<p>▓█▓▒░ harness alive ▓█▓▒░</p>\n<p class=\"meta\">phase 6a placeholder — turn-loop status / inbox / xterm.js coming in 6b+</p>\n</body>\n</html>\n",
|
|
label = state.label,
|
|
))
|
|
}
|
|
|
|
const STYLE: &str = r#"
|
|
<style>
|
|
:root {
|
|
--bg: #0a0014;
|
|
--fg: #e0d4ff;
|
|
--muted: #6c5c8c;
|
|
--purple: #cc66ff;
|
|
--purple-dim: #4a1a6a;
|
|
}
|
|
body {
|
|
background: var(--bg);
|
|
color: var(--fg);
|
|
font-family: "JetBrains Mono", "Fira Code", "Cascadia Code", "Source Code Pro", monospace;
|
|
max-width: 70em;
|
|
margin: 1.5em auto;
|
|
padding: 0 1.5em;
|
|
line-height: 1.6;
|
|
}
|
|
.banner {
|
|
color: var(--purple);
|
|
text-align: center;
|
|
margin: 0 0 1em 0;
|
|
font-size: 0.95em;
|
|
text-shadow: 0 0 6px rgba(204, 102, 255, 0.5);
|
|
overflow-x: auto;
|
|
}
|
|
h2 {
|
|
color: var(--purple);
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.15em;
|
|
text-shadow: 0 0 8px rgba(204, 102, 255, 0.4);
|
|
}
|
|
.divider {
|
|
color: var(--purple-dim);
|
|
overflow: hidden;
|
|
white-space: nowrap;
|
|
margin-bottom: 0.5em;
|
|
}
|
|
.meta { color: var(--muted); font-size: 0.85em; }
|
|
</style>
|
|
"#;
|