From 82f12c9d61600d91cf6f6b173dec821e94aa54bc Mon Sep 17 00:00:00 2001 From: Paul Payne Date: Sun, 14 Jun 2026 15:52:57 -0700 Subject: [PATCH] Remove buttons: confirm + block program removal with active deployments MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- app/src/components/detail/JobFields.tsx | 1 + app/src/components/detail/ProgramFields.tsx | 6 ++++++ app/src/components/detail/ServiceFields.tsx | 1 + app/src/components/detail/fields.tsx | 13 +++++++++++-- castle-api/src/castle_api/config_editor.py | 16 +++++++++++++++- cli/src/castle_cli/commands/delete.py | 13 +++++++++++++ 6 files changed, 47 insertions(+), 3 deletions(-) diff --git a/app/src/components/detail/JobFields.tsx b/app/src/components/detail/JobFields.tsx index df17aae..07f2176 100644 --- a/app/src/components/detail/JobFields.tsx +++ b/app/src/components/detail/JobFields.tsx @@ -74,6 +74,7 @@ export function JobFields({ job, onSave, onDelete }: Props) { onSave={handleSave} onDelete={onDelete ? () => onDelete(job.id) : undefined} deleteLabel="Remove job" + confirmMessage={`Remove job "${job.id}" from castle.yaml? Run a deploy afterward to tear down its timer.`} /> ) diff --git a/app/src/components/detail/ProgramFields.tsx b/app/src/components/detail/ProgramFields.tsx index 8e2d3bb..f4eb8ff 100644 --- a/app/src/components/detail/ProgramFields.tsx +++ b/app/src/components/detail/ProgramFields.tsx @@ -95,6 +95,12 @@ export function ProgramFields({ program, onSave, onDelete }: Props) { onSave={handleSave} onDelete={onDelete ? () => onDelete(program.id) : undefined} deleteLabel="Remove program" + confirmMessage={`Remove program "${program.id}" from castle.yaml? (Source on disk is untouched.)`} + deleteBlocked={ + program.services.length + program.jobs.length > 0 + ? "Programs with active jobs or services cannot be removed — delete those first." + : undefined + } /> ) diff --git a/app/src/components/detail/ServiceFields.tsx b/app/src/components/detail/ServiceFields.tsx index aa54b51..9185286 100644 --- a/app/src/components/detail/ServiceFields.tsx +++ b/app/src/components/detail/ServiceFields.tsx @@ -119,6 +119,7 @@ export function ServiceFields({ service, onSave, onDelete }: Props) { onSave={handleSave} onDelete={onDelete ? () => onDelete(service.id) : undefined} deleteLabel="Remove service" + confirmMessage={`Remove service "${service.id}" from castle.yaml? Run a deploy afterward to tear down its unit.`} /> ) diff --git a/app/src/components/detail/fields.tsx b/app/src/components/detail/fields.tsx index ee0ceb6..672a334 100644 --- a/app/src/components/detail/fields.tsx +++ b/app/src/components/detail/fields.tsx @@ -122,18 +122,27 @@ export function FormFooter({ onSave, onDelete, deleteLabel, + confirmMessage, + deleteBlocked, }: { saving: boolean saved: boolean onSave: () => void onDelete?: () => void deleteLabel: string + confirmMessage?: string + /** When set, removal is disallowed and this reason is shown instead of the button. */ + deleteBlocked?: string }) { return (
- {onDelete ? ( + {deleteBlocked ? ( + {deleteBlocked} + ) : onDelete ? (