From 414168dc43d374d302dcc42ea02c701ea92afd35 Mon Sep 17 00:00:00 2001 From: Paul Payne Date: Mon, 15 Jun 2026 20:25:20 -0700 Subject: [PATCH] ui: Apply (deploy & restart) after a service/job config edit MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Editing a service/job only persisted to castle.yaml; the unit isn't regenerated and the process isn't restarted (the Restart button just runs 'systemctl restart' on the *existing* unit), so a change like a new port appeared saved but never took effect. The config panel now, after saving a service/job, shows an amber 'not live yet' banner with an Apply button that runs POST /deploy {name} (regenerate unit + Caddyfile) then, for services, POST /services/{name}/restart — so the edit actually goes live. Programs are unaffected (no deploy/restart needed). App builds clean. --- app/src/components/detail/ConfigPanel.tsx | 71 ++++++++++++++++++++--- 1 file changed, 62 insertions(+), 9 deletions(-) diff --git a/app/src/components/detail/ConfigPanel.tsx b/app/src/components/detail/ConfigPanel.tsx index 0178796..9b03526 100644 --- a/app/src/components/detail/ConfigPanel.tsx +++ b/app/src/components/detail/ConfigPanel.tsx @@ -1,7 +1,7 @@ import { useState } from "react" import { useNavigate } from "react-router-dom" import { useQueryClient } from "@tanstack/react-query" -import { Check } from "lucide-react" +import { Check, RefreshCw } from "lucide-react" import { apiClient } from "@/services/api/client" import type { AnyDetail, ProgramDetail, ServiceDetail, JobDetail } from "@/types" import { ProgramFields } from "./ProgramFields" @@ -18,12 +18,23 @@ export function ConfigPanel({ deployment, configSection, onRefetch }: ConfigPane const navigate = useNavigate() const qc = useQueryClient() const [message, setMessage] = useState<{ type: "ok" | "error"; text: string } | null>(null) + // A deployment edit only persists to castle.yaml; the unit/process still need + // a deploy (regenerate) + restart to actually take effect. + const [pendingApply, setPendingApply] = useState(false) + const [applying, setApplying] = useState(false) + const isDeployment = configSection !== "programs" const handleSave = async (compName: string, config: Record) => { setMessage(null) try { await apiClient.put(`/config/${configSection}/${compName}`, { config }) - setMessage({ type: "ok", text: "Saved to castle.yaml" }) + setMessage({ + type: "ok", + text: isDeployment + ? "Saved to castle.yaml — not live yet; apply to deploy & restart." + : "Saved to castle.yaml", + }) + if (isDeployment) setPendingApply(true) onRefetch() qc.invalidateQueries({ queryKey: [configSection] }) } catch (e: unknown) { @@ -32,6 +43,32 @@ export function ConfigPanel({ deployment, configSection, onRefetch }: ConfigPane } } + const handleApply = async () => { + setApplying(true) + setMessage(null) + try { + await apiClient.post(`/deploy`, { name: deployment.id }) + if (configSection === "services") { + await apiClient.post(`/services/${deployment.id}/restart`, {}) + } + setPendingApply(false) + setMessage({ type: "ok", text: "Applied — the change is now live." }) + qc.invalidateQueries({ queryKey: ["status"] }) + qc.invalidateQueries({ queryKey: [configSection] }) + onRefetch() + } catch (e: unknown) { + let msg = e instanceof Error ? e.message : String(e) + try { + msg = JSON.parse((e as Error).message).detail ?? msg + } catch { + /* keep msg */ + } + setMessage({ type: "error", text: `Apply failed: ${msg}` }) + } finally { + setApplying(false) + } + } + const handleDelete = async (compName: string) => { try { await apiClient.delete(`/config/${configSection}/${compName}`) @@ -45,18 +82,34 @@ export function ConfigPanel({ deployment, configSection, onRefetch }: ConfigPane return ( <> - {message && ( + {(message || pendingApply) && (
- {message.type === "ok" && } - {message.text} + {message?.type === "ok" && !pendingApply && } + {message?.text} + {pendingApply && ( + + )}
)}