diff --git a/config/Service.pkl b/config/Service.pkl index 51e54fb872d6804b9a9e8b5b8e93d6f0cefcaaad..70b148abeb46ec9ecec2f7e480ba1174c743c2f9 100644 --- a/config/Service.pkl +++ b/config/Service.pkl @@ -8,6 +8,9 @@ const nomadHostPort = "{{ if regexMatch \":\" .Address }}[{{ .Address }}]{{ else class Metadata { name: String tagline: String = "" + launcher: Boolean = true + /// Discovery group; absent uses the public route's auth role. + access: String? } /// One HTTP listener and its public route. @@ -156,6 +159,8 @@ output { id = module.id name = module.meta.name tagline = module.meta.tagline + launcher = module.meta.launcher + access = module.meta.access rollout = module.rollout stageIsolation = module.stageIsolation healthyDeadline = vmStartupGrace ?? module.healthyDeadline diff --git a/dashboard/src/core.rs b/dashboard/src/core.rs index 57874837de2ab6649afc84047b40062a40359543..535723c74c5915a4e3ba5f12b3229d8d9941fe87 100644 --- a/dashboard/src/core.rs +++ b/dashboard/src/core.rs @@ -350,7 +350,7 @@ async fn summarize(app: Arc, id: &str, found: &Value) -> Value { } }) .sum(); - json!({"id":id,"name":meta["studio_name"].as_str().unwrap_or(id),"health":found["health"].as_str().unwrap_or("down"),"url":meta["studio_hostname"].as_str().filter(|h| !h.is_empty()).map(|h| format!("https://{h}")),"icon":icon_of(app.clone(),id).await,"access":meta["studio_auth_role"],"tagline":meta["studio_tagline"],"cpu":usage["cpu"],"cpuLimit":if mhz > 0.0 { cpu/mhz } else {0.0},"memory":usage["memory"],"memoryLimit":memory*1048576.0}) + json!({"id":id,"name":meta["studio_name"].as_str().unwrap_or(id),"health":found["health"].as_str().unwrap_or("down"),"url":meta["studio_hostname"].as_str().filter(|h| !h.is_empty() && meta["studio_launcher"] != "false").map(|h| format!("https://{h}")),"icon":icon_of(app.clone(),id).await,"access":meta["studio_auth_role"],"tagline":meta["studio_tagline"],"cpu":usage["cpu"],"cpuLimit":if mhz > 0.0 { cpu/mhz } else {0.0},"memory":usage["memory"],"memoryLimit":memory*1048576.0}) } async fn summaries(app: Arc) -> Result> { let state = app.clone(); @@ -432,21 +432,13 @@ async fn issues(app: Arc) -> Result { .cache .peek("nomad-scan", Duration::from_secs(10)) .ok_or_else(|| Error::new(503, "Service status is unavailable."))?; - let ack = read_json(&app.data.join("restarts-acknowledged.json"), json!({})).await?; let mut issues = Vec::new(); for service in array(&summaries.value) { let id = string(&service["id"]); let health = string(&service["health"]); let found = &jobs.value[id]; - let cs = containers(found); - let restart = array(&cs) - .iter() - .map(|c| &c["lastRestart"]) - .filter(|r| !r.is_null() && (ack[id].is_null() || number(&r["t"]) > number(&ack[id]))) - .max_by(|a, b| number(&a["t"]).total_cmp(&number(&b["t"]))); - let trouble = health == "down" || health == "degraded"; - if trouble || restart.is_some() { - issues.push(json!({"service":{"id":id,"name":service["name"],"icon":service["icon"],"url":service["url"]},"health":health,"failing":if trouble {array(&found["allocs"]).iter().flat_map(|a| array(&a["home_checks"])).find(|c| c["Status"] == "failure").map(|c| c["Output"].clone())} else {None},"restart":restart})); + if matches!(health, "down" | "degraded") { + issues.push(json!({"service":{"id":id,"name":service["name"],"icon":service["icon"],"url":service["url"]},"health":health,"failing":array(&found["allocs"]).iter().flat_map(|a| array(&a["home_checks"])).find(|c| c["Status"] == "failure").map(|c| c["Output"].clone())})); } } Ok(json!(issues)) @@ -493,9 +485,9 @@ async fn launcher_module(repo: &Path) -> Result { } module.push_str(&format!("local services = Map({})\n", services.join(", "))); module.push_str(r#"output { renderer = new JsonRenderer { omitNullProperties = false } -value = services.filter((_, s) -> s.enabled).mapValues((_, s) -> +value = services.filter((_, s) -> s.enabled && s.meta.launcher).mapValues((_, s) -> let (route = (s.containers?.toMap()?.values ?? List()).add(s.container).filterNonNull().map((task) -> task.http).filterNonNull().findOrNull((http) -> http.hostname != null)) -new Dynamic { name = s.meta.name; tagline = s.meta.tagline; hostname = route?.hostname; access = route?.authRole }) }"#); +new Dynamic { name = s.meta.name; tagline = s.meta.tagline; hostname = route?.hostname; access = s.meta.access ?? route?.authRole }) }"#); Ok(module) } @@ -690,7 +682,13 @@ pub async fn route( && can_open(&groups, a["access"].as_str()) }) }) - .cloned() + .map(|issue| { + let mut issue = issue.clone(); + if !admin { + issue["failing"] = Value::Null; + } + issue + }) .collect::>() }); let sample = host::sample(app).await?; diff --git a/dashboard/web/components/Sidebar.tsx b/dashboard/web/components/Sidebar.tsx index 1dc3b93c9029f4f8b92a1190b1d61e583349a90c..ba6df3cdc734a03274d694706dae4a0459ae1a83 100644 --- a/dashboard/web/components/Sidebar.tsx +++ b/dashboard/web/components/Sidebar.tsx @@ -15,7 +15,7 @@ import UserRound from "lucide-solid/icons/user-round"; import Plug from "lucide-solid/icons/plug"; import Users from "lucide-solid/icons/users"; import { createSignal, For, type JSX, Show } from "solid-js"; -import { type Health, type Me, type Section, trouble, VIEW_AS } from "../types/model.ts"; +import { type Health, type Me, type Section, VIEW_AS } from "../types/model.ts"; import { queries } from "../api.ts"; import snowflake from "../snowflake.svg"; import { bytes, cores, plural } from "../format.ts"; @@ -75,7 +75,7 @@ function IssueCount() { - `${issue.service.name} ${trouble(issue.health) ? issue.health : "restarted"}`).join(", ")}> + `${issue.service.name} ${issue.health}`).join(", ")}> issue.health === "down") ? "down" : "degraded"}>{issues().length} need a look @@ -223,7 +223,7 @@ export function viewAs(groups: string[] | null) { location.reload(); } -/** The signed-in user's corner; it falls back to the forwarded name while Keycloak is out of reach. */ +/** The signed-in user's corner. */ function Whoami(props: { me: Me }) { const user = lastGood(account); const name = () => { @@ -237,12 +237,14 @@ function Whoami(props: { me: Me }) {