From f4ccfc3c0c34fa0b8d43a287044baa9c2fbd5c10 Mon Sep 17 00:00:00 2001 From: Debian Date: Mon, 11 May 2026 07:56:57 +0200 Subject: [PATCH] feat(ui): Update-Modal mit Multi-Package-Liste + Live-Progress MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Vorher: Update-Banner war eine Inline-Alert mit "Apply now"-Button — ein Click → Hintergrund-Apt-Run, kein Feedback ob's durch ist. Jetzt: * Banner zeigt nur noch Hint + "Details anzeigen"-Button. * Modal listet alle Pakete mit Upgrade (installed → available), prominenter Warnhinweis dass edgeguard-api+scheduler restartet werden (~2-5s Unterbrechung) — HAProxy/nft/WG/Squid/Unbound/Chrony bleiben durch. * "Jetzt anwenden" startet den apt-Run, Modal schaltet auf Apply- Mode (kein Cancel mehr, Progress-Alert mit aktueller→Ziel-Version). * Polling von /system/health auf 2s erhöht während Apply. Sobald health.version == target → success-toast + auto-reload der UI (1.5s delay damit das neue UI-Bundle gecached werden kann). Pattern entlehnt von enconf/mail-gateway. Plus i18n-Erweiterung für update.modalTitle / modalIntro / modalWarn / applyingHint / applyingDetail / success. Version 1.0.45. Co-Authored-By: Claude Opus 4.7 (1M context) --- VERSION | 2 +- cmd/edgeguard-api/main.go | 2 +- cmd/edgeguard-ctl/main.go | 2 +- cmd/edgeguard-scheduler/main.go | 2 +- management-ui/package.json | 2 +- .../src/components/Layout/Sidebar.tsx | 2 +- management-ui/src/components/UpdateBanner.tsx | 168 ++++++++++++++---- management-ui/src/i18n/locales/de/common.json | 9 +- management-ui/src/i18n/locales/en/common.json | 9 +- 9 files changed, 158 insertions(+), 40 deletions(-) diff --git a/VERSION b/VERSION index 6ae122f..2fec751 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.0.44 +1.0.45 diff --git a/cmd/edgeguard-api/main.go b/cmd/edgeguard-api/main.go index 688fb04..f702550 100644 --- a/cmd/edgeguard-api/main.go +++ b/cmd/edgeguard-api/main.go @@ -45,7 +45,7 @@ import ( wgsvc "git.netcell-it.de/projekte/edgeguard-native/internal/services/wireguard" ) -var version = "1.0.44" +var version = "1.0.45" func main() { addr := os.Getenv("EDGEGUARD_API_ADDR") diff --git a/cmd/edgeguard-ctl/main.go b/cmd/edgeguard-ctl/main.go index 74be907..7138414 100644 --- a/cmd/edgeguard-ctl/main.go +++ b/cmd/edgeguard-ctl/main.go @@ -9,7 +9,7 @@ import ( "os" ) -var version = "1.0.44" +var version = "1.0.45" const usage = `edgeguard-ctl — EdgeGuard CLI diff --git a/cmd/edgeguard-scheduler/main.go b/cmd/edgeguard-scheduler/main.go index 5e6e4db..57fd08c 100644 --- a/cmd/edgeguard-scheduler/main.go +++ b/cmd/edgeguard-scheduler/main.go @@ -21,7 +21,7 @@ import ( "git.netcell-it.de/projekte/edgeguard-native/internal/services/tlscerts" ) -var version = "1.0.44" +var version = "1.0.45" const ( // renewTickInterval — how often we re-evaluate expiring certs. diff --git a/management-ui/package.json b/management-ui/package.json index 2932f37..11d0a4a 100644 --- a/management-ui/package.json +++ b/management-ui/package.json @@ -1,7 +1,7 @@ { "name": "edgeguard-management-ui", "private": true, - "version": "1.0.44", + "version": "1.0.45", "type": "module", "scripts": { "dev": "vite", diff --git a/management-ui/src/components/Layout/Sidebar.tsx b/management-ui/src/components/Layout/Sidebar.tsx index cee0a31..987f766 100644 --- a/management-ui/src/components/Layout/Sidebar.tsx +++ b/management-ui/src/components/Layout/Sidebar.tsx @@ -75,7 +75,7 @@ const NAV: NavSection[] = [ }, ] -const VERSION = '1.0.44' +const VERSION = '1.0.45' export default function Sidebar({ isOpen, onClose }: SidebarProps) { const { t } = useTranslation() diff --git a/management-ui/src/components/UpdateBanner.tsx b/management-ui/src/components/UpdateBanner.tsx index b3d65e4..c5193d8 100644 --- a/management-ui/src/components/UpdateBanner.tsx +++ b/management-ui/src/components/UpdateBanner.tsx @@ -1,35 +1,49 @@ -import { Alert, Button, message } from 'antd' +import { Alert, Button, List, Modal, Tag, Typography, message } from 'antd' +import { CloudDownloadOutlined } from '@ant-design/icons' import { useQuery, useMutation } from '@tanstack/react-query' import { useTranslation } from 'react-i18next' -import { useState } from 'react' +import { useState, useEffect } from 'react' import apiClient, { isEnvelope } from '../api/client' +const { Text } = Typography + interface PackageVersions { [key: string]: string } -// hasUpdate compares an *_installed value against its *_available -// counterpart. Returns the package base name + both versions when an -// update is pending; null otherwise. We pick the first package that -// has a real upgrade — matching enconf's "show one upgrade hint at -// a time" UX. -function pickUpdate(v: PackageVersions): { pkg: string; installed: string; available: string } | null { +interface PendingUpdate { + pkg: string + installed: string + available: string +} + +// allUpdates lists every package with installed != available. +// Match enconf/mail-gateway pattern of showing the multi-package +// list in a confirm-modal rather than just one banner-line. +function allUpdates(v: PackageVersions): PendingUpdate[] { + const out: PendingUpdate[] = [] for (const key of Object.keys(v)) { if (!key.endsWith('_installed')) continue const pkg = key.replace('_installed', '') const installed = v[key] const available = v[`${pkg}_available`] if (installed && available && installed !== available) { - return { pkg, installed, available } + out.push({ pkg, installed, available }) } } - return null + return out } +interface SystemHealth { status: string; version: string } + export default function UpdateBanner() { const { t } = useTranslation() - const [applying, setApplying] = useState(false) + const [open, setOpen] = useState(false) + // applyingFor holds the version we expect AFTER the upgrade so the + // modal can poll system/health and detect the version-flip. Switches + // back to null + closes the modal once the new version is live. + const [applyingFor, setApplyingFor] = useState(null) const { data } = useQuery({ queryKey: ['system', 'package-versions'], @@ -38,34 +52,124 @@ export default function UpdateBanner() { if (isEnvelope(r.data)) return r.data.data as PackageVersions return {} as PackageVersions }, - refetchInterval: 5 * 60 * 1000, // 5 min poll matches enconf cadence + refetchInterval: 5 * 60 * 1000, + }) + + const { data: health } = useQuery({ + queryKey: ['system', 'health'], + queryFn: async () => { + const r = await apiClient.get('/system/health') + return isEnvelope(r.data) ? (r.data.data as SystemHealth) : null + }, + // poll faster while we're applying so the version-flip is caught + refetchInterval: applyingFor ? 2_000 : 30_000, }) const upgrade = useMutation({ - mutationFn: async () => { - await apiClient.post('/system/upgrade') - }, - onSuccess: () => { - setApplying(true) - message.success(t('update.started')) - }, + mutationFn: async () => { await apiClient.post('/system/upgrade') }, + onError: (e: Error) => message.error(e.message), }) + // Detect version flip: api came back with the new version after + // restart → upgrade succeeded, close modal + tell user. + useEffect(() => { + if (!applyingFor || !health) return + if (health.version === applyingFor) { + message.success(t('update.success', { version: applyingFor })) + setApplyingFor(null) + setOpen(false) + // page reload picks up the rebuilt UI bundle + setTimeout(() => window.location.reload(), 1500) + } + }, [applyingFor, health, t]) + if (!data) return null - const u = pickUpdate(data) - if (!u) return null + const updates = allUpdates(data) + if (updates.length === 0) return null + + // Anchor: prefer the meta package's available version as the + // "target" (since edgeguard depends on edgeguard-api/ui at the + // same version). Falls back to the first listed package. + const meta = updates.find(u => u.pkg === 'edgeguard') ?? updates[0] return ( - upgrade.mutate()}> - {applying ? t('update.applying') : t('update.applyNow')} - - } - /> + <> + } + message={t('update.available', meta)} + action={ + + } + /> + + { if (!applyingFor) setOpen(false) }} + maskClosable={!applyingFor} + closable={!applyingFor} + width={620} + footer={ + applyingFor ? null : [ + , + , + ] + } + > + {applyingFor ? ( +
+ +
+ {t('update.applyingDetail', { current: health?.version ?? '—', target: applyingFor })} +
+
+ ) : ( + <> + {t('update.modalIntro')} + ( + + {u.pkg}} + description={ + + {u.installed}{u.available} + + } + /> + + )} + /> + + + )} +
+ ) } diff --git a/management-ui/src/i18n/locales/de/common.json b/management-ui/src/i18n/locales/de/common.json index 16634f0..74b1358 100644 --- a/management-ui/src/i18n/locales/de/common.json +++ b/management-ui/src/i18n/locales/de/common.json @@ -284,9 +284,16 @@ }, "update": { "available": "Update verfügbar: {{pkg}} {{installed}} → {{available}}", + "viewDetails": "Details anzeigen", "applyNow": "Jetzt aktualisieren", "applying": "Update läuft …", - "started": "Update wurde gestartet — der Server wird in Kürze neu starten." + "applyingHint": "Pakete werden installiert + Services restartet. Verbindung kann kurz brechen — die UI lädt automatisch neu sobald Version {{version}} live ist.", + "applyingDetail": "aktuell {{current}} · Ziel {{target}}", + "started": "Update wurde gestartet — der Server wird in Kürze neu starten.", + "success": "Update auf {{version}} abgeschlossen.", + "modalTitle": "EdgeGuard-Update", + "modalIntro": "Folgende Pakete werden aktualisiert:", + "modalWarn": "Während des Updates werden edgeguard-api + edgeguard-scheduler neu gestartet (~2-5 Sekunden Service-Unterbrechung). HAProxy + nft + WireGuard + Squid + Unbound + Chrony bleiben durchgehend aktiv." }, "wg": { "title": "WireGuard", diff --git a/management-ui/src/i18n/locales/en/common.json b/management-ui/src/i18n/locales/en/common.json index 5878639..ab31155 100644 --- a/management-ui/src/i18n/locales/en/common.json +++ b/management-ui/src/i18n/locales/en/common.json @@ -284,9 +284,16 @@ }, "update": { "available": "Update available: {{pkg}} {{installed}} → {{available}}", + "viewDetails": "View details", "applyNow": "Apply now", "applying": "Update in progress …", - "started": "Update has started — the server will restart shortly." + "applyingHint": "Packages are installed and services restarted. Connection may briefly break — the UI reloads automatically once version {{version}} is live.", + "applyingDetail": "current {{current}} · target {{target}}", + "started": "Update has started — the server will restart shortly.", + "success": "Updated to {{version}}.", + "modalTitle": "EdgeGuard update", + "modalIntro": "The following packages will be updated:", + "modalWarn": "During the update edgeguard-api + edgeguard-scheduler restart (~2-5 s service interruption). HAProxy + nft + WireGuard + Squid + Unbound + Chrony stay up the whole time." }, "wg": { "title": "WireGuard",