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.
The Stage 2 editor split dropped the old confirm() — Remove program/service/job
deleted immediately on click. Restored confirmation on all three (FormFooter
window.confirm with a per-type message).
Removing a program that still has services/jobs referencing it would orphan
their program: ref. Now blocked:
- API: DELETE /config/programs/{name} → 409 'Programs with active jobs or
services cannot be removed. Delete these first: …' (authoritative).
- App: ProgramFields shows the blocked reason instead of a Remove button when
the program has deployments.
- CLI: castle delete refuses a program with referencing deployments not named
the same (which would survive the delete dangling).
Verified live: DELETE program lakehouse → 409. api 52 / cli 24 green; build clean.
The config editor read service/job detail 'manifest' as the castle.yaml spec
but the endpoint returned the flat *runtime registry* shape for deployed
entries — so service fields (port/health/proxy) never prefilled and a save
would 422/corrupt. Detail 'manifest' is now always the editable castle.yaml
ServiceSpec/JobSpec when the entry is declared there; the flat registry shape
is a display-only fallback for entries that exist only at runtime.
Also:
- ServiceSummary/JobSummary gain run_target (what it runs) + program ref;
ServiceSummary gains proxy_host.
- ProgramSummary gains services[]/jobs[] — the deployments referencing a
program (a program → 0-N services/jobs), for linking + create prefill.
- App types updated to match; deleted orphaned DeploymentEditor/AddDeployment
(leftovers from the removed ConfigEditor).
Verified live: service manifest nested (expose.http.internal.port, proxy.caddy
.host prefill), run_target=lakehoused, program lakehouse → services:[lakehouse].
api 52 green; ruff + app build clean.
Convergent deploy (prefix-based prune): castle deploy now removes orphaned
castle-* systemd units/timers no longer in the registry. Full deploy only;
partial (--name) deploys preserve siblings. Fixes the orphaned-timer / stale-
path drift class.
Container runner: derive --name from the service name (castle-<name>) instead
of the image name; skip the <PREFIX>_DATA_DIR env injection for containers
(avoids colliding with image env namespaces like NEO4J_*).
Unified active lifecycle (core/lifecycle.py): install/uninstall keep their
names but become activate/deactivate, dispatching by behavior — tool→PATH,
daemon/self-serving-frontend/job→systemd, static frontend→served. is_active
reports a uniform state (PATH-independent for tools). The CLI install/uninstall
and service enable/disable route through the extracted core.
API + dashboard: surface a uniform `active` state on programs alongside
`installed`; ProgramDetail shows active/inactive.
Tests: test_deploy_prune (6), test_lifecycle (5). 168 tests pass; lint clean.
- Extract all deploy logic from CLI into castle-core/deploy.py as a reusable deploy() function
* Resolves services/jobs to DeployedComponents
* Generates systemd units and Caddyfile
* Copies frontend assets and reloads systemd
* Returns DeployResult with deployed count and messages
- Simplify CLI deploy command to a thin wrapper calling castle_core.deploy()
- Add POST /deploy endpoint to castle-api for remote deployment management
* Supports optional {name} body param to deploy one or all services
* Returns deployment result as JSON
- Register deploy router in castle-api main.py
- Update README with new deploy endpoint documentation
This enables full remote management: build, test, lint, and deploy any castle
node over HTTP (e.g. POST http://node:9000/api/deploy)
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.
Relocates the POST /programs/{name}/{action} endpoint from the tools
router to a new programs_router for better API documentation
organization. Program lifecycle actions (build, test, lint, etc.) now
appear under 'programs' in the API docs instead of 'tools'.
Move all instance state into ~/.castle/ with clear separation:
- code/ — user project source (was components/)
- artifacts/ — generated specs, built content (was generated/, static/)
- data/ — service runtime data (was /data/castle/)
- secrets/ — credentials
castle.yaml moves to ~/.castle/castle.yaml as the canonical location.
Source paths use repo: prefix for git repo programs (castle-api, app)
and relative paths for user projects (code/central-context).
Add idempotent install.sh for bootstrapping infrastructure (Docker,
Caddy, MQTT, Postgres, Neo4j) with interactive service detection.
Remove components/ from repo (now in ~/.castle/code/), deinit
submodules, remove .gitmodules.
- 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