Register an existing repo as a program from the /programs page — the web
equivalent of `castle program add`. An inline form with an editable,
autocompleting path bar browses the *server's* filesystem (a browser's own
file dialog only sees the client machine) and also accepts a git URL.
- core: extract the adopt logic (target parsing, stack/command detection,
ProgramSpec build) into castle_core.adopt so the CLI and API share it; the
CLI's add.py now delegates to it.
- castle-api: GET /fs/browse (directory listing, per-entry stat errors skipped
so one unreadable child can't 403 the dir) and POST /programs/adopt.
- app: AddProgramForm + Add-program button on the Programs page; useBrowse /
useAdoptProgram hooks.
Also tidy ProgramCard: deployments render below the description, and the
deployment name label is hidden when it matches the program title.
- new Secrets page (/secrets, nav entry): lists vault secrets, reveal/copy,
add/edit/delete, shows active backend + addr + role; read-only on followers
- GET /secrets/info (backend/addr/role/writable); fix /secrets CRUD to use the
castle.yaml-selected backend (was defaulting to the now-empty file backend)
- OpenBaoBackend.list_names drops folder entries (node-prefix groups)
- tests: conftest forces file backend so host castle.yaml can't leak into tests
app builds clean (tsc 0); endpoints verified live against the vault.
Reframes the map around consumes / consumed_by and adds:
- Inspect on select: dim the rest, light a node's consumes/consumed-by, and a
side panel listing both (removable/navigable) with protocol chips.
- Protocol-typed dependency edges (http/pg/bolt/mqtt) + reach badges; TCP infra
now shows its port.
- External resources: a registry lane of reference nodes with an "add external"
form; consumption of one renders as a chip on the consumer (no cross-map line).
- Suggested (undeclared) consumption as dashed amber edges — click to accept.
- Capability/candidates in the panel (alternative providers of the same protocol).
- A "Machines" band rendering other mesh nodes' deployments (read-only).
Adds useSuggestions / useMeshDeployments / useSaveReference hooks and the
GraphEndpoint / GraphSuggestion / MeshDeployment types.
A spatial control surface for the whole node. Tools, jobs, services, and
frontends sit in lanes with program-source grips; edges show requires
(dependency), same-program siblings, and exposure to LAN/Internet targets.
Direct manipulation, each backed by the existing config PUT + apply:
- drag a node to LAN/Internet to set reach (internal/public)
- delete an exposure line or a node (with confirm) to unexpose/remove
- right-click or the per-node ⋯ menu: open, restart, expose, delete
- click the source grip to open the backing program
Nodes are draggable with Shift-/toggle box-select; layout persists to
localStorage with a reset control. Adds useSetReach/useDeleteDeployment
hooks, the @xyflow/react dep, and the nav entry + route.
A derived, mostly-computed model of how programs, deployments, and repos relate,
plus the git-sync surfaces that motivated it. See docs/relationships.md.
Core:
- `requires: [{kind, ref, version?, bind?}]` on programs + deployments — one
precondition relation; `system_dependencies` is its `{kind: system}` alias.
kind fixes meaning + check (system=installed, deployment=exists).
- relations.py: derives repos (git toplevel / monorepo), fan-in, and the
predicates functional?/fresh?/deployed? — nothing stored.
- env is generated FROM a `{kind: deployment, bind}` requirement (target URL →
consumer env), never scraped back into one; explicit defaults.env still wins.
- git.py: working-copy status/pull, repo toplevel + remote url.
Surfaces:
- `castle graph` + GET /graph — the relationship diagnostic.
- GET /repos, /repos/{key}/git|sync — repo-scoped sync (a repo is the sync unit;
a monorepo backs several programs). GET /programs/{name}/git|sync + repo context.
- Dashboard: Graph screen, program-page git status + repo-aware Sync, and a
monorepo banner on Programs.
Governing principle: predicates are derived; encode only the non-derivable, as a
node or edge property. Pull-only sync — converge stays a separate step.
The gateway is a deployment (castle-gateway) — its lifecycle is the same
convergence as everything else, so the bespoke start/stop/reload verbs
(which still routed through the retired enable/disable path) are gone.
- CLI: `castle gateway` is now inspection only — bare or `status` shows
the route table. Start/stop/reload it via `castle apply` (render routes
+ reload) or `castle restart castle-gateway`. Deletes the last users of
`_service_enable`/`_service_disable`, so those are removed too.
- API: retire `POST /gateway/reload` (making routes live is `POST /apply`);
`PUT /gateway/config` message + docstring now say apply, not deploy.
- UI: GatewayPanel's "Reload" button → "Apply" (useApply); drop the
useGatewayReload hook; GatewaySettings points at `castle apply`.
- docs: `castle gateway reload|status` → `castle gateway` (status lens).
core 129 / cli 31 / api 59 pass; dashboard builds clean; live `castle
gateway` shows routes with no start/stop/reload; /gateway/reload removed
from the API.
Rework the dashboard around convergence:
- ServiceControls / ServiceCard / JobCard: replace start/restart/stop
with a Power toggle (set enabled → apply, i.e. activate/deactivate)
plus Restart (the one imperative bounce).
- ToolDetail: install/uninstall becomes an Enable/Disable toggle;
`installed` stays the live state, `enabled` the desired one.
- ConfigPanel Apply and CreateDeploymentForm now POST /apply (one
converge that renders + restarts only what changed) instead of
/deploy + a separate start/restart.
- New ConvergePanel on Overview: a terraform-style Preview (POST /apply
plan=true) showing would-activate/restart/deactivate, then Apply.
- useApply + useSetEnabled hooks; `enabled` added to Service/Job/
Deployment types; both handle the self-restart connection drop.
Backend: PUT /config/deployments/{name}/enabled sets desired on/off
(edit config; caller runs apply) — the declarative toggle the UI uses.
Dashboard builds clean (tsc + vite); 59 API tests pass; live /apply and
/services?enabled verified after an api restart.
Replace the conflated `runner` axis with two orthogonal ones: `manager`
(systemd|caddy|path|none) — who supervises/realizes a deployment — and, for
systemd only, a nested `launcher` (python|command|container|compose|node) — how
the process starts. ServiceSpec and JobSpec collapse into one manager-
discriminated DeploymentSpec union (Systemd/Caddy/Path/Remote); the services/
and jobs/ config dirs collapse into one deployments/ dir. The human "kind"
(service|job|tool|static|reference) is fully derived (kind_for), never stored —
the frontend kind is renamed static. behavior is gone.
- core: DeploymentSpec union + LaunchSpec + kind_for; legacy-aware loader
normalizes old runner shapes; CastleConfig.deployments with derived
services/jobs/tools views; registry.Deployment carries manager/launcher/kind.
- cli: service/job/tool as filtered views + a deployment group; --behavior→--kind,
create --runner→--launcher; lifecycle dispatches over config.deployments.
- castle-api: /deployments primary with /services,/jobs as views; summaries
derive kind; PUT/DELETE /config/deployments/{name} (services/jobs aliased).
- app: KindBadge, frontend→static everywhere, pick-a-kind creation wizard,
per-kind config editors.
- docs: single deployments/ layout, manager/launcher, static kind throughout.
Live migration verified byte-identical: regenerated Caddyfile and every unit
ExecStart line unchanged, so nothing restarted. Suites: core 124, cli 25,
castle-api 55; dashboard build + type-check clean.
Surfaces the DNS/Cloudflare/public state that previously required editing
castle.yaml + curl:
- API: GET /gateway now returns domain, public_domain, tunnel_id,
tunnel_connected, and a public_url per public route; new PUT /gateway/config
edits tls/domain/public_domain/tunnel_id in castle.yaml (deploy to apply).
save_config now persists public_domain/tunnel_id (previously dropped).
- UI: a GatewaySettings row in the Gateway panel shows TLS / domain / public
(tunnel up/down + public_domain) with inline edit; each public route gets a
green "public" badge linking to its <name>.<public_domain> URL.
The dashboard's stack dropdown hardcoded its options (python-cli/fastapi/
react-vite), so a supabase program showed "(none)" — the value had no matching
option. Make the backend the single source of truth instead:
- core: available_stacks() returns sorted(HANDLERS) — the stacks castle has
handlers for.
- castle-api: GET /stacks exposes it.
- cli: --stack choices derive from available_stacks() (drops the duplicated list).
- app: ProgramFields fetches /stacks (useStacks) and renders options from it,
always including the current value so it never silently blanks; StackBadge
gets a Supabase label.
A new backend stack now appears everywhere without a frontend edit.
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).
Collapse the two proxy shapes (path_prefix + host) to a single checkbox: a
service is either port-only, or exposed at <service-name>.<gateway.domain>. Path
routes and their prefix-stripping foot-guns are gone; the subdomain is always the
service name (rename the service to change it).
- Schema: CaddySpec is just `enable` (presence = expose). service_proxy_targets
returns (expose, port, base_url); Deployment carries `subdomain` (was
proxy_path/proxy_host). Registry/deploy/mesh/CLI updated in lockstep.
- Generator: compute_routes emits one host route per exposed service/frontend
(address = name). acme mode = one *.<domain> site with a reverse_proxy matcher
per service + a file_server matcher per frontend; the :<port> site redirects to
the dashboard. off mode = a HTTP control plane on :<port> (dashboard at / +
/api → castle-api); other services are port-only.
- Dashboard/API: castle-app and castle-api are ordinary subdomains. The dashboard
derives the API base at runtime (castle-api.<domain> on a subdomain, else /api)
and calls it cross-origin — castle-api already allows CORS *. Frontends build
with VITE_BASE=/ (served at their subdomain root).
- CLI: `service create` drops --path/--host; --no-proxy makes it port-only.
- Docs/tests reworked for the model; docs use generic placeholders only (no
personal host/domain/IP details).
The dashboard widgets were named after the old 'component' concept. Renamed
each to what it actually renders:
- ComponentCard → ServiceCard (ServiceSummary, /services/, service actions)
- ComponentTable → ProgramTable (ProgramSummary[], the program catalog)
- ComponentEditor→ DeploymentEditor (DeploymentDetail, the unified type)
- ComponentFields→ DeploymentFields (AnyDetail)
- AddComponent → AddDeployment
Also purged the domain term 'component' from the rest of the app: the
useComponent hook → useDeployment, ConfigPanel + the three detail pages'
`component` prop/var → `deployment`, and the gateway/node table headers
(Component → Program / Deployment). Props renamed to match (component →
service / program / deployment). The React UI directory app/src/components/
keeps its conventional name.
App type-checks and builds clean.
The 'tools' concept was just a filtered view of programs and added unnecessary
complexity to the frontend. This commit:
- Removes ToolCard.tsx and Tools.tsx (pages/components never used in router)
- Renames ComponentDetail.tsx → ProgramDetail.tsx (exports ProgramDetailPage)
- Renames ComponentRedirect.tsx → ProgramRedirect.tsx (exports ProgramRedirect)
- Updates imports in routes.tsx to match new file names
- Fixes stale comment in hooks.ts
Frontend naming now aligns with the unified programs model.
Eliminates inconsistency where tools had their own filtered view but daemons and frontends didn't.
Changes:
- API: Removed tools router and ToolSummary/ToolDetail models. Added optional behavior query parameter to GET /programs for filtering by program type (tool, daemon, frontend).
- Frontend: Updated hooks to pass behavior param to usePrograms instead of separate useTools. Updated components to use ProgramSummary type. Removed useToolDetail hook.
- Docs: Updated API documentation to reflect program behavior filtering.
- Updated the terminology from "components" to "programs" across the codebase, including in config loading, saving, and manifest specifications.
- Introduced a new `stacks.py` file to handle lifecycle actions for development stacks, implementing handlers for Python and React Vite stacks.
- Adjusted tests to reflect the new program structure and ensure proper functionality.
- Revised documentation to align with the new terminology and structure, ensuring clarity on the purpose and configuration of programs, services, and jobs.
feat: implement HealthBadge and RoleBadge components for displaying health status and roles
feat: create LogViewer component for streaming logs of services
feat: develop SecretsEditor for managing secrets with CRUD operations
feat: introduce ToolCard component for displaying tool information
style: add global CSS variables and styles for consistent theming
feat: set up API client for handling requests to the backend
feat: implement hooks for fetching components, statuses, and tools
feat: create routes for dashboard, component details, and tools
feat: add service management endpoints for starting, stopping, and restarting services
feat: implement event bus for handling real-time updates via SSE
feat: create health check and logs endpoints for monitoring services
feat: add tests for health and tools endpoints to ensure functionality
chore: update project configuration files and dependencies for better development experience