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:
@@ -28,6 +28,16 @@ export function ProgramDetailPage() {
|
||||
)
|
||||
}
|
||||
|
||||
// A static frontend (frontend behavior, build outputs, no service) is served
|
||||
// by the gateway in place — show where.
|
||||
const buildOutputs = (deployment.manifest.build as { outputs?: string[] } | undefined)?.outputs
|
||||
const servedAt =
|
||||
deployment.behavior === "frontend" && deployment.services.length === 0 && buildOutputs?.length
|
||||
? deployment.id === "castle-app"
|
||||
? "/"
|
||||
: `/${deployment.id}/`
|
||||
: null
|
||||
|
||||
return (
|
||||
<div className="max-w-3xl mx-auto px-6 py-8">
|
||||
<DetailHeader
|
||||
@@ -101,6 +111,14 @@ export function ProgramDetailPage() {
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
{servedAt && (
|
||||
<>
|
||||
<span className="text-[var(--muted)]">Reachable at</span>
|
||||
<a href={servedAt} className="font-mono text-[var(--primary)] hover:underline">
|
||||
{servedAt} <span className="text-[var(--muted)]">· served (static)</span>
|
||||
</a>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{deployment.commands && Object.keys(deployment.commands).length > 0 && (
|
||||
|
||||
Reference in New Issue
Block a user