diff --git a/hive-ag3nt/src/web_ui.rs b/hive-ag3nt/src/web_ui.rs index c09e929..d4f22ea 100644 --- a/hive-ag3nt/src/web_ui.rs +++ b/hive-ag3nt/src/web_ui.rs @@ -91,8 +91,12 @@ async fn index(State(state): State) -> Html { (LoginState::NeedsLogin, None) => render_needs_login_idle(), (LoginState::NeedsLogin, Some(session)) => render_login_in_progress(&session), }; + let dashboard_port = std::env::var("HIVE_DASHBOARD_PORT") + .ok() + .and_then(|s| s.parse::().ok()) + .unwrap_or(7000); Html(format!( - "\n\n\n\n{label} // hyperhive\n{STYLE}\n\n\n
░▒▓█▓▒░  {label}  ░▒▓█▓▒░  hyperhive ag3nt  ░▒▓█▓▒░
\n

◆ {label} ◆

\n
══════════════════════════════════════════════════════════════
\n{body}\n\n\n", + "\n\n\n\n{label} // hyperhive\n{STYLE}\n\n\n
░▒▓█▓▒░  {label}  ░▒▓█▓▒░  hyperhive ag3nt  ░▒▓█▓▒░
\n

◆ {label} ◆ ↻ R3BU1LD

\n
══════════════════════════════════════════════════════════════
\n{body}\n\n\n\n", label = state.label, )) } @@ -435,6 +439,19 @@ const STYLE: &str = r#" .btn:hover { background: rgba(204, 102, 255, 0.1); } .btn-login { color: var(--amber); border-color: var(--amber); } .btn-cancel { color: #ff6b6b; border-color: #ff6b6b; font-size: 0.85em; padding: 0.15em 0.6em; } + .btn-rebuild { + color: var(--amber); + border: 1px solid var(--amber); + padding: 0.15em 0.6em; + font-size: 0.55em; + font-family: inherit; + text-decoration: none; + letter-spacing: 0.1em; + margin-left: 0.6em; + vertical-align: middle; + cursor: pointer; + } + .btn-rebuild:hover { background: rgba(255, 184, 77, 0.1); } .btn-send { color: var(--green); border-color: var(--green); } .sendform { display: flex; gap: 0.6em; margin-top: 0.5em; } .sendform input { diff --git a/hive-c0re/src/actions.rs b/hive-c0re/src/actions.rs index 212e254..5b0dd7c 100644 --- a/hive-c0re/src/actions.rs +++ b/hive-c0re/src/actions.rs @@ -48,6 +48,7 @@ pub async fn approve(coord: Arc, id: i64) -> Result<()> { &agent_dir, &applied_dir, &claude_dir, + coord.dashboard_port, ) .await } @@ -69,6 +70,7 @@ pub async fn approve(coord: Arc, id: i64) -> Result<()> { &proposed_dir, &applied_dir, &claude_dir, + coord_bg.dashboard_port, ) .await; coord_bg.clear_transient(&agent_bg); diff --git a/hive-c0re/src/auto_update.rs b/hive-c0re/src/auto_update.rs index 51d6e6a..bfcc464 100644 --- a/hive-c0re/src/auto_update.rs +++ b/hive-c0re/src/auto_update.rs @@ -64,6 +64,7 @@ pub async fn rebuild_agent(coord: &Arc, name: &str, current_rev: &s &agent_dir, &applied_dir, &claude_dir, + coord.dashboard_port, ) .await?; std::fs::write(rev_marker_path(name), current_rev) @@ -112,6 +113,7 @@ pub async fn ensure_manager(coord: &Arc) -> Result<()> { &proposed, &applied, &claude_dir, + coord.dashboard_port, ) .await?; if let Some(rev) = current_rev { diff --git a/hive-c0re/src/coordinator.rs b/hive-c0re/src/coordinator.rs index 8267ac5..f2a0f24 100644 --- a/hive-c0re/src/coordinator.rs +++ b/hive-c0re/src/coordinator.rs @@ -29,6 +29,10 @@ pub struct Coordinator { /// URL of the hyperhive flake (no fragment). Inlined into per-agent /// `flake.nix` files as `inputs.hyperhive.url`. pub hyperhive_flake: String, + /// TCP port the host's hive-c0re dashboard listens on. Inlined into + /// each per-agent flake so the agent's web UI can build the right + /// rebuild-button URL pointing back at the dashboard. + pub dashboard_port: u16, agents: Mutex>, /// Agents whose lifecycle action (currently just spawn) is in flight. /// Read by the dashboard to render a spinner; cleared when the action @@ -51,13 +55,14 @@ pub enum TransientKind { } impl Coordinator { - pub fn open(db_path: &Path, hyperhive_flake: String) -> Result { + pub fn open(db_path: &Path, hyperhive_flake: String, dashboard_port: u16) -> Result { let broker = Broker::open(db_path).context("open broker")?; let approvals = Approvals::open(db_path).context("open approvals")?; Ok(Self { broker: Arc::new(broker), approvals: Arc::new(approvals), hyperhive_flake, + dashboard_port, agents: Mutex::new(HashMap::new()), transient: Mutex::new(HashMap::new()), }) diff --git a/hive-c0re/src/lifecycle.rs b/hive-c0re/src/lifecycle.rs index 110079e..3a5337b 100644 --- a/hive-c0re/src/lifecycle.rs +++ b/hive-c0re/src/lifecycle.rs @@ -101,10 +101,11 @@ pub async fn spawn( proposed_dir: &Path, applied_dir: &Path, claude_dir: &Path, + dashboard_port: u16, ) -> Result<()> { validate(name)?; setup_proposed(proposed_dir, name).await?; - setup_applied(applied_dir, name, hyperhive_flake).await?; + setup_applied(applied_dir, name, hyperhive_flake, dashboard_port).await?; ensure_claude_dir(claude_dir)?; let container = container_name(name); let flake_ref = format!("{}#default", applied_dir.display()); @@ -145,9 +146,10 @@ pub async fn rebuild( agent_dir: &Path, applied_dir: &Path, claude_dir: &Path, + dashboard_port: u16, ) -> Result<()> { validate(name)?; - setup_applied(applied_dir, name, hyperhive_flake).await?; + setup_applied(applied_dir, name, hyperhive_flake, dashboard_port).await?; ensure_claude_dir(claude_dir)?; let container = container_name(name); let flake_ref = format!("{}#default", applied_dir.display()); @@ -205,7 +207,12 @@ pub async fn setup_proposed(proposed_dir: &Path, name: &str) -> Result<()> { /// Maintain the authoritative applied repo. Rewrites `flake.nix` every call /// (so a new hyperhive flake URL propagates on rebuild); seeds `agent.nix` /// only on first call. `apply_commit` overwrites `agent.nix` later. -pub async fn setup_applied(applied_dir: &Path, name: &str, hyperhive_flake: &str) -> Result<()> { +pub async fn setup_applied( + applied_dir: &Path, + name: &str, + hyperhive_flake: &str, + dashboard_port: u16, +) -> Result<()> { std::fs::create_dir_all(applied_dir) .with_context(|| format!("create {}", applied_dir.display()))?; @@ -242,6 +249,7 @@ pub async fn setup_applied(applied_dir: &Path, name: &str, hyperhive_flake: &str systemd.services.{service}.environment = {{ HIVE_PORT = "{port}"; HIVE_LABEL = "{name}"; + HIVE_DASHBOARD_PORT = "{dashboard_port}"; }}; }} ]; diff --git a/hive-c0re/src/main.rs b/hive-c0re/src/main.rs index d27ac4c..9de9bee 100644 --- a/hive-c0re/src/main.rs +++ b/hive-c0re/src/main.rs @@ -85,7 +85,7 @@ async fn main() -> Result<()> { db, dashboard_port, } => { - let coord = Arc::new(Coordinator::open(&db, hyperhive_flake)?); + let coord = Arc::new(Coordinator::open(&db, hyperhive_flake, dashboard_port)?); manager_server::start(coord.clone())?; // Auto-create the manager container if it isn't there yet. Block // on this — without hm1nd the system has no manager harness. diff --git a/hive-c0re/src/server.rs b/hive-c0re/src/server.rs index afbc2f1..4e6705f 100644 --- a/hive-c0re/src/server.rs +++ b/hive-c0re/src/server.rs @@ -72,6 +72,7 @@ async fn dispatch(req: &HostRequest, coord: Arc) -> HostResponse { &proposed_dir, &applied_dir, &claude_dir, + coord.dashboard_port, ) .await { @@ -110,6 +111,7 @@ async fn dispatch(req: &HostRequest, coord: Arc) -> HostResponse { &agent_dir, &applied_dir, &claude_dir, + coord.dashboard_port, ) .await?; HostResponse::success()