import { useEffect, useMemo, useState, type CSSProperties, type ReactNode } from 'react'; import { resolveDashboardScope } from './dashboardScope'; import { LiveChartAdapter, historicalSamplesFromData, type LiveFreshness } from './liveBuffer'; import { LiveClient } from './liveClient'; import { MetricClient, rangeForSelection, type MetricQueryRequest } from './metricClient'; import { MetricWidget, RankedListWidget, StatusGridWidget, type MetricWidgetProps, type RankedListItem, type StatusGridItem } from './MetricWidgets'; import { buildStorageNodes, type StorageData } from './StoragePage'; import { StorageMapWidget } from './StorageVisuals'; import { aggregateStatus, useSystemStatus } from './systemStatus'; import { useLiveMetric } from './useLiveMetric'; import { useMetricQuery } from './useMetricQuery'; import { formatDateTime } from './locale'; import { copy } from './copy'; import { presentEventSummary, presentEventType, presentReason, presentStatus } from './presentation'; import { useOperatorContext } from './operatorContext'; import { wallboardPlacement } from './wallboardLayout'; import { spatialStyle } from './dashboardSpatial'; import { isWidgetType, widgetLabel } from './widgetCatalog'; type RecordValue = Record; export type RuntimeWidget = { id: string; type: string; title: string; description?: string; data?: RecordValue; visualization?: RecordValue; behavior?: RecordValue; layouts?: RecordValue }; export type RuntimeState = 'loading' | 'usable' | 'empty' | 'stale' | 'partial' | 'error' | 'capability-disabled'; type RuntimeRow = { id: string; label: string; status: string; value?: string; detail?: string; occurredAt?: string; severity?: string; acknowledged?: boolean }; type EventItem = { id: string; type: string; severity: string; summary: string; occurredAt: string; sourceId?: string }; type Resource = { state: RuntimeState; rows?: RuntimeRow[]; storage?: StorageData; events?: EventItem[]; text?: string; error?: string }; const metricClient = new MetricClient(); const liveClient = new LiveClient(); const metricTypes = new Set(['stat', 'timeseries', 'gauge', 'query-inspector']); function field(record: RecordValue | 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 safeLimit(widget: RuntimeWidget): number { return Math.min(100, Math.max(1, Math.trunc(Number(field(widget.data, 'limit') ?? 100) || 100))); } function safeInterval(widget: RuntimeWidget): number { return Math.min(300, Math.max(5, Math.trunc(Number(field(widget.behavior, 'liveIntervalSeconds') ?? 30) || 30))) * 1000; } function stringValue(value: unknown, fallback = ''): string { return typeof value === 'string' ? value.slice(0, 500) : fallback; } function Badge({ state }: { state: RuntimeState }) { const labels: Record = { usable: copy.dashboards.runtime.current, loading: copy.dashboards.runtime.loading, empty: copy.dashboards.runtime.empty, stale: copy.dashboards.runtime.stale, partial: copy.dashboards.runtime.partial, error: copy.dashboards.runtime.error, 'capability-disabled': copy.dashboards.runtime.capabilityDisabled, }; const ready = state === 'usable'; return {labels[state]}; } async function readJSON(url: string, signal: AbortSignal): Promise { const response = await fetch(url, { signal, cache: 'no-store' }); if (!response.ok) throw new Error(`HTTP ${response.status}`); return response.json() as Promise; } function useBoundedResource(widget: RuntimeWidget): Resource { const sourceType = String(field(widget.data, 'sourceType') ?? ''); const scope = field(widget.data, 'scope') ?? {}; const entityType = String(field(scope, 'entityType') ?? ''); const entityTypes = field(scope, 'entityTypes'); const limit = safeLimit(widget); const interval = safeInterval(widget); const rawText = field(widget.data, 'text'); const text = typeof rawText === 'string' ? rawText.slice(0, 5000) : ''; const key = JSON.stringify({ sourceType, entityType, entityTypes, limit, interval, type: widget.type, text }); const [resource, setResource] = useState(() => sourceType === 'text' ? { state: text.trim() ? 'usable' : 'empty', text } : { state: 'loading' }); useEffect(() => { if (sourceType === 'semantic-metric' || (sourceType === 'inventory' && entityType === 'server')) return undefined; if (sourceType === 'text') { setResource({ state: text.trim() ? 'usable' : 'empty', text }); return undefined; } if (!['inventory', 'events', 'alerts', 'incidents'].includes(sourceType)) { setResource({ state: 'capability-disabled' }); return undefined; } let active = true; let controller: AbortController | null = null; const load = async () => { controller?.abort(); const current = new AbortController(); controller = current; try { if (sourceType === 'inventory' && Array.isArray(entityTypes)) { const [array, disks, pools] = await Promise.all([ readJSON('/api/v1/array', current.signal), readJSON('/api/v1/disks?limit=' + limit, current.signal), readJSON('/api/v1/pools?limit=' + limit, current.signal), ]); const storage = { array, disks, pools }; if (active) setResource({ state: buildStorageNodes(storage).length ? 'usable' : 'empty', storage }); return; } if (sourceType === 'inventory' && entityType === 'application') { const value = await readJSON<{ applications?: Array }>('/api/v1/applications', current.signal); const rows = (value.applications ?? []).slice(0, limit).map((item) => ({ id: stringValue(item.id), label: stringValue(item.name, stringValue(item.id)), status: stringValue(item.status, 'unknown'), detail: presentReason(stringValue(field>(item, 'reasons')?.[0]?.message)) })); if (active) setResource({ state: rows.length ? 'usable' : 'empty', rows }); return; } if (sourceType === 'inventory' && (entityType === 'service' || widget.type === 'service-matrix')) { const value = await readJSON<{ services?: Array; total?: number }>('/api/v1/services?limit=' + limit, current.signal); const rows = (value.services ?? []).slice(0, limit).map((item) => ({ id: stringValue(item.id), label: stringValue(item.name, stringValue(item.id)), status: stringValue(item.state, 'unknown'), value: field(item, 'responseTimeMs') == null ? '—' : `${field(item, 'responseTimeMs')} ms`, detail: field(item, 'availabilityPercent') == null ? stringValue(item.reason) : `${field(item, 'availabilityPercent')?.toFixed(2)}%` })); if (active) setResource({ state: (value.total ?? rows.length) > rows.length ? 'partial' : rows.length ? 'usable' : 'empty', rows }); return; } if (sourceType === 'inventory') { const query = new URLSearchParams({ limit: String(limit) }); if (entityType) query.set('type', entityType); const value = await readJSON<{ items?: Array; hasMore?: boolean }>('/api/v1/entities?' + query, current.signal); const rows = (value.items ?? []).slice(0, limit).map((item) => ({ id: stringValue(item.id), label: stringValue(item.displayName, stringValue(item.canonicalName, stringValue(item.id))), status: stringValue(item.status, 'unknown'), value: String(field(item, 'factCount') ?? ''), detail: stringValue(item.entityType) })); if (active) setResource({ state: value.hasMore ? 'partial' : rows.length ? 'usable' : 'empty', rows }); return; } if (sourceType === 'events') { const value = await readJSON<{ items?: EventItem[] }>('/api/v1/events?limit=' + limit, current.signal); const events = (value.items ?? []).slice(0, limit); if (active) setResource({ state: events.length ? 'usable' : 'empty', events }); return; } if (sourceType === 'alerts') { const value = await readJSON<{ items?: Array }>('/api/v1/alerts?limit=' + limit, current.signal); const rows = (value.items ?? []).slice(0, limit).map((item) => ({ id: stringValue(item.id), label: stringValue(item.ruleName, stringValue(item.id)), status: stringValue(item.state, 'unknown'), severity: stringValue(item.severity, 'unknown'), value: stringValue(item.entityName, '—'), detail: stringValue(item.reason), occurredAt: stringValue(item.updatedAt), acknowledged: Boolean(item.acknowledgedBy) })); if (active) setResource({ state: rows.length ? 'usable' : 'empty', rows }); return; } const value = await readJSON<{ items?: Array }>('/api/v1/incidents?limit=' + limit, current.signal); const rows = (value.items ?? []).slice(0, limit).map((item) => ({ id: stringValue(item.id), label: stringValue(item.title, stringValue(item.id)), status: stringValue(item.status, 'unknown'), severity: stringValue(item.severity, 'unknown'), value: stringValue(item.ownerUserId, '—'), detail: stringValue(item.summary), occurredAt: stringValue(item.updatedAt) })); if (active) setResource({ state: rows.length ? 'usable' : 'empty', rows }); } catch (error: unknown) { if (error instanceof DOMException && error.name === 'AbortError') return; if (active) setResource({ state: 'error', error: error instanceof Error ? error.message : 'bron niet beschikbaar' }); } }; void load(); const timer = window.setInterval(load, interval); return () => { active = false; controller?.abort(); window.clearInterval(timer); }; }, [key, sourceType, entityType, entityTypes, limit, interval, text, widget.type]); return resource; } function RuntimeNotice({ state }: { state: RuntimeState }) { const title = state === 'error' ? copy.dashboards.runtime.sourceUnavailable : state === 'loading' ? copy.dashboards.runtime.telemetryLoading : state === 'capability-disabled' ? copy.dashboards.runtime.capabilityDisabled : copy.dashboards.runtime.noCurrentData; const detail = state === 'error' ? copy.dashboards.runtime.unavailableDetail : state === 'capability-disabled' ? 'Kies een passend widgettype of een andere datasource.' : copy.dashboards.runtime.retryDetail; return
{title}{detail}
; } function RuntimeTable({ rows, service = false }: { rows: RuntimeRow[]; service?: boolean }) { return
{rows.slice(0, 100).map((row) => )}
{rows.length} {copy.dashboards.runtime.rows}
NaamStatus{service ? 'Respons / beschikbaarheid' : 'Waarde'}
{row.label}{row.detail}{presentStatus(row.status)}{row.value || '—'}
; } function AlertSummary({ rows }: { rows: RuntimeRow[] }) { const count = (severity: string) => rows.filter((row) => row.severity === severity && row.status !== 'resolved').length; return
Kritiek
{count('critical')}
Waarschuwing
{count('warning')}
Actief
{rows.filter((row) => row.status !== 'resolved').length}
Bevestigd
{rows.filter((row) => row.acknowledged).length}
; } function safeInline(value: string): ReactNode[] { const parts: ReactNode[] = []; const pattern = /\[([^\]]{1,120})\]\((\/[^)\s]*|https:\/\/[^)\s]+)\)/g; let cursor = 0; let match: RegExpExecArray | null; while ((match = pattern.exec(value)) !== null) { if (match.index > cursor) parts.push(value.slice(cursor, match.index)); parts.push({match[1]}); cursor = match.index + match[0].length; } if (cursor < value.length) parts.push(value.slice(cursor)); return parts; } function TextWidget({ text }: { text: string }) { return
{text.slice(0, 5000).split(/\r?\n/).slice(0, 100).filter(Boolean).map((line, index) =>

{safeInline(line.replace(/^#{1,3}\s+/, '').replace(/^[-*]\s+/, '• '))}

)}
; } function sourceLabel(sourceType: string): string { if (sourceType === 'semantic-metric') return copy.dashboards.runtime.semanticMetric; if (sourceType === 'events') return copy.dashboards.runtime.events; if (sourceType === 'alerts') return copy.dashboards.runtime.alerts; if (sourceType === 'incidents') return copy.dashboards.runtime.incidents; if (sourceType === 'text') return copy.dashboards.runtime.text; return copy.dashboards.runtime.inventory; } export function DashboardRuntimeWidget({ widget, viewport, document, timeRange, onState, onFilter }: { widget: RuntimeWidget; viewport: 'desktop' | 'tablet' | 'mobile' | 'wallboard'; document: RecordValue; timeRange: string; onState: (id: string, state: RuntimeState) => void; onFilter: () => void }) { const sourceType = String(field(widget.data, 'sourceType') ?? ''); const semantic = sourceType === 'semantic-metric'; const system = useSystemStatus(); const resource = useBoundedResource(widget); const operator = useOperatorContext(); if (operator.paused && viewport !== 'wallboard' && timeRange === 'live') timeRange = 'paused'; const range = useMemo(() => rangeForSelection(timeRange), [timeRange, operator.refreshVersion]); const request = useMemo(() => { if (!semantic || !metricTypes.has(widget.type)) return null; const metric = String(field(widget.data, 'metric') ?? ''); if (!metric) return null; return { metric, scope: resolveDashboardScope(field(widget.data, 'scope') ?? {}, field(document, 'variables') ?? []), range, aggregation: String(field(widget.data, 'aggregation') ?? 'avg') }; }, [semantic, widget.type, widget.data, document, range]); const metricState = useMetricQuery(metricClient, request); const historicalSamples = useMemo(() => metricState.status === 'success' ? historicalSamplesFromData(metricState.response?.data) : [], [metricState.status, metricState.response?.data]); const freshness: LiveFreshness = metricState.response?.freshness ?? 'unavailable'; const historicalSeries = useMemo(() => { const adapter = new LiveChartAdapter(4000); adapter.append(historicalSamples.map((sample) => ({ ...sample, freshness }))); return adapter.snapshot(); }, [historicalSamples, freshness]); const live = useLiveMetric(liveClient, semantic && timeRange === 'live' ? request : null, historicalSamples); const systemWidget = sourceType === 'inventory' && String(field(field(widget.data, 'scope'), 'entityType') ?? '') === 'server'; const systemStatus = aggregateStatus(system); const metricRuntime: RuntimeState = !metricTypes.has(widget.type) ? 'capability-disabled' : metricState.status === 'error' || (timeRange === 'live' && live.state === 'error') ? 'error' : (timeRange === 'live' ? live.series.length : historicalSeries.length) > 0 ? (freshness === 'stale' ? 'stale' : 'usable') : metricState.status === 'loading' || (timeRange === 'live' && live.state === 'connecting') ? 'loading' : 'empty'; let runtime: RuntimeState = semantic ? metricRuntime : systemWidget ? (system.state === 'loading' ? 'loading' : system.state === 'ready' ? 'usable' : 'error') : resource.state; if (!isWidgetType(widget.type)) runtime = 'error'; const rows = resource.rows ?? []; const compatible = semantic ? metricTypes.has(widget.type) : systemWidget ? ['stat', 'status-grid'].includes(widget.type) : sourceType === 'text' ? widget.type === 'text' : resource.storage ? widget.type === 'storage-map' : sourceType === 'events' ? widget.type === 'event-timeline' : ['stat', 'ranked-list', 'status-grid', 'table', 'event-timeline', 'service-matrix', 'alert-summary'].includes(widget.type); if (isWidgetType(widget.type) && !compatible && runtime !== 'error') runtime = 'capability-disabled'; useEffect(() => { onState(widget.id, runtime); }, [onState, runtime, widget.id]); const activeLayout = field(widget.layouts, viewport) ?? field(widget.layouts, 'desktop') ?? {}; const placement = wallboardPlacement(activeLayout); const layoutStyle = viewport === 'wallboard' ? { '--widget-span': String(placement.columnSpan), gridColumn: `${placement.columnStart} / span ${placement.columnSpan}`, gridRow: `${placement.rowStart} / span ${placement.rowSpan}` } as CSSProperties : spatialStyle(activeLayout, viewport) as CSSProperties; let content: ReactNode; if (!isWidgetType(widget.type)) content =
{copy.dashboards.runtime.unknownType}{widget.type.slice(0, 80)}
; else if (semantic && metricTypes.has(widget.type)) content = ; else if (systemWidget && system.state === 'ready') content =
{systemStatus.label}{systemStatus.detail}{system.status?.components.length ?? 0} {copy.dashboards.runtime.checkedComponents}
; else if (resource.storage && widget.type === 'storage-map') content = ; else if (widget.type === 'text' && resource.text?.trim()) content = ; else if (widget.type === 'alert-summary' && rows.length) content = ; else if (widget.type === 'service-matrix' && rows.length) content = ; else if (widget.type === 'table' && rows.length) content = ; else if (widget.type === 'ranked-list' && rows.length) content = ({ id: row.id, label: row.label, value: row.value || presentStatus(row.status), detail: row.detail }))} onSelect={onFilter} />; else if (widget.type === 'status-grid' && rows.length) content = ({ id: row.id, label: row.label, status: row.status, reason: row.detail }))} onSelect={onFilter} />; else if (widget.type === 'stat' && rows.length) content =
{rows.length}{sourceLabel(sourceType)}{rows.filter((row) => !['healthy', 'up', 'operational', 'resolved'].includes(row.status)).length} vragen aandacht
; else if (widget.type === 'event-timeline' && (resource.events?.length || rows.length)) { const events = resource.events ?? rows.map((row) => ({ id: row.id, type: sourceType, severity: row.severity ?? row.status, summary: row.label, occurredAt: row.occurredAt ?? '', sourceId: row.id })); content =
    {events.slice(0, viewport === 'mobile' ? 6 : 12).map((event) =>
  1. )}
; } else content = ; return

{sourceLabel(sourceType)} · {widgetLabel(widget.type)}

{widget.title}

{widget.description &&

{widget.description}

}{content}
; }