From cd66562e9dbac340f537b6b7c4a005635059b445 Mon Sep 17 00:00:00 2001 From: Vadym Samoilenko Date: Wed, 1 Apr 2026 12:53:45 +0100 Subject: [PATCH] Feat: add deploy widget with one-click deploy buttons - components/widgets/deploy/deploy.jsx: new widget with deploy button, status polling (2s when running, 10s idle), last deploy time display - widget.jsx: register 'deploy' widget type - config/widgets.yaml: add deploy buttons for all 6 server services Deploy API runs as systemd service on host :9000, proxied via Apache at /deploy-api/. Widget polls GET /status/{service} and triggers POST /deploy/{service} on button click. Co-Authored-By: Claude Sonnet 4.6 --- config/widgets.yaml | 24 +++++++ src/components/widgets/deploy/deploy.jsx | 87 ++++++++++++++++++++++++ src/components/widgets/widget.jsx | 1 + 3 files changed, 112 insertions(+) create mode 100644 src/components/widgets/deploy/deploy.jsx diff --git a/config/widgets.yaml b/config/widgets.yaml index 8aa127a8..4ea53eec 100644 --- a/config/widgets.yaml +++ b/config/widgets.yaml @@ -12,3 +12,27 @@ timeStyle: short dateStyle: short hourCycle: h23 + +- deploy: + service: ppt-tool + label: DeckForge + +- deploy: + service: semblance + label: Semblance + +- deploy: + service: olivas + label: OliVAS + +- deploy: + service: gmal-scope-builder + label: Scope Builder + +- deploy: + service: cc-dashboard + label: CC Dashboard + +- deploy: + service: homepage + label: Homepage diff --git a/src/components/widgets/deploy/deploy.jsx b/src/components/widgets/deploy/deploy.jsx new file mode 100644 index 00000000..a9243aca --- /dev/null +++ b/src/components/widgets/deploy/deploy.jsx @@ -0,0 +1,87 @@ +import { useState } from "react"; +import useSWR from "swr"; + +const STATUS_COLORS = { + idle: "text-theme-500 dark:text-theme-400", + running: "text-blue-500 dark:text-blue-400", + success: "text-emerald-500 dark:text-emerald-400", + failed: "text-red-500 dark:text-red-400", +}; + +const STATUS_LABELS = { + idle: "Never deployed", + running: "Deploying...", + success: "Deployed", + failed: "Failed", +}; + +function formatTime(iso) { + if (!iso) return null; + const d = new Date(iso); + return d.toLocaleString("en-GB", { dateStyle: "short", timeStyle: "short", hourCycle: "h23" }); +} + +export default function Deploy({ options }) { + const { service, label, apiBase = "/deploy-api" } = options ?? {}; + const bp = process.env.NEXT_PUBLIC_BASE_PATH ?? ""; + const statusUrl = service ? `${bp}${apiBase}/status/${service}` : null; + + const { data, mutate } = useSWR(statusUrl, { + refreshInterval: (d) => (d?.status === "running" ? 2000 : 10000), + }); + + const [triggering, setTriggering] = useState(false); + + const status = data?.status ?? "idle"; + const lastRun = data?.last_run ? formatTime(data.last_run) : null; + const isRunning = status === "running"; + + const handleDeploy = async () => { + if (isRunning || triggering) return; + setTriggering(true); + try { + await fetch(`${bp}${apiBase}/deploy/${service}`, { method: "POST" }); + await mutate(); + } finally { + setTriggering(false); + } + }; + + if (!service) { + return ( +
+ No service configured +
+ ); + } + + return ( +
+
+ {isRunning ? ( + {STATUS_LABELS.running} + ) : ( + STATUS_LABELS[status] ?? status + )} +
+ + {lastRun && ( +
{lastRun}
+ )} + + +
+ ); +} diff --git a/src/components/widgets/widget.jsx b/src/components/widgets/widget.jsx index ebc706ac..52bcc1e1 100644 --- a/src/components/widgets/widget.jsx +++ b/src/components/widgets/widget.jsx @@ -15,6 +15,7 @@ const widgetMappings = { longhorn: dynamic(() => import("components/widgets/longhorn/longhorn")), kubernetes: dynamic(() => import("components/widgets/kubernetes/kubernetes")), stocks: dynamic(() => import("components/widgets/stocks/stocks")), + deploy: dynamic(() => import("components/widgets/deploy/deploy"), { ssr: false }), }; export default function Widget({ widget, style }) {