diff --git a/Cargo.lock b/Cargo.lock index 189080c6..fe9c026c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -325,7 +325,6 @@ dependencies = [ "rusqlite", "serde", "serde_json", - "similar", "tokio", "tracing", "tracing-subscriber", @@ -682,12 +681,6 @@ dependencies = [ "libc", ] -[[package]] -name = "similar" -version = "2.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbbb5d9659141646ae647b42fe094daf6c6192d1620870b449d9557f748b2daa" - [[package]] name = "slab" version = "0.4.12" diff --git a/Cargo.toml b/Cargo.toml index a01086ce..7f348719 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -22,7 +22,6 @@ hive-sh4re = { path = "hive-sh4re" } rusqlite = { version = "0.37", features = ["bundled"] } serde = { version = "1", features = ["derive"] } serde_json = "1" -similar = "2" tokio = { version = "1", features = [ "io-util", "macros", diff --git a/hive-c0re/Cargo.toml b/hive-c0re/Cargo.toml index f48ad29e..553a7d3c 100644 --- a/hive-c0re/Cargo.toml +++ b/hive-c0re/Cargo.toml @@ -14,7 +14,6 @@ hive-sh4re.workspace = true rusqlite.workspace = true serde.workspace = true serde_json.workspace = true -similar.workspace = true tokio.workspace = true tracing.workspace = true tracing-subscriber.workspace = true diff --git a/hive-c0re/src/actions.rs b/hive-c0re/src/actions.rs deleted file mode 100644 index aebc5d71..00000000 --- a/hive-c0re/src/actions.rs +++ /dev/null @@ -1,45 +0,0 @@ -//! Operations that are exposed through more than one surface (the host admin -//! socket *and* the dashboard's POST endpoints). Each function takes a -//! `&Coordinator` and the request parameters; callers stitch the response -//! shape they want (HTTP redirect vs JSON). - -use anyhow::Result; - -use crate::coordinator::Coordinator; -use crate::lifecycle; - -/// Approve a pending request: read the agent.nix at the approval's commit from -/// the proposed repo, copy into the applied repo, commit there, and rebuild -/// the agent container. On failure marks the approval failed (with the error -/// note) and returns the error. -pub async fn approve(coord: &Coordinator, id: i64) -> Result<()> { - let approval = coord.approvals.mark_approved(id)?; - tracing::info!(%approval.id, %approval.agent, %approval.commit_ref, "approval: applying + rebuilding"); - - let agent_dir = coord.register_agent(&approval.agent)?; - let proposed_dir = Coordinator::agent_proposed_dir(&approval.agent); - let applied_dir = Coordinator::agent_applied_dir(&approval.agent); - let result: Result<()> = async { - lifecycle::apply_commit(&applied_dir, &proposed_dir, &approval.commit_ref).await?; - lifecycle::rebuild( - &approval.agent, - &coord.hyperhive_flake, - &agent_dir, - &applied_dir, - ) - .await - } - .await; - if let Err(e) = result { - let note = format!("{e:#}"); - let _ = coord.approvals.mark_failed(approval.id, ¬e); - return Err(e); - } - Ok(()) -} - -pub fn deny(coord: &Coordinator, id: i64) -> Result<()> { - coord.approvals.mark_denied(id)?; - tracing::info!(%id, "approval denied"); - Ok(()) -} diff --git a/hive-c0re/src/dashboard.rs b/hive-c0re/src/dashboard.rs index 535b981b..83ec30cd 100644 --- a/hive-c0re/src/dashboard.rs +++ b/hive-c0re/src/dashboard.rs @@ -1,24 +1,14 @@ //! Hyperhive dashboard. Lists managed containers (with deep-links to each -//! container's web UI), pending approvals (with unified diff vs the applied -//! repo, plus approve/deny buttons), and the manager. +//! container's web UI), pending approvals, and the manager. use std::fmt::Write as _; use std::net::SocketAddr; -use std::path::Path; use std::sync::Arc; use anyhow::{Context, Result}; -use axum::{ - Router, - extract::{Path as AxumPath, State}, - http::{HeaderMap, StatusCode}, - response::{Html, IntoResponse, Redirect, Response}, - routing::{get, post}, -}; +use axum::{Router, extract::State, http::HeaderMap, response::Html, routing::get}; use hive_sh4re::Approval; -use tokio::process::Command; -use crate::actions; use crate::coordinator::Coordinator; use crate::lifecycle::{self, AGENT_PREFIX, MANAGER_NAME}; @@ -32,8 +22,6 @@ struct AppState { pub async fn serve(port: u16, coord: Arc) -> Result<()> { let app = Router::new() .route("/", get(index)) - .route("/approve/{id}", post(post_approve)) - .route("/deny/{id}", post(post_deny)) .with_state(AppState { coord }); let addr = SocketAddr::from(([0, 0, 0, 0], port)); let listener = tokio::net::TcpListener::bind(addr) @@ -53,42 +41,17 @@ async fn index(headers: HeaderMap, State(state): State) -> Html\n\n\n\nhyperhive // h1ve-c0re\n{STYLE}\n\n\n{BANNER}\n{containers}\n{approvals_html}\n{FOOTER}\n\n\n", + "\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, )) } -async fn post_approve( - State(state): State, - AxumPath(id): AxumPath, -) -> Response { - match actions::approve(&state.coord, id).await { - Ok(()) => Redirect::to("/").into_response(), - Err(e) => error_response(&format!("approve {id} failed: {e:#}")), - } -} - -async fn post_deny(State(state): State, AxumPath(id): AxumPath) -> Response { - match actions::deny(&state.coord, id) { - Ok(()) => Redirect::to("/").into_response(), - Err(e) => error_response(&format!("deny {id} failed: {e:#}")), - } -} - -fn error_response(message: &str) -> Response { - ( - StatusCode::INTERNAL_SERVER_ERROR, - Html(format!( - "\n\n{STYLE}\n{BANNER}\n

◆ ERR0R ◆

\n
{message}
\n

← back

\n", - message = html_escape(message), - )), - ) - .into_response() -} - fn render_containers(containers: &[String], hostname: &str) -> String { let mut out = String::from( "

◆ C0NTAINERS ◆

\n
══════════════════════════════════════════════════════════════
\n", @@ -116,7 +79,7 @@ fn render_containers(containers: &[String], hostname: &str) -> String { out } -async fn render_approvals(approvals: &[Approval]) -> String { +fn render_approvals(approvals: &[Approval]) -> String { let mut out = String::from( "

◆ P3NDING APPR0VALS ◆

\n
══════════════════════════════════════════════════════════════
\n", ); @@ -124,63 +87,20 @@ async fn render_approvals(approvals: &[Approval]) -> String { out.push_str("

▓ queue empty ▓

\n"); return out; } - out.push_str("
    \n"); + out.push_str("
      \n"); for a in approvals { let sha_short = &a.commit_ref[..a.commit_ref.len().min(12)]; - let diff = approval_diff(&a.agent, &a.commit_ref).await; let _ = writeln!( out, - "
    • \n
      #{id} {agent} {sha_short}\n
      \n
      \n
      \n
      diff vs applied
      {diff}
      \n
    • ", + "
    • #{id} {agent} {sha_short} approve via hive-c0re approve {id}
    • ", id = a.id, agent = a.agent, - diff = html_escape(&diff), ); } out.push_str("
    \n"); out } -async fn approval_diff(agent: &str, commit_ref: &str) -> String { - let applied = Coordinator::agent_applied_dir(agent).join("agent.nix"); - let proposed = Coordinator::agent_proposed_dir(agent); - let applied_text = std::fs::read_to_string(&applied).unwrap_or_default(); - let proposed_text = match git_show(&proposed, commit_ref).await { - Ok(s) => s, - Err(e) => return format!("(error reading proposed agent.nix: {e:#})"), - }; - unified_diff(&applied_text, &proposed_text) -} - -async fn git_show(proposed_dir: &Path, commit_ref: &str) -> Result { - let out = Command::new("git") - .current_dir(proposed_dir) - .args(["show", &format!("{commit_ref}:agent.nix")]) - .output() - .await - .context("git show")?; - if !out.status.success() { - anyhow::bail!( - "git show {commit_ref}:agent.nix failed: {}", - String::from_utf8_lossy(&out.stderr).trim() - ); - } - Ok(String::from_utf8_lossy(&out.stdout).into_owned()) -} - -fn unified_diff(applied: &str, proposed: &str) -> String { - let diff = similar::TextDiff::from_lines(applied, proposed); - diff.unified_diff() - .context_radius(3) - .header("applied", "proposed") - .to_string() -} - -fn html_escape(s: &str) -> String { - s.replace('&', "&") - .replace('<', "<") - .replace('>', ">") -} - const BANNER: &str = r#""#; @@ -194,7 +114,6 @@ const STYLE: &str = r#"