Files
ITWorx Pulse release export bd774932d5
Public source validation / validate (push) Failing after 3m8s
Publish ITWorx Pulse source
2026-09-03 02:09:19 +02:00

808 lines
79 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { Component, Suspense, lazy, type CSSProperties, type ErrorInfo, type ReactNode, useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { MetricWidget, metricStatus, RankedListWidget, StatusGridWidget, type MetricWidgetProps, type RankedListItem, type StatusGridItem } from './MetricWidgets';
import { DashboardRuntimeWidget, type RuntimeState } from './DashboardRuntimeWidget';
import { copy } from './copy';
import { routeFromLocation, type RoutePath } from './routes';
import { HostPage } from './HostPage';
import { ArrayPage } from './ArrayPage';
import { DiskDetailPage, DiskPage } from './DiskPage';
import { PoolPage } from './PoolPage';
import { SharePage } from './SharePage';
import { StoragePage } from './StoragePage';
import { CapacityPage } from './CapacityPage';
import { StorageMapWidget, TemperatureHeatmap, type HeatmapPoint, type StorageMapNode } from './StorageVisuals';
import { ContainerDetailPage, ContainerPage } from './ContainerPage';
import { ApplicationPage } from './ApplicationPage';
import { ServicePage } from './ServicePage';
import type { TopologyData } from './TopologyPage';
import { NetworkPage, NetworkHealthWidget, type NetworkData } from './NetworkPage';
import { IncidentPage } from './IncidentPage';
import { OnboardingPage } from './OnboardingPage';
import { SystemStatusPage } from './SystemStatusPage';
import { InventoryPage } from './InventoryPage';
import { EventsPage } from './EventsPage';
import { NotFoundPage } from './NotFoundPage';
import { formatDateTime } from './locale';
import { installSessionWatcher, onUnauthenticated } from './auth';
import { AuthNoticeBanner, SignInButton } from './SignIn';
import { aggregateStatus, refreshSystemStatus, statusProblems, useSystemStatus } from './systemStatus';
import { operationalStorageState, presentComponent, presentReason, presentStatus } from './presentation';
import { wallboardColumns, wallboardPlacement, wallboardSlideIndex } from './wallboardLayout';
import { OperationalSignalPath, type OperationalSignalStage } from './OperationalSignalPath';
import { containerSignalTone, signalToneFromState, signalToneRank, sourceSignalTone, worstSignalTone, type SignalTone } from './overviewSignals';
// Heavy, rarely-used surfaces. The wallboard and mobile personas never execute
// the editor stack, the alert-rule editor, the topology graph or the process
// explorer, so those stay out of the initial chunk (FRONTEND_STANDARDS "Charts").
const DashboardEditor = lazy(() => import('./DashboardEditor').then((module) => ({ default: module.DashboardEditor })));
const AlertRulesPage = lazy(() => import('./AlertRulesPage').then((module) => ({ default: module.AlertRulesPage })));
const TopologyPage = lazy(() => import('./TopologyPage').then((module) => ({ default: module.TopologyPage })));
const TopologyWidget = lazy(() => import('./TopologyPage').then((module) => ({ default: module.TopologyWidget })));
const ProcessPage = lazy(() => import('./ProcessPage').then((module) => ({ default: module.ProcessPage })));
const navigation = [
{ path: '/', label: copy.navigation.overview, icon: '⌂', group: copy.navigation.groups.command },
{ path: '/dashboards', label: copy.navigation.dashboards, icon: '▦', group: copy.navigation.groups.command },
{ path: '/host', label: copy.navigation.host, icon: '▣', group: copy.navigation.groups.infrastructure },
{ path: '/array', label: copy.navigation.array, icon: '▥', group: copy.navigation.groups.infrastructure },
{ path: '/disks', label: copy.navigation.disks, icon: '◉', group: copy.navigation.groups.infrastructure },
{ path: '/pools', label: copy.navigation.pools, icon: '◫', group: copy.navigation.groups.infrastructure },
{ path: '/shares', label: copy.navigation.shares, icon: '⇄', group: copy.navigation.groups.infrastructure },
{ path: '/storage', label: copy.navigation.storage, icon: '▤', group: copy.navigation.groups.infrastructure },
{ path: '/capacity', label: copy.navigation.capacity, icon: '⌁', group: copy.navigation.groups.infrastructure },
{ path: '/network', label: copy.navigation.network, icon: '⌘', group: copy.navigation.groups.infrastructure },
{ path: '/processes', label: copy.navigation.processes, icon: '≋', group: copy.navigation.groups.workloads },
{ path: '/containers', label: copy.navigation.containers, icon: '⬡', group: copy.navigation.groups.workloads },
{ path: '/applications', label: copy.navigation.applications, icon: '◆', group: copy.navigation.groups.workloads },
{ path: '/services', label: copy.navigation.services, icon: '◉', group: copy.navigation.groups.services },
{ path: '/topology', label: copy.navigation.topology, icon: '⌬', group: copy.navigation.groups.services },
{ path: '/alerts', label: copy.navigation.alerts, icon: '!', group: copy.navigation.groups.response },
{ path: '/events', label: copy.navigation.events, icon: '≡', group: copy.navigation.groups.response },
{ path: '/incidents', label: copy.navigation.incidents, icon: '△', group: copy.navigation.groups.response },
{ path: '/inventory', label: copy.navigation.inventory, icon: '▥', group: copy.navigation.groups.manage },
{ path: '/wallboard', label: copy.navigation.wallboard, icon: '▰', group: copy.navigation.groups.manage },
{ path: '/settings', label: copy.navigation.settings, icon: '⚙', group: copy.navigation.groups.manage },
{ path: '/status', label: copy.navigation.status, icon: '♥', group: copy.navigation.groups.manage },
{ path: '/onboarding', label: copy.navigation.onboarding, icon: '→', group: copy.navigation.groups.manage },
] as const satisfies ReadonlyArray<{ path: RoutePath; label: string; icon: string; group: string }>;
const mobilePrimaryPaths = new Set<RoutePath>(['/', '/incidents', '/containers', '/storage']);
const mobilePrimaryNavigation = ['/', '/incidents', '/containers', '/storage'].map((path) => navigation.find((item) => item.path === path)).filter((item): item is NavigationItem => Boolean(item));
const mobileMoreNavigation = navigation.filter((item) => !mobilePrimaryPaths.has(item.path));
type NavigationItem = (typeof navigation)[number];
const navigationGroups = Array.from(new Set(navigation.map((item) => item.group)));
function NavigationLink({ item, label = item.label }: { item: NavigationItem; label?: string }) {
const active = routeFromLocation(window.location.pathname) === item.path;
return <li><a className={active ? 'nav-link nav-link--active' : 'nav-link'} aria-current={active ? 'page' : undefined} href={item.path} title={label} onClick={(event) => { event.preventDefault(); navigate(item.path); }}><span className="nav-icon" aria-hidden="true">{item.icon}</span><span>{label}</span></a></li>;
}
function DesktopNavigation({ route }: { route: RoutePath }) {
const activeGroup = navigation.find((item) => route === item.path || (item.path !== '/' && route.startsWith(item.path + '/')))?.group ?? navigationGroups[0];
const [openGroups, setOpenGroups] = useState<Set<string>>(() => new Set([navigationGroups[0], activeGroup]));
useEffect(() => setOpenGroups((current) => current.has(activeGroup) ? current : new Set([...current, activeGroup])), [activeGroup]);
return <nav className="desktop-navigation" aria-label={copy.navigation.label}>{navigationGroups.map((group) => {
const items = navigation.filter((item) => item.group === group);
return <details className="nav-group" key={group} open={openGroups.has(group)} onToggle={(event) => { const open = event.currentTarget.open; setOpenGroups((current) => { if (current.has(group) === open) return current; const next = new Set(current); if (open) next.add(group); else next.delete(group); return next; }); }}><summary><span>{group}</span><span aria-hidden="true">⌄</span></summary><ul className="nav-list">{items.map((item) => <NavigationLink key={item.path} item={item} />)}</ul></details>;
})}</nav>;
}
function navigate(path: string) {
window.history.pushState({}, '', path);
window.dispatchEvent(new PopStateEvent('popstate'));
}
function StatusBadge({ label, tone = 'unknown' }: { label: string; tone?: 'unknown' | 'ready' }) {
return <span className={'status-badge status-badge--' + tone}><span className="status-icon" aria-hidden="true">{tone === 'ready' ? '✓' : '?'}</span>{label}</span>;
}
function PageIntro({ eyebrow, title, intro }: { eyebrow: string; title: string; intro: string }) {
return <header className="page-intro"><p className="eyebrow">{eyebrow}</p><h1>{title}</h1><p className="intro">{intro}</p></header>;
}
type OverviewSource = { state?: string; freshness?: string; reason?: string };
type OverviewHost = { identity?: { name?: string }; cpu?: { totalPercent?: number }; memory?: { utilizationPercent?: number }; source?: OverviewSource };
type OverviewContainer = { id?: string; state?: string; health?: string; intentionalStop?: boolean };
type OverviewPool = { id: string; name: string; state: string; capacitySeverity?: string; utilizationPercent: number };
type OverviewService = { id: string; name: string; state: string };
type OverviewIncident = { id: string; title: string; severity: string; startedAt: string };
type OverviewContainerSnapshot = { source?: OverviewSource; containers?: OverviewContainer[]; total?: number; nextCursor?: string };
type OverviewPoolSnapshot = { source?: OverviewSource; pools?: OverviewPool[]; total?: number };
type OverviewServiceSnapshot = { capabilityState?: string; configurationState?: string; reason?: string; services?: OverviewService[]; total?: number };
type OverviewResourceState = 'loading' | 'ready' | 'unavailable' | 'unauthorized' | 'forbidden';
type OverviewResource = 'host' | 'containers' | 'pools' | 'services' | 'incidents';
type OverviewData = {
host?: OverviewHost;
containers: OverviewContainer[];
containerSource?: OverviewSource;
containerTotal: number;
containersPartial: boolean;
pools: OverviewPool[];
poolSource?: OverviewSource;
poolTotal: number;
poolsPartial: boolean;
services: OverviewService[];
serviceTotal: number;
servicesPartial: boolean;
serviceCapability?: string;
serviceConfiguration?: string;
incidents: OverviewIncident[];
incidentsPartial: boolean;
resources: Record<OverviewResource, OverviewResourceState>;
};
const loadingOverviewResources: Record<OverviewResource, OverviewResourceState> = {
host: 'loading', containers: 'loading', pools: 'loading', services: 'loading', incidents: 'loading',
};
const overviewInitialData: OverviewData = {
containers: [], containerTotal: 0, containersPartial: false,
pools: [], poolTotal: 0, poolsPartial: false,
services: [], serviceTotal: 0, servicesPartial: false,
incidents: [], incidentsPartial: false,
resources: loadingOverviewResources,
};
const OVERVIEW_REFRESH_MS = 30_000;
const OVERVIEW_REQUEST_TIMEOUT_MS = 10_000;
const CONTAINER_PAGE_LIMIT = 100;
const CONTAINER_MAX_PAGES = 3;
type ReadResult<T> = { state: OverviewResourceState; data?: T };
function collectionExtent(reported: number | undefined, count: number): { total: number; partial: boolean } {
const valid = Number.isInteger(reported) && (reported ?? -1) >= count;
const total = valid ? reported as number : count;
return { total, partial: !valid || total > count };
}
function useOverviewData(): { data: OverviewData; refresh: () => void } {
const [data, setData] = useState<OverviewData>(overviewInitialData);
const [generation, setGeneration] = useState(0);
const refresh = useCallback(() => {
setData((current) => ({ ...current, resources: { ...loadingOverviewResources } }));
setGeneration((current) => current + 1);
}, []);
useEffect(() => {
const controller = new AbortController();
const read = async <T,>(url: string): Promise<ReadResult<T>> => {
const requestController = new AbortController();
const abortRequest = () => requestController.abort();
if (controller.signal.aborted) abortRequest();
else controller.signal.addEventListener('abort', abortRequest, { once: true });
const timeout = window.setTimeout(abortRequest, OVERVIEW_REQUEST_TIMEOUT_MS);
try {
const response = await fetch(url, { signal: requestController.signal, cache: 'no-store' });
if (requestController.signal.aborted) return { state: 'unavailable' };
if (response.status === 401) return { state: 'unauthorized' };
if (response.status === 403) return { state: 'forbidden' };
return response.ok ? { state: 'ready', data: await response.json() as T } : { state: 'unavailable' };
} catch {
return { state: 'unavailable' };
} finally {
window.clearTimeout(timeout);
controller.signal.removeEventListener('abort', abortRequest);
}
};
const readContainers = async (): Promise<ReadResult<{ source?: OverviewSource; items: OverviewContainer[]; total: number; partial: boolean }>> => {
const items: OverviewContainer[] = [];
const seen = new Set<string>();
let source: OverviewSource | undefined;
let expectedTotal: number | undefined;
let after = '';
let complete = false;
let inconsistent = false;
for (let page = 0; page < CONTAINER_MAX_PAGES; page += 1) {
const params = new URLSearchParams({ limit: String(CONTAINER_PAGE_LIMIT), sort: 'name' });
if (after) params.set('after', after);
const result = await read<OverviewContainerSnapshot>('/api/v1/containers?' + params);
if (result.state !== 'ready' || !result.data) return { state: result.state };
const pageItems = result.data.containers ?? [];
const reportedTotal = result.data.total;
if (!Number.isInteger(reportedTotal) || (reportedTotal ?? -1) < pageItems.length) {
inconsistent = true;
} else if (expectedTotal == null) {
expectedTotal = reportedTotal as number;
} else {
if (reportedTotal !== expectedTotal) inconsistent = true;
expectedTotal = Math.max(expectedTotal, reportedTotal as number);
}
if (!source || signalToneRank[sourceSignalTone(result.data.source)] < signalToneRank[sourceSignalTone(source)]) source = result.data.source;
for (const item of pageItems) {
const id = item.id?.trim();
if (!id) {
inconsistent = true;
items.push(item);
} else if (seen.has(id)) {
inconsistent = true;
} else {
seen.add(id);
items.push(item);
}
}
const next = result.data.nextCursor?.trim() ?? '';
if (!next) {
complete = true;
break;
}
if (next === after) {
inconsistent = true;
break;
}
after = next;
}
const total = Math.max(expectedTotal ?? 0, items.length);
return { state: 'ready', data: { source, items, total, partial: !complete || inconsistent || items.length < total } };
};
void read<OverviewHost>('/api/v1/host').then((result) => {
if (controller.signal.aborted) return;
setData((current) => ({ ...current, host: result.data, resources: { ...current.resources, host: result.state } }));
});
void readContainers().then((result) => {
if (controller.signal.aborted) return;
setData((current) => ({ ...current, containers: result.data?.items ?? [], containerSource: result.data?.source, containerTotal: result.data?.total ?? 0, containersPartial: result.data?.partial ?? false, resources: { ...current.resources, containers: result.state } }));
});
void read<OverviewPoolSnapshot>('/api/v1/pools?limit=64').then((result) => {
if (controller.signal.aborted) return;
const items = result.data?.pools ?? [];
const extent = collectionExtent(result.data?.total, items.length);
setData((current) => ({ ...current, pools: items, poolSource: result.data?.source, poolTotal: extent.total, poolsPartial: result.state === 'ready' && extent.partial, resources: { ...current.resources, pools: result.state } }));
});
void read<OverviewServiceSnapshot>('/api/v1/services?limit=100').then((result) => {
if (controller.signal.aborted) return;
const items = result.data?.services ?? [];
const extent = collectionExtent(result.data?.total, items.length);
setData((current) => ({ ...current, services: items, serviceTotal: extent.total, servicesPartial: result.state === 'ready' && extent.partial, serviceCapability: result.data?.capabilityState, serviceConfiguration: result.data?.configurationState, resources: { ...current.resources, services: result.state } }));
});
void read<{ items?: OverviewIncident[] }>('/api/v1/incidents?limit=100&status=open').then((result) => {
if (controller.signal.aborted) return;
const items = result.data?.items ?? [];
setData((current) => ({ ...current, incidents: items, incidentsPartial: result.state === 'ready' && items.length >= 100, resources: { ...current.resources, incidents: result.state } }));
});
return () => controller.abort();
}, [generation]);
useEffect(() => {
const timer = window.setInterval(() => {
if (document.visibilityState === 'visible') setGeneration((current) => current + 1);
}, OVERVIEW_REFRESH_MS);
return () => window.clearInterval(timer);
}, []);
return { data, refresh };
}
function signalResourceLabel(state: OverviewResourceState, tone: SignalTone): string {
if (state === 'loading') return copy.overview.signalPathLoading;
if (state === 'unauthorized') return copy.overview.signalPathUnauthorized;
if (state === 'forbidden') return copy.overview.signalPathForbidden;
if (state === 'unavailable') return copy.overview.signalPathUnavailable;
return presentStatus(tone === 'attention' ? 'attention' : tone);
}
function resourceStateDetail(state: OverviewResourceState): string | undefined {
if (state === 'loading') return copy.overview.resourceLoadingDetail;
if (state === 'unauthorized') return copy.overview.resourceUnauthorizedDetail;
if (state === 'forbidden') return copy.overview.resourceForbiddenDetail;
if (state === 'unavailable') return copy.overview.resourceUnavailableDetail;
return undefined;
}
function thresholdTone(values: Array<number | undefined>): SignalTone {
const usable = values.filter((value): value is number => value != null && Number.isFinite(value));
if (usable.length !== values.length) return 'unknown';
if (usable.some((value) => value >= 95)) return 'critical';
if (usable.some((value) => value >= 85)) return 'attention';
return 'healthy';
}
function metric(value: number | undefined, suffix = '%'): string {
return value == null || !Number.isFinite(value) ? '—' : value.toLocaleString('nl-BE', { maximumFractionDigits: 1 }) + suffix;
}
function boundedRatio(known: number, total: number, partial: boolean): string {
return `${partial ? '≥' : ''}${known}/${total}`;
}
function signalCollectionLabel(state: OverviewResourceState, tone: SignalTone, options: { partial?: boolean; notConfigured?: boolean; emptyLabel?: string } = {}): string {
if (state !== 'ready') return signalResourceLabel(state, tone);
if (tone === 'critical' || tone === 'attention' || tone === 'stale') return presentStatus(tone);
if (options.notConfigured) return copy.overview.signalPathNotConfigured;
if (options.partial) return copy.overview.signalPathPartial;
if (options.emptyLabel) return options.emptyLabel;
return presentStatus(tone);
}
function OverviewPage() {
const snapshot = useSystemStatus();
const status = aggregateStatus(snapshot);
const { data: overview, refresh: refreshOverview } = useOverviewData();
const resourcesLoading = snapshot.state === 'loading' || Object.values(overview.resources).some((state) => state === 'loading');
const overviewNeedsAuthentication = snapshot.state === 'unauthorized' || Object.values(overview.resources).some((state) => state === 'unauthorized');
const systemResourceState: OverviewResourceState = snapshot.state === 'ready' ? 'ready' : snapshot.state === 'loading' ? 'loading' : snapshot.state === 'unauthorized' ? 'unauthorized' : snapshot.state === 'forbidden' ? 'forbidden' : 'unavailable';
const poolRank: Record<string, number> = { critical: 0, faulted: 0, degraded: 1, attention: 2, unknown: 3 };
const poolProblems = (overview.resources.pools === 'ready' && sourceSignalTone(overview.poolSource) === 'healthy' ? overview.pools : [])
.map((pool) => ({ pool, state: operationalStorageState(pool.state, pool.capacitySeverity) }))
.filter(({ state }) => state !== 'healthy' && state !== 'normal')
.sort((a, b) => (poolRank[a.state] ?? 4) - (poolRank[b.state] ?? 4))
.map(({ pool, state }) => ({ id: 'pool:' + pool.id, label: `${pool.name}: ${presentStatus(state)}`, reason: state === 'critical' || state === 'faulted' ? copy.overview.poolCapacityCritical : state === 'attention' ? copy.overview.poolCapacityAttention : presentReason('source_health_unknown') }));
const resourceLabels: Record<OverviewResource, string> = { host: copy.navigation.host, containers: copy.overview.signalWorkloads, pools: copy.overview.signalStorage, services: copy.navigation.services, incidents: copy.overview.signalIncidents };
const resourceProblems = (Object.keys(overview.resources) as OverviewResource[]).flatMap((resource) => {
const state = overview.resources[resource];
if (state !== 'unavailable' && state !== 'unauthorized' && state !== 'forbidden') return [];
return [{ id: `resource:${resource}`, label: `${resourceLabels[resource]}: ${signalResourceLabel(state, 'unknown')}`, reason: resourceStateDetail(state) ?? copy.overview.resourceUnavailableDetail }];
});
const systemProblems = systemResourceState === 'ready' || systemResourceState === 'loading' ? [] : [{ id: 'resource:system-status', label: `${copy.overview.sources}: ${signalResourceLabel(systemResourceState, 'unknown')}`, reason: resourceStateDetail(systemResourceState) ?? copy.overview.resourceUnavailableDetail }];
const partialProblems: Array<{ id: string; label: string; reason: string }> = [];
if (overview.containersPartial) partialProblems.push({ id: 'partial:containers', label: `${copy.overview.signalWorkloads}: ${copy.overview.signalPathPartial}`, reason: copy.overview.resourcePartialDetail });
if (overview.poolsPartial) partialProblems.push({ id: 'partial:pools', label: `${copy.overview.signalStorage}: ${copy.overview.signalPathPartial}`, reason: copy.overview.resourcePartialDetail });
if (overview.servicesPartial) partialProblems.push({ id: 'partial:services', label: `${copy.navigation.services}: ${copy.overview.signalPathPartial}`, reason: copy.overview.resourcePartialDetail });
if (overview.incidentsPartial) partialProblems.push({ id: 'partial:incidents', label: `${copy.overview.signalIncidents}: ${copy.overview.signalPathPartial}`, reason: copy.overview.resourcePartialDetail });
const problems = [...systemProblems, ...resourceProblems, ...partialProblems, ...poolProblems, ...statusProblems(snapshot.status)].slice(0, 10);
const poolUsage = overview.pools.length ? Math.max(...overview.pools.map((pool) => pool.utilizationPercent)) : undefined;
const runningContainers = overview.containers.filter((item) => item.state?.toLowerCase() === 'running').length;
const availableServices = overview.services.filter((item) => item.state?.toLowerCase() === 'up').length;
const sourceLags = snapshot.status?.sourceLag ?? [];
const sourceTones = sourceLags.map((lag) => signalToneFromState(lag.state));
const healthySources = sourceTones.filter((tone) => tone === 'healthy').length;
const sourceTone = snapshot.state !== 'ready' || sourceTones.length === 0 ? 'unknown' : status.stale ? 'stale' : worstSignalTone(sourceTones);
const hostSourceTone = sourceSignalTone(overview.host?.source);
const hostTone = overview.resources.host !== 'ready' || !overview.host ? 'unknown' : hostSourceTone !== 'healthy' ? hostSourceTone : thresholdTone([overview.host.cpu?.totalPercent, overview.host.memory?.utilizationPercent]);
const poolSourceTone = sourceSignalTone(overview.poolSource);
const knownStorageTone = overview.pools.length ? worstSignalTone(overview.pools.map((pool) => signalToneFromState(operationalStorageState(pool.state, pool.capacitySeverity)))) : 'unknown';
const storageTone = overview.resources.pools !== 'ready' ? 'unknown' : poolSourceTone !== 'healthy' ? poolSourceTone : overview.poolsPartial ? worstSignalTone([knownStorageTone, 'unknown']) : overview.poolTotal === 0 ? 'unknown' : knownStorageTone;
const containerSourceTone = sourceSignalTone(overview.containerSource);
const knownWorkloadTone = overview.containerTotal === 0 ? 'healthy' : worstSignalTone(overview.containers.map(containerSignalTone));
const workloadTone = overview.resources.containers !== 'ready' ? 'unknown' : containerSourceTone !== 'healthy' ? containerSourceTone : overview.containersPartial ? worstSignalTone([knownWorkloadTone, 'unknown']) : knownWorkloadTone;
const serviceConfigured = overview.serviceCapability === 'available' && overview.serviceConfiguration === 'configured';
const knownServiceTone = overview.services.length ? worstSignalTone(overview.services.map((service) => signalToneFromState(service.state))) : 'unknown';
const serviceTone = overview.resources.services !== 'ready' || !serviceConfigured ? 'unknown' : overview.servicesPartial ? worstSignalTone([knownServiceTone, 'unknown']) : overview.serviceTotal === 0 ? 'unknown' : knownServiceTone;
const incidentTone = overview.resources.incidents !== 'ready' ? 'unknown' : overview.incidents.length === 0 ? 'healthy' : overview.incidents.some((incident) => signalToneFromState(incident.severity) === 'critical') ? 'critical' : 'attention';
const orderedIncidents = [...overview.incidents].sort((left, right) => signalToneRank[signalToneFromState(left.severity)] - signalToneRank[signalToneFromState(right.severity)] || right.startedAt.localeCompare(left.startedAt) || left.id.localeCompare(right.id));
const highestIncident = orderedIncidents[0];
const incidentSeverity = highestIncident ? presentStatus(highestIncident.severity) : copy.overview.noOpenIncidents;
const hostUsable = overview.resources.host === 'ready' && hostSourceTone === 'healthy';
const poolsUsable = overview.resources.pools === 'ready' && poolSourceTone === 'healthy';
const containersUsable = overview.resources.containers === 'ready' && containerSourceTone === 'healthy';
const servicesUsable = overview.resources.services === 'ready' && serviceConfigured;
const signalStages: OperationalSignalStage[] = [
{ id: 'sources', label: copy.overview.sources, icon: '◉', tone: sourceTone, statusLabel: signalResourceLabel(systemResourceState, sourceTone), primaryLabel: copy.overview.connectedSources, primaryValue: snapshot.state === 'ready' ? `${healthySources}/${sourceTones.length || '—'}` : '—', secondaryLabel: copy.overview.freshness, secondaryValue: signalResourceLabel(systemResourceState, sourceTone), detail: copy.overview.signalPathSourcesDetail, route: '/status' },
{ id: 'host', label: copy.navigation.host, icon: '▣', tone: hostTone, statusLabel: signalResourceLabel(overview.resources.host, hostTone), primaryLabel: copy.overview.cpu, primaryValue: hostUsable ? metric(overview.host?.cpu?.totalPercent) : '—', secondaryLabel: copy.overview.memoryShort, secondaryValue: hostUsable ? metric(overview.host?.memory?.utilizationPercent) : '—', detail: copy.overview.signalPathHostDetail, route: '/host' },
{ id: 'storage', label: copy.overview.signalStorage, icon: '▤', tone: storageTone, statusLabel: signalCollectionLabel(overview.resources.pools, storageTone, { partial: overview.poolsPartial }), primaryLabel: copy.overview.storage, primaryValue: poolsUsable ? (overview.poolsPartial && poolUsage != null ? `≥${metric(poolUsage)}` : metric(poolUsage)) : '—', secondaryLabel: copy.navigation.pools, secondaryValue: poolsUsable ? String(overview.poolTotal) : '—', detail: copy.overview.signalPathStorageDetail, route: '/storage' },
{ id: 'workloads', label: copy.overview.signalWorkloads, icon: '⬡', tone: workloadTone, statusLabel: signalCollectionLabel(overview.resources.containers, workloadTone, { partial: overview.containersPartial, emptyLabel: overview.containerTotal === 0 ? copy.overview.signalPathNoWorkloads : undefined }), primaryLabel: copy.overview.activeContainers, primaryValue: containersUsable ? boundedRatio(runningContainers, overview.containerTotal, overview.containersPartial) : '—', secondaryLabel: copy.overview.total, secondaryValue: containersUsable ? String(overview.containerTotal) : '—', detail: copy.overview.signalPathWorkloadsDetail, route: '/containers' },
{ id: 'services', label: copy.navigation.services, icon: '◇', tone: serviceTone, statusLabel: signalCollectionLabel(overview.resources.services, serviceTone, { partial: overview.servicesPartial, notConfigured: overview.serviceConfiguration === 'not_configured' }), primaryLabel: copy.overview.available, primaryValue: servicesUsable ? boundedRatio(availableServices, overview.serviceTotal, overview.servicesPartial) : '—', secondaryLabel: copy.overview.total, secondaryValue: servicesUsable ? String(overview.serviceTotal) : '—', detail: copy.overview.signalPathServicesDetail, route: '/services' },
{ id: 'incidents', label: copy.overview.signalIncidents, icon: '△', tone: incidentTone, statusLabel: signalCollectionLabel(overview.resources.incidents, incidentTone, { partial: overview.incidentsPartial }), primaryLabel: copy.overview.openIncidents, primaryValue: overview.resources.incidents === 'ready' ? `${overview.incidents.length}${overview.incidentsPartial ? '+' : ''}` : '—', secondaryLabel: copy.overview.highestSeverity, secondaryValue: overview.resources.incidents === 'ready' ? incidentSeverity : '—', detail: copy.overview.signalPathIncidentsDetail, route: '/incidents' },
];
const hasSignalAttention = signalStages.some((stage) => stage.tone === 'critical' || stage.tone === 'attention');
const hasSignalUncertainty = signalStages.some((stage) => stage.tone === 'stale' || stage.tone === 'unknown');
const heading = hasSignalAttention || problems.length > 0 || (poolUsage != null && poolUsage >= 90) ? copy.overview.attentionTitle : hasSignalUncertainty ? copy.overview.unknownTitle : copy.overview.title;
return <div className="command-overview">
<header className="overview-heading"><div><p className="eyebrow">{copy.overview.eyebrow}</p><h1>{heading}</h1><p className="intro">{copy.overview.intro}</p></div><div className="overview-heading-status"><StatusBadge label={status.label} tone={status.tone} /><small>{snapshot.status ? formatDateTime(snapshot.status.generatedAt) : copy.overview.statusLoading}</small></div></header>
<section className="source-health-strip" aria-label={copy.overview.sourceLag} tabIndex={0}>
{sourceLags.slice(0, 6).map((source) => <span className={'health-chip health-chip--' + source.state} key={source.sourceId}><span aria-hidden="true">{source.state === 'healthy' ? '✓' : source.state === 'degraded' ? '!' : '?'}</span><strong>{presentComponent(source.sourceId)}</strong><small>{presentStatus(source.state)}</small></span>)}
{sourceLags.length === 0 && <span className="health-chip health-chip--unknown"><span aria-hidden="true">?</span><strong>{copy.overview.sourceLag}</strong><small>{copy.overview.unknown}</small></span>}
</section>
<section className="overview-kpi-grid instrument-band" aria-label={copy.overview.metrics}>
<article className="overview-kpi instrument-cell"><p>{copy.overview.cpu}</p><strong>{hostUsable ? metric(overview.host?.cpu?.totalPercent) : '—'}</strong><small>{hostUsable ? presentStatus(overview.host?.source?.freshness ?? 'unknown') : signalResourceLabel(overview.resources.host, hostTone)}</small></article>
<article className="overview-kpi instrument-cell"><p>{copy.overview.memory}</p><strong>{hostUsable ? metric(overview.host?.memory?.utilizationPercent) : '—'}</strong><small>{hostUsable ? overview.host?.identity?.name ?? copy.overview.unknown : signalResourceLabel(overview.resources.host, hostTone)}</small></article>
<article className="overview-kpi instrument-cell"><p>{copy.overview.storage}</p><strong>{poolsUsable ? (overview.poolsPartial && poolUsage != null ? `≥${metric(poolUsage)}` : metric(poolUsage)) : '—'}</strong><small>{poolsUsable ? `${overview.poolTotal} ${copy.navigation.pools.toLowerCase()}` : signalResourceLabel(overview.resources.pools, storageTone)}</small></article>
<article className="overview-kpi instrument-cell"><p>{copy.overview.services}</p><strong>{servicesUsable ? boundedRatio(availableServices, overview.serviceTotal, overview.servicesPartial) : '—'}</strong><small>{overview.resources.services !== 'ready' ? signalResourceLabel(overview.resources.services, serviceTone) : !serviceConfigured ? copy.overview.signalPathNotConfigured : overview.servicesPartial ? copy.overview.signalPathPartial : copy.overview.available}</small></article>
</section>
<section className="overview-layout">
<article className="card focus-panel action-queue" aria-labelledby="overview-actions-title"><div className="card-heading"><div><p className="card-kicker">{copy.overview.actionQueue}</p><h2 id="overview-actions-title">{copy.overview.problems}</h2></div><span className="queue-count">{problems.length}</span></div>{problems.length ? <ol>{problems.map((problem, index) => <li key={problem.id}><span className="queue-severity">{index === 0 ? '!' : '?'}</span><span><strong>{problem.label}</strong><small>{problem.reason}</small></span></li>)}</ol> : <p className="card-copy">{resourcesLoading ? copy.overview.loadingResources : copy.overview.noProblems}</p>}<div className="overview-status-actions">{overviewNeedsAuthentication && <SignInButton />}<button className="button button--secondary" type="button" onClick={() => { refreshSystemStatus(); refreshOverview(); }}>{copy.overview.retry}</button><button className="button button--secondary" type="button" onClick={() => navigate('/status')}>{copy.overview.openStatus}</button></div></article>
<OperationalSignalPath stages={signalStages} onNavigate={navigate} />
<article className="card overview-table-card capacity-plane" aria-labelledby="pool-overview-title"><div className="card-heading"><div><p className="card-kicker">{copy.overview.storagePools}</p><h2 id="pool-overview-title">{copy.overview.capacity}</h2></div><button className="text-action" type="button" onClick={() => navigate('/pools')}>{copy.overview.viewAll}</button></div>{overview.resources.pools !== 'ready' ? <p className="card-copy">{resourceStateDetail(overview.resources.pools)}</p> : poolSourceTone !== 'healthy' ? <p className="card-copy">{copy.overview.resourceStaleDetail}</p> : overview.pools.length ? <ul className="overview-data-list">{overview.pools.slice(0, 5).map((pool) => { const state = operationalStorageState(pool.state, pool.capacitySeverity); return <li key={pool.id}><span><strong>{pool.name}</strong><small>{presentStatus(state)} · device-health {presentStatus(pool.state).toLowerCase()}</small></span><span className="mono-value">{metric(pool.utilizationPercent)}</span></li>; })}</ul> : <p className="card-copy">{copy.overview.noPools}</p>}</article>
<article className="card context-inspector overview-table-card workload-inspector" aria-labelledby="workload-overview-title"><div className="card-heading"><div><p className="card-kicker">Nu</p><h2 id="workload-overview-title">{copy.navigation.containers}</h2></div><button className="text-action" type="button" onClick={() => navigate('/containers')}>{copy.overview.viewAll}</button></div><div className="workload-summary"><strong>{containersUsable ? `${overview.containersPartial ? '≥' : ''}${runningContainers}` : '—'}</strong><span>{copy.overview.running}</span><strong>{containersUsable && !overview.containersPartial ? Math.max(0, overview.containerTotal - runningContainers) : '—'}</strong><span>{copy.overview.other}</span></div><p className="card-copy">{overview.resources.containers !== 'ready' ? resourceStateDetail(overview.resources.containers) : containerSourceTone !== 'healthy' ? copy.overview.resourceStaleDetail : overview.containersPartial ? copy.overview.resourcePartialDetail : overview.containerTotal > 0 ? `${runningContainers} van ${overview.containerTotal} ${copy.overview.containersRunning}.` : copy.overview.signalPathNoWorkloads}</p><dl className="overview-now-list"><div><dt>{copy.overview.cpu}</dt><dd>{hostUsable ? metric(overview.host?.cpu?.totalPercent) : '—'}</dd></div><div><dt>{copy.overview.memory}</dt><dd>{hostUsable ? metric(overview.host?.memory?.utilizationPercent) : '—'}</dd></div><div><dt>Bron</dt><dd>{hostUsable ? presentStatus(overview.host?.source?.freshness ?? 'unknown') : signalResourceLabel(overview.resources.host, hostTone)}</dd></div></dl></article>
<article className="card overview-table-card incident-queue" aria-labelledby="incident-overview-title"><div className="card-heading"><div><p className="card-kicker">{copy.overview.recentIncidents}</p><h2 id="incident-overview-title">{copy.navigation.incidents}</h2></div><button className="text-action" type="button" onClick={() => navigate('/incidents')}>{copy.overview.viewAll}</button></div>{overview.resources.incidents !== 'ready' ? <p className="card-copy">{resourceStateDetail(overview.resources.incidents)}</p> : orderedIncidents.length ? <ul className="overview-data-list">{orderedIncidents.slice(0, 5).map((incident) => <li key={incident.id}><span className={'incident-dot incident-dot--' + incident.severity} aria-hidden="true" /><span><strong>{incident.title}</strong><small>{formatDateTime(incident.startedAt)}</small></span></li>)}</ul> : <p className="card-copy">{copy.overview.noIncidents}</p>}</article>
</section>
</div>;
}
type ApiRecord = Record<string, unknown>;
type DashboardSummary = { id: string; slug: string; name: string; description: string; scope: string; revision: number; currentVersion: number };
type DashboardWidget = { id: string; type: string; title: string; description?: string; data?: ApiRecord; visualization?: ApiRecord; behavior?: ApiRecord; layouts?: ApiRecord };
type DashboardViewport = 'desktop' | 'tablet' | 'mobile' | 'wallboard';
type CrossFilter = { key: string; value: string; label: string; sourceWidgetId: string };
class ApiError extends Error { constructor(readonly status: number) { super('api request failed'); } }
function field<T>(record: ApiRecord | undefined, name: string): T | undefined {
if (!record) return undefined;
const upper = name.charAt(0).toUpperCase() + name.slice(1);
return (record[name] ?? record[upper] ?? record[name.toUpperCase()]) as T | undefined;
}
function summaryFromApi(record: ApiRecord): DashboardSummary {
return { id: String(field(record, 'id') ?? ''), slug: String(field(record, 'slug') ?? ''), name: String(field(record, 'name') ?? copy.dashboards.unnamed), description: String(field(record, 'description') ?? ''), scope: String(field(record, 'scope') ?? 'unknown'), revision: Number(field(record, 'revision') ?? 0), currentVersion: Number(field(record, 'currentVersion') ?? 0) };
}
async function getJSON<T>(url: string, signal: AbortSignal): Promise<T> {
const response = await fetch(url, { signal, cache: 'no-store' });
if (!response.ok) throw new ApiError(response.status);
return response.json() as Promise<T>;
}
function DashboardsPage() {
const [state, setState] = useState<'loading' | 'error' | 'unauthorized' | 'empty' | 'ready'>('loading');
const [items, setItems] = useState<DashboardSummary[]>([]);
const [reload, setReload] = useState(0);
useEffect(() => {
const controller = new AbortController();
// Rotation is a background replacement once a dashboard is visible. Keep
// the current view (and any identical shared live subscription) mounted
// until the next document arrives instead of bouncing through `loading`.
setState((current) => current === 'ready' ? current : 'loading');
getJSON<{ items?: ApiRecord[] }>('/api/v1/dashboards?limit=100', controller.signal).then((data) => {
const next = (data.items ?? []).map(summaryFromApi).filter((item) => item.id !== '');
setItems(next); setState(next.length === 0 ? 'empty' : 'ready');
}).catch((error: unknown) => {
if (error instanceof DOMException && error.name === 'AbortError') return;
setState(error instanceof ApiError && error.status === 401 ? 'unauthorized' : 'error');
});
return () => controller.abort();
}, [reload]);
const stateContent = state === 'loading' ? <p className="card-copy" role="status">{copy.dashboards.loading}</p> :
state === 'unauthorized' ? <div className="dashboard-message" role="alert"><h2>{copy.dashboards.unauthorizedTitle}</h2><p>{copy.dashboards.unauthorizedDetail}</p><SignInButton /></div> :
state === 'error' ? <div className="dashboard-message" role="alert"><h2>{copy.dashboards.errorTitle}</h2><p>{copy.dashboards.errorDetail}</p><button className="button button--secondary" type="button" onClick={() => setReload((value) => value + 1)}>{copy.dashboards.retry}</button></div> :
state === 'empty' ? <div className="dashboard-message dashboard-message--empty"><span className="empty-state-icon" aria-hidden="true">▦</span><h2>{copy.dashboards.empty}</h2><p>{copy.dashboards.emptyDetail}</p></div> :
<ul className="dashboard-list">{items.map((item) => <li key={item.id}><button type="button" className="dashboard-list-item" onClick={() => navigate('/dashboards/' + encodeURIComponent(item.id))}><span><strong>{item.name}</strong><small>{item.description || item.slug}</small></span><span className="dashboard-list-meta"><span>{copy.dashboards.version} {item.currentVersion}</span><StatusBadge label={item.scope === 'personal' ? copy.dashboards.personal : copy.dashboards.shared} tone="ready" /></span></button></li>)}</ul>;
return <><PageIntro eyebrow={copy.dashboards.eyebrow} title={copy.dashboards.title} intro={copy.dashboards.intro} /><section className="card dashboard-list-panel" aria-labelledby="dashboard-list-title"><div className="card-heading"><div><p className="card-kicker">{copy.dashboards.catalog}</p><h2 id="dashboard-list-title">{copy.dashboards.listTitle}</h2></div><StatusBadge label={state === 'ready' ? copy.dashboards.ready : copy.dashboards.unknown} tone={state === 'ready' ? 'ready' : 'unknown'} /></div>{stateContent}</section>{state === 'ready' && <p className="dashboard-count">{items.length} {copy.dashboards.available}</p>}</>;
}
class WidgetBoundary extends Component<{ title: string; children: ReactNode }, { failed: boolean }> {
state = { failed: false };
static getDerivedStateFromError(): { failed: boolean } { return { failed: true }; }
componentDidCatch(_error: Error, _info: ErrorInfo): void {}
render() { return this.state.failed ? <article className="widget-card widget-card--error" role="alert"><p className="card-kicker">{copy.dashboards.widgetError}</p><h3>{this.props.title}</h3><p>{copy.dashboards.widgetErrorDetail}</p></article> : this.props.children; }
}
const widgetLabels: Record<string, string> = { stat: copy.widgets.stat, timeseries: copy.widgets.timeseries, gauge: copy.widgets.gauge, 'ranked-list': copy.widgets.rankedList, 'status-grid': copy.widgets.statusGrid, table: copy.widgets.table, heatmap: copy.widgets.heatmap, 'event-timeline': copy.widgets.eventTimeline, 'storage-map': copy.widgets.storageMap, topology: copy.widgets.topology, 'service-matrix': copy.widgets.serviceMatrix, 'alert-summary': copy.widgets.alertSummary, text: copy.widgets.text, 'query-inspector': copy.widgets.queryInspector };
// Installed before any client captures the global `fetch`, so every API call in
// the app funnels its 401s through one place.
installSessionWatcher();
function responsiveViewport(): 'desktop' | 'tablet' | 'mobile' { return window.innerWidth <= 700 ? 'mobile' : window.innerWidth <= 900 ? 'tablet' : 'desktop'; }
function widgetFilter(widget: DashboardWidget): { key: string; value: string; label: string } {
const data = widget.data ?? {};
const scope = field<ApiRecord>(data, 'scope') ?? {};
const entityType = field<string>(scope, 'entityType');
const sourceType = String(field(data, 'sourceType') ?? 'unknown');
return entityType ? { key: 'entityType', value: entityType, label: entityType } : { key: 'sourceType', value: sourceType, label: sourceType };
}
function filterAllowed(document: ApiRecord, key: string, value: string): boolean {
const safeSources = ['semantic-metric', 'inventory', 'events', 'alerts', 'incidents', 'text'];
if (key === 'sourceType') return safeSources.includes(value);
const variables = (field<unknown[]>(document, 'variables') ?? []) as ApiRecord[];
return variables.some((variable) => { const options = field<unknown[]>(variable, 'options') ?? []; return options.includes(value); });
}
function filterFromURL(document: ApiRecord): CrossFilter | null {
const params = new URLSearchParams(window.location.search);
const key = params.get('filterKey');
const value = params.get('filterValue');
if (!key || !value || !filterAllowed(document, key, value)) return null;
return { key, value, label: value, sourceWidgetId: '' };
}
function compatibleWithFilter(widget: DashboardWidget, filter: CrossFilter | null): boolean {
if (!filter || widget.id === filter.sourceWidgetId) return true;
const next = widgetFilter(widget);
return next.key === filter.key && next.value === filter.value;
}
function wallboardSlideFor(widget: DashboardWidget): number {
const layout = field<ApiRecord>(widget.layouts ?? {}, 'wallboard') ?? field<ApiRecord>(widget.layouts ?? {}, 'desktop') ?? {};
return wallboardSlideIndex(field(layout, 'y'));
}
function widgetLayoutStyle(layout: ApiRecord, viewport: DashboardViewport): CSSProperties {
const columns = viewport === 'wallboard' ? wallboardColumns : viewport === 'tablet' ? 8 : viewport === 'mobile' ? 1 : 18;
const width = viewport === 'mobile' ? 1 : Math.min(columns, Math.max(1, Number(field<number>(layout, 'w') ?? 6)));
if (viewport !== 'wallboard') return { '--widget-span': String(width) } as CSSProperties;
const placement = wallboardPlacement(layout);
return { '--widget-span': String(placement.columnSpan), gridColumn: `${placement.columnStart} / span ${placement.columnSpan}`, gridRow: `${placement.rowStart} / span ${placement.rowSpan}` } as CSSProperties;
}
function DashboardWidgetView({ widget, viewport, onFilter, metric }: { widget: DashboardWidget; viewport: DashboardViewport; onFilter: (widget: DashboardWidget) => void; metric?: MetricWidgetProps }) {
if (!widget.id || !widget.title) return <article className="widget-card widget-card--error" role="alert"><p className="card-kicker">{copy.dashboards.widgetError}</p><h3>{copy.widgets.unknown}</h3><p>{copy.dashboards.widgetErrorDetail}</p></article>;
const behavior = widget.behavior ?? {};
if (field<boolean>(behavior, 'hidden')) return null;
const activeLayout = field<ApiRecord>(widget.layouts ?? {}, viewport) ?? field<ApiRecord>(widget.layouts ?? {}, 'desktop') ?? {};
if (field<boolean>(activeLayout, 'visible') === false) return null;
const typeLabel = widgetLabels[widget.type] ?? copy.widgets.unknown;
const source = String(field(widget.data ?? {}, 'sourceType') ?? copy.dashboards.unknown);
const status = metric ? metricStatus(metric) : { label: copy.dashboards.unknown, tone: 'unknown' as const };
const metricKind = metric && (widget.type === 'stat' || widget.type === 'timeseries' || widget.type === 'gauge' || widget.type === 'query-inspector');
const widgetItems = (field<unknown[]>(widget.data ?? {}, 'items') ?? []) as ApiRecord[];
const rankedItems: RankedListItem[] = widgetItems.map((item) => ({ id: String(field(item, 'id') ?? ''), label: String(field(item, 'label') ?? field(item, 'name') ?? ''), value: String(field(item, 'value') ?? ''), detail: field<string>(item, 'detail') } )).filter((item) => item.id !== '' && item.label !== '');
const statusItems: StatusGridItem[] = widgetItems.map((item) => ({ id: String(field(item, 'id') ?? ''), label: String(field(item, 'label') ?? field(item, 'name') ?? ''), status: String(field(item, 'status') ?? 'unknown'), reason: field<string>(item, 'reason') } )).filter((item) => item.id !== '' && item.label !== '');
const storageNodes: StorageMapNode[] = widgetItems.map((item) => ({ id: String(field(item, 'id') ?? ''), label: String(field(item, 'label') ?? field(item, 'name') ?? ''), kind: String(field(item, 'kind') ?? 'storage'), state: String(field(item, 'status') ?? 'unknown'), detail: field<string>(item, 'detail'), href: field<string>(item, 'href') })).filter((item) => item.id !== '' && item.label !== '');
const topologyData = field<TopologyData>(widget.data ?? {}, 'topology');
const networkData = field<NetworkData>(widget.data ?? {}, 'network');
const heatmapPoints: HeatmapPoint[] = widgetItems.map((item) => { const value = Number(field(item, 'value')); return { id: String(field(item, 'id') ?? ''), label: String(field(item, 'label') ?? field(item, 'name') ?? ''), observedAt: String(field(item, 'observedAt') ?? new Date(0).toISOString()), value: Number.isFinite(value) ? value : null, status: String(field(item, 'status') ?? 'unknown'), href: field<string>(item, 'href') }; }).filter((item) => item.id !== '' && item.label !== '');
return <article className="widget-card" data-viewport={viewport} style={widgetLayoutStyle(activeLayout, viewport)} aria-labelledby={'widget-' + widget.id}><div className="widget-card-heading"><div><p className="card-kicker">{typeLabel}</p><h3 id={'widget-' + widget.id}>{widget.title}</h3></div><StatusBadge label={status.label} tone={status.tone} /></div>{widget.description && <p className="widget-description">{widget.description}</p>}{metricKind ? <MetricWidget {...metric!} /> : widget.type === 'ranked-list' && rankedItems.length > 0 ? <RankedListWidget items={rankedItems} onSelect={() => onFilter(widget)} /> : widget.type === 'status-grid' && statusItems.length > 0 ? <StatusGridWidget items={statusItems} onSelect={() => onFilter(widget)} /> : widget.type === 'storage-map' && storageNodes.length > 0 ? <StorageMapWidget nodes={storageNodes} title={widget.title} description={widget.description || copy.storage.mapDescription} idPrefix={'storage-map-' + widget.id} /> : widget.type === 'topology' && topologyData ? <Suspense fallback={<p className="card-copy" role="status">{copy.topology.loading}</p>}><TopologyWidget topology={topologyData} compact /></Suspense> : widget.type === 'network' && networkData ? <NetworkHealthWidget snapshot={networkData} compact /> : widget.type === 'heatmap' && heatmapPoints.length > 0 ? <TemperatureHeatmap points={heatmapPoints} title={widget.title} description={widget.description || copy.storage.heatmapDescription} idPrefix={'heatmap-' + widget.id} /> : <button className="widget-placeholder" type="button" data-widget-type={widget.type} aria-label={typeLabel + ': ' + widget.title + ' filteren'} onClick={() => onFilter(widget)}><span className="widget-placeholder-icon" aria-hidden="true">{widget.type === 'text' ? 'T' : '◌'}</span><strong>{copy.dashboards.noData}</strong><small>{source} · {copy.dashboards.dataPending}</small></button>}</article>;
}
function DashboardViewPage({ dashboardId, wallboard = false, wallboardSlide = 0, onWallboardSlideCount, onRuntimeState }: { dashboardId: string; wallboard?: boolean; wallboardSlide?: number; onWallboardSlideCount?: (count: number) => void; onRuntimeState?: (state: RuntimeState) => void }) {
const [state, setState] = useState<'loading' | 'error' | 'unauthorized' | 'ready'>('loading');
const [summary, setSummary] = useState<DashboardSummary | null>(null);
const [document, setDocument] = useState<ApiRecord>({});
const [widgets, setWidgets] = useState<DashboardWidget[]>([]);
const [editing, setEditing] = useState(false);
const [crossFilter, setCrossFilter] = useState<CrossFilter | null>(null);
const [runtimeStates, setRuntimeStates] = useState<Record<string, RuntimeState>>({});
const hasWallboardContent = useRef(false);
const systemSnapshot = useSystemStatus();
const storageKey = 'pulse.dashboard.view.' + dashboardId;
const [timeRange, setTimeRange] = useState(() => wallboard ? 'live' : window.localStorage.getItem(storageKey + '.range') ?? '1h');
const [filter, setFilter] = useState(() => window.localStorage.getItem(storageKey + '.filter') ?? '');
const updateRuntimeState = useCallback((id: string, next: RuntimeState) => setRuntimeStates((current) => current[id] === next ? current : { ...current, [id]: next }), []);
useEffect(() => {
const controller = new AbortController();
const replacingVisibleWallboard = wallboard && hasWallboardContent.current;
if (!replacingVisibleWallboard) setState('loading');
getJSON<{ dashboard: ApiRecord; version: ApiRecord }>('/api/v1/dashboards/' + encodeURIComponent(dashboardId), controller.signal).then((data) => {
const rawDocument = field<ApiRecord>(data.version, 'document') ?? {};
setDocument(rawDocument);
setCrossFilter(filterFromURL(rawDocument));
setSummary(summaryFromApi(data.dashboard));
setWidgets((field<unknown[]>(rawDocument, 'widgets') ?? []) as DashboardWidget[]);
setRuntimeStates({});
if (wallboard) hasWallboardContent.current = true;
setState('ready');
}).catch((error: unknown) => {
if (error instanceof DOMException && error.name === 'AbortError') return;
if (error instanceof ApiError && error.status === 401) {
setState('unauthorized');
return;
}
// A wallboard is a continuous operational display. During a transient
// failed rotation, keep the last verified document visible and let the
// next bounded rotation/refresh retry. Never apply this to first load or
// authentication failure.
if (!replacingVisibleWallboard) setState('error');
});
return () => controller.abort();
}, [dashboardId, wallboard]);
useEffect(() => { window.localStorage.setItem(storageKey + '.range', timeRange); }, [storageKey, timeRange]);
useEffect(() => { window.localStorage.setItem(storageKey + '.filter', filter); }, [storageKey, filter]);
useEffect(() => {
const values = Object.values(runtimeStates);
const aggregate: RuntimeState = values.includes('usable') ? 'usable' : values.includes('error') ? 'error' : values.length > 0 && values.every((value) => value === 'empty') ? 'empty' : 'loading';
onRuntimeState?.(aggregate);
}, [onRuntimeState, runtimeStates]);
useEffect(() => { if (wallboard) setRuntimeStates({}); }, [wallboard, wallboardSlide]);
const wallboardWidgets = widgets.filter((widget) => {
const layout = field<ApiRecord>(widget.layouts ?? {}, 'wallboard') ?? field<ApiRecord>(widget.layouts ?? {}, 'desktop') ?? {};
return field<boolean>(widget.behavior ?? {}, 'hidden') !== true && field<boolean>(layout, 'visible') !== false;
});
const wallboardSlideCount = Math.max(1, ...wallboardWidgets.map((widget) => wallboardSlideFor(widget) + 1));
useEffect(() => { if (wallboard) onWallboardSlideCount?.(wallboardSlideCount); }, [onWallboardSlideCount, wallboard, wallboardSlideCount]);
if (state === 'loading') return <section className="state-page" role="status"><span className="loader" aria-hidden="true" /><h1>{copy.dashboards.loading}</h1></section>;
if (state === 'unauthorized') return <StatePage kind="unauthorized" />;
if (state === 'error' || !summary) return <StatePage kind="error" />;
if (editing) return <Suspense fallback={<StatePage kind="loading" />}><DashboardEditor dashboardId={dashboardId} revision={summary.revision} document={document} onExit={() => setEditing(false)} onSaved={(revision, nextDocument) => { setDocument(nextDocument); setWidgets((field<unknown[]>(nextDocument, 'widgets') ?? []) as DashboardWidget[]); setSummary({ ...summary, revision, currentVersion: summary.currentVersion + 1 }); setEditing(false); }} /></Suspense>;
const viewport: DashboardViewport = wallboard ? 'wallboard' : responsiveViewport();
const normalized = filter.trim().toLowerCase();
const shown = widgets.filter((widget) => { const activeLayout = field<ApiRecord>(widget.layouts ?? {}, viewport) ?? field<ApiRecord>(widget.layouts ?? {}, 'desktop') ?? {}; return field<boolean>(widget.behavior ?? {}, 'hidden') !== true && field<boolean>(activeLayout, 'visible') !== false && (!wallboard || wallboardSlideFor(widget) === Math.min(wallboardSlide, wallboardSlideCount - 1)) && (!normalized || widget.title.toLowerCase().includes(normalized)) && compatibleWithFilter(widget, crossFilter); });
const systemStatus = aggregateStatus(systemSnapshot);
const applyCrossFilter = (widget: DashboardWidget) => { const next = widgetFilter(widget); const filter = { ...next, sourceWidgetId: widget.id }; setCrossFilter(filter); const params = new URLSearchParams(window.location.search); params.set('filterKey', filter.key); params.set('filterValue', filter.value); window.history.replaceState({}, '', window.location.pathname + '?' + params.toString()); };
const clearCrossFilter = () => { setCrossFilter(null); const params = new URLSearchParams(window.location.search); params.delete('filterKey'); params.delete('filterValue'); const query = params.toString(); window.history.replaceState({}, '', window.location.pathname + (query ? '?' + query : '')); };
const usableCount = Object.values(runtimeStates).filter((value) => value === 'usable').length;
return <section className={wallboard ? 'dashboard-view wallboard-view' : 'dashboard-view'} aria-labelledby="dashboard-view-title">{!wallboard && <button className="back-link" type="button" onClick={() => navigate('/dashboards')}>← {copy.dashboards.back}</button>}<header className="dashboard-view-header"><div><p className="eyebrow">{copy.dashboards.viewMode}</p>{wallboard ? <h2 id="dashboard-view-title">{summary.name}</h2> : <h1 id="dashboard-view-title">{summary.name}</h1>}<p className="intro">{summary.description || copy.dashboards.noDescription}</p></div><div className="dashboard-view-meta">{wallboard && <span className="wallboard-read-only">{copy.wallboard.readOnly}</span>}<StatusBadge label={systemStatus.label} tone={systemStatus.tone} /><span>{copy.dashboards.version} {summary.currentVersion}</span>{!wallboard && <button className="button button--secondary" type="button" onClick={() => setEditing(true)}>{copy.dashboards.edit}</button>}</div></header>{!wallboard && <div className="dashboard-controls" aria-label={copy.dashboards.controls}><label>{copy.dashboards.timeRange}<select value={timeRange} onChange={(event) => setTimeRange(event.target.value)}><option value="live">{copy.dashboards.live}</option><option value="15m">15 {copy.dashboards.minutes}</option><option value="1h">1 {copy.dashboards.hour}</option><option value="6h">6 {copy.dashboards.hours}</option><option value="24h">24 {copy.dashboards.hours}</option><option value="7d">7 {copy.dashboards.days}</option></select></label><label>{copy.dashboards.filter}<input value={filter} onChange={(event) => setFilter(event.target.value)} placeholder={copy.dashboards.filterPlaceholder} /></label><span className="view-mode-note">{crossFilter ? copy.dashboards.filterActive + ': ' + crossFilter.label : copy.dashboards.fixedView}</span><span className="metric-query-status" role="status">{usableCount} van {shown.length} {copy.dashboards.widgetsWithData}</span>{crossFilter && <button className="button button--secondary clear-cross-filter" type="button" onClick={clearCrossFilter}>{copy.dashboards.clearFilter}</button>}</div>}{shown.length === 0 ? <div className="card dashboard-message"><h2>{copy.dashboards.noMatchingWidgets}</h2><p>{copy.dashboards.clearFilterHint}</p></div> : <><h2 className="sr-only" id="dashboard-widgets-title">{copy.dashboards.widgetCollection}</h2><div className="dashboard-grid" aria-labelledby="dashboard-widgets-title">{shown.map((widget) => <WidgetBoundary key={widget.id} title={widget.title || copy.widgets.unknown}>{['semantic-metric', 'inventory', 'events'].includes(String(field(widget.data, 'sourceType') ?? '')) ? <DashboardRuntimeWidget widget={widget} viewport={viewport} document={document} timeRange={timeRange} onState={updateRuntimeState} onFilter={() => applyCrossFilter(widget)} /> : <DashboardWidgetView widget={widget} viewport={viewport} onFilter={applyCrossFilter} />}</WidgetBoundary>)}</div></>}</section>;
}
type WallboardPriorityData = { serviceProblems: number; openIncidents: number; loading: boolean; unavailable: boolean };
function useWallboardPriorityData(): WallboardPriorityData {
const [value, setValue] = useState<WallboardPriorityData>({ serviceProblems: 0, openIncidents: 0, loading: true, unavailable: false });
useEffect(() => {
let active = true;
let controller: AbortController | null = null;
const load = async () => {
controller?.abort();
const current = new AbortController();
controller = current;
try {
const [servicesResponse, incidentsResponse] = await Promise.all([
fetch('/api/v1/services?limit=100', { signal: current.signal, cache: 'no-store' }),
fetch('/api/v1/incidents?limit=100&status=open', { signal: current.signal, cache: 'no-store' }),
]);
if (!servicesResponse.ok || !incidentsResponse.ok) throw new Error('priority');
const services = await servicesResponse.json() as { services?: Array<{ state?: string }> };
const incidents = await incidentsResponse.json() as { items?: unknown[] };
if (active) setValue({ serviceProblems: (services.services ?? []).filter((item) => item.state !== 'up').length, openIncidents: (incidents.items ?? []).length, loading: false, unavailable: false });
} catch (error: unknown) {
if (error instanceof DOMException && error.name === 'AbortError') return;
if (active) setValue((currentValue) => ({ ...currentValue, loading: false, unavailable: true }));
}
};
void load();
const timer = window.setInterval(load, 30000);
return () => { active = false; controller?.abort(); window.clearInterval(timer); };
}, []);
return value;
}
function WallboardPage() {
const [state, setState] = useState<'loading' | 'ready' | 'error' | 'unauthorized' | 'empty'>('loading');
const [items, setItems] = useState<DashboardSummary[]>([]);
const [activeIndex, setActiveIndex] = useState(0);
const [activeSlide, setActiveSlide] = useState(0);
const [slideCount, setSlideCount] = useState(1);
const [lastUpdated, setLastUpdated] = useState<string | undefined>();
const [transport, setTransport] = useState<'connected' | 'reconnecting' | 'unavailable'>('reconnecting');
const [dataState, setDataState] = useState<RuntimeState>('loading');
const [fullscreen, setFullscreen] = useState(false);
const [shift, setShift] = useState(0);
const systemSnapshot = useSystemStatus();
const system = aggregateStatus(systemSnapshot);
const storage = systemSnapshot.status?.components.find((component) => component.id === 'storage');
const priority = useWallboardPriorityData();
const params = new URLSearchParams(window.location.search);
const intervalSeconds = Math.min(300, Math.max(10, Number(params.get('interval') ?? 30) || 30));
const refreshSeconds = Math.min(300, Math.max(10, Number(params.get('refresh') ?? 30) || 30));
useEffect(() => onUnauthenticated(() => { setTransport('unavailable'); setState('unauthorized'); }), []);
useEffect(() => {
let active = true;
let inFlight: AbortController | null = null;
const load = async () => {
inFlight?.abort();
const controller = new AbortController();
inFlight = controller;
try {
const response = await fetch('/api/v1/dashboards?limit=100', { signal: controller.signal, cache: 'no-store' });
if (!response.ok) throw new Error('wallboard');
const data = await response.json() as { items?: ApiRecord[] };
const next = (data.items ?? []).map(summaryFromApi).filter((item) => item.id !== '').sort((a, b) => a.id.localeCompare(b.id));
if (!active) return;
setItems(next);
setActiveIndex((value) => next.length === 0 ? 0 : Math.min(value, next.length - 1));
setLastUpdated(new Date().toISOString());
setTransport('connected');
setState(next.length === 0 ? 'empty' : 'ready');
} catch (error: unknown) {
if (error instanceof DOMException && error.name === 'AbortError') return;
if (!active) return;
setTransport('unavailable');
setState((value) => value === 'ready' || value === 'unauthorized' ? value : 'error');
} finally {
if (inFlight === controller) inFlight = null;
}
};
void load();
const refresh = window.setInterval(() => { setTransport('reconnecting'); void load(); }, refreshSeconds * 1000);
return () => { active = false; inFlight?.abort(); window.clearInterval(refresh); };
}, []);
useEffect(() => {
if (items.length === 0) return undefined;
const rotation = window.setInterval(() => setActiveSlide((value) => {
if (value + 1 < slideCount) return value + 1;
if (items.length > 1) setActiveIndex((dashboard) => (dashboard + 1) % items.length);
return 0;
}), intervalSeconds * 1000);
return () => window.clearInterval(rotation);
}, [items.length, intervalSeconds, slideCount]);
useEffect(() => {
const timer = window.setInterval(() => setShift((value) => (value + 1) % 2), 60000);
return () => window.clearInterval(timer);
}, []);
useEffect(() => {
const update = () => setFullscreen(Boolean(document.fullscreenElement));
document.addEventListener('fullscreenchange', update);
update();
return () => document.removeEventListener('fullscreenchange', update);
}, []);
const toggleFullscreen = async () => {
try {
if (document.fullscreenElement) await document.exitFullscreen();
else if (document.documentElement.requestFullscreen) await document.documentElement.requestFullscreen();
} catch { /* Fullscreen is optional; transport and data state remain truthful. */ }
};
const handleRuntimeState = useCallback((runtime: RuntimeState) => setDataState(runtime), []);
const handleSlideCount = useCallback((count: number) => { setSlideCount(Math.max(1, count)); setActiveSlide((value) => Math.min(value, Math.max(1, count) - 1)); }, []);
if (state === 'loading') return <section className="wallboard-shell wallboard-shell--state" aria-live="polite"><p className="eyebrow">{copy.wallboard.eyebrow}</p><h1>{copy.wallboard.loading}</h1></section>;
if (state === 'unauthorized') return <section className="wallboard-shell wallboard-shell--state" role="alert"><p className="eyebrow">{copy.wallboard.eyebrow}</p><h1>{copy.states.unauthorizedTitle}</h1><p>{copy.states.unauthorizedDetail}</p><SignInButton /></section>;
if (state === 'error') return <section className="wallboard-shell wallboard-shell--state" role="alert"><p className="eyebrow">{copy.wallboard.eyebrow}</p><h1>{copy.wallboard.errorTitle}</h1><p>{copy.wallboard.errorDetail}</p></section>;
if (state === 'empty') return <section className="wallboard-shell wallboard-shell--state"><p className="eyebrow">{copy.wallboard.eyebrow}</p><h1>{copy.wallboard.noDashboards}</h1></section>;
const current = items[activeIndex];
return <section className={'wallboard-shell wallboard-shell--shift-' + shift} aria-labelledby="wallboard-title">
<header className="wallboard-header"><div><p className="eyebrow">{copy.wallboard.eyebrow}</p><h1 id="wallboard-title">{copy.wallboard.title}</h1><p className="intro">{copy.wallboard.intro}</p></div><div className="wallboard-actions"><span className={'wallboard-connection wallboard-connection--' + transport} role="status">{copy.wallboard.transport}: {transport === 'connected' ? copy.wallboard.connected : transport === 'reconnecting' ? copy.wallboard.reconnecting : copy.wallboard.unavailable}</span><span className={'wallboard-connection wallboard-connection--' + (dataState === 'usable' ? 'connected' : dataState === 'loading' ? 'reconnecting' : 'unavailable')} role="status">{copy.wallboard.data}: {dataState === 'usable' ? copy.wallboard.dataUsable : dataState === 'loading' ? copy.wallboard.dataLoading : dataState === 'empty' ? copy.wallboard.dataEmpty : copy.wallboard.unavailable}</span><button className="button button--secondary" type="button" onClick={toggleFullscreen}>{fullscreen ? copy.wallboard.exitFullscreen : copy.wallboard.enterFullscreen}</button></div></header>
<div className="wallboard-priority" aria-label={copy.wallboard.priority}><span className={'wallboard-priority-item wallboard-priority-item--' + (system.state === 'healthy' ? 'ready' : 'attention')}><strong>{copy.wallboard.overall}</strong><small>{system.label}</small></span><span className={'wallboard-priority-item wallboard-priority-item--' + (storage?.state === 'healthy' ? 'ready' : 'attention')}><strong>{copy.wallboard.storage}</strong><small>{storage ? presentStatus(storage.state) : copy.wallboard.unknown}</small></span><span className={'wallboard-priority-item wallboard-priority-item--' + (priority.serviceProblems === 0 && !priority.unavailable ? 'ready' : 'attention')}><strong>{copy.wallboard.services}</strong><small>{priority.loading ? copy.wallboard.dataLoading : priority.unavailable ? copy.wallboard.unknown : `${priority.serviceProblems} ${copy.wallboard.problems}`}</small></span><span className={'wallboard-priority-item wallboard-priority-item--' + (priority.openIncidents === 0 && !priority.unavailable ? 'ready' : 'attention')}><strong>{copy.wallboard.incidents}</strong><small>{priority.loading ? copy.wallboard.dataLoading : priority.unavailable ? copy.wallboard.unknown : `${priority.openIncidents} ${copy.wallboard.open}`}</small></span></div>
<div className="wallboard-status"><span>{copy.wallboard.lastUpdated}: {lastUpdated ? formatDateTime(lastUpdated) : copy.wallboard.reconnecting}</span><span>{copy.wallboard.rotate} {copy.wallboard.every} {intervalSeconds} {copy.wallboard.seconds}</span><span>{copy.wallboard.slide} {activeSlide + 1} / {slideCount}</span><span>{copy.wallboard.dashboard} {activeIndex + 1} / {items.length}</span></div>
<div className="wallboard-frame"><DashboardViewPage dashboardId={current.id} wallboard wallboardSlide={activeSlide} onWallboardSlideCount={handleSlideCount} onRuntimeState={handleRuntimeState} /></div>
</section>;
}
const AlertsPage = AlertRulesPage;
function SettingsPage() {
const snapshot = useSystemStatus();
const status = aggregateStatus(snapshot);
const connected = snapshot.status?.sourceLag.filter((source) => source.state === 'healthy').length ?? 0;
const total = snapshot.status?.sourceLag.length ?? 0;
const sourceDetail = snapshot.state === 'ready' && total > 0 ? `${connected} van ${total} ${copy.settings.sourcesCurrent}` : status.detail;
const groups = [
{ title: copy.settings.healthTitle, detail: copy.settings.healthDetail, links: [
{ href: '/status', title: copy.settings.systemStatus, detail: copy.settings.systemStatusDetail, access: copy.settings.adminActions },
{ href: '/onboarding', title: copy.settings.onboarding, detail: copy.settings.onboardingDetail, access: copy.settings.adminChanges },
] },
{ title: copy.settings.alertingTitle, detail: copy.settings.alertingDetail, links: [
{ href: '/alerts?section=rules', title: copy.settings.alertRules, detail: copy.settings.alertRulesDetail, access: copy.settings.editorChanges },
{ href: '/alerts?section=controls', title: copy.settings.alertControls, detail: copy.settings.alertControlsDetail, access: copy.settings.operatorChanges },
] },
{ title: copy.settings.presentationTitle, detail: copy.settings.presentationDetail, links: [
{ href: '/dashboards', title: copy.settings.dashboards, detail: copy.settings.dashboardsDetail, access: copy.settings.editorChanges },
{ href: '/inventory', title: copy.settings.inventory, detail: copy.settings.inventoryDetail, access: copy.settings.viewAccess },
] },
];
return <><PageIntro eyebrow={copy.settings.eyebrow} title={copy.settings.title} intro={copy.settings.intro} />
<section className="settings-overview card" aria-labelledby="settings-overview-title"><div className="card-heading"><div><p className="card-kicker">{copy.settings.current}</p><h2 id="settings-overview-title">{copy.settings.environment}</h2></div><StatusBadge label={status.label} tone={status.tone} /></div><div className="setting-row"><span><strong>{copy.settings.source}</strong><small>{sourceDetail}</small></span><span>{connected}/{total || '—'}</span></div><div className="setting-row"><span><strong>{copy.settings.language}</strong><small>{copy.settings.languageDetail}</small></span><strong>{copy.settings.languageValue}</strong></div></section>
<section className="settings-hub" aria-label={copy.settings.management}><h2 className="sr-only">{copy.settings.management}</h2>{groups.map((group) => <article className="card settings-hub-card" key={group.title}><p className="card-kicker">{copy.settings.management}</p><h3>{group.title}</h3><p className="card-copy">{group.detail}</p><ul>{group.links.map((link) => <li key={link.href}><a href={link.href} onClick={(event) => { event.preventDefault(); navigate(link.href); }}><span><strong>{link.title}</strong><small>{link.detail}</small></span><span className="settings-access">{link.access}<span aria-hidden="true">→</span></span></a></li>)}</ul></article>)}</section>
</>;
}
function StatePage({ kind }: { kind: 'loading' | 'error' | 'unauthorized' }) {
if (kind === 'loading') return <section className="state-page" role="status"><span className="loader" aria-hidden="true" /><h1>{copy.states.loading}</h1></section>;
if (kind === 'unauthorized') {
// The API answered 401, so the visitor is not signed in: offer the real
// sign-in entry point and come back to the page they asked for.
return <section className="state-page" role="alert"><span className="state-icon" aria-hidden="true">!</span><h1>{copy.states.unauthorizedTitle}</h1><p>{copy.states.unauthorizedDetail}</p><p>{copy.auth.signInHint}</p><div className="state-page-actions"><SignInButton /><button className="button button--secondary" type="button" onClick={() => navigate('/')}>{copy.states.returnHome}</button></div></section>;
}
return <section className="state-page" role="alert"><span className="state-icon" aria-hidden="true">×</span><h1>{copy.states.errorTitle}</h1><p>{copy.states.errorDetail}</p><button className="button" type="button" onClick={() => navigate(routeFromLocation(window.location.pathname))}>{copy.states.retry}</button></section>;
}
/** Wraps lazily loaded routes in the same loading state the rest of the app uses. */
function RouteSuspense({ children }: { children: ReactNode }) {
return <Suspense fallback={<StatePage kind="loading" />}>{children}</Suspense>;
}
function Page({ route }: { route: RoutePath }) {
if (route.startsWith('/inventory/')) return <InventoryPage id={decodeURIComponent(route.slice('/inventory/'.length))} />;
if (route.startsWith('/dashboards/')) return <DashboardViewPage dashboardId={decodeURIComponent(route.slice('/dashboards/'.length))} />;
if (route.startsWith('/services/')) return <ServicePage id={decodeURIComponent(route.slice('/services/'.length))} />;
if (route.startsWith('/incidents/')) return <IncidentPage id={decodeURIComponent(route.slice('/incidents/'.length))} />;
if (route.startsWith('/containers/')) return <ContainerDetailPage id={decodeURIComponent(route.slice('/containers/'.length))} />;
if (route.startsWith('/disks/')) return <DiskDetailPage id={decodeURIComponent(route.slice('/disks/'.length))} />;
if (route.startsWith('/pools/')) return <PoolPage id={decodeURIComponent(route.slice('/pools/'.length))} />;
if (route.startsWith('/shares/')) return <SharePage id={decodeURIComponent(route.slice('/shares/'.length))} />;
if (route.startsWith('/applications/')) return <ApplicationPage id={decodeURIComponent(route.slice('/applications/'.length))} />;
switch (route) {
case '/': return <OverviewPage />;
case '/processes': return <RouteSuspense><ProcessPage /></RouteSuspense>;
case '/containers': return <ContainerPage />;
case '/services': return <ServicePage />;
case '/topology': return <RouteSuspense><TopologyPage /></RouteSuspense>;
case '/network': return <NetworkPage />;
case '/applications': return <ApplicationPage />;
case '/host': return <HostPage />;
case '/array': return <ArrayPage />;
case '/disks': return <DiskPage />;
case '/pools': return <PoolPage />;
case '/shares': return <SharePage />;
case '/storage': return <StoragePage />;
case '/capacity': return <CapacityPage />;
case '/inventory': return <InventoryPage />;
case '/dashboards': return <DashboardsPage />;
case '/wallboard': return <WallboardPage />;
case '/alerts': return <RouteSuspense><AlertsPage /></RouteSuspense>;
case '/events': return <EventsPage />;
case '/incidents': return <IncidentPage />;
case '/settings': return <SettingsPage />;
case '/status': return <SystemStatusPage />;
case '/onboarding': return <OnboardingPage />;
case '/loading': return <StatePage kind="loading" />;
case '/error': return <StatePage kind="error" />;
case '/unauthorized': return <StatePage kind="unauthorized" />;
case '/404': return <NotFoundPage />;
default: return <NotFoundPage />;
}
}
function App() {
const [route, setRoute] = useState<RoutePath>(() => routeFromLocation(window.location.pathname));
const shellStatus = aggregateStatus(useSystemStatus());
useEffect(() => { const handleNavigation = () => setRoute(routeFromLocation(window.location.pathname)); window.addEventListener('popstate', handleNavigation); return () => window.removeEventListener('popstate', handleNavigation); }, []);
useEffect(() => {
const mobileMenu = document.querySelector<HTMLDetailsElement>('.mobile-more');
if (mobileMenu?.open) mobileMenu.open = false;
}, [route]);
const currentNavigation = navigation.find((item) => item.path === route)
?? navigation.find((item) => item.path !== '/' && route.startsWith(item.path + '/'))
?? navigation[0];
if (route === '/wallboard') return <div className="app-shell app-shell--wallboard"><a className="skip-link" href="#main-content">{copy.accessibility.skipToContent}</a><main id="main-content" className="content"><Page route={route} /></main></div>;
return <div className="app-shell"><a className="skip-link" href="#main-content">{copy.accessibility.skipToContent}</a><aside className="sidebar"><a className="brand" href="/" title={copy.brand.name} onClick={(event) => { event.preventDefault(); navigate('/'); }}><span className="brand-mark" aria-hidden="true"><span>P</span></span><span className="brand-copy"><strong>{copy.brand.name}</strong><small>{copy.brand.context}</small></span></a><DesktopNavigation route={route} /><nav className="mobile-navigation" aria-label={copy.navigation.label}><ul className="nav-list mobile-primary-list">{mobilePrimaryNavigation.map((item) => <NavigationLink key={item.path} item={item} label={item.path === '/storage' ? copy.navigation.mobileStorage : item.label} />)}</ul><details className="mobile-more" open={mobileMoreNavigation.some((item) => route === item.path)}><summary>{copy.navigation.more}</summary><ul className="nav-list">{mobileMoreNavigation.map((item) => <NavigationLink key={item.path} item={item} />)}</ul></details></nav><div className="sidebar-status" title={shellStatus.detail}><StatusBadge label={shellStatus.label} tone={shellStatus.tone} /><span>{shellStatus.detail}</span></div></aside><div className="app-workspace"><header className="context-bar"><div className="context-location"><span className="context-server"><span className="context-server-mark" aria-hidden="true">T</span><span><small>Server</small><strong>Tower</strong></span></span><span className="context-divider" aria-hidden="true" /><span className="context-product">Pulse</span><span aria-hidden="true">/</span><strong>{route === '/404' ? copy.notFound.context : currentNavigation.label}</strong></div><div className="context-actions"><span className="context-live"><span aria-hidden="true">●</span> Live verbonden</span><span className="context-read-only">Alle bronnen · alleen-lezen</span><StatusBadge label={shellStatus.label} tone={shellStatus.tone} /></div></header><main id="main-content" className="content"><AuthNoticeBanner /><Page route={route} /></main></div></div>;
}
export default App;