Phase 6b: vibec0re-styled dashboard on hive-c0re + agent web UI restyled
This commit is contained in:
parent
6dbf4eedd7
commit
8cf5d72798
5 changed files with 266 additions and 13 deletions
197
hive-c0re/src/dashboard.rs
Normal file
197
hive-c0re/src/dashboard.rs
Normal file
|
|
@ -0,0 +1,197 @@
|
|||
//! Hyperhive dashboard. Lists managed containers (with deep-links to each
|
||||
//! container's web UI), pending approvals, and the manager.
|
||||
|
||||
use std::net::SocketAddr;
|
||||
use std::sync::Arc;
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
use axum::{Router, extract::State, http::HeaderMap, response::Html, routing::get};
|
||||
use hive_sh4re::Approval;
|
||||
|
||||
use crate::coordinator::Coordinator;
|
||||
use crate::lifecycle::{self, AGENT_PREFIX, MANAGER_NAME};
|
||||
|
||||
const MANAGER_PORT: u16 = 8000;
|
||||
|
||||
#[derive(Clone)]
|
||||
struct AppState {
|
||||
coord: Arc<Coordinator>,
|
||||
}
|
||||
|
||||
pub async fn serve(port: u16, coord: Arc<Coordinator>) -> Result<()> {
|
||||
let app = Router::new()
|
||||
.route("/", get(index))
|
||||
.with_state(AppState { coord });
|
||||
let addr = SocketAddr::from(([0, 0, 0, 0], port));
|
||||
let listener = tokio::net::TcpListener::bind(addr)
|
||||
.await
|
||||
.with_context(|| format!("bind dashboard on port {port}"))?;
|
||||
tracing::info!(%port, "dashboard listening");
|
||||
axum::serve(listener, app).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn index(headers: HeaderMap, State(state): State<AppState>) -> Html<String> {
|
||||
let host = headers
|
||||
.get("host")
|
||||
.and_then(|h| h.to_str().ok())
|
||||
.unwrap_or("localhost");
|
||||
let hostname = host.split(':').next().unwrap_or(host).to_owned();
|
||||
|
||||
let containers = lifecycle::list().await.unwrap_or_default();
|
||||
let approvals = state.coord.approvals.pending().unwrap_or_default();
|
||||
|
||||
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}\n{footer}\n</body>\n</html>\n",
|
||||
style = STYLE,
|
||||
banner = BANNER,
|
||||
containers = render_containers(&containers, &hostname),
|
||||
approvals = render_approvals(&approvals),
|
||||
footer = FOOTER,
|
||||
))
|
||||
}
|
||||
|
||||
fn render_containers(containers: &[String], hostname: &str) -> String {
|
||||
let mut out = String::from(
|
||||
"<h2>◆ C0NTAINERS ◆</h2>\n<div class=\"divider\">══════════════════════════════════════════════════════════════</div>\n",
|
||||
);
|
||||
if containers.is_empty() {
|
||||
out.push_str("<p class=\"empty\">▓ no managed containers ▓</p>\n");
|
||||
return out;
|
||||
}
|
||||
out.push_str("<ul>\n");
|
||||
for container in containers {
|
||||
if container == MANAGER_NAME {
|
||||
out.push_str(&format!(
|
||||
"<li><span class=\"glyph\">▓█▓▒░</span> <a href=\"http://{hostname}:{MANAGER_PORT}/\">{container}</a> <span class=\"role role-m1nd\">m1nd</span> <span class=\"meta\">:{MANAGER_PORT}</span></li>\n",
|
||||
));
|
||||
} else if let Some(name) = container.strip_prefix(AGENT_PREFIX) {
|
||||
let port = lifecycle::agent_web_port(name);
|
||||
out.push_str(&format!(
|
||||
"<li><span class=\"glyph\">▒░▒░░</span> <a href=\"http://{hostname}:{port}/\">{name}</a> <span class=\"role role-ag3nt\">ag3nt</span> <span class=\"meta\">{container} :{port}</span></li>\n",
|
||||
));
|
||||
}
|
||||
}
|
||||
out.push_str("</ul>\n");
|
||||
out
|
||||
}
|
||||
|
||||
fn render_approvals(approvals: &[Approval]) -> String {
|
||||
let mut out = String::from(
|
||||
"<h2>◆ P3NDING APPR0VALS ◆</h2>\n<div class=\"divider\">══════════════════════════════════════════════════════════════</div>\n",
|
||||
);
|
||||
if approvals.is_empty() {
|
||||
out.push_str("<p class=\"empty\">▓ queue empty ▓</p>\n");
|
||||
return out;
|
||||
}
|
||||
out.push_str("<ul>\n");
|
||||
for a in approvals {
|
||||
let sha_short = &a.commit_ref[..a.commit_ref.len().min(12)];
|
||||
out.push_str(&format!(
|
||||
"<li><span class=\"glyph\">→</span> <span class=\"id\">#{id}</span> <span class=\"agent\">{agent}</span> <code>{sha_short}</code> <span class=\"meta\">approve via <code>hive-c0re approve {id}</code></span></li>\n",
|
||||
id = a.id,
|
||||
agent = a.agent,
|
||||
));
|
||||
}
|
||||
out.push_str("</ul>\n");
|
||||
out
|
||||
}
|
||||
|
||||
const BANNER: &str = r#"<pre class="banner">
|
||||
░▒▓█▓▒░ HYPERHIVE ░▒▓█▓▒░ HIVE-C0RE ░▒▓█▓▒░ WE ARE THE WIRED ░▒▓█▓▒░
|
||||
</pre>"#;
|
||||
|
||||
const FOOTER: &str = r#"<footer>
|
||||
<div class="divider">══════════════════════════════════════════════════════════════</div>
|
||||
<p>▲△▲ <a href="https://git.berlin.ccc.de/vinzenz/hyperhive">hyperhive</a> ▲△▲ hive-c0re on this host ▲△▲</p>
|
||||
</footer>"#;
|
||||
|
||||
const STYLE: &str = r#"
|
||||
<style>
|
||||
:root {
|
||||
--bg: #0a0014;
|
||||
--fg: #e0d4ff;
|
||||
--muted: #6c5c8c;
|
||||
--purple: #cc66ff;
|
||||
--purple-dim: #4a1a6a;
|
||||
--cyan: #00ffff;
|
||||
--pink: #ff3399;
|
||||
--amber: #ffaa00;
|
||||
--border: #2a0a4a;
|
||||
}
|
||||
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;
|
||||
}
|
||||
h1, h2 {
|
||||
color: var(--purple);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.15em;
|
||||
margin-top: 2em;
|
||||
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;
|
||||
}
|
||||
ul { list-style: none; padding-left: 0; }
|
||||
li { padding: 0.5em 0; }
|
||||
.glyph { color: var(--purple); margin-right: 0.5em; }
|
||||
a {
|
||||
color: var(--cyan);
|
||||
text-decoration: none;
|
||||
text-shadow: 0 0 4px rgba(0, 255, 255, 0.5);
|
||||
font-weight: bold;
|
||||
}
|
||||
a:hover {
|
||||
color: #fff;
|
||||
text-shadow: 0 0 12px rgba(0, 255, 255, 0.9);
|
||||
}
|
||||
.role {
|
||||
display: inline-block;
|
||||
margin-left: 0.4em;
|
||||
padding: 0.05em 0.5em;
|
||||
border: 1px solid;
|
||||
border-radius: 2px;
|
||||
font-size: 0.8em;
|
||||
letter-spacing: 0.1em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
.role-m1nd { color: var(--pink); border-color: var(--pink); background: rgba(255, 51, 153, 0.1); }
|
||||
.role-ag3nt { color: var(--amber); border-color: var(--amber); background: rgba(255, 170, 0, 0.1); }
|
||||
.meta { color: var(--muted); font-size: 0.85em; margin-left: 0.4em; }
|
||||
.id { color: var(--pink); font-weight: bold; margin-right: 0.4em; }
|
||||
.agent { color: var(--amber); font-weight: bold; margin-right: 0.6em; }
|
||||
.empty { color: var(--muted); font-style: italic; }
|
||||
code {
|
||||
color: var(--amber);
|
||||
background: #18002a;
|
||||
padding: 0.1em 0.4em;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 2px;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
footer {
|
||||
margin-top: 4em;
|
||||
text-align: center;
|
||||
color: var(--muted);
|
||||
font-size: 0.9em;
|
||||
}
|
||||
footer a { color: var(--purple); }
|
||||
</style>
|
||||
"#;
|
||||
|
|
@ -10,6 +10,7 @@ mod approvals;
|
|||
mod broker;
|
||||
mod client;
|
||||
mod coordinator;
|
||||
mod dashboard;
|
||||
mod lifecycle;
|
||||
mod manager_server;
|
||||
mod server;
|
||||
|
|
@ -38,6 +39,9 @@ enum Cmd {
|
|||
/// Path to the sqlite message store.
|
||||
#[arg(long, default_value = "/var/lib/hyperhive/broker.sqlite")]
|
||||
db: PathBuf,
|
||||
/// Dashboard HTTP port.
|
||||
#[arg(long, default_value_t = 7000)]
|
||||
dashboard_port: u16,
|
||||
},
|
||||
/// Spawn a new agent container (`hive-agent-<name>`).
|
||||
Spawn { name: String },
|
||||
|
|
@ -69,9 +73,16 @@ async fn main() -> Result<()> {
|
|||
Cmd::Serve {
|
||||
hyperhive_flake,
|
||||
db,
|
||||
dashboard_port,
|
||||
} => {
|
||||
let coord = Arc::new(Coordinator::open(&db, hyperhive_flake)?);
|
||||
manager_server::start(coord.clone())?;
|
||||
let dash_coord = coord.clone();
|
||||
tokio::spawn(async move {
|
||||
if let Err(e) = dashboard::serve(dashboard_port, dash_coord).await {
|
||||
tracing::error!(error = ?e, "dashboard failed");
|
||||
}
|
||||
});
|
||||
server::serve(&cli.socket, coord).await
|
||||
}
|
||||
Cmd::Spawn { name } => {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue