+ )
+}
+
+function RelatedRow({ related }: { related: Related }) {
+ const inner = (
+ <>
+ {related.name}
+ {related.kind && }
+ {related.bind && (
+ · {related.bind}
+ )}
+ >
+ )
+ // A local deployment resolves to a detail page; a reference or bare ref (no
+ // detail route) is shown inert — there's nothing on this node to navigate to.
+ if (!related.kind || !NAVIGABLE.has(related.kind)) {
+ return
{inner}
+ }
+ return (
+
+ {inner}
+
+ )
+}
diff --git a/app/src/pages/ScheduledDetail.tsx b/app/src/pages/ScheduledDetail.tsx
index a6cb944..b2c60a0 100644
--- a/app/src/pages/ScheduledDetail.tsx
+++ b/app/src/pages/ScheduledDetail.tsx
@@ -1,11 +1,12 @@
-import { useParams } from "react-router-dom"
-import { Clock } from "lucide-react"
+import { useParams, Link } from "react-router-dom"
+import { Clock, Package } from "lucide-react"
import { useJob } from "@/services/api/hooks"
import { LogViewer } from "@/components/LogViewer"
import { DetailHeader } from "@/components/detail/DetailHeader"
import { ServiceControls } from "@/components/detail/ServiceControls"
import { SystemdPanel } from "@/components/detail/SystemdPanel"
import { ConfigPanel } from "@/components/detail/ConfigPanel"
+import { RelatedDeployments } from "@/components/detail/RelatedDeployments"
export function ScheduledDetailPage() {
const { name } = useParams<{ name: string }>()
@@ -39,20 +40,32 @@ export function ScheduledDetailPage() {
- {deployment.schedule && (
-
+
+
diff --git a/app/src/pages/ServiceDetail.tsx b/app/src/pages/ServiceDetail.tsx
index 6507ecb..53557fc 100644
--- a/app/src/pages/ServiceDetail.tsx
+++ b/app/src/pages/ServiceDetail.tsx
@@ -8,6 +8,7 @@ import { DetailHeader } from "@/components/detail/DetailHeader"
import { ServiceControls } from "@/components/detail/ServiceControls"
import { SystemdPanel } from "@/components/detail/SystemdPanel"
import { ConfigPanel } from "@/components/detail/ConfigPanel"
+import { RelatedDeployments } from "@/components/detail/RelatedDeployments"
export function ServiceDetailPage() {
const { name } = useParams<{ name: string }>()
@@ -35,8 +36,11 @@ export function ServiceDetailPage() {
// A static is a caddy-served site, not a systemd unit — no start/stop, no logs;
// it shows its served URL and the dir it serves instead of a port/launcher.
const isStatic = deployment.kind === "static" || deployment.manager === "caddy"
- const servedUrl = subdomainUrl(deployment.subdomain ?? deployment.id)
const root = (deployment.manifest?.root as string | undefined) ?? undefined
+ // The gateway address to launch: a static is always served there (falls back to
+ // its id); a systemd service only when it's actually proxied (has a subdomain).
+ const launchLabel = deployment.subdomain ?? (isStatic ? deployment.id : undefined)
+ const launchUrl = launchLabel ? subdomainUrl(launchLabel) : null
return (
)
diff --git a/castle-api/src/castle_api/routes.py b/castle-api/src/castle_api/routes.py
index ec1eed4..7bf89a6 100644
--- a/castle-api/src/castle_api/routes.py
+++ b/castle-api/src/castle_api/routes.py
@@ -518,6 +518,10 @@ def get_service(name: str) -> ServiceDetail:
# Serve the editable spec (reach/root/program) when it's in castle.yaml.
spec = config.deployment(deployed.kind, name) if config is not None else None
if spec is not None:
+ # The registry Deployment carries no `program` ref, so backfill it from
+ # the spec — lets the detail page link back to the program (statics too).
+ if summary.program is None:
+ summary.program = getattr(spec, "program", None)
manifest = spec.model_dump(mode="json", exclude_none=True)
else:
manifest = {
@@ -622,6 +626,12 @@ def get_job(name: str) -> JobDetail:
summary = _job_from_deployed(name, deployed)
if config is not None and summary.source is None:
summary.source = _backfill_source(name, config)
+ # The registry Deployment carries no `program` ref — backfill from the spec
+ # so the job page can link back to its program (matches the service path).
+ if summary.program is None and config is not None:
+ spec = config.deployment("job", name)
+ if spec is not None:
+ summary.program = getattr(spec, "program", None)
manifest = {
"manager": deployed.manager,
"launcher": deployed.launcher,