import { Alert, Button, Popconfirm, Space, message } from 'antd' import { CloudDownloadOutlined, ReloadOutlined, RocketOutlined } from '@ant-design/icons' import { useQuery } from '@tanstack/react-query' import { useEffect, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' import apiClient, { isEnvelope } from '../api/client' interface PackageVersions { [key: string]: string } interface SystemHealth { status: string; version: string } interface PendingUpdate { pkg: string; installed: string; available: string } // allUpdates parsed das flache map-Format ({pkg_installed,pkg_available}) // das /system/package-versions zurückliefert. Eines davon ist meist // das meta-Paket "edgeguard" → die "Ziel-Version". 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) { out.push({ pkg, installed, available }) } } return out } export default function UpdateBanner() { const { t } = useTranslation() const [msg, msgCtx] = message.useMessage() const pkgVersions = useQuery({ queryKey: ['system', 'package-versions'], queryFn: async () => { const r = await apiClient.get('/system/package-versions') return isEnvelope(r.data) ? (r.data.data as PackageVersions) : ({} as PackageVersions) }, refetchInterval: 30_000, refetchOnMount: 'always', staleTime: 0, gcTime: 0, }) const [upgrading, setUpgrading] = useState(false) const [upgradeElapsed, setUpgradeElapsed] = useState(0) const [forceChecking, setForceChecking] = useState(false) const upgradePollRef = useRef | null>(null) const upgradeTickRef = useRef | null>(null) const installedRef = useRef('') const targetRef = useRef('') useEffect(() => () => { if (upgradePollRef.current) clearInterval(upgradePollRef.current) if (upgradeTickRef.current) clearInterval(upgradeTickRef.current) }, []) const data = pkgVersions.data ?? {} const updates = allUpdates(data) const updateAvailable = updates.length > 0 const meta = updates.find(u => u.pkg === 'edgeguard') ?? updates[0] const installedVersion = meta?.installed ?? '' const targetVersion = meta?.available ?? '' const forceCheck = async () => { setForceChecking(true) try { await pkgVersions.refetch() const fresh = pkgVersions.data ?? {} const found = allUpdates(fresh).length > 0 msg[found ? 'success' : 'info']( found ? t('update.checkDone') : t('update.noUpdate'), ) } catch { msg.error(t('update.checkFailed')) } finally { setForceChecking(false) } } const startUpgrade = () => { installedRef.current = installedVersion targetRef.current = targetVersion setUpgrading(true) setUpgradeElapsed(0) upgradeTickRef.current = setInterval(() => setUpgradeElapsed((e) => e + 1), 1000) apiClient.post('/system/upgrade') .then(() => { // Poll /healthz (kein Auth, robust auch wenn die API gerade // restartet und Cookie ihre Session nicht erkennt). let sawDown = false upgradePollRef.current = setInterval(async () => { try { const res = await apiClient.get('/system/health') const newV = (isEnvelope(res.data) ? (res.data.data as SystemHealth).version : '') const flipped = newV && installedRef.current && newV !== installedRef.current if (flipped || sawDown) { if (upgradePollRef.current) clearInterval(upgradePollRef.current) if (upgradeTickRef.current) clearInterval(upgradeTickRef.current) setUpgrading(false) msg.success(t('update.success', { version: targetRef.current })) setTimeout(() => window.location.reload(), 1500) } } catch { // Connection refused / 502 → API restartet. Beim nächsten // erfolgreichen Poll erkennen wir den Version-Flip. sawDown = true } }, 3000) // Sicherheits-Timeout: nach 2 Min einfach reload — falls der // Restart länger braucht als erwartet, kommt die UI in jedem // Fall wieder hoch. setTimeout(() => { if (upgradePollRef.current) clearInterval(upgradePollRef.current) if (upgradeTickRef.current) clearInterval(upgradeTickRef.current) setUpgrading(false) window.location.reload() }, 120_000) }) .catch((e: Error) => { if (upgradeTickRef.current) clearInterval(upgradeTickRef.current) if (upgradePollRef.current) clearInterval(upgradePollRef.current) setUpgrading(false) msg.error(t('update.failed') + ': ' + e.message) }) } if (!updateAvailable && !upgrading) { return <>{msgCtx} } return ( <> {msgCtx} {updateAvailable && !upgrading && ( } message={t('update.available', { version: targetVersion })} description={updates.length > 1 ? t('update.multiPackageHint', { count: updates.length }) : undefined} action={ } /> )} {upgrading && (
{t('update.running')}
v{installedRef.current || '…'} → v{targetRef.current || '…'}
= 5} active={upgradeElapsed < 5} label={t('update.stepDownload')} /> = 15} active={upgradeElapsed >= 5 && upgradeElapsed < 15} label={t('update.stepInstall')} /> = 25} active={upgradeElapsed >= 15 && upgradeElapsed < 25} label={t('update.stepRestart')} /> = 25} label={t('update.stepVerify')} />
{upgradeElapsed}s
{t('update.waitHint')}
)} ) } function Step({ done, active, label }: { done: boolean; active: boolean; label: string }) { const cls = 'update-modal__step' + (active ? ' update-modal__step--active' : '') + (done ? ' update-modal__step--done' : '') return (
{label}
) }