Rename the dashboard program castle-app -> castle
The dashboard is now served at castle.<domain> (was castle-app.<domain>). Since the subdomain is the program name, this is just a program rename plus the one special-case constant. - Generator: _DASHBOARD = "castle" (the :9000 redirect target and off-mode root). - Program file renamed (runtime): programs/castle-app.yaml -> programs/castle.yaml (source unchanged: repo:app). - Dashboard UI: ProgramDetail shows a frontend's subdomain (via subdomainUrl), dropping the old castle-app-at-root special-case; comment refs updated. - Docs: registry.md/design.md/react-vite.md updated to the subdomain model and the `castle` name (react-vite.md also corrected — frontends serve at their subdomain root with VITE_BASE=/, and builds are manual via `castle program build`, not deploy).
This commit is contained in:
@@ -50,7 +50,7 @@ export function stackLabel(stack: string): string {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Full URL for a service exposed at <subdomain>.<gateway.domain>. The domain is
|
* Full URL for a service exposed at <subdomain>.<gateway.domain>. The domain is
|
||||||
* derived from the dashboard's own host (it is served at castle-app.<domain>), so
|
* derived from the dashboard's own host (it is served at castle.<domain>), so
|
||||||
* this returns null when the dashboard is on a bare host (off mode, no subdomains).
|
* this returns null when the dashboard is on a bare host (off mode, no subdomains).
|
||||||
*/
|
*/
|
||||||
export function subdomainUrl(subdomain: string): string | null {
|
export function subdomainUrl(subdomain: string): string | null {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { useState } from "react"
|
import { useState } from "react"
|
||||||
import { useParams } from "react-router-dom"
|
import { useParams } from "react-router-dom"
|
||||||
import { useProgram, useEventStream } from "@/services/api/hooks"
|
import { useProgram, useEventStream } from "@/services/api/hooks"
|
||||||
import { runnerLabel } from "@/lib/labels"
|
import { runnerLabel, subdomainUrl } from "@/lib/labels"
|
||||||
import { DetailHeader } from "@/components/detail/DetailHeader"
|
import { DetailHeader } from "@/components/detail/DetailHeader"
|
||||||
import { ConfigPanel } from "@/components/detail/ConfigPanel"
|
import { ConfigPanel } from "@/components/detail/ConfigPanel"
|
||||||
import { DeploymentsSection } from "@/components/detail/DeploymentsSection"
|
import { DeploymentsSection } from "@/components/detail/DeploymentsSection"
|
||||||
@@ -28,14 +28,12 @@ export function ProgramDetailPage() {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// A static frontend (frontend behavior, build outputs, no service) is served
|
// A static frontend (frontend behavior, build outputs, no service) is served by
|
||||||
// by the gateway in place — show where.
|
// the gateway in place at its own subdomain — show where.
|
||||||
const buildOutputs = (deployment.manifest.build as { outputs?: string[] } | undefined)?.outputs
|
const buildOutputs = (deployment.manifest.build as { outputs?: string[] } | undefined)?.outputs
|
||||||
const servedAt =
|
const servedAt =
|
||||||
deployment.behavior === "frontend" && deployment.services.length === 0 && buildOutputs?.length
|
deployment.behavior === "frontend" && deployment.services.length === 0 && buildOutputs?.length
|
||||||
? deployment.id === "castle-app"
|
? (subdomainUrl(deployment.id) ?? `${deployment.id}.<gateway.domain>`)
|
||||||
? "/"
|
|
||||||
: `/${deployment.id}/`
|
|
||||||
: null
|
: null
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// Resolve the castle-api base URL. The gateway serves each service at its own
|
// Resolve the castle-api base URL. The gateway serves each service at its own
|
||||||
// subdomain (<name>.<domain>), so when the dashboard runs at castle-app.<domain>
|
// subdomain (<name>.<domain>), so when the dashboard runs at castle.<domain>
|
||||||
// the API lives at castle-api.<domain> — a cross-origin call (castle-api allows
|
// the API lives at castle-api.<domain> — a cross-origin call (castle-api allows
|
||||||
// CORS *). When served at a bare host (dev, or the off-mode :9000 gateway), the
|
// CORS *). When served at a bare host (dev, or the off-mode :9000 gateway), the
|
||||||
// API is reachable same-origin at /api.
|
// API is reachable same-origin at /api.
|
||||||
|
|||||||
@@ -161,7 +161,7 @@ def _host_static_block(label: str, host: str, serve_dir: str) -> list[str]:
|
|||||||
# Castle's own control plane: the dashboard frontend and the API it calls. These
|
# Castle's own control plane: the dashboard frontend and the API it calls. These
|
||||||
# names are the subdomains they're published at in acme mode, and the pair served
|
# names are the subdomains they're published at in acme mode, and the pair served
|
||||||
# on the :<port> site in off mode (no domain → no subdomains).
|
# on the :<port> site in off mode (no domain → no subdomains).
|
||||||
_DASHBOARD = "castle-app"
|
_DASHBOARD = "castle"
|
||||||
_API = "castle-api"
|
_API = "castle-api"
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -87,7 +87,7 @@ class TestAcmeMode:
|
|||||||
def test_port_9000_redirects_to_dashboard(self) -> None:
|
def test_port_9000_redirects_to_dashboard(self) -> None:
|
||||||
cf = generate_caddyfile_from_registry(_acme({"api": _dep(9020, expose=True, name="api")}))
|
cf = generate_caddyfile_from_registry(_acme({"api": _dep(9020, expose=True, name="api")}))
|
||||||
assert ":9000 {" in cf
|
assert ":9000 {" in cf
|
||||||
assert "redir https://castle-app.example.com{uri}" in cf
|
assert "redir https://castle.example.com{uri}" in cf
|
||||||
|
|
||||||
def test_no_path_routes(self) -> None:
|
def test_no_path_routes(self) -> None:
|
||||||
cf = generate_caddyfile_from_registry(_acme({"api": _dep(9020, expose=True, name="api")}))
|
cf = generate_caddyfile_from_registry(_acme({"api": _dep(9020, expose=True, name="api")}))
|
||||||
@@ -113,7 +113,7 @@ class TestAcmeMode:
|
|||||||
cfg = _config(
|
cfg = _config(
|
||||||
services={},
|
services={},
|
||||||
programs={
|
programs={
|
||||||
"castle-app": ProgramSpec(
|
"castle": ProgramSpec(
|
||||||
behavior="frontend", source="/data/repos/castle/app",
|
behavior="frontend", source="/data/repos/castle/app",
|
||||||
build=BuildSpec(outputs=["dist"]),
|
build=BuildSpec(outputs=["dist"]),
|
||||||
)
|
)
|
||||||
@@ -121,7 +121,7 @@ class TestAcmeMode:
|
|||||||
)
|
)
|
||||||
monkeypatch.setattr(config_mod, "load_config", lambda *a, **k: cfg)
|
monkeypatch.setattr(config_mod, "load_config", lambda *a, **k: cfg)
|
||||||
cf = generate_caddyfile_from_registry(_acme({}))
|
cf = generate_caddyfile_from_registry(_acme({}))
|
||||||
assert "@host_castle_app host castle-app.example.com" in cf
|
assert "@host_castle host castle.example.com" in cf
|
||||||
assert "root * /data/repos/castle/app/dist" in cf
|
assert "root * /data/repos/castle/app/dist" in cf
|
||||||
assert "try_files {path} /index.html" in cf
|
assert "try_files {path} /index.html" in cf
|
||||||
assert "file_server" in cf
|
assert "file_server" in cf
|
||||||
|
|||||||
@@ -242,7 +242,7 @@ Three interfaces expose the registry:
|
|||||||
and managing programs.
|
and managing programs.
|
||||||
- **API** (`castle-api`) — For programmatic access over HTTP. Used by
|
- **API** (`castle-api`) — For programmatic access over HTTP. Used by
|
||||||
the dashboard, other nodes, and remote agents.
|
the dashboard, other nodes, and remote agents.
|
||||||
- **Dashboard** (`castle-app`) — For human discoverability. Visual
|
- **Dashboard** (`castle`) — For human discoverability. Visual
|
||||||
overview of what's running, health status, logs.
|
overview of what's running, health status, logs.
|
||||||
|
|
||||||
### Coordination Layer
|
### Coordination Layer
|
||||||
@@ -307,7 +307,7 @@ Local paths always take precedence.
|
|||||||
|
|
||||||
### Dashboard
|
### Dashboard
|
||||||
|
|
||||||
The web dashboard (`castle-app`) is a React SPA served by Caddy in place from
|
The web dashboard (`castle`) is a React SPA served by Caddy in place from
|
||||||
its repo build output (`<source>/dist/`) at the root `/`. It talks to
|
its repo build output (`<source>/dist/`) at the root `/`. It talks to
|
||||||
`castle-api` via the gateway proxy at `/api`.
|
`castle-api` via the gateway proxy at `/api`.
|
||||||
|
|
||||||
@@ -344,7 +344,7 @@ Components · Software catalog
|
|||||||
Name Stack Behavior Schedule Status
|
Name Stack Behavior Schedule Status
|
||||||
pdf2md Python / CLI tool — installed
|
pdf2md Python / CLI tool — installed
|
||||||
protonmail Python / CLI tool */5 * * * * installed
|
protonmail Python / CLI tool */5 * * * * installed
|
||||||
castle-app React / Vite frontend — —
|
castle React / Vite frontend — —
|
||||||
backup-collect Python / CLI tool 0 2 * * * —
|
backup-collect Python / CLI tool 0 2 * * * —
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -455,7 +455,7 @@ $CASTLE_HOME/ ← Config & artifacts (default ~/.castle)
|
|||||||
│ │ ├── Caddyfile
|
│ │ ├── Caddyfile
|
||||||
│ │ └── registry.yaml ← Node config (what's deployed here)
|
│ │ └── registry.yaml ← Node config (what's deployed here)
|
||||||
│ └── content/ ← Built frontend assets
|
│ └── content/ ← Built frontend assets
|
||||||
│ └── castle-app/ ← (index.html + assets, served at root)
|
│ └── castle/ ← (index.html + assets, served at root)
|
||||||
└── secrets/ ← Secret files (NAME → value)
|
└── secrets/ ← Secret files (NAME → value)
|
||||||
└── PROTONMAIL_API_KEY
|
└── PROTONMAIL_API_KEY
|
||||||
|
|
||||||
@@ -531,7 +531,7 @@ What exists today:
|
|||||||
- **Gateway** — Caddy on port 9000, Caddyfile generated from registry
|
- **Gateway** — Caddy on port 9000, Caddyfile generated from registry
|
||||||
- **API** — `castle-api` on port 9020, reads from registry (optional
|
- **API** — `castle-api` on port 9020, reads from registry (optional
|
||||||
castle.yaml fallback for non-deployed programs)
|
castle.yaml fallback for non-deployed programs)
|
||||||
- **Dashboard** — `castle-app` React/Vite frontend, static assets
|
- **Dashboard** — `castle` React/Vite frontend, static assets
|
||||||
served in place from its repo build output (`<source>/dist/`)
|
served in place from its repo build output (`<source>/dist/`)
|
||||||
- **Services** — central-context (content storage), notification-bridge
|
- **Services** — central-context (content storage), notification-bridge
|
||||||
(desktop notification forwarder)
|
(desktop notification forwarder)
|
||||||
@@ -576,7 +576,7 @@ What doesn't exist yet:
|
|||||||
| Node config | `$CASTLE_HOME/artifacts/specs/registry.yaml` | Active |
|
| Node config | `$CASTLE_HOME/artifacts/specs/registry.yaml` | Active |
|
||||||
| CLI | castle (Python, uv) | Active |
|
| CLI | castle (Python, uv) | Active |
|
||||||
| API | castle-api (FastAPI) | Active |
|
| API | castle-api (FastAPI) | Active |
|
||||||
| Dashboard | castle-app (React, Vite, shadcn/ui) | Active |
|
| Dashboard | castle (React, Vite, shadcn/ui) | Active |
|
||||||
| Python packaging | uv | Active |
|
| Python packaging | uv | Active |
|
||||||
| Node packaging | pnpm | Active |
|
| Node packaging | pnpm | Active |
|
||||||
| Linting | ruff (Python), ESLint (TS) | Active |
|
| Linting | ruff (Python), ESLint (TS) | Active |
|
||||||
|
|||||||
@@ -301,13 +301,13 @@ route — differing only in whether the target is files on disk or a live proces
|
|||||||
The table is shown by `castle gateway status`, the dashboard Gateway panel, and
|
The table is shown by `castle gateway status`, the dashboard Gateway panel, and
|
||||||
`GET /gateway`; the Caddyfile is generated from it.
|
`GET /gateway`; the Caddyfile is generated from it.
|
||||||
|
|
||||||
**The dashboard and its API.** `castle-app` (the dashboard frontend) and
|
**The dashboard and its API.** `castle` (the dashboard frontend) and `castle-api`
|
||||||
`castle-api` are just two such subdomains (`castle-app.<domain>`,
|
are just two such subdomains (`castle.<domain>`, `castle-api.<domain>`); the
|
||||||
`castle-api.<domain>`); the dashboard calls the API **cross-origin** (castle-api
|
dashboard calls the API **cross-origin** (castle-api allows CORS `*`). The bare
|
||||||
allows CORS `*`). The bare gateway port (`:9000`) redirects to the dashboard
|
gateway port (`:9000`) redirects to the dashboard subdomain. On a node with **no
|
||||||
subdomain. On a node with **no domain** (`gateway.tls: off`), there are no
|
domain** (`gateway.tls: off`), there are no subdomains, so `:9000` serves just the
|
||||||
subdomains, so `:9000` serves just the control plane — the dashboard at `/` plus a
|
control plane — the dashboard at `/` plus a `/api` reverse-proxy to castle-api —
|
||||||
`/api` reverse-proxy to castle-api — and other services stay port-only.
|
and other services stay port-only.
|
||||||
|
|
||||||
#### Host routes need DNS, and the gateway is HTTP-only
|
#### Host routes need DNS, and the gateway is HTTP-only
|
||||||
|
|
||||||
|
|||||||
@@ -69,11 +69,10 @@ export default defineConfig({
|
|||||||
})
|
})
|
||||||
```
|
```
|
||||||
|
|
||||||
> **Serving behind the gateway.** A static frontend mounts at `/<name>/` (the
|
> **Serving behind the gateway.** A static frontend is served at its own subdomain
|
||||||
> root `castle-app` at `/`). Castle's `react-vite` build passes that prefix as
|
> — `<name>.<gateway.domain>` — rooted at `/`, so `VITE_BASE` is always `/`. Castle's
|
||||||
> `VITE_BASE`, so a frontend that reads it (above) works at its subpath with no
|
> `react-vite` build passes `VITE_BASE=/`, and a `vite.config` that reads it (above)
|
||||||
> manual `base`. Frontends that don't read `VITE_BASE` must hand-set `base` to
|
> works unchanged. (Frontends are no longer mounted under a `/<name>/` path.)
|
||||||
> match, or they'll request assets from the wrong path.
|
|
||||||
|
|
||||||
## Project layout
|
## Project layout
|
||||||
|
|
||||||
@@ -159,7 +158,7 @@ services:
|
|||||||
http:
|
http:
|
||||||
internal: { port: 5173 }
|
internal: { port: 5173 }
|
||||||
proxy:
|
proxy:
|
||||||
caddy: { path_prefix: /app }
|
caddy: {} # expose the dev server at my-frontend.<gateway.domain>
|
||||||
```
|
```
|
||||||
|
|
||||||
See @docs/registry.md for the full registry reference.
|
See @docs/registry.md for the full registry reference.
|
||||||
@@ -170,18 +169,21 @@ For production, the static build output is served by Caddy rather than a Node
|
|||||||
process. You do **not** write this block by hand — `castle deploy` generates it.
|
process. You do **not** write this block by hand — `castle deploy` generates it.
|
||||||
The flow:
|
The flow:
|
||||||
|
|
||||||
1. `castle deploy` runs the program's `build.commands` (so `dist/` is current).
|
1. You build the frontend with `castle program build <name>` (deploy does **not**
|
||||||
|
build — it only points Caddy at `dist/`).
|
||||||
2. The Caddyfile generator emits a route per `behavior: frontend` program that
|
2. The Caddyfile generator emits a route per `behavior: frontend` program that
|
||||||
has `build.outputs`, rooted **in place** at `<source>/<build.outputs[0]>` —
|
has `build.outputs`, rooted **in place** at `<source>/<build.outputs[0]>` —
|
||||||
no copy. A static frontend mounts at `/<name>/`; `castle-app` is
|
no copy. Each frontend is served at its own subdomain `<name>.<gateway.domain>`
|
||||||
special-cased to serve at the root `/`.
|
(the dashboard, `castle`, is also the target of the `:9000` redirect).
|
||||||
|
|
||||||
The build is run with `VITE_BASE` set to that serve prefix, so a `vite.config`
|
The build is run with `VITE_BASE=/`, so a `vite.config` that reads it (see
|
||||||
that reads it (see [Vite config](#vite-config)) emits asset URLs that resolve at
|
[Vite config](#vite-config)) emits root-relative asset URLs. The generated block
|
||||||
the subpath. The generated block (in `~/.castle/artifacts/specs/Caddyfile`):
|
(in `~/.castle/artifacts/specs/Caddyfile`) is a host matcher inside the
|
||||||
|
`*.<domain>` site:
|
||||||
|
|
||||||
```caddyfile
|
```caddyfile
|
||||||
handle_path /my-frontend/* {
|
@host_my_frontend host my-frontend.example.com
|
||||||
|
handle @host_my_frontend {
|
||||||
root * /data/repos/my-frontend/dist
|
root * /data/repos/my-frontend/dist
|
||||||
try_files {path} /index.html
|
try_files {path} /index.html
|
||||||
file_server
|
file_server
|
||||||
@@ -190,8 +192,8 @@ handle_path /my-frontend/* {
|
|||||||
|
|
||||||
The `try_files {path} /index.html` directive is essential for SPA routing —
|
The `try_files {path} /index.html` directive is essential for SPA routing —
|
||||||
it falls back to `index.html` for any path that doesn't match a static file,
|
it falls back to `index.html` for any path that doesn't match a static file,
|
||||||
letting React Router handle client-side routes. The serving prefix is derived
|
letting React Router handle client-side routes. The subdomain is the program name,
|
||||||
from the program name, not hand-configured.
|
not hand-configured.
|
||||||
|
|
||||||
## API integration
|
## API integration
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user