diff --git a/management-ui/src/pages/Backends/Detail.tsx b/management-ui/src/pages/Backends/Detail.tsx index 2ddf9dd..e8d1157 100644 --- a/management-ui/src/pages/Backends/Detail.tsx +++ b/management-ui/src/pages/Backends/Detail.tsx @@ -74,12 +74,28 @@ interface HAProxyStat { req_tot: number; req_rate: number last_change_sec: number; health: string } -async function listHAProxyStats(): Promise { +// Der Cache-Eintrag ['haproxy','stats'] wird mit dem Dashboard geteilt, +// das aus derselben Antwort zusaetzlich `frontends` liest. Deshalb hier +// IMMER die vollstaendige Antwort cachen und erst per `select` auf die +// Backends reduzieren, die diese Seite braucht. +// +// Befund 2026-09-11: Lieferte diese Funktion nur das Backend-Array, hing +// es vom zuletzt besuchten Screen ab, welche Form unter dem Key lag — +// nach einem Wechsel hierher und zurueck riss das Dashboard mit +// "Cannot read properties of undefined (reading 'length')" die ganze +// Oberflaeche in die ErrorBoundary. +interface HAProxyStatsPayload { + backends: HAProxyStat[] + frontends: unknown[] + error?: string +} +async function fetchHAProxyStats(): Promise { try { const r = await apiClient.get('/haproxy/stats') - if (!isEnvelope(r.data)) return [] - return (r.data.data as { backends?: HAProxyStat[] }).backends ?? [] - } catch { return [] } + if (!isEnvelope(r.data)) return { backends: [], frontends: [] } + const d = r.data.data as Partial + return { backends: d.backends ?? [], frontends: d.frontends ?? [], error: d.error } + } catch { return { backends: [], frontends: [] } } } function fmtBytes(n: number): string { @@ -112,7 +128,8 @@ export default function BackendDetailPage() { const { data: domains } = useQuery({ queryKey: ['domains'], queryFn: listDomains }) const { data: haproxyStats } = useQuery({ queryKey: ['haproxy', 'stats'], - queryFn: listHAProxyStats, + queryFn: fetchHAProxyStats, + select: (d: HAProxyStatsPayload) => d.backends, refetchInterval: 10_000, }) const [form] = Form.useForm() diff --git a/management-ui/src/pages/Backends/index.tsx b/management-ui/src/pages/Backends/index.tsx index 1416dd9..e923658 100644 --- a/management-ui/src/pages/Backends/index.tsx +++ b/management-ui/src/pages/Backends/index.tsx @@ -118,12 +118,28 @@ function fmtBytes(n: number): string { if (n >= 1_024) return (n / 1_024).toFixed(0) + ' KB' return n + ' B' } -async function listHAProxyStats(): Promise { +// Der Cache-Eintrag ['haproxy','stats'] wird mit dem Dashboard geteilt, +// das aus derselben Antwort zusaetzlich `frontends` liest. Deshalb hier +// IMMER die vollstaendige Antwort cachen und erst per `select` auf die +// Backends reduzieren, die diese Seite braucht. +// +// Befund 2026-09-11: Lieferte diese Funktion nur das Backend-Array, hing +// es vom zuletzt besuchten Screen ab, welche Form unter dem Key lag — +// nach einem Wechsel hierher und zurueck riss das Dashboard mit +// "Cannot read properties of undefined (reading 'length')" die ganze +// Oberflaeche in die ErrorBoundary. +interface HAProxyStatsPayload { + backends: HAProxyStat[] + frontends: unknown[] + error?: string +} +async function fetchHAProxyStats(): Promise { try { const r = await apiClient.get('/haproxy/stats') - if (!isEnvelope(r.data)) return [] - return (r.data.data as { backends?: HAProxyStat[] }).backends ?? [] - } catch { return [] } + if (!isEnvelope(r.data)) return { backends: [], frontends: [] } + const d = r.data.data as Partial + return { backends: d.backends ?? [], frontends: d.frontends ?? [], error: d.error } + } catch { return { backends: [], frontends: [] } } } export default function BackendsPage() { @@ -146,7 +162,8 @@ export default function BackendsPage() { const haproxyService = services?.find(s => s.unit === 'haproxy.service' || s.unit === 'haproxy') const { data: haproxyStats } = useQuery({ queryKey: ['haproxy', 'stats'], - queryFn: listHAProxyStats, + queryFn: fetchHAProxyStats, + select: (d: HAProxyStatsPayload) => d.backends, refetchInterval: 15_000, }) diff --git a/management-ui/src/pages/Dashboard/index.tsx b/management-ui/src/pages/Dashboard/index.tsx index 2e0da67..fb1075c 100644 --- a/management-ui/src/pages/Dashboard/index.tsx +++ b/management-ui/src/pages/Dashboard/index.tsx @@ -613,11 +613,11 @@ function VIPCard({ data }: { data?: VIPStatus | null }) { > {!data ? ( - ) : data.vips.length === 0 ? ( + ) : (data.vips ?? []).length === 0 ? ( {t('dashboard.vipCard.noVips')} ) : ( - {data.vips.map((v) => ( + {(data.vips ?? []).map((v) => (
{t('dashboard.haproxyCard.title')}} extra={ - stats.frontends.length > 0 && ( + (stats.frontends ?? []).length > 0 && ( {totalSessions} sess {totalReqRate > 0 && {totalReqRate}/s} @@ -731,7 +731,7 @@ function HAProxyFullCard({ stats, resolveHAName }: HAProxyFullCardProps) { )} {/* Listeners */} - {stats.frontends.length > 0 && ( + {(stats.frontends ?? []).length > 0 && ( <> {t('dashboard.haproxyCard.frontends')} @@ -752,7 +752,7 @@ function HAProxyFullCard({ stats, resolveHAName }: HAProxyFullCardProps) { )} {/* Backends */} - {stats.backends.length === 0 && !stats.error ? ( + {(stats.backends ?? []).length === 0 && !stats.error ? ( {t('dashboard.haproxyCard.empty')} ) : ( <> diff --git a/management-ui/src/pages/Domains/Detail.tsx b/management-ui/src/pages/Domains/Detail.tsx index 49fe998..ebc7e6b 100644 --- a/management-ui/src/pages/Domains/Detail.tsx +++ b/management-ui/src/pages/Domains/Detail.tsx @@ -101,12 +101,28 @@ async function listCerts(): Promise { if (!isEnvelope(r.data)) return [] return (r.data.data as { tls_certs?: TLSCertLite[] }).tls_certs ?? [] } -async function listHAProxyStats(): Promise { +// Der Cache-Eintrag ['haproxy','stats'] wird mit dem Dashboard geteilt, +// das aus derselben Antwort zusaetzlich `frontends` liest. Deshalb hier +// IMMER die vollstaendige Antwort cachen und erst per `select` auf die +// Backends reduzieren, die diese Seite braucht. +// +// Befund 2026-09-11: Lieferte diese Funktion nur das Backend-Array, hing +// es vom zuletzt besuchten Screen ab, welche Form unter dem Key lag — +// nach einem Wechsel hierher und zurueck riss das Dashboard mit +// "Cannot read properties of undefined (reading 'length')" die ganze +// Oberflaeche in die ErrorBoundary. +interface HAProxyStatsPayload { + backends: HAProxyStat[] + frontends: unknown[] + error?: string +} +async function fetchHAProxyStats(): Promise { try { const r = await apiClient.get('/haproxy/stats') - if (!isEnvelope(r.data)) return [] - return (r.data.data as { backends?: HAProxyStat[] }).backends ?? [] - } catch { return [] } + if (!isEnvelope(r.data)) return { backends: [], frontends: [] } + const d = r.data.data as Partial + return { backends: d.backends ?? [], frontends: d.frontends ?? [], error: d.error } + } catch { return { backends: [], frontends: [] } } } export default function DomainDetailPage() { @@ -126,7 +142,8 @@ export default function DomainDetailPage() { const { data: certs } = useQuery({ queryKey: ['tls-certs'], queryFn: listCerts }) const { data: haproxyStats } = useQuery({ queryKey: ['haproxy', 'stats'], - queryFn: listHAProxyStats, + queryFn: fetchHAProxyStats, + select: (d: HAProxyStatsPayload) => d.backends, refetchInterval: 15_000, }) diff --git a/management-ui/src/pages/Domains/index.tsx b/management-ui/src/pages/Domains/index.tsx index edc6d57..44cc980 100644 --- a/management-ui/src/pages/Domains/index.tsx +++ b/management-ui/src/pages/Domains/index.tsx @@ -83,12 +83,28 @@ async function listCerts(): Promise { } interface HAProxyStat { backend: string; server: string; status: string } -async function listHAProxyStats(): Promise { +// Der Cache-Eintrag ['haproxy','stats'] wird mit dem Dashboard geteilt, +// das aus derselben Antwort zusaetzlich `frontends` liest. Deshalb hier +// IMMER die vollstaendige Antwort cachen und erst per `select` auf die +// Backends reduzieren, die diese Seite braucht. +// +// Befund 2026-09-11: Lieferte diese Funktion nur das Backend-Array, hing +// es vom zuletzt besuchten Screen ab, welche Form unter dem Key lag — +// nach einem Wechsel hierher und zurueck riss das Dashboard mit +// "Cannot read properties of undefined (reading 'length')" die ganze +// Oberflaeche in die ErrorBoundary. +interface HAProxyStatsPayload { + backends: HAProxyStat[] + frontends: unknown[] + error?: string +} +async function fetchHAProxyStats(): Promise { try { const r = await apiClient.get('/haproxy/stats') - if (!isEnvelope(r.data)) return [] - return (r.data.data as { backends?: HAProxyStat[] }).backends ?? [] - } catch { return [] } + if (!isEnvelope(r.data)) return { backends: [], frontends: [] } + const d = r.data.data as Partial + return { backends: d.backends ?? [], frontends: d.frontends ?? [], error: d.error } + } catch { return { backends: [], frontends: [] } } } export default function DomainsPage() { @@ -109,7 +125,8 @@ export default function DomainsPage() { const { data: certs } = useQuery({ queryKey: ['tls-certs'], queryFn: listCerts }) const { data: haproxyStats } = useQuery({ queryKey: ['haproxy', 'stats'], - queryFn: listHAProxyStats, + queryFn: fetchHAProxyStats, + select: (d: HAProxyStatsPayload) => d.backends, refetchInterval: 15_000, }) const certByDomain = new Map((certs ?? []).map(c => [c.domain, c])) diff --git a/management-ui/src/pages/RoutingRules/index.tsx b/management-ui/src/pages/RoutingRules/index.tsx index cf8c19b..6fc33e2 100644 --- a/management-ui/src/pages/RoutingRules/index.tsx +++ b/management-ui/src/pages/RoutingRules/index.tsx @@ -58,12 +58,28 @@ function fmtBytes(n: number): string { if (n >= 1_024) return (n / 1_024).toFixed(0) + ' KB' return n + ' B' } -async function listHAProxyStats(): Promise { +// Der Cache-Eintrag ['haproxy','stats'] wird mit dem Dashboard geteilt, +// das aus derselben Antwort zusaetzlich `frontends` liest. Deshalb hier +// IMMER die vollstaendige Antwort cachen und erst per `select` auf die +// Backends reduzieren, die diese Seite braucht. +// +// Befund 2026-09-11: Lieferte diese Funktion nur das Backend-Array, hing +// es vom zuletzt besuchten Screen ab, welche Form unter dem Key lag — +// nach einem Wechsel hierher und zurueck riss das Dashboard mit +// "Cannot read properties of undefined (reading 'length')" die ganze +// Oberflaeche in die ErrorBoundary. +interface HAProxyStatsPayload { + backends: HAProxyStat[] + frontends: unknown[] + error?: string +} +async function fetchHAProxyStats(): Promise { try { const r = await apiClient.get('/haproxy/stats') - if (!isEnvelope(r.data)) return [] - return (r.data.data as { backends?: HAProxyStat[] }).backends ?? [] - } catch { return [] } + if (!isEnvelope(r.data)) return { backends: [], frontends: [] } + const d = r.data.data as Partial + return { backends: d.backends ?? [], frontends: d.frontends ?? [], error: d.error } + } catch { return { backends: [], frontends: [] } } } export default function RoutingRulesPage() { @@ -76,7 +92,8 @@ export default function RoutingRulesPage() { const { data: backends } = useQuery({ queryKey: ['backends'], queryFn: listBackends }) const { data: haproxyStats } = useQuery({ queryKey: ['haproxy', 'stats'], - queryFn: listHAProxyStats, + queryFn: fetchHAProxyStats, + select: (d: HAProxyStatsPayload) => d.backends, refetchInterval: 15_000, })