fix(#1112): build agent nav-links from disk; drop broken TCP proxy

get_agent_links was proxying to http://127.0.0.1:{port}/api/state.
Since all agents now bind exclusively to a unix socket (HIVE_WEB_SOCKET
set unconditionally in harness-base.nix), the TCP fetch always fails
silently, returning [] — so the nav-strip icons on every card disappear.

Replace with container_view::build_nav_links(), which reconstructs the
same link list from disk:
  - stats.html always (container kind)
  - /{name} and /agent-configs/{name} when forge-token is present (forge kind)
  - extras from hyperhive-dashboard-links.json (external kind)

The GUI screen link is intentionally omitted — /etc/hyperhive/gui.json
lives inside the agent container and is not host-visible. GUI agents
are rare; the omission is acceptable until a host-visible marker lands.

No new dependencies. reqwest is no longer used in dashboard.rs for
this handler (still used by forge.rs and hivectl.rs).
This commit is contained in:
iris 2026-06-03 09:41:12 +02:00 committed by mara
commit cd3ba24c3d
2 changed files with 58 additions and 39 deletions

View file

@ -26,7 +26,7 @@ use tokio_stream::{Stream, StreamExt};
use tower_http::services::ServeDir;
use crate::actions;
use crate::container_view::{ContainerView, claude_has_session};
use crate::container_view::{self, ContainerView, claude_has_session};
use crate::coordinator::Coordinator;
use crate::lifecycle::{self, MANAGER_NAME};
@ -2122,47 +2122,15 @@ async fn post_schedule_cancel(
/// Failure modes (agent down, slow response, malformed JSON) all
/// degrade to an empty list so the dashboard still renders.
async fn get_agent_links(AxumPath(name): AxumPath<String>) -> Response {
// Format-only guard. GET routes return empty gracefully on
// unknown names; bad format we reject early so the downstream
// port-hash + HTTP fetch never sees garbage.
// Format-only guard. Unknown/malformed names get an empty list.
if validate_agent_name(&name).is_some() {
return axum::Json(serde_json::json!([])).into_response();
}
let port = lifecycle::agent_web_port(&name);
let url = format!("http://127.0.0.1:{port}/api/state");
let client = match reqwest::Client::builder()
.timeout(std::time::Duration::from_secs(2))
.build()
{
Ok(c) => c,
Err(e) => {
tracing::warn!(%name, error = %e, "agent-links proxy: client build failed");
return axum::Json(serde_json::json!([])).into_response();
}
};
match client.get(&url).send().await {
Ok(resp) if resp.status().is_success() => match resp.json::<serde_json::Value>().await {
Ok(body) => {
let links = body
.get("links")
.cloned()
.unwrap_or_else(|| serde_json::json!([]));
axum::Json(links).into_response()
}
Err(e) => {
tracing::debug!(%name, error = %e, "agent-links proxy: response parse failed");
axum::Json(serde_json::json!([])).into_response()
}
},
Ok(resp) => {
tracing::debug!(%name, status = %resp.status(), "agent-links proxy: non-2xx");
axum::Json(serde_json::json!([])).into_response()
}
Err(e) => {
tracing::debug!(%name, error = %e, "agent-links proxy: fetch failed");
axum::Json(serde_json::json!([])).into_response()
}
}
// Links are built from disk — no TCP call to the agent web UI.
// The old TCP proxy broke when all agents switched to unix-socket
// binding (HIVE_WEB_SOCKET). See container_view::build_nav_links
// for the full rationale.
axum::Json(container_view::build_nav_links(&name)).into_response()
}
async fn post_cancel_reminder(