//! 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, } pub async fn serve(port: u16, coord: Arc) -> 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) -> Html { 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!( "\n\n\n\nhyperhive // h1ve-c0re\n{style}\n\n\n{banner}\n{containers}\n{approvals}\n{footer}\n\n\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( "

◆ C0NTAINERS ◆

\n
══════════════════════════════════════════════════════════════
\n", ); if containers.is_empty() { out.push_str("

▓ no managed containers ▓

\n"); return out; } out.push_str("
    \n"); for container in containers { if container == MANAGER_NAME { out.push_str(&format!( "
  • ▓█▓▒░ {container} m1nd :{MANAGER_PORT}
  • \n", )); } else if let Some(name) = container.strip_prefix(AGENT_PREFIX) { let port = lifecycle::agent_web_port(name); out.push_str(&format!( "
  • ▒░▒░░ {name} ag3nt {container} :{port}
  • \n", )); } } out.push_str("
\n"); out } fn render_approvals(approvals: &[Approval]) -> String { let mut out = String::from( "

◆ P3NDING APPR0VALS ◆

\n
══════════════════════════════════════════════════════════════
\n", ); if approvals.is_empty() { out.push_str("

▓ queue empty ▓

\n"); return out; } out.push_str("
    \n"); for a in approvals { let sha_short = &a.commit_ref[..a.commit_ref.len().min(12)]; out.push_str(&format!( "
  • #{id} {agent} {sha_short} approve via hive-c0re approve {id}
  • \n", id = a.id, agent = a.agent, )); } out.push_str("
\n"); out } const BANNER: &str = r#""#; const FOOTER: &str = r#"
══════════════════════════════════════════════════════════════

▲△▲ hyperhive ▲△▲ hive-c0re on this host ▲△▲

"#; const STYLE: &str = r#" "#;