diff --git a/hive-c0re/src/container_view.rs b/hive-c0re/src/container_view.rs index a0d0afb3..f07596bb 100644 --- a/hive-c0re/src/container_view.rs +++ b/hive-c0re/src/container_view.rs @@ -109,9 +109,11 @@ impl From for hive_sh4re::container::AgentStatusRow { /// syntactically, just always empty, until iris's frontend follow-up /// decides whether to drop the column entirely. `port`, `cpu_quota`, /// `memory_max` have no equivalent on the row at all and are simply - /// not carried over. + /// not carried over. `url` has no source on `ContainerView` at all — + /// see [`agent_url`]. fn from(v: ContainerView) -> Self { Self { + url: agent_url(&v.name), name: v.name, running: v.running, failed: v.failed, @@ -128,6 +130,30 @@ impl From for hive_sh4re::container::AgentStatusRow { } } +/// This agent's own web UI, behind the gateway. `None` when the hive's +/// domain isn't configured — same env var, same "empty string counts as +/// unset" filter `server.rs::hive_urls()` uses for `HiveUrls::home`, kept +/// in sync with it by reading the one env var directly rather than +/// threading a resolved domain through `HiveEnv` for a value nothing else +/// needs pre-resolved. +fn agent_url(name: &str) -> Option { + let domain = std::env::var("HYPERHIVE_HIVE_DOMAIN") + .ok() + .filter(|v| !v.is_empty())?; + Some(agent_url_for_domain(&domain, name)) +} + +/// The pure half of [`agent_url`], split out so the path-scheme formatting +/// is testable without an env var in the loop. `/agent//` is the +/// gateway's real path scheme +/// (`nix/host-modules/hive-gateway/vhosts.nix`'s per-agent `agentLocations` +/// block) — computed once here so a client (trollshell's sidebar, the +/// dashboard) never has to derive it and silently drift if that scheme +/// ever changes. +fn agent_url_for_domain(domain: &str, name: &str) -> String { + format!("https://{domain}/agent/{name}/") +} + /// Build the full container list. Wraps `lifecycle::list()` and /// resolves every per-agent attribute the dashboard surfaces. /// @@ -548,5 +574,21 @@ mod tests { // No source for reminders on this side (see the `From` impl's doc // comment) — always the stub, never left uninitialised. assert_eq!(row.pending_reminders, 0); + // `url` depends on process-global env (`HYPERHIVE_HIVE_DOMAIN`), so + // it isn't asserted here — mutating env vars in a test race with + // every other test in this binary. `agent_url_for_domain` below + // covers the actual path-formatting logic without touching env. + } + + /// The pure half of the per-agent URL: given a domain, is the path + /// exactly `/agent//`? (`agent_url` itself, the thin env-reading + /// wrapper, is intentionally untested — see the note on the test + /// above.) + #[test] + fn agent_url_for_domain_uses_the_gateways_real_path_scheme() { + assert_eq!( + super::agent_url_for_domain("hive.example.com", "alice"), + "https://hive.example.com/agent/alice/" + ); } } diff --git a/hive-sh4re/src/container.rs b/hive-sh4re/src/container.rs index 27ace066..3af61f95 100644 --- a/hive-sh4re/src/container.rs +++ b/hive-sh4re/src/container.rs @@ -83,6 +83,16 @@ pub struct AgentStatusRow { /// `None`. #[serde(default, skip_serializing_if = "Option::is_none")] pub status_set_at: Option>, + /// This agent's own web UI, behind the gateway + /// (`https:///agent//`) — the same URL the operator + /// dashboard's "open" action uses. `None` when the hive's domain isn't + /// configured (`HYPERHIVE_HIVE_DOMAIN` unset), same condition + /// `HostRequest::Urls`'s `HiveUrls::home` is `None` under. Present here + /// (not just derivable from `HiveUrls`) so a client never has to build + /// this path itself — see `hive-c0re/container_view.rs`'s `agent_url` + /// for why a client deriving it is the wrong shape to depend on. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub url: Option, } /// One matrix identity an agent can act as, surfaced in `GetAgentMeta`'s