- 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.
The confirm dialog (window.confirm in FormFooter) now spells out exactly what
cascade will do — uninstall the tool from PATH / drop the static route / stop,
disable and remove named service+job deployments, and remove the catalog entry —
each as its own bullet, ending with the reassurance that source on disk is kept.
A program and its 1:1 tool/static deployment are one thing to the user, so
'Delete program' now just works instead of refusing. DELETE /config/programs/
{name}?cascade=true tears down each referencing deployment (manager-aware:
uninstall a tool from PATH, stop+disable a service/job, drop a static route),
removes them and the program, then converges the runtime (prune orphan units,
regenerate the Caddyfile, reload the gateway). Without cascade it still 409s
(safe API default). The dashboard drops the 'can't remove while deployed' block,
labels the action 'Delete program', and the confirm names exactly what will go.
Verified live: throwaway tool → cascade delete removes program + path deployment,
real config untouched; plain delete of a referenced program still 409s.
castle-api 57 passed.
- Gateway route table: a public route's address now links to its public URL
(green, with the tunnel icon) instead of the internal one.
- Program detail: remove the editable `behavior` selector — behavior is a derived
label now and drives no logic, so editing it was meaningless (the stored value
is preserved on save via the manifest spread).
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.
- 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.