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

@@ -39,12 +39,12 @@ STACK_BUILD_OUTPUTS: dict[str, str] = {
"react-vite": "dist",
}
# Substrate a stack's apps depend on — seeded as a `requires` at creation so the
# relationship graph shows it. This keeps `stack` uncoupled from the runtime model:
# the stack declares the edge once here; the graph only ever reads the encoded
# Substrate a stack's apps depend on — seeded as a deployment `requires` at creation
# so the relationship graph shows it. This keeps `stack` uncoupled from the runtime
# model: the stack declares the edge once here; the graph only ever reads the encoded
# `requires`, never the stack. See docs/relationships.md.
STACK_REQUIRES: dict[str, list[Requirement]] = {
"supabase": [Requirement(kind="deployment", ref="supabase")],
"supabase": [Requirement(ref="supabase")],
}
@@ -120,13 +120,18 @@ def run_create(args: argparse.Namespace) -> int:
source=str(project_dir),
stack=stack,
build=build,
# Seed the stack's substrate dependency (e.g. supabase) as a real `requires`.
requires=list(STACK_REQUIRES.get(stack or "", [])),
)
# The stack's substrate dependency (e.g. supabase) is a deployment-to-deployment
# `requires` — seeded on the deployment so the relationship graph shows the edge.
seeded_requires = list(STACK_REQUIRES.get(stack or "", []))
if kind == "tool":
# A PATH-managed deployment: installed via `uv tool install`, no unit/route.
config.tools[name] = PathDeployment(
id=name, manager="path", program=name, description=description
id=name,
manager="path",
program=name,
description=description,
requires=seeded_requires,
)
elif kind == "static":
# A caddy-managed static deployment: no systemd unit, served from the build dir.
@@ -136,6 +141,7 @@ def run_create(args: argparse.Namespace) -> int:
program=name,
root=static_root or "dist",
description=description,
requires=seeded_requires,
)
elif kind == "service":
prefix = name.replace("-", "_").upper()
@@ -151,6 +157,7 @@ def run_create(args: argparse.Namespace) -> int:
health_path="/health",
)
),
requires=seeded_requires,
proxy=True, # expose at <name>.<gateway.domain>
manage=ManageSpec(systemd=SystemdSpec()),
# python-fastapi scaffold reads env_prefix MY_SERVICE_ — map castle's