feat: deployment-scoped requires + UI editors, gateway nav, services filter

Model:
- Drop `requires` from ProgramSpec; requires is now deployment-only.
  Collapse Requirement to {ref, bind} with kind defaulting to "deployment"
  (drop the unused `version`). System-package preconditions stay on the
  program as `system_dependencies`, synthesized into {kind: system} for the
  functional check. relations.py/deploy.py read only the deployment's requires.
- create.py seeds a stack's substrate dependency onto the deployment, not the
  program. Docs + tests updated.

UI:
- Add a `requires` editor (useRequires) on service, job, and static detail
  pages — edit service→service deps (ref + optional bind env var).
- Restore the gateway route table "Open" column: a kind-aware in-app link to
  each route's deployment.
- Reformat the reach control: name baked into each option
  (localhost:PORT (local) / <name>.<domain> (internal) / <name>.<pubdomain> (public)).
- Services page: sort statics in with systemd services by name, add a
  search + kind-filter bar mirroring the programs page.
This commit is contained in:
2026-07-06 04:00:24 -07:00
parent 20bf78caf1
commit e438f45b54
13 changed files with 329 additions and 132 deletions

View File

@@ -503,18 +503,12 @@ def _target_url(config: CastleConfig, target_name: str) -> str | None:
return _public_url(config, target_name, getattr(dep, "http_exposed", False), tport)
def _requires_env(
config: CastleConfig, dep: DeploymentSpec, config_key: str
) -> dict[str, str]:
"""Env generated FROM a deployment's ``requires`` — a ``{kind: deployment,
bind: VAR}`` requirement sets ``VAR`` to the target's URL. Env is derived from
the dependency, never scraped back into one (see docs/relationships.md)."""
prog = config.programs.get(config_key)
reqs = list(getattr(dep, "requires", []) or [])
if prog:
reqs += list(prog.requires)
def _requires_env(config: CastleConfig, dep: DeploymentSpec) -> dict[str, str]:
"""Env generated FROM a deployment's ``requires`` — a ``{ref, bind: VAR}``
requirement sets ``VAR`` to the target deployment's URL. Env is derived from the
dependency, never scraped back into one (see docs/relationships.md)."""
out: dict[str, str] = {}
for r in reqs:
for r in getattr(dep, "requires", []) or []:
if r.kind == "deployment" and r.bind:
url = _target_url(config, r.ref)
if url:
@@ -676,7 +670,7 @@ def _build_deployed(
raw_env = dict(dep.defaults.env) if (dep.defaults and dep.defaults.env) else {}
# Env generated from `requires` ({kind: deployment, bind: VAR} → target URL).
# An explicit defaults.env value always wins — a hand-set var is never clobbered.
for var, url in _requires_env(config, dep, config_key).items():
for var, url in _requires_env(config, dep).items():
raw_env.setdefault(var, url)
public_url = _public_url(config, name, expose, port)
ctx = _env_context(