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 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