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

@ -202,6 +202,57 @@ pub fn claude_has_session(dir: &Path) -> bool {
.any(|e| e.file_type().is_ok_and(|t| t.is_file()))
}
/// Build the navigation link list for an agent's dashboard card.
///
/// Mirrors the logic in `hive_ag3nt::web_ui::agent_links` but runs on
/// the host via disk reads — no network call to the agent web UI needed.
/// The GUI screen link is omitted here; `/etc/hyperhive/gui.json` lives
/// inside the agent container and is not host-visible.
///
/// Returns a `serde_json::Value` array matching the `AgentLink` JSON
/// shape the harness returns from `GET /api/state`, so
/// `dashboard::get_agent_links` can serve it directly.
pub fn build_nav_links(name: &str) -> serde_json::Value {
let state_dir = Coordinator::agent_notes_dir(name);
let mut links: Vec<serde_json::Value> = Vec::new();
// Stats page — always present.
links.push(serde_json::json!({
"url": "stats.html",
"icon": "📊",
"label": "stats",
"kind": "container",
}));
// Forge profile + config mirror — only when the agent has a forge account.
if state_dir.join("forge-token").is_file() {
links.push(serde_json::json!({
"url": format!("/{name}"),
"icon": "",
"label": "forge",
"kind": "forge",
}));
links.push(serde_json::json!({
"url": format!("/agent-configs/{name}"),
"icon": "",
"label": "config",
"kind": "forge",
}));
}
// Agent-declared extras (absolute URLs — passed through verbatim).
for lnk in read_dashboard_links(name) {
links.push(serde_json::json!({
"url": lnk.url,
"icon": lnk.icon,
"label": lnk.label,
"kind": "external",
}));
}
serde_json::Value::Array(links)
}
/// Read agent-declared extra dashboard links from
/// `{state_dir}/hyperhive-dashboard-links.json`. Returns an empty vec when
/// the file is absent, empty, or unparseable — best-effort, never panics.