app: cross-link entity pages along graph edges

- service/static/tool/job detail pages now link back to their program;
  backfill the program ref on registry-only static+job details (the
  registry Deployment carries no program ref).
- new RelatedDeployments card surfaces a deployment's requires edges both
  directions (Depends on / Required by) as links; references render inert.
- replace the service detail Subdomain/Served-at rows with a launch icon
  in the header that opens the gateway URL in a new tab.
This commit is contained in:
2026-07-07 13:27:46 -07:00
parent 5beebfd739
commit ea59d846bc
5 changed files with 179 additions and 46 deletions

View File

@@ -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,