The field showing a deployment's launcher + target is now labelled 'Launch'
(consistent with launcher/manager vocabulary) on ServiceDetail and the Service/
Job/Create field editors.
- Programs page: each card lists the program's deployments (name · kind) instead
of a single kind badge; kind-filter chips removed (nav is the kind lens now).
protonmail honestly shows tool + job.
- New Tools nav page (Wrench) — programs with a tool deployment (/programs?kind=tool).
- Services page now shows statics too: /services returns service + static, and a
static renders as a caddy-served card (KindBadge, served URL, no start/stop).
- Program detail: DeploymentsSection + deleteConfirm rebuilt over program.deployments;
header drops the (now nonexistent) program kind.
- ServiceFields / JobFields: launcher is now an editable select (python|command|
container|compose|node), persisted into run.launcher via the normal save.
clean pnpm build + tsc --noEmit.
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.
- Add a 'hint' to the shared Field/TextField; annotate the config forms with
short, accurate descriptions of each field:
- program: source (working copy castle builds from), behavior (tool/daemon/
frontend), stack (optional toolchain template), repo/ref (for clone),
system deps (listed for reference, NOT auto-installed), commands.
- service: port (listened-on, health-checked, proxied; map via ${port}),
health path, proxy path vs proxy host, runs.
- job: schedule (cron → systemd timer), runs.
- ProgramFields: the Commands section is now editable (build/test/lint/
type-check/run), so a program with no stack can declare its own dev verbs
by hand — a declared command overrides the stack default. Verified the
commands/build shape round-trips through the config API.
App builds clean.
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 single service-shaped DeploymentFields was reused for all three sections,
so the program page bled service fields (port/proxy/health) and couldn't edit
program config, and the job page couldn't edit its schedule. Replaced it with:
- ProgramFields — description, source, stack, behavior, version, repo/ref,
system deps (+ read-only commands). Edits the program catalog entry.
- ServiceFields — run target, port, port_env, health, proxy path + host,
env/secrets. Prefills correctly now (Stage 1) and round-trips through PUT.
- JobFields — schedule, run target, env/secrets.
ConfigPanel selects the form by section. Shared Field/TextField/useEnvSecrets/
FormFooter helpers in detail/fields.tsx. Retired DeploymentFields. run editing
only touches python/command specs (container/node/remote run blocks preserved).
Verified: GET service manifest → PUT back validates 200. App type-checks/builds.