feat: move deploy buttons into service cards, fix siteMonitor URLs

- Create src/widgets/deploy/component.jsx — service-card widget variant
  that receives { service } prop (not { options }) for use in services.yaml
- Register deploy in src/widgets/components.js
- Move deploy widgets from config/widgets.yaml into each service card
  via widget: type: deploy — buttons now live inline under each app
- Fix Deploy API siteMonitor URL to use Apache-proxied path
- Reduce Widgets layout columns to 2 (only resources + datetime remain)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Vadym Samoilenko 2026-04-01 13:12:46 +01:00
parent 370ea5dae9
commit 7e6d8517b0
5 changed files with 105 additions and 26 deletions

View file

@ -8,6 +8,10 @@
server: local
siteMonitor: https://optical-dev.oliver.solutions/ppt-tool
showStats: true
widget:
type: deploy
service: ppt-tool
label: DeckForge
- GMAL Scope Builder:
icon: mdi-briefcase-outline
@ -17,6 +21,10 @@
server: local
siteMonitor: https://optical-dev.oliver.solutions/gsb
showStats: true
widget:
type: deploy
service: gmal-scope-builder
label: Scope Builder
- Semblance:
icon: mdi-account-group-outline
@ -26,6 +34,10 @@
server: local
siteMonitor: https://optical-dev.oliver.solutions/semblance
showStats: true
widget:
type: deploy
service: semblance
label: Semblance
- CC Dashboard:
icon: mdi-view-dashboard-outline
@ -35,6 +47,10 @@
server: local
siteMonitor: https://optical-dev.oliver.solutions/cc-dashboard
showStats: true
widget:
type: deploy
service: cc-dashboard
label: CC Dashboard
- OliVAS:
icon: mdi-robot-outline
@ -44,6 +60,10 @@
server: local
siteMonitor: https://optical-dev.oliver.solutions/api/health
showStats: true
widget:
type: deploy
service: olivas
label: OliVAS
- Infrastructure:
- Homepage:
@ -54,12 +74,16 @@
server: local
siteMonitor: https://optical-dev.oliver.solutions/homepage
showStats: true
widget:
type: deploy
service: homepage
label: Homepage
- Deploy API:
icon: mdi-rocket-launch-outline
href: https://optical-dev.oliver.solutions/deploy-api/docs
description: One-click deploy service
siteMonitor: http://127.0.0.1:9000/services
siteMonitor: https://optical-dev.oliver.solutions/deploy-api/services
- PostgreSQL × 4:
icon: mdi-database-outline

View file

@ -18,7 +18,7 @@ target: _blank
layout:
Widgets:
style: row
columns: 9
columns: 2
AI Tools:
style: row
columns: 5

View file

@ -13,27 +13,3 @@
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

View file

@ -26,6 +26,7 @@ const components = {
iframe: dynamic(() => import("./iframe/component")),
customapi: dynamic(() => import("./customapi/component")),
deluge: dynamic(() => import("./deluge/component")),
deploy: dynamic(() => import("./deploy/component")),
develancacheui: dynamic(() => import("./develancacheui/component")),
diskstation: dynamic(() => import("./diskstation/component")),
dispatcharr: dynamic(() => import("./dispatcharr/component")),

View file

@ -0,0 +1,78 @@
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 DeployComponent({ service }) {
const { service: svcName, label, apiBase = "/deploy-api" } = service?.widget ?? {};
const bp = process.env.NEXT_PUBLIC_BASE_PATH ?? "";
// SWR middleware already prepends bp, so statusUrl must NOT include it
const statusUrl = svcName ? `${apiBase}/status/${svcName}` : 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/${svcName}`, { method: "POST" });
await mutate();
} finally {
setTriggering(false);
}
};
if (!svcName) return null;
return (
<div className="flex flex-row items-center justify-between px-2 py-1 gap-2 w-full">
<div className="flex flex-col">
<span className={`text-xs font-semibold ${STATUS_COLORS[status] ?? STATUS_COLORS.idle}`}>
{isRunning ? <span className="animate-pulse">{STATUS_LABELS.running}</span> : (STATUS_LABELS[status] ?? status)}
</span>
{lastRun && (
<span className="text-theme-500 dark:text-theme-400 text-xs opacity-60">{lastRun}</span>
)}
</div>
<button
type="button"
onClick={handleDeploy}
disabled={isRunning || triggering}
className={[
"px-3 py-1 rounded text-xs font-medium transition-colors shrink-0",
isRunning || triggering
? "bg-theme-300 dark:bg-theme-600 text-theme-500 dark:text-theme-400 cursor-not-allowed"
: "bg-theme-500 hover:bg-theme-600 dark:bg-theme-600 dark:hover:bg-theme-500 text-white cursor-pointer",
].join(" ")}
>
{isRunning ? "Running..." : `Deploy ${label ?? svcName}`}
</button>
</div>
);
}