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:
@@ -844,40 +844,41 @@ async def get_status() -> StatusResponse:
|
||||
|
||||
@router.get("/gateway", response_model=GatewayInfo)
|
||||
def get_gateway() -> GatewayInfo:
|
||||
"""Get gateway configuration summary."""
|
||||
"""Get gateway configuration summary, including the full route table.
|
||||
|
||||
Routes are computed by the same function that generates the Caddyfile, so
|
||||
the table matches reality: static-served frontends, path/host proxies, and
|
||||
cross-node routes all appear, each tagged with its kind and target.
|
||||
"""
|
||||
from castle_core.generators.caddyfile import compute_routes
|
||||
|
||||
registry = get_registry()
|
||||
deployed_count = len(registry.deployed)
|
||||
service_count = sum(1 for d in registry.deployed.values() if d.port is not None)
|
||||
managed_count = sum(1 for d in registry.deployed.values() if d.managed)
|
||||
|
||||
# Local routes
|
||||
routes: list[GatewayRoute] = [
|
||||
config = None
|
||||
root = get_castle_root()
|
||||
if root:
|
||||
try:
|
||||
from castle_core.config import load_config
|
||||
|
||||
config = load_config(root)
|
||||
except FileNotFoundError:
|
||||
pass
|
||||
|
||||
remote = {h: r.registry for h, r in mesh_state.all_nodes().items()}
|
||||
routes = [
|
||||
GatewayRoute(
|
||||
path=d.proxy_path,
|
||||
target_port=d.port,
|
||||
program=name,
|
||||
node=registry.node.hostname,
|
||||
address=r.address,
|
||||
kind=r.kind,
|
||||
target=r.target,
|
||||
name=r.name,
|
||||
node=r.node or registry.node.hostname,
|
||||
)
|
||||
for name, d in registry.deployed.items()
|
||||
if d.proxy_path and d.port
|
||||
for r in compute_routes(registry, config, remote or None)
|
||||
]
|
||||
|
||||
# Remote routes from mesh (local paths take precedence)
|
||||
local_paths = {r.path for r in routes}
|
||||
for hostname, remote in mesh_state.all_nodes().items():
|
||||
for name, d in remote.registry.deployed.items():
|
||||
if d.proxy_path and d.port and d.proxy_path not in local_paths:
|
||||
routes.append(
|
||||
GatewayRoute(
|
||||
path=d.proxy_path,
|
||||
target_port=d.port,
|
||||
program=name,
|
||||
node=hostname,
|
||||
)
|
||||
)
|
||||
|
||||
routes.sort(key=lambda r: r.path)
|
||||
|
||||
return GatewayInfo(
|
||||
port=registry.node.gateway_port,
|
||||
hostname=registry.node.hostname,
|
||||
|
||||
Reference in New Issue
Block a user