feat: unified gateway route table (static · proxy · remote)

The gateway does two things — reverse-proxy services and serve static
frontends — but the dashboard/API route table only ever showed the proxy
routes, so 'serving a frontend' and 'proxying a service' looked like unrelated
features and castle-app/power-graph-app were invisible in the route view.

Now there's one concept: a route maps an address (path '/foo' or host 'foo.lan')
to a target of one kind — static (a built dist served by file_server), proxy
(a local service port), or remote (a service on another node).

- core: caddyfile.py gains compute_routes() — the single source of truth for
  the route list; generate_caddyfile_from_registry renders it (output byte-for-
  byte identical, verified against the live Caddyfile). Adds a GatewayRoute
  dataclass.
- api: GatewayRoute model → {address, kind, target, name, node}; GET /gateway
  builds from compute_routes (incl. config for static frontends + mesh for
  remote), so the table matches what Caddy actually does.
- app: Gateway panel shows Address · Kind · Target for every route (static
  frontends + host routes now appear); program detail shows 'Reachable at
  /foo/ · served (static)' for static frontends.
- cli: 'castle gateway status' prints the full route table.
- docs: registry.md/design.md/CLAUDE.md describe routes as one concept with
  three target kinds.

core 94 / cli 24 / api 52 green; ruff + app build clean; Caddyfile unchanged.
This commit is contained in:
2026-06-14 17:48:35 -07:00
parent 7314b5cddb
commit c40f84104d
11 changed files with 304 additions and 194 deletions

View File

@@ -221,16 +221,31 @@ expose:
health_path: /health # Used by health polling
```
### `proxy` — How to proxy it
### `proxy` — How the gateway routes to it
```yaml
proxy:
caddy:
path_prefix: /my-service # Proxied at gateway:9000/my-service/
path_prefix: /my-service # reachable at gateway:9000/my-service/
host: my-service.lan # …or by hostname (whole host → backend root)
```
Castle generates a Caddyfile from these entries. Only needed for services
accessible through the gateway.
Castle generates the Caddyfile from these entries. Only needed for services
reachable through the gateway.
**Gateway routes — one concept, three target kinds.** The gateway (`:9000`) maps
a public **address** (a path prefix `/foo`, or a host `foo.lan`) to a **target**:
| Kind | Target | Declared by |
|------|--------|-------------|
| **proxy** | a local service on a port — Caddy `reverse_proxy localhost:PORT` | a service's `proxy.caddy` |
| **remote** | a service on another node — `reverse_proxy host:PORT` | mesh discovery |
| **static** | a built frontend's `dist/` — Caddy `file_server` (no process) | a `frontend` program with `build.outputs` and **no** service (implicit; served at `/<name>/`, `castle-app` at `/`) |
"Serving a frontend" and "proxying a service" are the same thing — a route —
differing only in whether the target is files on disk or a live process. The
complete table (all kinds) is shown by `castle gateway status`, the dashboard
Gateway panel, and `GET /gateway`; the Caddyfile is generated from it.
### `manage` — How to manage it