authorgravatar for git@paperclover.netclover caruso <git@paperclover.net> 2026-10-04 21:28:12-07:00
committergravatar for git@paperclover.netclover caruso <git@paperclover.net> 2026-10-04 21:28:29-07:00
logdf1c802f8e4c1f91746ce7761590af6849d12d41
tree01ea44df004e73b1ee8e773b754dea84d3e58f8f
parentd16b9185f320c372908c7c4bdc6fba091e403b19
signature Signed by SSH key SHA256:52mNGHRsVFBDED9IAX5pe+LRWUefqTbxEReunq21QvU

Preserve metrics and logs shortcuts for grouped Victoria

Expose the grouped app routes from the canonical service definitions so its metrics shortcut still reaches VictoriaMetrics and deployment logs still reach VictoriaLogs. Assisted-by: gpt-6.1-sol

4 files changed, 9 insertions(+), 7 deletions(-)

dashboard/src/core.rs+5-3
...@@ -486,8 +486,10 @@ async fn launcher_module(repo: &Path) -> Result<String> {...@@ -486,8 +486,10 @@ async fn launcher_module(repo: &Path) -> Result<String> {
486 module.push_str(&format!("local services = Map({})\n", services.join(", ")));486 module.push_str(&format!("local services = Map({})\n", services.join(", ")));
487 module.push_str(r#"output { renderer = new JsonRenderer { omitNullProperties = false }487 module.push_str(r#"output { renderer = new JsonRenderer { omitNullProperties = false }
488value = services.filter((_, s) -> s.enabled && s.meta.launcher).mapValues((_, s) ->488value = services.filter((_, s) -> s.enabled && s.meta.launcher).mapValues((_, s) ->
489let (route = (s.containers?.toMap()?.values ?? List()).add(s.container).filterNonNull().map((task) -> task.http).filterNonNull().findOrNull((http) -> http.hostname != null))489let (tasks = s.containers?.toMap() ?? Map("app", s.container))
490new Dynamic { name = s.meta.name; tagline = s.meta.tagline; hostname = route?.hostname; access = s.meta.access ?? route?.authRole }) }"#);490let (routes = tasks.filter((_, task) -> task?.http?.hostname != null).mapValues((_, task) -> task.http))
491let (route = routes.values.firstOrNull)
492new Dynamic { name = s.meta.name; tagline = s.meta.tagline; hostname = route?.hostname; access = s.meta.access ?? route?.authRole; urls = routes.mapValues((_, http) -> "https://\(http.hostname)") }) }"#);
491 Ok(module)493 Ok(module)
492}494}
493495
...@@ -502,7 +504,7 @@ async fn launcher(app: Arc<App>) -> Result<Arc<Document>> {...@@ -502,7 +504,7 @@ async fn launcher(app: Arc<App>) -> Result<Arc<Document>> {
502 if let Some(host) = value["hostname"].as_str() {504 if let Some(host) = value["hostname"].as_str() {
503 let path = std::path::Path::new(file);505 let path = std::path::Path::new(file);
504 let id = if path.file_name().unwrap() == "service.pkl" { path.parent().unwrap().file_name().unwrap() } else { path.file_stem().unwrap() }.to_string_lossy();506 let id = if path.file_name().unwrap() == "service.pkl" { path.parent().unwrap().file_name().unwrap() } else { path.file_stem().unwrap() }.to_string_lossy();
505 apps.push(json!({"id":id,"name":value["name"],"tagline":value["tagline"].as_str().filter(|s| !s.is_empty()),"url":format!("https://{host}"),"access":value["access"],"icon":icon_of(state.clone(),&id).await}));507 apps.push(json!({"id":id,"name":value["name"],"tagline":value["tagline"].as_str().filter(|s| !s.is_empty()),"url":format!("https://{host}"),"urls":value["urls"],"access":value["access"],"icon":icon_of(state.clone(),&id).await}));
506 }508 }
507 }509 }
508 Ok(json!(apps))510 Ok(json!(apps))
dashboard/web/api.contract.ts+1-1
...@@ -72,7 +72,7 @@ export type Api = Hono<{}, {...@@ -72,7 +72,7 @@ export type Api = Hono<{}, {
72 $get: Endpoint<Me>;72 $get: Endpoint<Me>;
73 };73 };
74 "/launcher": {74 "/launcher": {
75 $get: Endpoint<{ id: string; name: string; tagline: string | null; url: string; icon: { light: string; dark: string; } | null; health: Health | null; }[]>;75 $get: Endpoint<{ id: string; name: string; tagline: string | null; url: string; urls: Record<string, string>; icon: { light: string; dark: string; } | null; health: Health | null; }[]>;
76 };76 };
77 "/status": {77 "/status": {
78 $get: Endpoint<{ load: number; issues: Issue[] | null }>;78 $get: Endpoint<{ load: number; issues: Issue[] | null }>;
dashboard/web/pages/Deploy.tsx+1-1
...@@ -407,7 +407,7 @@ export function Deploy() {...@@ -407,7 +407,7 @@ export function Deploy() {
407407
408 /** The job's lines in the logs app, bounded to the window once its jobs stopped. */408 /** The job's lines in the logs app, bounded to the window once its jobs stopped. */
409 const logsUrl = (job: string, runtime: Runtime | undefined) => {409 const logsUrl = (job: string, runtime: Runtime | undefined) => {
410 const url = service("victoria-logs")?.url;410 const url = service("victoria")?.url;
411 const iso = (t: number) => new Date(t * 1000).toISOString();411 const iso = (t: number) => new Date(t * 1000).toISOString();
412 const window = runtime?.to ? ` _time:[${iso(runtime.from)}, ${iso(runtime.to)}]` : "";412 const window = runtime?.to ? ` _time:[${iso(runtime.from)}, ${iso(runtime.to)}]` : "";
413 return url && `${url}/select/vmui/#/?query=${encodeURIComponent(`{job="${job}"}${window}`)}`;413 return url && `${url}/select/vmui/#/?query=${encodeURIComponent(`{job="${job}"}${window}`)}`;
dashboard/web/pages/Overview.tsx+2-2
...@@ -160,7 +160,7 @@ function Dashboard(props: { me: Me }) {...@@ -160,7 +160,7 @@ function Dashboard(props: { me: Me }) {
160 const name = (id: string) => names()[id] ?? id;160 const name = (id: string) => names()[id] ?? id;
161 const health = (service: { id: string; health: Health | null }) =>161 const health = (service: { id: string; health: Health | null }) =>
162 stream.latest()?.services[service.id]?.health ?? service.health;162 stream.latest()?.services[service.id]?.health ?? service.health;
163 const metrics = () => apps()?.find((app) => app.id === "victoria-metrics");163 const metrics = () => apps()?.find((app) => app.id === "victoria");
164164
165 const [menuId, setMenuId] = createSignal<string>();165 const [menuId, setMenuId] = createSignal<string>();
166 const selectedStage = () => history()?.stages.find((stage) => stage.id === menuId());166 const selectedStage = () => history()?.stages.find((stage) => stage.id === menuId());
...@@ -340,7 +340,7 @@ function Dashboard(props: { me: Me }) {...@@ -340,7 +340,7 @@ function Dashboard(props: { me: Me }) {
340340
341 <div class="section-head">341 <div class="section-head">
342 <h2>machine</h2>342 <h2>machine</h2>
343 <Show when={metrics()}>{(app) => <OpenApp app={app()} href={`${app().url}/vmui/`} />}</Show>343 <Show when={metrics()}>{(app) => <OpenApp app={app()} href={`${app().urls.metrics}/vmui/`} />}</Show>
344 <RangePicker value={range()} onChange={setRange} />344 <RangePicker value={range()} onChange={setRange} />
345 </div>345 </div>
346 <div class="strips">346 <div class="strips">