diff --git a/app/src/lib/labels.ts b/app/src/lib/labels.ts index dff049c..a5e92ce 100644 --- a/app/src/lib/labels.ts +++ b/app/src/lib/labels.ts @@ -50,7 +50,7 @@ export function stackLabel(stack: string): string { /** * Full URL for a service exposed at .. The domain is - * derived from the dashboard's own host (it is served at castle-app.), so + * derived from the dashboard's own host (it is served at castle.), so * this returns null when the dashboard is on a bare host (off mode, no subdomains). */ export function subdomainUrl(subdomain: string): string | null { diff --git a/app/src/pages/ProgramDetail.tsx b/app/src/pages/ProgramDetail.tsx index c8d5842..d0fcf75 100644 --- a/app/src/pages/ProgramDetail.tsx +++ b/app/src/pages/ProgramDetail.tsx @@ -1,7 +1,7 @@ import { useState } from "react" import { useParams } from "react-router-dom" 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 { ConfigPanel } from "@/components/detail/ConfigPanel" import { DeploymentsSection } from "@/components/detail/DeploymentsSection" @@ -28,14 +28,12 @@ export function ProgramDetailPage() { ) } - // A static frontend (frontend behavior, build outputs, no service) is served - // by the gateway in place — show where. + // A static frontend (frontend behavior, build outputs, no service) is served by + // the gateway in place at its own subdomain — 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}/` + ? (subdomainUrl(deployment.id) ?? `${deployment.id}.`) : null return ( diff --git a/app/src/services/api/client.ts b/app/src/services/api/client.ts index 19507ce..9e758d0 100644 --- a/app/src/services/api/client.ts +++ b/app/src/services/api/client.ts @@ -1,5 +1,5 @@ // Resolve the castle-api base URL. The gateway serves each service at its own -// subdomain (.), so when the dashboard runs at castle-app. +// subdomain (.), so when the dashboard runs at castle. // the API lives at castle-api. — a cross-origin call (castle-api allows // CORS *). When served at a bare host (dev, or the off-mode :9000 gateway), the // API is reachable same-origin at /api. diff --git a/core/src/castle_core/generators/caddyfile.py b/core/src/castle_core/generators/caddyfile.py index caa2fc5..08aab09 100644 --- a/core/src/castle_core/generators/caddyfile.py +++ b/core/src/castle_core/generators/caddyfile.py @@ -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 # names are the subdomains they're published at in acme mode, and the pair served # on the : site in off mode (no domain → no subdomains). -_DASHBOARD = "castle-app" +_DASHBOARD = "castle" _API = "castle-api" diff --git a/core/tests/test_caddyfile.py b/core/tests/test_caddyfile.py index f51c4d7..5bb6aa5 100644 --- a/core/tests/test_caddyfile.py +++ b/core/tests/test_caddyfile.py @@ -87,7 +87,7 @@ class TestAcmeMode: def test_port_9000_redirects_to_dashboard(self) -> None: cf = generate_caddyfile_from_registry(_acme({"api": _dep(9020, expose=True, name="api")})) 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: cf = generate_caddyfile_from_registry(_acme({"api": _dep(9020, expose=True, name="api")})) @@ -113,7 +113,7 @@ class TestAcmeMode: cfg = _config( services={}, programs={ - "castle-app": ProgramSpec( + "castle": ProgramSpec( behavior="frontend", source="/data/repos/castle/app", build=BuildSpec(outputs=["dist"]), ) @@ -121,7 +121,7 @@ class TestAcmeMode: ) monkeypatch.setattr(config_mod, "load_config", lambda *a, **k: cfg) 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 "try_files {path} /index.html" in cf assert "file_server" in cf diff --git a/docs/design.md b/docs/design.md index d3b93f4..e8a6ef8 100644 --- a/docs/design.md +++ b/docs/design.md @@ -242,7 +242,7 @@ Three interfaces expose the registry: and managing programs. - **API** (`castle-api`) — For programmatic access over HTTP. Used by 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. ### Coordination Layer @@ -307,7 +307,7 @@ Local paths always take precedence. ### 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 (`/dist/`) at the root `/`. It talks to `castle-api` via the gateway proxy at `/api`. @@ -344,7 +344,7 @@ Components · Software catalog Name Stack Behavior Schedule Status pdf2md Python / CLI tool — installed protonmail Python / CLI tool */5 * * * * installed - castle-app React / Vite frontend — — + castle React / Vite frontend — — backup-collect Python / CLI tool 0 2 * * * — ``` @@ -455,7 +455,7 @@ $CASTLE_HOME/ ← Config & artifacts (default ~/.castle) │ │ ├── Caddyfile │ │ └── registry.yaml ← Node config (what's deployed here) │ └── content/ ← Built frontend assets -│ └── castle-app/ ← (index.html + assets, served at root) +│ └── castle/ ← (index.html + assets, served at root) └── secrets/ ← Secret files (NAME → value) └── PROTONMAIL_API_KEY @@ -531,7 +531,7 @@ What exists today: - **Gateway** — Caddy on port 9000, Caddyfile generated from registry - **API** — `castle-api` on port 9020, reads from registry (optional 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 (`/dist/`) - **Services** — central-context (content storage), notification-bridge (desktop notification forwarder) @@ -576,7 +576,7 @@ What doesn't exist yet: | Node config | `$CASTLE_HOME/artifacts/specs/registry.yaml` | Active | | CLI | castle (Python, uv) | 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 | | Node packaging | pnpm | Active | | Linting | ruff (Python), ESLint (TS) | Active | diff --git a/docs/registry.md b/docs/registry.md index 17fc214..75c3b30 100644 --- a/docs/registry.md +++ b/docs/registry.md @@ -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 `GET /gateway`; the Caddyfile is generated from it. -**The dashboard and its API.** `castle-app` (the dashboard frontend) and -`castle-api` are just two such subdomains (`castle-app.`, -`castle-api.`); the dashboard calls the API **cross-origin** (castle-api -allows CORS `*`). The bare gateway port (`:9000`) redirects to the dashboard -subdomain. On a node with **no domain** (`gateway.tls: off`), there are no -subdomains, so `:9000` serves just the control plane — the dashboard at `/` plus a -`/api` reverse-proxy to castle-api — and other services stay port-only. +**The dashboard and its API.** `castle` (the dashboard frontend) and `castle-api` +are just two such subdomains (`castle.`, `castle-api.`); the +dashboard calls the API **cross-origin** (castle-api allows CORS `*`). The bare +gateway port (`:9000`) redirects to the dashboard subdomain. On a node with **no +domain** (`gateway.tls: off`), there are no subdomains, so `:9000` serves just the +control plane — the dashboard at `/` plus a `/api` reverse-proxy to castle-api — +and other services stay port-only. #### Host routes need DNS, and the gateway is HTTP-only diff --git a/docs/stacks/react-vite.md b/docs/stacks/react-vite.md index 8c0b6cb..e3b0521 100644 --- a/docs/stacks/react-vite.md +++ b/docs/stacks/react-vite.md @@ -69,11 +69,10 @@ export default defineConfig({ }) ``` -> **Serving behind the gateway.** A static frontend mounts at `//` (the -> root `castle-app` at `/`). Castle's `react-vite` build passes that prefix as -> `VITE_BASE`, so a frontend that reads it (above) works at its subpath with no -> manual `base`. Frontends that don't read `VITE_BASE` must hand-set `base` to -> match, or they'll request assets from the wrong path. +> **Serving behind the gateway.** A static frontend is served at its own subdomain +> — `.` — rooted at `/`, so `VITE_BASE` is always `/`. Castle's +> `react-vite` build passes `VITE_BASE=/`, and a `vite.config` that reads it (above) +> works unchanged. (Frontends are no longer mounted under a `//` path.) ## Project layout @@ -159,7 +158,7 @@ services: http: internal: { port: 5173 } proxy: - caddy: { path_prefix: /app } + caddy: {} # expose the dev server at my-frontend. ``` 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. The flow: -1. `castle deploy` runs the program's `build.commands` (so `dist/` is current). +1. You build the frontend with `castle program build ` (deploy does **not** + build — it only points Caddy at `dist/`). 2. The Caddyfile generator emits a route per `behavior: frontend` program that has `build.outputs`, rooted **in place** at `/` — - no copy. A static frontend mounts at `//`; `castle-app` is - special-cased to serve at the root `/`. + no copy. Each frontend is served at its own subdomain `.` + (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` -that reads it (see [Vite config](#vite-config)) emits asset URLs that resolve at -the subpath. The generated block (in `~/.castle/artifacts/specs/Caddyfile`): +The build is run with `VITE_BASE=/`, so a `vite.config` that reads it (see +[Vite config](#vite-config)) emits root-relative asset URLs. The generated block +(in `~/.castle/artifacts/specs/Caddyfile`) is a host matcher inside the +`*.` site: ```caddyfile -handle_path /my-frontend/* { +@host_my_frontend host my-frontend.example.com +handle @host_my_frontend { root * /data/repos/my-frontend/dist try_files {path} /index.html file_server @@ -190,8 +192,8 @@ handle_path /my-frontend/* { 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, -letting React Router handle client-side routes. The serving prefix is derived -from the program name, not hand-configured. +letting React Router handle client-side routes. The subdomain is the program name, +not hand-configured. ## API integration