@harperfast/harper-pro 5.0.17 → 5.0.19

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (31) hide show
  1. package/core/resources/RecordEncoder.ts +15 -12
  2. package/core/resources/RocksTransactionLogStore.ts +47 -22
  3. package/core/resources/Table.ts +98 -32
  4. package/core/resources/auditStore.ts +87 -6
  5. package/core/resources/databases.ts +67 -7
  6. package/dist/cloneNode/cloneNode.js +13 -8
  7. package/dist/cloneNode/cloneNode.js.map +1 -1
  8. package/dist/core/resources/RecordEncoder.js +1 -1
  9. package/dist/core/resources/RecordEncoder.js.map +1 -1
  10. package/dist/core/resources/RocksTransactionLogStore.js +80 -21
  11. package/dist/core/resources/RocksTransactionLogStore.js.map +1 -1
  12. package/dist/core/resources/Table.js +96 -35
  13. package/dist/core/resources/Table.js.map +1 -1
  14. package/dist/core/resources/auditStore.js +83 -6
  15. package/dist/core/resources/auditStore.js.map +1 -1
  16. package/dist/core/resources/databases.js +68 -5
  17. package/dist/core/resources/databases.js.map +1 -1
  18. package/dist/replication/replicationConnection.js +63 -18
  19. package/dist/replication/replicationConnection.js.map +1 -1
  20. package/npm-shrinkwrap.json +2 -2
  21. package/package.json +1 -1
  22. package/replication/replicationConnection.ts +66 -20
  23. package/studio/web/assets/{index-DhLu-DHX.js → index-BIjBsaWw.js} +5 -5
  24. package/studio/web/assets/{index-DhLu-DHX.js.map → index-BIjBsaWw.js.map} +1 -1
  25. package/studio/web/assets/{index.lazy-DBjOisCz.js → index.lazy-DN6bSQzR.js} +2 -2
  26. package/studio/web/assets/{index.lazy-DBjOisCz.js.map → index.lazy-DN6bSQzR.js.map} +1 -1
  27. package/studio/web/assets/{profile-DSL-499E.js → profile-Dyrp-ZIJ.js} +2 -2
  28. package/studio/web/assets/{profile-DSL-499E.js.map → profile-Dyrp-ZIJ.js.map} +1 -1
  29. package/studio/web/assets/{status-BRW5QtzY.js → status-BrfTnnpt.js} +2 -2
  30. package/studio/web/assets/{status-BRW5QtzY.js.map → status-BrfTnnpt.js.map} +1 -1
  31. package/studio/web/index.html +1 -1
@@ -1 +1 @@
1
- {"version":3,"file":"status-BRW5QtzY.js","names":[],"sources":["../../src/features/instance/status/analytics/components/AnalyticsOnboardingHint.tsx","../../src/features/instance/status/analytics/context/timePresets.ts","../../src/features/instance/status/analytics/hooks/useAnalyticsFreshness.ts","../../src/features/instance/status/analytics/components/TimeRangePicker.tsx","../../src/features/instance/status/analytics/context/AnalyticsContext.tsx","../../src/features/instance/status/analytics/hooks/useAnalyticsCapability.ts","../../src/features/instance/status/analytics/lib/chartExport.ts","../../src/features/instance/status/analytics/components/ChartCopyButton.tsx","../../src/features/instance/status/analytics/components/ChartExportButton.tsx","../../src/features/instance/status/analytics/components/ChartExpandButton.tsx","../../src/features/instance/status/analytics/hooks/useAnalyticsRecords.ts","../../src/features/instance/status/analytics/pipeline/derived/error-rate.tsx","../../src/features/instance/status/analytics/lib/nodeColors.ts","../../src/features/instance/status/analytics/charts/NodeLegend.tsx","../../src/features/instance/status/analytics/hooks/useNodeSelection.ts","../../src/features/instance/status/analytics/lib/colorAllocators/typeColors.ts","../../src/features/instance/status/analytics/pipeline/aggregators.ts","../../src/features/instance/status/analytics/pipeline/approxLabel.ts","../../src/features/instance/status/analytics/pipeline/confidence.ts","../../src/features/instance/status/analytics/pipeline/fieldExpr.ts","../../src/features/instance/status/analytics/pipeline/transforms.ts","../../src/features/instance/status/analytics/pipeline/runTransform.ts","../../src/features/instance/status/analytics/pipeline/pipeline.ts","../../src/features/instance/status/analytics/lib/time.ts","../../src/features/instance/status/analytics/primitives/formatValue.ts","../../src/features/instance/status/analytics/primitives/tooltipStyle.ts","../../src/features/instance/status/analytics/primitives/LineChart.tsx","../../src/features/instance/status/analytics/primitives/SmallMultiples.tsx","../../src/features/instance/status/analytics/primitives/sortByMagnitude.ts","../../src/features/instance/status/analytics/primitives/StackedAreaChart.tsx","../../src/features/instance/status/analytics/primitives/TypeFilterChipRow.tsx","../../src/features/instance/status/analytics/primitives/TrafficByTypeRenderer.tsx","../../src/features/instance/status/analytics/pipeline/derived/mqtt-traffic-received.tsx","../../src/features/instance/status/analytics/pipeline/derived/mqtt-traffic-sent.tsx","../../src/features/instance/status/analytics/primitives/DimensionChipRow.tsx","../../src/features/instance/status/analytics/primitives/PerPathRateRenderer.tsx","../../src/features/instance/status/analytics/pipeline/derived/request-rate.tsx","../../src/features/instance/status/analytics/pipeline/derived/transaction-log-growth.tsx","../../src/features/instance/status/analytics/pipeline/derived/index.ts","../../src/features/instance/status/analytics/pipeline/bytes-received.tsx","../../src/features/instance/status/analytics/pipeline/bytes-sent.tsx","../../src/features/instance/status/analytics/primitives/DimensionCombobox.tsx","../../src/features/instance/status/analytics/primitives/DimensionSelectorRenderer.tsx","../../src/features/instance/status/analytics/pipeline/cache-hit.tsx","../../src/features/instance/status/analytics/pipeline/quantileFields.ts","../../src/features/instance/status/analytics/pipeline/cache-resolution.tsx","../../src/features/instance/status/analytics/pipeline/connection.tsx","../../src/features/instance/status/analytics/pipeline/connections.tsx","../../src/features/instance/status/analytics/pipeline/cpu-usage.tsx","../../src/features/instance/status/analytics/pipeline/database-size.tsx","../../src/features/instance/status/analytics/pipeline/db-message.tsx","../../src/features/instance/status/analytics/pipeline/db-read.tsx","../../src/features/instance/status/analytics/pipeline/db-write.tsx","../../src/features/instance/status/analytics/pipeline/duration.tsx","../../src/features/instance/status/analytics/pipeline/main-thread-utilization.tsx","../../src/features/instance/status/analytics/pipeline/memory.tsx","../../src/features/instance/status/analytics/primitives/computeCellSize.ts","../../src/features/instance/status/analytics/primitives/HeatmapMatrix.tsx","../../src/features/instance/status/analytics/pipeline/pathParser.ts","../../src/features/instance/status/analytics/pipeline/replication-latency.tsx","../../src/features/instance/status/analytics/pipeline/resource-usage.ts","../../src/features/instance/status/analytics/pipeline/response-200.tsx","../../src/features/instance/status/analytics/pipeline/storage-volume.ts","../../src/features/instance/status/analytics/pipeline/success.tsx","../../src/features/instance/status/analytics/pipeline/tls-reused.ts","../../src/features/instance/status/analytics/pipeline/transfer.tsx","../../src/features/instance/status/analytics/pipeline/utilization.ts","../../src/features/instance/status/analytics/pipeline/index.ts","../../src/features/instance/status/analytics/lib/specRequiredFields.ts","../../src/features/instance/status/analytics/primitives/FallbackRenderer.tsx","../../src/features/instance/status/analytics/primitives/LineChartWithNodeLegend.tsx","../../src/features/instance/status/analytics/primitives/MetricRenderer.tsx","../../src/features/instance/status/analytics/tabs/PanelErrorBoundary.tsx","../../src/features/instance/status/analytics/tabs/MetricPanel.tsx","../../src/features/instance/status/analytics/tabs/DatabaseTab.tsx","../../src/features/instance/status/analytics/tabs/HealthTab.tsx","../../src/components/ui/accordion.tsx","../../src/integrations/api/instance/status/getSystemInformation.ts","../../src/features/instance/status/analytics/lib/crawlData.ts","../../src/features/instance/status/analytics/tabs/OverviewTab.tsx","../../src/features/instance/status/analytics/tabs/ReplicationTab.tsx","../../src/features/instance/status/analytics/tabs/RequestsTab.tsx","../../src/features/instance/status/analytics/lib/tableColors.ts","../../src/features/instance/status/analytics/lib/tableSize.ts","../../src/features/instance/status/analytics/lib/theme.ts","../../src/features/instance/status/analytics/charts/TableSizeChipRow.tsx","../../src/features/instance/status/analytics/charts/TableSizeSnapshot.tsx","../../src/features/instance/status/analytics/charts/TableSizeTrend.tsx","../../src/features/instance/status/analytics/tabs/StorageTab.tsx","../../src/features/instance/status/analytics/tabs/ConnectionsPanel.tsx","../../src/features/instance/status/analytics/tabs/TrafficTab.tsx","../../src/features/instance/status/analytics/StatusTabs.tsx","../../src/features/instance/status/index.tsx"],"sourcesContent":["import { Button } from '@/components/ui/button';\nimport { X } from 'lucide-react';\nimport { useEffect, useState } from 'react';\n\n// Versioned key — bump the suffix to re-show the tip after a major UX\n// change (e.g. when we add a new keyboard shortcut or remove an existing\n// one). Past dismissals at older versions are ignored on purpose.\nconst STORAGE_KEY = 'studio:analytics:onboarding-dismissed:v1';\n\n/** First-visit hint explaining the chart interactions that aren't visually\n * discoverable: click a legend entry to solo a node, ⌘/Ctrl-click to\n * multi-select, and click bar segments / heatmap cells for drilldown.\n * Dismissal is persisted to localStorage so it doesn't reappear. */\nexport function AnalyticsOnboardingHint() {\n\tconst [dismissed, setDismissed] = useState<boolean | null>(null);\n\n\tuseEffect(() => {\n\t\ttry {\n\t\t\tsetDismissed(window.localStorage.getItem(STORAGE_KEY) === '1');\n\t\t} catch {\n\t\t\tsetDismissed(true);\n\t\t}\n\t}, []);\n\n\tif (dismissed !== false) { return null; }\n\n\tconst onDismiss = () => {\n\t\ttry {\n\t\t\twindow.localStorage.setItem(STORAGE_KEY, '1');\n\t\t} catch {\n\t\t\t// ignore — dismissal will replay next visit but the hint is harmless\n\t\t}\n\t\tsetDismissed(true);\n\t};\n\n\treturn (\n\t\t<div\n\t\t\trole=\"status\"\n\t\t\tclassName=\"mb-3 flex items-start gap-3 rounded-md border border-border bg-muted/30 px-3 py-2 text-sm text-muted-foreground\"\n\t\t>\n\t\t\t<div className=\"flex-1 min-w-0\">\n\t\t\t\t<span className=\"font-medium text-foreground\">Tip:</span>\n\t\t\t\t{' Click a legend entry to isolate one node, '}\n\t\t\t\t<kbd className=\"rounded border border-border px-1 py-0.5 text-xs\">⌘</kbd>\n\t\t\t\t{' / '}\n\t\t\t\t<kbd className=\"rounded border border-border px-1 py-0.5 text-xs\">Ctrl</kbd>\n\t\t\t\t{'-click to compare a few. Bar segments and heatmap cells are clickable for drilldown.'}\n\t\t\t</div>\n\t\t\t<Button\n\t\t\t\tvariant=\"ghost\"\n\t\t\t\tsize=\"icon\"\n\t\t\t\tonClick={onDismiss}\n\t\t\t\taria-label=\"Dismiss tip\"\n\t\t\t\tclassName=\"-mt-1 h-7 w-7 shrink-0\"\n\t\t\t>\n\t\t\t\t<X className=\"h-4 w-4\" />\n\t\t\t</Button>\n\t\t</div>\n\t);\n}\n","// Bucket-by-window clamps. Per the SRE review: a 30d window with 1m buckets\n// across N nodes and 50 tables is on the order of 11M rows and OOMs the tab.\n// Each preset declares the densest bucket Harper should serve.\n\nexport interface TimePreset {\n\tid: TimePresetId;\n\tlabel: string;\n\tdurationMs: number;\n\tbucketMs: number;\n}\n\nexport type TimePresetId = '1h' | '6h' | '24h' | '7d' | '30d';\n\nconst MIN = 60_000;\nconst HOUR = 60 * MIN;\nconst DAY = 24 * HOUR;\n\nexport const TIME_PRESETS: readonly TimePreset[] = [\n\t{ id: '1h', label: 'Last 1 hour', durationMs: HOUR, bucketMs: 1 * MIN },\n\t{ id: '6h', label: 'Last 6 hours', durationMs: 6 * HOUR, bucketMs: 1 * MIN },\n\t{ id: '24h', label: 'Last 24 hours', durationMs: DAY, bucketMs: 5 * MIN },\n\t{ id: '7d', label: 'Last 7 days', durationMs: 7 * DAY, bucketMs: 15 * MIN },\n\t{ id: '30d', label: 'Last 30 days', durationMs: 30 * DAY, bucketMs: HOUR },\n];\n\nexport const DEFAULT_PRESET_ID: TimePresetId = '1h';\n\nexport function getPreset(id: TimePresetId): TimePreset {\n\tconst p = TIME_PRESETS.find((x) => x.id === id);\n\tif (!p) { throw new Error(`Unknown preset: ${id}`); }\n\treturn p;\n}\n\nexport interface RefreshOption {\n\tlabel: string;\n\tvalue: number;\n}\n\nexport const REFRESH_OPTIONS: readonly RefreshOption[] = [\n\t{ label: 'Off', value: 0 },\n\t{ label: '30s', value: 30_000 },\n\t{ label: '60s', value: 60_000 },\n\t{ label: '5m', value: 300_000 },\n];\n\nexport const DEFAULT_REFRESH_MS = 60_000;\n","import { ANALYTICS_QUERY_KEY_PREFIX } from '@/integrations/api/instance/status/getAnalytics.ts';\nimport { useQueryClient } from '@tanstack/react-query';\nimport { useEffect, useState } from 'react';\n\nconst PREFIX = ANALYTICS_QUERY_KEY_PREFIX;\n\nexport interface AnalyticsFreshness {\n\t/** True while at least one panel-level get_analytics query is fetching. */\n\tisFetching: boolean;\n\t/** ms-since-epoch of the most recent successful fetch on any panel\n\t * query, or null until the first one resolves. */\n\tlastFetchedAt: number | null;\n\t/** Bumps every second so consumers can re-render relative time\n\t * (\"updated Xs ago\") without subscribing themselves. */\n\tnow: number;\n}\n\n/** Watches the React Query cache for activity on the `get_analytics`\n * prefix and exposes a busy flag + most-recent-success timestamp. The\n * TimeRangePicker uses these to show an active/spinning refresh icon\n * and a \"last updated\" relative-time label. */\nexport function useAnalyticsFreshness(): AnalyticsFreshness {\n\tconst client = useQueryClient();\n\tconst [isFetching, setIsFetching] = useState(false);\n\tconst [lastFetchedAt, setLastFetchedAt] = useState<number | null>(null);\n\tconst [now, setNow] = useState(() => Date.now());\n\n\tuseEffect(() => {\n\t\tconst cache = client.getQueryCache();\n\t\tconst isOurs = (q: { queryKey: unknown }) => Array.isArray(q.queryKey) && q.queryKey[0] === PREFIX;\n\t\tconst sync = () => {\n\t\t\tlet fetching = false;\n\t\t\tlet mostRecent: number | null = null;\n\t\t\tfor (const q of cache.getAll()) {\n\t\t\t\tif (!isOurs(q)) { continue; }\n\t\t\t\tif (q.state.fetchStatus === 'fetching') { fetching = true; }\n\t\t\t\tif (q.state.dataUpdatedAt > 0 && (mostRecent === null || q.state.dataUpdatedAt > mostRecent)) {\n\t\t\t\t\tmostRecent = q.state.dataUpdatedAt;\n\t\t\t\t}\n\t\t\t}\n\t\t\t// Only setState when the value actually changed — RQ fires the\n\t\t\t// subscription on every cache event in the entire app, and a\n\t\t\t// no-op setState would still trigger a re-render of the picker.\n\t\t\tsetIsFetching((prev) => (prev === fetching ? prev : fetching));\n\t\t\tsetLastFetchedAt((prev) => (prev === mostRecent ? prev : mostRecent));\n\t\t};\n\t\tsync();\n\t\t// Subscribe filter: we only care about events on `get_analytics_raw`\n\t\t// queries. Filter the *event* (not just the post-aggregate) so we\n\t\t// don't recompute the cache scan for every unrelated query mutation.\n\t\tconst unsub = cache.subscribe((event) => {\n\t\t\tif (!event?.query || !isOurs(event.query)) { return; }\n\t\t\tsync();\n\t\t});\n\t\treturn () => unsub();\n\t}, [client]);\n\n\tuseEffect(() => {\n\t\t// Tick rate adapts to age so we don't burn a 1Hz timer per picker\n\t\t// indefinitely. Updates per-second for the first minute (where \"Xs\"\n\t\t// is changing), every 5s for the next 9 minutes, then every 30s.\n\t\tlet cancelled = false;\n\t\tlet timerId: number | undefined;\n\t\tconst tick = () => {\n\t\t\tif (cancelled) { return; }\n\t\t\tsetNow(Date.now());\n\t\t\tconst age = lastFetchedAt === null ? 0 : Date.now() - lastFetchedAt;\n\t\t\tconst delay = age < 60_000 ? 1000 : age < 600_000 ? 5000 : 30_000;\n\t\t\ttimerId = window.setTimeout(tick, delay);\n\t\t};\n\t\ttimerId = window.setTimeout(tick, 1000);\n\t\treturn () => {\n\t\t\tcancelled = true;\n\t\t\tif (timerId !== undefined) { window.clearTimeout(timerId); }\n\t\t};\n\t}, [lastFetchedAt]);\n\n\treturn { isFetching, lastFetchedAt, now };\n}\n\n/** Format `lastFetchedAt` as a short relative-time label suitable for a\n * toolbar pill: \"just now\" / \"Xs ago\" / \"Xm ago\". Returns null when no\n * fetch has resolved yet. */\nexport function formatRelativeUpdate(lastFetchedAt: number | null, now: number): string | null {\n\tif (lastFetchedAt === null) { return null; }\n\tconst seconds = Math.max(0, Math.floor((now - lastFetchedAt) / 1000));\n\tif (seconds < 5) { return 'just now'; }\n\tif (seconds < 60) { return `${seconds}s ago`; }\n\tconst minutes = Math.floor(seconds / 60);\n\tif (minutes < 60) { return `${minutes}m ago`; }\n\tconst hours = Math.floor(minutes / 60);\n\treturn `${hours}h ago`;\n}\n","import { Button } from '@/components/ui/button';\nimport { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';\nimport { cn } from '@/lib/cn';\nimport { RefreshCw } from 'lucide-react';\nimport { REFRESH_OPTIONS, TIME_PRESETS, type TimePresetId } from '../context/timePresets.ts';\nimport { formatRelativeUpdate, useAnalyticsFreshness } from '../hooks/useAnalyticsFreshness.ts';\n\ninterface Props {\n\tpresetId: TimePresetId;\n\tonPresetChange: (id: TimePresetId) => void;\n\trefreshMs: number;\n\tonRefreshChange: (ms: number) => void;\n\tonManualRefresh: () => void;\n}\n\nexport function TimeRangePicker({\n\tpresetId,\n\tonPresetChange,\n\trefreshMs,\n\tonRefreshChange,\n\tonManualRefresh,\n}: Props) {\n\tconst { isFetching, lastFetchedAt, now } = useAnalyticsFreshness();\n\tconst updatedLabel = formatRelativeUpdate(lastFetchedAt, now);\n\n\treturn (\n\t\t<div className=\"flex items-center gap-2\">\n\t\t\t{updatedLabel && (\n\t\t\t\t<span\n\t\t\t\t\tclassName=\"text-xs text-muted-foreground tabular-nums\"\n\t\t\t\t\t// No aria-live: a self-ticking timestamp causes screen\n\t\t\t\t\t// readers to re-announce every interval. Full ISO time\n\t\t\t\t\t// is exposed via title for hover.\n\t\t\t\t\ttitle={lastFetchedAt ? new Date(lastFetchedAt).toLocaleString() : undefined}\n\t\t\t\t\taria-label={lastFetchedAt ? `Last updated ${new Date(lastFetchedAt).toLocaleString()}` : undefined}\n\t\t\t\t>\n\t\t\t\t\tUpdated {updatedLabel}\n\t\t\t\t</span>\n\t\t\t)}\n\t\t\t<Select value={presetId} onValueChange={(v) => onPresetChange(v as TimePresetId)}>\n\t\t\t\t<SelectTrigger className=\"w-[180px]\">\n\t\t\t\t\t<SelectValue />\n\t\t\t\t</SelectTrigger>\n\t\t\t\t<SelectContent>\n\t\t\t\t\t{TIME_PRESETS.map((p) => <SelectItem key={p.id} value={p.id}>{p.label}</SelectItem>)}\n\t\t\t\t</SelectContent>\n\t\t\t</Select>\n\t\t\t<Select value={String(refreshMs)} onValueChange={(v) => onRefreshChange(Number(v))}>\n\t\t\t\t<SelectTrigger className=\"w-[100px]\">\n\t\t\t\t\t<SelectValue />\n\t\t\t\t</SelectTrigger>\n\t\t\t\t<SelectContent>\n\t\t\t\t\t{REFRESH_OPTIONS.map((o) => <SelectItem key={o.value} value={String(o.value)}>{o.label}</SelectItem>)}\n\t\t\t\t</SelectContent>\n\t\t\t</Select>\n\t\t\t<Button\n\t\t\t\tvariant=\"ghost\"\n\t\t\t\tsize=\"icon\"\n\t\t\t\tonClick={onManualRefresh}\n\t\t\t\tdisabled={isFetching}\n\t\t\t\taria-busy={isFetching}\n\t\t\t\taria-label={isFetching ? 'Refreshing…' : 'Refresh now'}\n\t\t\t\ttitle={isFetching ? 'Refreshing…' : 'Refresh now'}\n\t\t\t>\n\t\t\t\t<RefreshCw className={cn('h-4 w-4', isFetching && 'animate-spin')} />\n\t\t\t</Button>\n\t\t</div>\n\t);\n}\n","import type { InstanceClientIdConfig, InstanceTypeConfig } from '@/config/instanceClientConfig.ts';\nimport { createContext, type ReactNode, useContext, useMemo } from 'react';\nimport type { TimeRange } from '../types/analytics.ts';\n\nexport type AnalyticsTheme = 'light' | 'dark';\n\nexport interface AnalyticsContextValue {\n\ttimeRange: TimeRange;\n\tbucketMs: number;\n\trefreshIntervalMs: number;\n\ttheme: AnalyticsTheme;\n\tinstanceParams: InstanceClientIdConfig & InstanceTypeConfig;\n}\n\nconst Ctx = createContext<AnalyticsContextValue | null>(null);\n\ninterface ProviderProps {\n\tvalue: AnalyticsContextValue;\n\tchildren: ReactNode;\n}\n\nexport function AnalyticsProvider({ value, children }: ProviderProps) {\n\tconst memo = useMemo(() => value, [\n\t\tvalue.timeRange.startTime,\n\t\tvalue.timeRange.endTime,\n\t\tvalue.bucketMs,\n\t\tvalue.refreshIntervalMs,\n\t\tvalue.theme,\n\t\tvalue.instanceParams.entityId,\n\t]);\n\treturn <Ctx.Provider value={memo}>{children}</Ctx.Provider>;\n}\n\nexport function useAnalyticsContext(): AnalyticsContextValue {\n\tconst v = useContext(Ctx);\n\tif (!v) { throw new Error('useAnalyticsContext must be used inside <AnalyticsProvider>'); }\n\treturn v;\n}\n","import type { InstanceClientIdConfig, InstanceTypeConfig } from '@/config/instanceClientConfig.ts';\nimport { useQuery } from '@tanstack/react-query';\n\nexport interface AnalyticsCapability {\n\tsupported: boolean;\n\terror?: Error;\n\tisLoading: boolean;\n\tretry: () => void;\n}\n\n/** Capability-probe metrics tried in priority order. The probe considers\n * `get_analytics` supported if any metric resolves without a transport\n * error (an empty response is fine — it just means the instance is\n * quiet). Querying a list rather than a single metric avoids a false\n * negative on Harper builds where the chosen metric was renamed,\n * disabled, or never emitted. Order from most-likely-emitted to least. */\nconst PROBE_METRICS: readonly string[] = [\n\t'utilization',\n\t'cpu-usage',\n\t'memory',\n\t'main-thread-utilization',\n];\n\nconst PROBE_STALE_TIME_MS = 30 * 60_000;\n\n/** True when the error is plausibly a \"this metric isn't emitted on this\n * build\" signal (HTTP 4xx). False when the error is transport-level\n * (5xx, timeout, network) — those mean the *instance* is unhealthy, so\n * walking to the next metric just compounds load on an already\n * struggling Harper. */\nfunction isMetricNotFoundError(err: unknown): boolean {\n\tconst status = (err as { response?: { status?: number }; status?: number })?.response?.status\n\t\t?? (err as { status?: number })?.status;\n\tif (typeof status !== 'number') { return false; }\n\treturn status >= 400 && status < 500;\n}\n\n/** Probe `get_analytics` once per instance and cache the result for 30\n * minutes. Falls through the metric list ONLY on per-metric 4xx errors\n * (Harper version drift); bails immediately on transport-level errors so\n * a slow / unhealthy Harper isn't hit 4× per attempt. Retries up to twice\n * with exponential backoff for transient blips. The hook exposes a\n * `retry()` function for a Retry button on the fallback view. */\nexport function useAnalyticsCapability(\n\tinstanceParams: InstanceClientIdConfig & InstanceTypeConfig,\n): AnalyticsCapability {\n\tconst query = useQuery({\n\t\tqueryKey: ['analytics-capability', instanceParams.entityId] as const,\n\t\tqueryFn: async () => {\n\t\t\tconst endTime = Date.now();\n\t\t\tconst startTime = endTime - 5 * 60_000;\n\t\t\tlet lastError: unknown = null;\n\t\t\tfor (const metric of PROBE_METRICS) {\n\t\t\t\ttry {\n\t\t\t\t\tawait instanceParams.instanceClient.post('/', {\n\t\t\t\t\t\toperation: 'get_analytics',\n\t\t\t\t\t\tmetric,\n\t\t\t\t\t\tstart_time: startTime,\n\t\t\t\t\t\tend_time: endTime,\n\t\t\t\t\t});\n\t\t\t\t\treturn true;\n\t\t\t\t} catch (err) {\n\t\t\t\t\tlastError = err;\n\t\t\t\t\t// Only walk the list on 4xx (metric-not-found-style).\n\t\t\t\t\t// 5xx / network / timeout = instance-level problem;\n\t\t\t\t\t// re-throw so React Query's outer retry policy decides.\n\t\t\t\t\tif (!isMetricNotFoundError(err)) { throw err; }\n\t\t\t\t}\n\t\t\t}\n\t\t\tthrow lastError instanceof Error ? lastError : new Error('Analytics probe failed for all metrics');\n\t\t},\n\t\tretry: 2,\n\t\tretryDelay: (attempt) => Math.min(1000 * 2 ** attempt, 8000),\n\t\tstaleTime: PROBE_STALE_TIME_MS,\n\t\tgcTime: PROBE_STALE_TIME_MS,\n\t});\n\n\treturn {\n\t\tsupported: query.isSuccess === true,\n\t\terror: query.error as Error | undefined,\n\t\tisLoading: query.isLoading,\n\t\tretry: () => {\n\t\t\tvoid query.refetch();\n\t\t},\n\t};\n}\n","import { toBlob } from 'html-to-image';\n\n/** Resolves the on-screen background color of `el`, walking up the tree\n * until a non-transparent ancestor is found (Card surfaces are\n * rgba(0,0,0,0) by default — falling through to a literal white would\n * produce white-bg PNGs in dark mode). */\nfunction resolveBackground(el: HTMLElement): string {\n\tlet cur: HTMLElement | null = el;\n\twhile (cur) {\n\t\tconst bg = getComputedStyle(cur).backgroundColor;\n\t\tif (bg && bg !== 'rgba(0, 0, 0, 0)' && bg !== 'transparent') { return bg; }\n\t\tcur = cur.parentElement;\n\t}\n\t// Fall back to the document body's resolved background, which respects\n\t// the active theme via Tailwind tokens.\n\tconst bodyBg = typeof document !== 'undefined' ? getComputedStyle(document.body).backgroundColor : '';\n\tif (bodyBg && bodyBg !== 'rgba(0, 0, 0, 0)') { return bodyBg; }\n\treturn '#ffffff';\n}\n\n/** Default pixel ratio for chart exports. 3× yields ~9 megapixels for an\n * 800×1000 panel — enough for slide decks and incident reports without\n * blowing up clipboard payloads. The expanded-view export inherits the\n * same ratio applied against a much larger DOM, so its output is\n * proportionally bigger. */\nexport const DEFAULT_EXPORT_PIXEL_RATIO = 3;\n\n/** Converts a chart's DOM subtree to a PNG blob. backgroundColor walks\n * ancestors so dark-mode Card surfaces capture as dark, not white. */\nexport async function captureChartAsBlob(\n\tchartContainer: HTMLElement,\n\tpixelRatio: number = DEFAULT_EXPORT_PIXEL_RATIO,\n): Promise<Blob> {\n\tconst blob = await toBlob(chartContainer, {\n\t\tpixelRatio,\n\t\tbackgroundColor: resolveBackground(chartContainer),\n\t});\n\tif (!blob) { throw new Error('Failed to capture chart as image'); }\n\treturn blob;\n}\n\nexport async function downloadChart(chartContainer: HTMLElement, filename: string): Promise<void> {\n\tconst blob = await captureChartAsBlob(chartContainer);\n\tconst url = URL.createObjectURL(blob);\n\ttry {\n\t\tconst a = document.createElement('a');\n\t\ta.href = url;\n\t\ta.download = filename;\n\t\ta.click();\n\t} finally {\n\t\tURL.revokeObjectURL(url);\n\t}\n}\n\n/** Capture the chart and write it to the clipboard as a PNG ClipboardItem.\n * Resolves to `true` on success, `false` if either the capture failed or\n * the browser denied clipboard access (Safari + non-secure contexts are\n * the common offenders). The caller decides UX — typically a toast. */\nexport async function copyChartToClipboard(chartContainer: HTMLElement): Promise<boolean> {\n\tif (typeof ClipboardItem === 'undefined' || !navigator.clipboard?.write) {\n\t\treturn false;\n\t}\n\ttry {\n\t\tconst blob = await captureChartAsBlob(chartContainer);\n\t\tawait navigator.clipboard.write([\n\t\t\tnew ClipboardItem({ 'image/png': blob }),\n\t\t]);\n\t\treturn true;\n\t} catch {\n\t\treturn false;\n\t}\n}\n\n/** Slugify a metric/title for use as a download filename. */\nexport function makeExportFilename(prefix: string, range: { startTime: number; endTime: number }): string {\n\tconst safe = prefix.replace(/[^a-z0-9-]+/gi, '-').replace(/(^-|-$)/g, '').toLowerCase();\n\tconst stamp = new Date(range.endTime).toISOString().replace(/[:.]/g, '-');\n\treturn `${safe}-${stamp}.png`;\n}\n","import { Button } from '@/components/ui/button';\nimport { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';\nimport { ClipboardCopy } from 'lucide-react';\nimport { type RefObject, useState } from 'react';\nimport { toast } from 'sonner';\nimport { copyChartToClipboard } from '../lib/chartExport.ts';\n\ninterface Props {\n\t/** The DOM node that should be captured. Usually the chart's outer Card. */\n\tcaptureRef: RefObject<HTMLElement | null>;\n\t/** Slug used in the toast message and the aria-label. */\n\texportSlug: string;\n}\n\n/** Copies the chart's PNG capture to the clipboard. Same capture pipeline\n * as ChartExportButton (so the on-clipboard image matches what would be\n * downloaded), but bypasses the file-system save and lets the user paste\n * directly into Slack, Confluence, an issue tracker, etc.\n *\n * Falls back gracefully when the browser doesn't expose `ClipboardItem`\n * (Safari without permissions, http-served pages) — reports a friendly\n * error toast instead of throwing. */\nexport function ChartCopyButton({ captureRef, exportSlug }: Props) {\n\tconst [busy, setBusy] = useState(false);\n\n\tconst onClick = async () => {\n\t\tif (busy) { return; }\n\t\tconst el = captureRef.current;\n\t\tif (!el) { return; }\n\t\tsetBusy(true);\n\t\ttry {\n\t\t\tconst ok = await copyChartToClipboard(el);\n\t\t\tif (ok) {\n\t\t\t\ttoast.success('Chart copied to clipboard');\n\t\t\t} else {\n\t\t\t\ttoast.error('Could not copy chart', {\n\t\t\t\t\tdescription: 'Your browser blocked the clipboard write. Try Download or check site permissions.',\n\t\t\t\t});\n\t\t\t}\n\t\t} catch (err) {\n\t\t\tconsole.error('[chart-copy] capture failed', err);\n\t\t\ttoast.error('Could not copy chart', {\n\t\t\t\tdescription: err instanceof Error ? err.message : 'Unknown error',\n\t\t\t});\n\t\t} finally {\n\t\t\tsetBusy(false);\n\t\t}\n\t};\n\n\treturn (\n\t\t<Tooltip>\n\t\t\t<TooltipTrigger asChild>\n\t\t\t\t<Button\n\t\t\t\t\tvariant=\"ghost\"\n\t\t\t\t\tsize=\"icon\"\n\t\t\t\t\tonClick={onClick}\n\t\t\t\t\taria-disabled={busy}\n\t\t\t\t\taria-busy={busy}\n\t\t\t\t\taria-label={`Copy ${exportSlug} chart to clipboard`}\n\t\t\t\t>\n\t\t\t\t\t<ClipboardCopy className=\"h-4 w-4\" />\n\t\t\t\t</Button>\n\t\t\t</TooltipTrigger>\n\t\t\t<TooltipContent side=\"top\">Copy to clipboard</TooltipContent>\n\t\t</Tooltip>\n\t);\n}\n","import { Button } from '@/components/ui/button';\nimport { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';\nimport { Download } from 'lucide-react';\nimport { type RefObject, useState } from 'react';\nimport { toast } from 'sonner';\nimport { useAnalyticsContext } from '../context/AnalyticsContext.tsx';\nimport { downloadChart, makeExportFilename } from '../lib/chartExport.ts';\n\ninterface Props {\n\t/** The DOM node that should be captured. Usually the chart's outer Card. */\n\tcaptureRef: RefObject<HTMLElement | null>;\n\t/** Slug used as the filename prefix (e.g. metric id or panel title). */\n\texportSlug: string;\n}\n\n/** Renders a small icon-button that, when clicked, snapshots the referenced\n * element to PNG and triggers a browser download. Filenames include an ISO\n * timestamp of the panel's window end so multiple captures don't collide.\n * Uses Radix Tooltip (keyboard-discoverable) over native title, and shows\n * a sonner toast on success/error so the user gets feedback during the\n * 100–500 ms capture window. */\nexport function ChartExportButton({ captureRef, exportSlug }: Props) {\n\tconst { timeRange } = useAnalyticsContext();\n\tconst [busy, setBusy] = useState(false);\n\n\tconst onClick = async () => {\n\t\tif (busy) { return; }\n\t\tconst el = captureRef.current;\n\t\tif (!el) { return; }\n\t\tsetBusy(true);\n\t\tconst filename = makeExportFilename(exportSlug, timeRange);\n\t\ttry {\n\t\t\tawait downloadChart(el, filename);\n\t\t\ttoast.success(`Saved ${filename}`);\n\t\t} catch (err) {\n\t\t\tconsole.error('[chart-export] capture failed', err);\n\t\t\ttoast.error('Could not export chart', {\n\t\t\t\tdescription: err instanceof Error ? err.message : 'Unknown error',\n\t\t\t});\n\t\t} finally {\n\t\t\tsetBusy(false);\n\t\t}\n\t};\n\n\treturn (\n\t\t<Tooltip>\n\t\t\t<TooltipTrigger asChild>\n\t\t\t\t<Button\n\t\t\t\t\tvariant=\"ghost\"\n\t\t\t\t\tsize=\"icon\"\n\t\t\t\t\tonClick={onClick}\n\t\t\t\t\taria-disabled={busy}\n\t\t\t\t\taria-busy={busy}\n\t\t\t\t\taria-label={`Download ${exportSlug} as PNG`}\n\t\t\t\t>\n\t\t\t\t\t<Download className=\"h-4 w-4\" />\n\t\t\t\t</Button>\n\t\t\t</TooltipTrigger>\n\t\t\t<TooltipContent side=\"top\">Download as PNG</TooltipContent>\n\t\t</Tooltip>\n\t);\n}\n","import { Button } from '@/components/ui/button';\nimport { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from '@/components/ui/dialog';\nimport { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';\nimport { Maximize2 } from 'lucide-react';\nimport { type ReactNode, useRef, useState } from 'react';\nimport { ChartCopyButton } from './ChartCopyButton.tsx';\nimport { ChartExportButton } from './ChartExportButton.tsx';\n\ninterface Props {\n\t/** Slug for the export filename if the user exports from the expanded view. */\n\texportSlug: string;\n\t/** Title shown in the dialog header. */\n\ttitle: string;\n\t/** Optional description shown below the title in the dialog. */\n\tdescription?: ReactNode;\n\t/** A render function that produces the chart's content. Called twice in\n\t * practice — once inline by the parent panel, and once inside the\n\t * expanded dialog. We use a render function (not children) so the\n\t * inner ResponsiveContainer remeasures against the dialog's tall\n\t * parent and grows the chart. The `fillParent` arg lets primitives\n\t * switch from a fixed `height` to filling the dialog's `h-[90vh]`. */\n\trenderChart: (opts: { fillParent: boolean }) => ReactNode;\n}\n\n/** Adds an \"Expand\" icon next to other panel-header actions. Click opens\n * a near-fullscreen Radix Dialog containing the same chart re-rendered\n * at full size, plus its own export button so the capture grabs the\n * bigger DOM (and therefore produces a higher-resolution PNG). */\nexport function ChartExpandButton({ exportSlug, title, description, renderChart }: Props) {\n\tconst [open, setOpen] = useState(false);\n\tconst expandedRef = useRef<HTMLDivElement>(null);\n\n\treturn (\n\t\t<>\n\t\t\t<Tooltip>\n\t\t\t\t<TooltipTrigger asChild>\n\t\t\t\t\t<Button\n\t\t\t\t\t\tvariant=\"ghost\"\n\t\t\t\t\t\tsize=\"icon\"\n\t\t\t\t\t\tonClick={() => setOpen(true)}\n\t\t\t\t\t\taria-label={`Expand ${exportSlug}`}\n\t\t\t\t\t>\n\t\t\t\t\t\t<Maximize2 className=\"h-4 w-4\" />\n\t\t\t\t\t</Button>\n\t\t\t\t</TooltipTrigger>\n\t\t\t\t<TooltipContent side=\"top\">Expand</TooltipContent>\n\t\t\t</Tooltip>\n\t\t\t<Dialog open={open} onOpenChange={setOpen}>\n\t\t\t\t<DialogContent // Override the Card's max-width — we want a near-fullscreen\n\t\t\t\t // canvas. Tailwind's sm:max-w-* utility wins specificity-wise\n\t\t\t\t// in some shadcn dialogs, so pin both.\n\t\t\t\tclassName=\"!max-w-[95vw] sm:!max-w-[95vw] h-[90vh] flex flex-col\">\n\t\t\t\t\t<DialogHeader>\n\t\t\t\t\t\t<div className=\"flex items-start justify-between gap-2 pr-6\">\n\t\t\t\t\t\t\t<div className=\"flex-1 min-w-0\">\n\t\t\t\t\t\t\t\t<DialogTitle>{title}</DialogTitle>\n\t\t\t\t\t\t\t\t{description && <DialogDescription>{description}</DialogDescription>}\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t<div className=\"flex items-center gap-1 shrink-0\">\n\t\t\t\t\t\t\t\t<ChartCopyButton captureRef={expandedRef} exportSlug={`${exportSlug}-expanded`} />\n\t\t\t\t\t\t\t\t<ChartExportButton captureRef={expandedRef} exportSlug={`${exportSlug}-expanded`} />\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t</DialogHeader>\n\t\t\t\t\t{\n\t\t\t\t\t\t/* The expanded chart renders into this region. ref captures\n\t\t\t\t\t everything inside (including the chart's surrounding\n\t\t\t\t\t chips/legend) so the export PNG matches the on-screen view. */\n\t\t\t\t\t}\n\t\t\t\t\t<div ref={expandedRef} className=\"flex-1 min-h-0 overflow-hidden flex flex-col\">\n\t\t\t\t\t\t{renderChart({ fillParent: true })}\n\t\t\t\t\t</div>\n\t\t\t\t</DialogContent>\n\t\t\t</Dialog>\n\t\t</>\n\t);\n}\n","import type { InstanceClientIdConfig, InstanceTypeConfig } from '@/config/instanceClientConfig.ts';\nimport {\n\ttype AnalyticsCondition,\n\tgetRawAnalyticsQueryOptions,\n} from '@/integrations/api/instance/status/getAnalytics.ts';\nimport { keepPreviousData, useQuery } from '@tanstack/react-query';\nimport { useEffect, useMemo, useRef } from 'react';\nimport type { AnalyticsDataPoint } from '../types/analytics.ts';\n\nexport interface UseAnalyticsRecordsArgs {\n\tmetric: string;\n\tstartTime: number;\n\tendTime: number;\n\tconditions?: AnalyticsCondition[];\n\tinstanceParams: InstanceClientIdConfig & InstanceTypeConfig;\n\t/** Polling cadence in ms. Set to 0 to disable. */\n\trefetchIntervalMs?: number;\n\t/** Required keys this metric's spec depends on. Missing keys surface via\n\t * `missingFields`, which the renderer can use to show a precise empty\n\t * state instead of a blank chart. */\n\trequiredFields?: readonly string[];\n\t/** Hint to Harper for the desired bucket size in ms. Honored when the\n\t * server supports it; otherwise it's a client-side soft cap row guard. */\n\tbucketMs?: number;\n}\n\nexport interface UseAnalyticsRecordsResult {\n\tdata: AnalyticsDataPoint[];\n\tisLoading: boolean;\n\tisError: boolean;\n\terror: Error | null;\n\tisEmpty: boolean;\n\t/** Union of keys observed across all returned rows (excluding `time` and\n\t * `node` which are part of AnalyticsDataPoint by contract). */\n\tfieldKeys: Set<string>;\n\t/** Subset of `requiredFields` that did not appear on any row. */\n\tmissingFields: string[];\n\trefetch: () => void;\n}\n\nconst RESERVED = new Set(['time', 'node']);\n\n// Stable empty array so downstream memos keyed on `data` keep referential\n// identity while React Query's response is still undefined. Using a fresh `[]`\n// each render churned every dependent useMemo on every render.\nconst EMPTY: readonly AnalyticsDataPoint[] = Object.freeze([]);\n\n/** Adapter from studio's `get_analytics` operation to the analytics-viz spec\n * pipeline. Passes rows through verbatim, exposes a schema-drift signal so\n * callers can render an explicit \"field unavailable\" state, and applies\n * small jitter to the polling start time so a tab's many concurrent specs\n * do not fire in lockstep on every refresh tick. */\nexport function useAnalyticsRecords({\n\tmetric,\n\tstartTime,\n\tendTime,\n\tconditions,\n\tinstanceParams,\n\trefetchIntervalMs = 60_000,\n\trequiredFields,\n\tbucketMs,\n}: UseAnalyticsRecordsArgs): UseAnalyticsRecordsResult {\n\t// Per-spec startup jitter (0–500 ms) so 5–7 concurrent specs in one tab\n\t// do not refire in the same render frame on auto-refresh.\n\tconst jitterRef = useRef<number | null>(null);\n\tif (jitterRef.current === null) {\n\t\tjitterRef.current = Math.floor(Math.random() * 500);\n\t}\n\n\tconst queryOpts = getRawAnalyticsQueryOptions({\n\t\tmetric,\n\t\tstartTime,\n\t\tendTime,\n\t\tconditions,\n\t\tinstanceParams,\n\t\tbucketMs,\n\t});\n\n\tconst query = useQuery({\n\t\t...queryOpts,\n\t\tstaleTime: refetchIntervalMs > 0 ? refetchIntervalMs : Infinity,\n\t\trefetchInterval: refetchIntervalMs > 0 ? refetchIntervalMs + jitterRef.current : false,\n\t\trefetchOnWindowFocus: false,\n\t\trefetchOnReconnect: false,\n\t\tplaceholderData: keepPreviousData,\n\t});\n\n\t// React Query already pauses interval refetching when the tab is hidden;\n\t// we don't add a visibility-driven refetch ourselves because that turns\n\t// every alt-tab into a synchronized N-panel POST burst on the customer's\n\t// Harper, bypassing staleTime entirely.\n\n\tconst data = (query.data ?? EMPTY) as AnalyticsDataPoint[];\n\n\tconst { fieldKeys, missingFields } = useMemo(() => {\n\t\tconst keys = new Set<string>();\n\t\tfor (const row of data) {\n\t\t\tfor (const k of Object.keys(row)) {\n\t\t\t\tif (!RESERVED.has(k)) { keys.add(k); }\n\t\t\t}\n\t\t}\n\t\t// Schema-drift signal requires *evidence*: at least one row that\n\t\t// carries data but lacks the required field. An empty response is\n\t\t// \"no data in window\" (a quiet-traffic state), not drift; flagging\n\t\t// missing fields there blanks legitimately empty panels with a\n\t\t// misleading error and was the cause of fresh-Harper false\n\t\t// positives on cpu-usage / utilization / etc.\n\t\tconst missing: string[] = [];\n\t\tif (requiredFields && data.length > 0) {\n\t\t\tfor (const f of requiredFields) {\n\t\t\t\tif (!keys.has(f)) { missing.push(f); }\n\t\t\t}\n\t\t}\n\t\treturn { fieldKeys: keys, missingFields: missing };\n\t}, [data, requiredFields]);\n\n\t// Telemetry: warn only when we can be reasonably confident the empty\n\t// result is a schema-drift signal (caller declared required fields and at\n\t// least one is missing) — otherwise legitimate low-traffic windows would\n\t// spam the console for 5–7 panels every refresh tick.\n\tuseEffect(() => {\n\t\tif (!query.isLoading && data.length === 0 && missingFields.length > 0) {\n\t\t\tconsole.warn('[analytics] panel rendered empty with missing fields', {\n\t\t\t\tmetric,\n\t\t\t\tinstanceId: instanceParams.entityId,\n\t\t\t\tmissingFields,\n\t\t\t});\n\t\t}\n\t}, [data.length, query.isLoading, metric, instanceParams.entityId, missingFields]);\n\n\treturn {\n\t\tdata,\n\t\tisLoading: query.isLoading,\n\t\tisError: query.isError,\n\t\terror: query.error as Error | null,\n\t\tisEmpty: data.length === 0,\n\t\tfieldKeys,\n\t\tmissingFields,\n\t\trefetch: query.refetch,\n\t};\n}\n","import type { AnalyticsDataPoint, DerivedMetricSpec, Series, SeriesData, TimeRange } from '../../types/analytics.ts';\n\n/**\n * Derived: per-(path, time) error rate computed from raw `count` and `total`\n * source columns. Σ-arithmetic: errorRate = 1 − Σtotal/Σcount.\n *\n * NOT mean(1 − ratio) — that's the canonical \"ratio-of-ratios\" bug the spec\n * calls out (line 518). Two records with [{count: 1000, total: 990},\n * {count: 10, total: 1}] should yield ≈0.0188 (Σ-correct), not 0.455\n * (mean-of-ratios).\n */\nexport function recomputeErrorRate(\n\trecords: AnalyticsDataPoint[],\n\t_window: TimeRange,\n\t_nodes: string[],\n): SeriesData {\n\tconst buckets = new Map<string, Map<number, { sumCount: number; sumTotal: number }>>();\n\tfor (const r of records) {\n\t\tconst path = typeof r.path === 'string' ? r.path : null;\n\t\tif (!path) { continue; }\n\t\tif (typeof r.time !== 'number' || !Number.isFinite(r.time)) { continue; }\n\t\tconst count = typeof r.count === 'number' && Number.isFinite(r.count) ? r.count : 0;\n\t\tconst total = typeof r.total === 'number' && Number.isFinite(r.total) ? r.total : 0;\n\t\tlet perTime = buckets.get(path);\n\t\tif (!perTime) {\n\t\t\tperTime = new Map();\n\t\t\tbuckets.set(path, perTime);\n\t\t}\n\t\tlet entry = perTime.get(r.time);\n\t\tif (!entry) {\n\t\t\tentry = { sumCount: 0, sumTotal: 0 };\n\t\t\tperTime.set(r.time, entry);\n\t\t}\n\t\tentry.sumCount += count;\n\t\tentry.sumTotal += total;\n\t}\n\n\tconst series: Series[] = [...buckets.entries()].map(([path, perTime]) => {\n\t\tconst sortedTimes = [...perTime.keys()].sort((a, b) => a - b);\n\t\tconst points = sortedTimes.map((t) => {\n\t\t\tconst e = perTime.get(t)!;\n\t\t\tconst y = e.sumCount === 0 ? null : 1 - (e.sumTotal / e.sumCount);\n\t\t\treturn { x: t, y, count: e.sumCount };\n\t\t});\n\t\treturn { key: path, label: path, points };\n\t});\n\n\treturn {\n\t\tseries,\n\t\tthresholds: [\n\t\t\t{ value: 0.001, label: '0.1% error SLO', direction: 'above-is-bad', minCount: 1000 },\n\t\t],\n\t};\n}\n\nexport const errorRateDerived: DerivedMetricSpec = {\n\tid: 'error-rate',\n\ttitle: 'Error rate (≥1000 req)',\n\tsubtitle: 'errored-request fraction — 1 − Σtotal/Σcount',\n\ttab: 'requests',\n\tsourceMetric: 'success',\n\trecompute: recomputeErrorRate,\n\tprimitive: 'line',\n\tyAxis: { unit: '', formatter: 'percent' },\n\t// Threshold lives on the SeriesData returned by `recomputeErrorRate` — the\n\t// LineChart primitive reads thresholds from rendered SeriesData, not from\n\t// the DerivedMetricSpec entry. Single source of truth.\n};\n","export const NODE_PALETTE = [\n\t'#58a6ff', // blue\n\t'#3fb950', // green\n\t'#f0883e', // orange\n\t'#bc8cff', // purple\n\t'#f778ba', // pink\n\t'#79c0ff', // light blue\n\t'#d2a8ff', // lavender\n\t'#ffa657', // amber\n\t'#ff7b72', // red\n\t'#7ee787', // lime\n] as const;\n\nexport function getNodeColor(nodeId: string, allNodeIds: string[]): string {\n\tconst sorted = [...allNodeIds].sort();\n\tconst index = sorted.indexOf(nodeId);\n\treturn NODE_PALETTE[index % NODE_PALETTE.length];\n}\n","import { getNodeColor } from '../lib/nodeColors.ts';\n\ninterface NodeLegendProps {\n\tnodeIds: string[];\n\tisActive: (nodeId: string) => boolean;\n\tonClickNode: (nodeId: string, ctrlKey: boolean) => void;\n\t/** When true, renders all buttons as non-interactive (gray-out only).\n\t * Selection state is preserved — visual / interactivity change only. */\n\tdisabled?: boolean;\n}\n\nexport function NodeLegend({ nodeIds, isActive, onClickNode, disabled }: NodeLegendProps) {\n\treturn (\n\t\t<div\n\t\t\trole=\"group\"\n\t\t\taria-label={disabled ? 'Node filter (unavailable on this tab)' : 'Node filter'}\n\t\t\tclassName=\"flex flex-wrap justify-center gap-x-4 gap-y-1 pt-2 text-[11px]\"\n\t\t>\n\t\t\t{disabled && (\n\t\t\t\t<span className=\"sr-only\" aria-live=\"polite\">\n\t\t\t\t\tPer-node filter is unavailable on this tab. Buttons remain visible to preserve selection state.\n\t\t\t\t</span>\n\t\t\t)}\n\t\t\t{nodeIds.map((node) => {\n\t\t\t\tconst color = getNodeColor(node, nodeIds);\n\t\t\t\tconst active = isActive(node);\n\t\t\t\tconst baseOpacity = active ? 1 : 0.3;\n\t\t\t\tconst buttonStyle = disabled\n\t\t\t\t\t? { color, opacity: 0.5, cursor: 'not-allowed' as const }\n\t\t\t\t\t: { color, opacity: baseOpacity };\n\t\t\t\treturn (\n\t\t\t\t\t<button\n\t\t\t\t\t\tkey={node}\n\t\t\t\t\t\ttype=\"button\"\n\t\t\t\t\t\taria-pressed={active}\n\t\t\t\t\t\t// `aria-disabled` (not `disabled`) keeps the button in the\n\t\t\t\t\t\t// tab order and announces its state to AT, while the click\n\t\t\t\t\t\t// handler still no-ops when disabled. `disabled` would\n\t\t\t\t\t\t// remove the element from focus order entirely, so SR users\n\t\t\t\t\t\t// couldn't discover what the legend means on this tab.\n\t\t\t\t\t\taria-disabled={disabled ? 'true' : undefined}\n\t\t\t\t\t\ttitle={disabled ? \"Per-node filter unavailable on this tab's panels\" : undefined}\n\t\t\t\t\t\tonClick={(e) => {\n\t\t\t\t\t\t\tif (disabled) { return; }\n\t\t\t\t\t\t\tonClickNode(node, e.ctrlKey || e.metaKey);\n\t\t\t\t\t\t}}\n\t\t\t\t\t\tclassName=\"inline-flex items-center gap-1.5 cursor-pointer border-none bg-transparent p-0\"\n\t\t\t\t\t\tstyle={buttonStyle}\n\t\t\t\t\t>\n\t\t\t\t\t\t<span\n\t\t\t\t\t\t\tclassName=\"inline-block h-[3px] w-3 rounded\"\n\t\t\t\t\t\t\tstyle={{ backgroundColor: color }}\n\t\t\t\t\t\t/>\n\t\t\t\t\t\t<span>{node}</span>\n\t\t\t\t\t</button>\n\t\t\t\t);\n\t\t\t})}\n\t\t</div>\n\t);\n}\n","import { useCallback, useState } from 'react';\n\nexport function useNodeSelection(nodeIds: string[]) {\n\tconst [activeNodes, setActiveNodes] = useState<Set<string> | null>(null);\n\n\tconst isActive = useCallback((nodeId: string) => {\n\t\treturn activeNodes === null || activeNodes.has(nodeId);\n\t}, [activeNodes]);\n\n\tconst handleLegendClick = useCallback((nodeId: string, ctrlKey: boolean) => {\n\t\tsetActiveNodes((prev) => {\n\t\t\tif (ctrlKey) {\n\t\t\t\tif (prev === null) {\n\t\t\t\t\treturn new Set(nodeIds.filter((id) => id !== nodeId));\n\t\t\t\t}\n\t\t\t\tconst next = new Set(prev);\n\t\t\t\tif (next.has(nodeId)) {\n\t\t\t\t\tnext.delete(nodeId);\n\t\t\t\t\tif (next.size === 0) { return null; }\n\t\t\t\t} else {\n\t\t\t\t\tnext.add(nodeId);\n\t\t\t\t\tif (next.size === nodeIds.length) { return null; }\n\t\t\t\t}\n\t\t\t\treturn next;\n\t\t\t}\n\t\t\tif (prev !== null && prev.size === 1 && prev.has(nodeId)) {\n\t\t\t\treturn null;\n\t\t\t}\n\t\t\treturn new Set([nodeId]);\n\t\t});\n\t}, [nodeIds]);\n\n\treturn { isActive, handleLegendClick, activeNodes };\n}\n","/** Protocol / type series colors. Six hues spread across the color wheel for\n * accessibility and colorblind-friendliness. Disjoint from NODE_PALETTE and\n * TABLE_PALETTE (enforced in test/colorAllocators/disjoint.test.ts). */\nexport const TYPE_PALETTE: readonly string[] = [\n\t'#0d9488', // teal-600\n\t'#dc2626', // red-600\n\t'#7c3aed', // violet-600\n\t'#d97706', // amber-600\n\t'#db2777', // pink-600\n\t'#0284c7', // sky-600\n];\n\nexport function getTypeColor(typeKey: string, allKeys: readonly string[]): string {\n\tconst sorted = [...allKeys].sort();\n\tconst idx = sorted.indexOf(typeKey);\n\treturn TYPE_PALETTE[(idx < 0 ? 0 : idx) % TYPE_PALETTE.length];\n}\n","// Aggregators run in the data layer, before primitives mount. Percentile\n// implementations use the nearest-rank method: p50 of [10, 20, 30] = 20;\n// p50 of [10, 20] = 10 (the lower of two medians). No interpolation. If a\n// future spec needs interpolated quantiles, add `p50-interp` etc. to the\n// Aggregator union rather than silently changing this behavior.\nimport type { Aggregator } from '../types/analytics.ts';\n\nexport interface AggInput {\n\tvalue: number | null;\n\t/** Required when aggregator === 'count-weighted-mean'; ignored otherwise. */\n\tcount?: number;\n}\n\nexport function aggregate(op: Aggregator, items: AggInput[]): number | null {\n\tconst finite = items.filter(\n\t\t(i): i is AggInput & { value: number } => typeof i.value === 'number' && Number.isFinite(i.value),\n\t);\n\tif (finite.length === 0) { return null; }\n\tconst vals = finite.map((i) => i.value);\n\n\tswitch (op) {\n\t\tcase 'sum':\n\t\t\treturn vals.reduce((a, b) => a + b, 0);\n\t\tcase 'mean':\n\t\t\treturn vals.reduce((a, b) => a + b, 0) / vals.length;\n\t\tcase 'max': {\n\t\t\t// Reducer instead of Math.max(...vals) — argument-spread blows the\n\t\t\t// V8 stack around ~125k elements, and a single (time, dim) bucket\n\t\t\t// can collect tens of thousands of values when many nodes report\n\t\t\t// at high frequency.\n\t\t\tlet m = vals[0];\n\t\t\tfor (let i = 1; i < vals.length; i++) {\n\t\t\t\tif (vals[i] > m) { m = vals[i]; }\n\t\t\t}\n\t\t\treturn m;\n\t\t}\n\t\tcase 'min': {\n\t\t\tlet m = vals[0];\n\t\t\tfor (let i = 1; i < vals.length; i++) {\n\t\t\t\tif (vals[i] < m) { m = vals[i]; }\n\t\t\t}\n\t\t\treturn m;\n\t\t}\n\t\tcase 'last':\n\t\t\treturn vals[vals.length - 1];\n\t\tcase 'p50':\n\t\t\treturn percentile(vals, 0.5);\n\t\tcase 'p95':\n\t\t\treturn percentile(vals, 0.95);\n\t\tcase 'p99':\n\t\t\treturn percentile(vals, 0.99);\n\t\tcase 'count-weighted-mean': {\n\t\t\tlet numer = 0;\n\t\t\tlet denom = 0;\n\t\t\tfor (const item of finite) {\n\t\t\t\tconst c = Number.isFinite(item.count) ? (item.count as number) : 1;\n\t\t\t\tnumer += item.value * c;\n\t\t\t\tdenom += c;\n\t\t\t}\n\t\t\treturn denom === 0 ? null : numer / denom;\n\t\t}\n\t}\n}\n\nfunction percentile(vals: number[], p: number): number {\n\tconst sorted = [...vals].sort((a, b) => a - b);\n\tconst idx = Math.max(1, Math.ceil(p * sorted.length));\n\treturn sorted[idx - 1];\n}\n","import type { Aggregator } from '../types/analytics.ts';\n\nexport function isApproxAggregator(op: Aggregator): boolean {\n\treturn op === 'count-weighted-mean';\n}\n\nexport function labelWithApprox(label: string, op: Aggregator): string {\n\tif (!isApproxAggregator(op)) { return label; }\n\treturn label.endsWith('(approx)') ? label : `${label} (approx)`;\n}\n","import type { MetricSpec } from '../types/analytics.ts';\n\ntype Confidence = NonNullable<MetricSpec['confidence']>;\nexport type ConfidenceClass = 'ok' | 'grey' | 'suppress';\n\n/** Classify a post-aggregation window count.\n *\n * When both thresholds are provided:\n * count >= suppressBelow → 'ok'\n * greyBelow <= count < suppressBelow → 'grey'\n * count < greyBelow → 'suppress'\n *\n * When only suppressBelow is set, the grey tier is disabled: counts below\n * suppressBelow go straight to 'suppress' (greyBelow collapses to\n * suppressBelow, leaving no grey band).\n *\n * When only greyBelow is set, the suppress tier is disabled: counts below\n * greyBelow become 'grey' and never 'suppress'.\n *\n * No rule → 'ok'.\n */\nexport function classifyConfidence(\n\tcount: number | undefined,\n\trule: Pick<Confidence, 'greyBelow' | 'suppressBelow'> | undefined,\n): ConfidenceClass {\n\tif (!rule) { return 'ok'; }\n\tif (count === undefined) { return 'suppress'; }\n\tconst { suppressBelow, greyBelow } = rule;\n\t// Check suppress tier: only if suppressBelow is set and count is below it.\n\tif (suppressBelow !== undefined && count < suppressBelow) {\n\t\t// If greyBelow is also set and count is still >= greyBelow, it's grey\n\t\t// (in the band between greyBelow and suppressBelow). Otherwise suppress.\n\t\tif (greyBelow !== undefined && count >= greyBelow) { return 'grey'; }\n\t\treturn 'suppress';\n\t}\n\t// If we passed the suppress check (or no suppress tier), check grey tier.\n\tif (greyBelow !== undefined && count < greyBelow) { return 'grey'; }\n\treturn 'ok';\n}\n","import type { FieldExpr } from '../types/analytics.ts';\n\nexport function evalFieldExpr(\n\texpr: FieldExpr,\n\trecord: Record<string, unknown>,\n): number | null {\n\tswitch (expr.kind) {\n\t\tcase 'const':\n\t\t\treturn expr.value;\n\t\tcase 'ref': {\n\t\t\tconst v = record[expr.field];\n\t\t\treturn typeof v === 'number' && Number.isFinite(v) ? v : null;\n\t\t}\n\t\tcase 'op': {\n\t\t\tconst l = evalFieldExpr(expr.left, record);\n\t\t\tconst r = evalFieldExpr(expr.right, record);\n\t\t\tif (l === null || r === null) { return null; }\n\t\t\tswitch (expr.op) {\n\t\t\t\tcase '+':\n\t\t\t\t\treturn l + r;\n\t\t\t\tcase '-':\n\t\t\t\t\treturn l - r;\n\t\t\t\tcase '*':\n\t\t\t\t\treturn l * r;\n\t\t\t\tcase '/':\n\t\t\t\t\treturn r === 0 ? null : l / r;\n\t\t\t\tdefault: {\n\t\t\t\t\tconst _exhaustive: never = expr.op;\n\t\t\t\t\tthrow new Error(`unknown op: ${String(_exhaustive)}`);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tdefault: {\n\t\t\tconst _exhaustive: never = expr;\n\t\t\tthrow new Error(`unknown FieldExpr kind: ${(_exhaustive as { kind: string }).kind}`);\n\t\t}\n\t}\n}\n","// Named transforms let specs stay serializable while still supporting\n// non-trivial per-record math (e.g. `cpuUtilization × 100`). Add entries here\n// as specs need them; never accept arbitrary functions from a spec.\nexport const namedTransforms = {\n\t'percent-of-core': (v: number) => v * 100,\n} as const;\n\nexport type NamedTransformKey = keyof typeof namedTransforms;\n","import type { Transform } from '../types/analytics.ts';\nimport { type NamedTransformKey, namedTransforms } from './transforms.ts';\n\n/** Apply a Transform to a scalar. `period` is the record's `period` field in\n * ms; only `rate` consults it. Null input short-circuits to null. */\nexport function runTransform(\n\ttransform: Transform,\n\tvalue: number | null,\n\tperiod: number,\n): number | null {\n\tif (value === null) { return null; }\n\tswitch (transform.kind) {\n\t\tcase 'raw':\n\t\t\treturn value;\n\t\tcase 'scale':\n\t\t\treturn value * transform.factor;\n\t\tcase 'rate':\n\t\t\tif (!Number.isFinite(period) || period <= 0) { return null; }\n\t\t\treturn (value / period) * 1000;\n\t\tcase 'ratio':\n\t\t\treturn value;\n\t\tcase 'compose': {\n\t\t\tlet v: number | null = value;\n\t\t\tfor (const step of transform.steps) {\n\t\t\t\tv = runTransform(step, v, period);\n\t\t\t\tif (v === null) { return null; }\n\t\t\t}\n\t\t\treturn v;\n\t\t}\n\t\tcase 'named': {\n\t\t\tconst fn = namedTransforms[transform.name as NamedTransformKey];\n\t\t\tif (!fn) { throw new Error(`unknown named transform: ${transform.name}`); }\n\t\t\treturn fn(value);\n\t\t}\n\t\tdefault: {\n\t\t\t// Exhaustiveness check — adding a new Transform kind without handling\n\t\t\t// it here will fail typechecking on this assignment.\n\t\t\tconst _exhaustive: never = transform;\n\t\t\tthrow new Error(`unknown transform kind: ${(_exhaustive as { kind: string }).kind}`);\n\t\t}\n\t}\n}\n","// Generic spec pipeline. Both groupBy and field modes emit one SeriesPoint\n// per unique `record.time` per series (per-time bucketing landed in Step 3\n// for groupBy; Step 4 for field).\n//\n// Step 4.5 adds two-pass cross-node aggregation. Within each (dimensionValue,\n// time) bucket (groupBy) or each `time` bucket (field), records are\n// partitioned by `node`. The temporal aggregator runs per `(time, node)`\n// (inner pass), producing one (value, count) per node. Then the crossNode\n// aggregator runs across nodes within `(dim, time)` (outer pass) to yield\n// the final value. The per-node `count` is threaded through to the outer\n// pass's AggInput[] so count-weighted-mean stays well-defined when the\n// crossNode aggregator needs it.\n//\n// Known limitation — count-weighted-mean across nodes: the inner pass\n// invokes the temporal aggregator with values that share the per-node\n// bucket's totalCount as their weight. When a single (node, time) bucket\n// holds multiple records, the inner result is still correct (CWM uses each\n// record's own count internally), but the *weight* attached to each\n// per-node AggInput passed into the outer pass is the sum of all record\n// counts in that node-bucket — i.e., the outer pass weights nodes by total\n// observations, not by the inner-mean's effective sample size. For the\n// shipped specs this is fine: replication-latency does not flow through\n// this pipeline, mqtt-traffic-* is sum/sum (associative). Revisit if a CWM\n// crossNode spec lands.\nimport type {\n\tAggregator,\n\tAnalyticsDataPoint,\n\tFieldExpr,\n\tFieldSpec,\n\tMetricSpec,\n\tSeries,\n\tSeriesData,\n\tSeriesPoint,\n\tTimeRange,\n} from '../types/analytics.ts';\nimport { type AggInput, aggregate } from './aggregators.ts';\nimport { labelWithApprox } from './approxLabel.ts';\nimport { classifyConfidence } from './confidence.ts';\nimport { evalFieldExpr } from './fieldExpr.ts';\nimport { runTransform } from './runTransform.ts';\n\nexport interface RunPipelineOptions {\n\t/** When true, runGroupBy emits one Series per (dim, node) instead of\n\t * collapsing nodes via the crossNode aggregator. The series key is\n\t * `${dim}|${node}` so consumers (e.g. DimensionSelectorRenderer) can\n\t * filter by selected dim while keeping per-node detail. No-op for\n\t * `kind: 'field'` series sources or for the OTHER bucket.\n\t * Used by chip-selector panels (duration, success, transfer, db-*,\n\t * connection, response_200) so the operator can spot a hot node\n\t * instead of reading a cluster-mean line. */\n\tperNode?: boolean;\n\t/** When true, each record's resolved time is snapped to its period\n\t * boundary (`floor(time/period)*period`). Harper emits per-node\n\t * records at slightly offset instants within the same minute; without\n\t * this snap, downstream stacks/lines render with sparse staggered\n\t * rows that look jagged. MetricRenderer enables this for production\n\t * rendering; pipeline tests that use synthetic small-integer times\n\t * leave it off so they keep their distinct buckets. */\n\tsnapToPeriod?: boolean;\n}\n\nexport function runPipeline(\n\tspec: MetricSpec,\n\trecords: AnalyticsDataPoint[],\n\t_window: TimeRange,\n\t_nodes: string[],\n\toptions?: RunPipelineOptions,\n): SeriesData {\n\tif (spec.series.kind === 'field') {\n\t\treturn runFieldSpecs(spec, spec.series.fields, records, options?.perNode ?? false, options?.snapToPeriod ?? false);\n\t}\n\treturn runGroupBy(spec, spec.series, records, options?.perNode ?? false, options?.snapToPeriod ?? false);\n}\n\n/** Snap a record's time to its period boundary so per-node staggering\n * doesn't leak into downstream visuals. Round-to-nearest (not floor) so\n * records arriving a few seconds before the boundary still group with\n * their siblings on the *other* side — Math.floor produced zigzag\n * aggregates because nodes reporting at e.g. 1:59:43, 2:00:03, 2:00:16\n * would split across two buckets. */\nfunction snapToBucketTime(spec: MetricSpec, record: AnalyticsDataPoint, time: number): number {\n\tlet period = 0;\n\tconst p = record.period;\n\tif (typeof p === 'number' && Number.isFinite(p) && p > 0) { period = p; }\n\tif (period <= 0) { period = spec.bucket?.fallbackMs ?? 60_000; }\n\treturn Math.round(time / period) * period;\n}\n\n/** Resolve the timestamp on a record per `spec.timestamp`. Defaults to 'time'.\n * Records like database-size / storage-volume carry `id` (ms since epoch)\n * instead of `time`; `timestamp: 'id'` reads `id`; `'time-or-id'` falls back. */\nfunction resolveTime(spec: MetricSpec, record: AnalyticsDataPoint): number | null {\n\tconst which = spec.timestamp ?? 'time';\n\tif (which === 'time') {\n\t\tconst t = record.time;\n\t\treturn typeof t === 'number' && Number.isFinite(t) ? t : null;\n\t}\n\tif (which === 'id') {\n\t\tconst t = (record as any).id;\n\t\treturn typeof t === 'number' && Number.isFinite(t) ? t : null;\n\t}\n\t// 'time-or-id'\n\tconst t = record.time;\n\tif (typeof t === 'number' && Number.isFinite(t)) { return t; }\n\tconst id = (record as any).id;\n\treturn typeof id === 'number' && Number.isFinite(id) ? id : null;\n}\n\nfunction projectValue(\n\tfieldSpec: FieldSpec,\n\trecord: AnalyticsDataPoint,\n): number | null {\n\tconst raw = typeof fieldSpec.field === 'string'\n\t\t? (typeof record[fieldSpec.field] === 'number' ? (record[fieldSpec.field] as number) : null)\n\t\t: evalFieldExpr(fieldSpec.field as FieldExpr, record);\n\tconst period = typeof record.period === 'number' ? record.period : 0;\n\treturn runTransform(fieldSpec.transform ?? { kind: 'raw' }, raw, period);\n}\n\ninterface NodeBucket {\n\titems: AggInput[]; // per-record {value, count} for this (dim?, time, node)\n\ttotalCount: number; // Σ of record counts within this node-bucket\n}\n\nfunction runGroupBy(\n\tspec: MetricSpec,\n\tsrc: Extract<MetricSpec['series'], { kind: 'groupBy' }>,\n\trecords: AnalyticsDataPoint[],\n\tperNode: boolean,\n\tsnapToPeriod: boolean,\n): SeriesData {\n\t// Step 4.5 structure: dim → time → node → NodeBucket. Per-dimension totals\n\t// are accumulated separately for topN ranking + per-series confidence\n\t// gating.\n\tconst buckets = new Map<string | number, Map<number, Map<string, NodeBucket>>>();\n\tconst dimTotals = new Map<string | number, number>();\n\tconst warnedTimes = new Set<string>();\n\tfor (const r of records) {\n\t\tconst dimVal = r[src.dimension];\n\t\tif (typeof dimVal !== 'string' && typeof dimVal !== 'number') { continue; }\n\t\tconst v = projectValue(src.field, r);\n\t\tif (v === null) { continue; }\n\t\tconst resolvedTime = resolveTime(spec, r);\n\t\tif (resolvedTime === null) {\n\t\t\tconst key = `${String(r[src.dimension])}|${String(r.time)}`;\n\t\t\tif (!warnedTimes.has(key)) {\n\t\t\t\twarnedTimes.add(key);\n\t\t\t\tconsole.warn('[runGroupBy] Dropping record with no resolvable timestamp:', {\n\t\t\t\t\tdimension: r[src.dimension],\n\t\t\t\t\ttime: r.time,\n\t\t\t\t\tid: (r as any).id,\n\t\t\t\t\ttimestamp: spec.timestamp ?? 'time',\n\t\t\t\t});\n\t\t\t}\n\t\t\tcontinue;\n\t\t}\n\t\tconst time = snapToPeriod ? snapToBucketTime(spec, r, resolvedTime) : resolvedTime;\n\t\tconst recordCount = typeof r.count === 'number' && Number.isFinite(r.count) ? r.count : 1;\n\t\tconst node = typeof r.node === 'string' ? r.node : '_no_node';\n\n\t\tdimTotals.set(dimVal, (dimTotals.get(dimVal) ?? 0) + recordCount);\n\n\t\tlet perTime = buckets.get(dimVal);\n\t\tif (!perTime) {\n\t\t\tperTime = new Map();\n\t\t\tbuckets.set(dimVal, perTime);\n\t\t}\n\t\tlet perNodeBucket = perTime.get(time);\n\t\tif (!perNodeBucket) {\n\t\t\tperNodeBucket = new Map();\n\t\t\tperTime.set(time, perNodeBucket);\n\t\t}\n\t\tlet nodeBucket = perNodeBucket.get(node);\n\t\tif (!nodeBucket) {\n\t\t\tnodeBucket = { items: [], totalCount: 0 };\n\t\t\tperNodeBucket.set(node, nodeBucket);\n\t\t}\n\t\tnodeBucket.items.push({ value: v, count: recordCount });\n\t\tnodeBucket.totalCount += recordCount;\n\t}\n\n\tconst tempAgg: Aggregator = src.field.aggregator?.temporal ?? spec.aggregator.temporal;\n\tconst crossAgg: Aggregator = src.field.aggregator?.crossNode ?? spec.aggregator.crossNode;\n\tconst isApprox = tempAgg === 'count-weighted-mean' || crossAgg === 'count-weighted-mean';\n\n\t// Apply topN + otherBucket: rank dimensions by totalCount descending, keep\n\t// the top N, roll the rest into an `Other` aggregate if otherBucket is on.\n\tconst ranked = [...dimTotals.entries()].sort((a, b) => b[1] - a[1]);\n\tconst topN = src.topN ?? Infinity;\n\tconst kept = ranked.slice(0, topN);\n\tconst rest = ranked.slice(topN);\n\n\tconst series: Series[] = [];\n\tlet suppressedSeriesCount = 0;\n\tfor (const [key, total] of kept) {\n\t\tconst confClass = classifyConfidence(\n\t\t\ttotal,\n\t\t\tspec.confidence && {\n\t\t\t\tgreyBelow: spec.confidence.greyBelow,\n\t\t\t\tsuppressBelow: spec.confidence.suppressBelow,\n\t\t\t},\n\t\t);\n\t\tif (confClass === 'suppress') {\n\t\t\tsuppressedSeriesCount++;\n\t\t\tcontinue;\n\t\t}\n\t\tconst perTime = buckets.get(key);\n\t\tif (!perTime) { continue; }\n\t\t// When the spec already groups by node, perNode is redundant — emitting\n\t\t// one series per (node, node) would duplicate. Fall through to the\n\t\t// cluster-aggregate path which, for dimension='node', is naturally\n\t\t// one-series-per-node.\n\t\tconst dimensionIsNode = src.dimension === 'node';\n\t\tif (perNode && !dimensionIsNode) {\n\t\t\t// Emit one Series per (dim, node). Skip the crossNode pass; each\n\t\t\t// node's points come from the inner temporal aggregation alone.\n\t\t\t// Series key is `${dim}|${node}` so renderers can filter by dim\n\t\t\t// prefix; label is the node name.\n\t\t\tconst nodeBuckets = new Map<string, Map<number, NodeBucket>>();\n\t\t\tfor (const [time, byNode] of perTime) {\n\t\t\t\tfor (const [node, nb] of byNode) {\n\t\t\t\t\tlet perTimeForNode = nodeBuckets.get(node);\n\t\t\t\t\tif (!perTimeForNode) {\n\t\t\t\t\t\tperTimeForNode = new Map();\n\t\t\t\t\t\tnodeBuckets.set(node, perTimeForNode);\n\t\t\t\t\t}\n\t\t\t\t\tperTimeForNode.set(time, nb);\n\t\t\t\t}\n\t\t\t}\n\t\t\tfor (const [node, perTimeForNode] of nodeBuckets) {\n\t\t\t\tconst points: SeriesPoint[] = [];\n\t\t\t\tconst sortedTimes = [...perTimeForNode.keys()].sort((a, b) => a - b);\n\t\t\t\tfor (const time of sortedTimes) {\n\t\t\t\t\tconst nb = perTimeForNode.get(time)!;\n\t\t\t\t\tconst y = aggregate(tempAgg, nb.items);\n\t\t\t\t\tpoints.push({ x: time, y, count: nb.totalCount });\n\t\t\t\t}\n\t\t\t\tseries.push({\n\t\t\t\t\tkey: `${String(key)}|${node}`,\n\t\t\t\t\tlabel: labelWithApprox(node, tempAgg),\n\t\t\t\t\tpoints,\n\t\t\t\t\tapprox: isApprox,\n\t\t\t\t});\n\t\t\t}\n\t\t} else {\n\t\t\t// Cluster-aggregate path (default). Two-pass: temporal-per-node,\n\t\t\t// then crossNode across nodes within each time bucket.\n\t\t\tconst points: SeriesPoint[] = [];\n\t\t\tconst sortedTimes = [...perTime.keys()].sort((a, b) => a - b);\n\t\t\tfor (const time of sortedTimes) {\n\t\t\t\tconst byNode = perTime.get(time)!;\n\t\t\t\tconst { y, count } = aggregateTwoPass(tempAgg, crossAgg, byNode);\n\t\t\t\tpoints.push({ x: time, y, count });\n\t\t\t}\n\t\t\tseries.push({\n\t\t\t\tkey: String(key),\n\t\t\t\tlabel: labelWithApprox(String(key), tempAgg),\n\t\t\t\tpoints,\n\t\t\t\tapprox: isApprox,\n\t\t\t});\n\t\t}\n\t}\n\n\t// OTHER bucket: if enabled and there are more buckets beyond topN, aggregate\n\t// them into one. Bucket per-(time, node) across all \"rest\" dimension values,\n\t// then run the same two-pass aggregation.\n\tif (src.otherBucket && rest.length > 0) {\n\t\tconst otherTotal = rest.reduce((acc, [, c]) => acc + c, 0);\n\t\tconst confClass = classifyConfidence(\n\t\t\totherTotal,\n\t\t\tspec.confidence && {\n\t\t\t\tgreyBelow: spec.confidence.greyBelow,\n\t\t\t\tsuppressBelow: spec.confidence.suppressBelow,\n\t\t\t},\n\t\t);\n\t\tif (confClass !== 'suppress') {\n\t\t\tconst otherPerTime = new Map<number, Map<string, NodeBucket>>();\n\t\t\tfor (const [key] of rest) {\n\t\t\t\tconst perTime = buckets.get(key);\n\t\t\t\tif (!perTime) { continue; }\n\t\t\t\tfor (const [time, perNodeBucket] of perTime) {\n\t\t\t\t\tlet mergedPerNode = otherPerTime.get(time);\n\t\t\t\t\tif (!mergedPerNode) {\n\t\t\t\t\t\tmergedPerNode = new Map();\n\t\t\t\t\t\totherPerTime.set(time, mergedPerNode);\n\t\t\t\t\t}\n\t\t\t\t\tfor (const [node, nb] of perNodeBucket) {\n\t\t\t\t\t\tlet merged = mergedPerNode.get(node);\n\t\t\t\t\t\tif (!merged) {\n\t\t\t\t\t\t\tmerged = { items: [], totalCount: 0 };\n\t\t\t\t\t\t\tmergedPerNode.set(node, merged);\n\t\t\t\t\t\t}\n\t\t\t\t\t\tfor (const item of nb.items) { merged.items.push(item); }\n\t\t\t\t\t\tmerged.totalCount += nb.totalCount;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\tconst otherPoints: SeriesPoint[] = [];\n\t\t\tconst sortedTimes = [...otherPerTime.keys()].sort((a, b) => a - b);\n\t\t\tfor (const time of sortedTimes) {\n\t\t\t\tconst byNode = otherPerTime.get(time)!;\n\t\t\t\tconst { y, count } = aggregateTwoPass(tempAgg, crossAgg, byNode);\n\t\t\t\totherPoints.push({ x: time, y, count });\n\t\t\t}\n\t\t\tseries.push({\n\t\t\t\tkey: 'Other',\n\t\t\t\tlabel: labelWithApprox('Other', tempAgg),\n\t\t\t\tpoints: otherPoints,\n\t\t\t\tapprox: isApprox,\n\t\t\t});\n\t\t} else {\n\t\t\tsuppressedSeriesCount++;\n\t\t}\n\t}\n\n\treturn {\n\t\tseries,\n\t\tthresholds: spec.thresholds,\n\t\t...(suppressedSeriesCount > 0 ? { suppressedSeriesCount } : {}),\n\t};\n}\n\nfunction runFieldSpecs(\n\tspec: MetricSpec,\n\tfields: FieldSpec[],\n\trecords: AnalyticsDataPoint[],\n\tperNode: boolean,\n\tsnapToPeriod: boolean,\n): SeriesData {\n\tconst warnedTimes = new Set<string>();\n\tconst seriesArrays: Series[][] = fields.map((f) => {\n\t\t// time → node → NodeBucket\n\t\tconst buckets = new Map<number, Map<string, NodeBucket>>();\n\t\tfor (const r of records) {\n\t\t\tconst resolvedTime = resolveTime(spec, r);\n\t\t\tif (resolvedTime === null) {\n\t\t\t\tconst key = `${f.label}|${String(r.time)}`;\n\t\t\t\tif (!warnedTimes.has(key)) {\n\t\t\t\t\twarnedTimes.add(key);\n\t\t\t\t\tconsole.warn('[runFieldSpecs] Dropping record with no resolvable timestamp:', {\n\t\t\t\t\t\tfield: f.label,\n\t\t\t\t\t\ttime: r.time,\n\t\t\t\t\t\tid: (r as any).id,\n\t\t\t\t\t\ttimestamp: spec.timestamp ?? 'time',\n\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tconst v = projectValue(f, r);\n\t\t\tif (v === null) { continue; }\n\t\t\tconst recordCount = typeof r.count === 'number' && Number.isFinite(r.count) ? r.count : 1;\n\t\t\tconst node = typeof r.node === 'string' ? r.node : '_no_node';\n\t\t\tconst time = snapToPeriod ? snapToBucketTime(spec, r, resolvedTime) : resolvedTime;\n\t\t\tlet byNode = buckets.get(time);\n\t\t\tif (!byNode) {\n\t\t\t\tbyNode = new Map();\n\t\t\t\tbuckets.set(time, byNode);\n\t\t\t}\n\t\t\tlet nodeBucket = byNode.get(node);\n\t\t\tif (!nodeBucket) {\n\t\t\t\tnodeBucket = { items: [], totalCount: 0 };\n\t\t\t\tbyNode.set(node, nodeBucket);\n\t\t\t}\n\t\t\tnodeBucket.items.push({ value: v, count: recordCount });\n\t\t\tnodeBucket.totalCount += recordCount;\n\t\t}\n\t\tconst tempAgg = f.aggregator?.temporal ?? spec.aggregator.temporal;\n\t\tconst crossAgg = f.aggregator?.crossNode ?? spec.aggregator.crossNode;\n\t\tconst isApprox = tempAgg === 'count-weighted-mean' || crossAgg === 'count-weighted-mean';\n\t\tconst fieldKey = typeof f.field === 'string' ? f.field : f.label;\n\n\t\tif (perNode) {\n\t\t\t// Emit one Series per (field, node). Series key is\n\t\t\t// `${fieldKey}|${node}` so callers can identify both axes; label is\n\t\t\t// `${fieldLabel} — ${node}` so legends stay readable.\n\t\t\tconst nodeBuckets = new Map<string, Map<number, NodeBucket>>();\n\t\t\tfor (const [time, byNode] of buckets) {\n\t\t\t\tfor (const [node, nb] of byNode) {\n\t\t\t\t\tlet perTimeForNode = nodeBuckets.get(node);\n\t\t\t\t\tif (!perTimeForNode) {\n\t\t\t\t\t\tperTimeForNode = new Map();\n\t\t\t\t\t\tnodeBuckets.set(node, perTimeForNode);\n\t\t\t\t\t}\n\t\t\t\t\tperTimeForNode.set(time, nb);\n\t\t\t\t}\n\t\t\t}\n\t\t\tconst out: Series[] = [];\n\t\t\tfor (const [node, perTimeForNode] of nodeBuckets) {\n\t\t\t\tconst points: SeriesPoint[] = [];\n\t\t\t\tconst sortedTimes = [...perTimeForNode.keys()].sort((a, b) => a - b);\n\t\t\t\tfor (const time of sortedTimes) {\n\t\t\t\t\tconst nb = perTimeForNode.get(time)!;\n\t\t\t\t\tconst y = aggregate(tempAgg, nb.items);\n\t\t\t\t\tpoints.push({ x: time, y, count: nb.totalCount });\n\t\t\t\t}\n\t\t\t\tout.push({\n\t\t\t\t\tkey: `${fieldKey}|${node}`,\n\t\t\t\t\tlabel: labelWithApprox(`${f.label} — ${node}`, tempAgg),\n\t\t\t\t\taxis: f.axis,\n\t\t\t\t\tpoints,\n\t\t\t\t\tapprox: isApprox,\n\t\t\t\t});\n\t\t\t}\n\t\t\treturn out;\n\t\t}\n\n\t\t// Cluster-aggregate path.\n\t\tconst points: SeriesPoint[] = [];\n\t\tconst sortedTimes = [...buckets.keys()].sort((a, b) => a - b);\n\t\tfor (const t of sortedTimes) {\n\t\t\tconst byNode = buckets.get(t)!;\n\t\t\tconst { y, count } = aggregateTwoPass(tempAgg, crossAgg, byNode);\n\t\t\tpoints.push({ x: t, y, count });\n\t\t}\n\t\treturn [{\n\t\t\tkey: fieldKey,\n\t\t\tlabel: labelWithApprox(f.label, tempAgg),\n\t\t\taxis: f.axis,\n\t\t\tpoints,\n\t\t\tapprox: isApprox,\n\t\t}];\n\t});\n\treturn { series: seriesArrays.flat(), thresholds: spec.thresholds };\n}\n\n/**\n * Two-pass aggregation: temporal (inner, per-node) → crossNode (outer, across\n * nodes within the same time bucket). Returns the final y plus the summed\n * per-node totalCount for the bucket.\n */\nfunction aggregateTwoPass(\n\ttemporal: Aggregator,\n\tcrossNode: Aggregator,\n\tperNode: Map<string, NodeBucket>,\n): { y: number | null; count: number } {\n\tconst perNodeAggs: AggInput[] = [];\n\tlet totalCount = 0;\n\tfor (const [, nodeBucket] of perNode) {\n\t\tconst nodeY = aggregate(temporal, nodeBucket.items);\n\t\tif (typeof nodeY === 'number' && Number.isFinite(nodeY)) {\n\t\t\tperNodeAggs.push({ value: nodeY, count: nodeBucket.totalCount });\n\t\t}\n\t\ttotalCount += nodeBucket.totalCount;\n\t}\n\tconst y = aggregate(crossNode, perNodeAggs);\n\treturn { y, count: totalCount };\n}\n","import type { PresetOption, TimeRange } from '../types/analytics.ts';\n\nexport const TIME_PRESETS: PresetOption[] = [\n\t{ label: '15m', value: '15m', durationMs: 15 * 60 * 1000 },\n\t{ label: '30m', value: '30m', durationMs: 30 * 60 * 1000 },\n\t{ label: '1h', value: '1h', durationMs: 60 * 60 * 1000 },\n\t{ label: '6h', value: '6h', durationMs: 6 * 60 * 60 * 1000 },\n\t{ label: '12h', value: '12h', durationMs: 12 * 60 * 60 * 1000 },\n\t{ label: '1d', value: '1d', durationMs: 24 * 60 * 60 * 1000 },\n\t{ label: '3d', value: '3d', durationMs: 3 * 24 * 60 * 60 * 1000 },\n\t{ label: '1w', value: '1w', durationMs: 7 * 24 * 60 * 60 * 1000 },\n\t{ label: '1mo', value: '1mo', durationMs: 30 * 24 * 60 * 60 * 1000 },\n];\n\nexport function getTimeRangeFromPreset(preset: string, now: number = Date.now()): TimeRange {\n\tconst found = TIME_PRESETS.find((p) => p.value === preset);\n\tif (!found) { throw new Error(`Unknown preset: ${preset}`); }\n\treturn { startTime: now - found.durationMs, endTime: now };\n}\n\nconst axisFormatter = new Intl.DateTimeFormat(undefined, {\n\thour: 'numeric',\n\tminute: '2-digit',\n});\n\nconst tooltipFormatter = new Intl.DateTimeFormat(undefined, {\n\tmonth: 'short',\n\tday: 'numeric',\n\tyear: 'numeric',\n\thour: 'numeric',\n\tminute: '2-digit',\n\tsecond: '2-digit',\n});\n\nconst rangeFormatter = new Intl.DateTimeFormat(undefined, {\n\tmonth: 'short',\n\tday: 'numeric',\n\thour: 'numeric',\n\tminute: '2-digit',\n});\n\nconst tzFormatter = new Intl.DateTimeFormat(undefined, {\n\ttimeZoneName: 'short',\n});\n\nexport function formatAxisTick(timestamp: number): string {\n\treturn axisFormatter.format(new Date(timestamp));\n}\n\nexport function formatTooltipTime(timestamp: number): string {\n\treturn tooltipFormatter.format(new Date(timestamp));\n}\n\nexport function formatTimeRange(startTime: number, endTime: number): string {\n\tconst start = rangeFormatter.format(new Date(startTime));\n\tconst end = rangeFormatter.format(new Date(endTime));\n\treturn `${start} – ${end}`;\n}\n\nexport function getTimezoneAbbr(): string {\n\tconst parts = tzFormatter.formatToParts(new Date());\n\tconst tz = parts.find((p) => p.type === 'timeZoneName');\n\treturn tz?.value ?? 'UTC';\n}\n\nexport function formatBytes(bytes: number): string {\n\tif (bytes === 0) { return '0 B'; }\n\tconst units = ['B', 'KB', 'MB', 'GB', 'TB'];\n\tconst k = 1000; // SI\n\tconst i = Math.floor(Math.log(bytes) / Math.log(k));\n\tconst value = bytes / k ** i;\n\treturn `${value.toFixed(value < 10 ? 1 : 0)} ${units[i]}`;\n}\n\nexport function formatBytesPerMin(bytes: number): string {\n\tif (bytes === 0) { return '0 B/min'; }\n\tconst units = ['B/min', 'KB/min', 'MB/min', 'GB/min', 'TB/min'];\n\tconst k = 1000;\n\tconst i = Math.floor(Math.log(bytes) / Math.log(k));\n\tconst value = bytes / k ** i;\n\treturn `${value.toFixed(value < 10 ? 1 : 0)} ${units[i]}`;\n}\n","import type { AxisSpec } from '../types/analytics.ts';\n\nexport function formatValue(\n\tv: number,\n\tformatter?: AxisSpec['formatter'],\n\tunitSuffix?: string,\n): string {\n\tif (v === null || v === undefined || !Number.isFinite(v)) { return '—'; }\n\tconst base = formatBase(v, formatter);\n\t// unitSuffix is meant to *compose* with the formatter's unit, not\n\t// duplicate it. Specs should set unitSuffix to '' when the formatter\n\t// already includes the right unit (e.g. formatter: 'ms' → spec sets\n\t// unit: ''), and to a modifier like '/s' when adding rate context\n\t// (formatter: 'bytes-si' + unit: '/s' → \"MB/s\").\n\treturn unitSuffix ? `${base}${unitSuffix}` : base;\n}\n\nfunction formatBase(v: number, formatter?: AxisSpec['formatter']): string {\n\tswitch (formatter) {\n\t\tcase 'percent':\n\t\t\treturn `${(v * 100).toFixed(1)}%`;\n\t\tcase 'ms':\n\t\t\treturn `${v.toFixed(1)} ms`;\n\t\tcase 'count':\n\t\t\treturn `${v.toFixed(0)}`;\n\t\tcase 'count-si': {\n\t\t\tconst abs = Math.abs(v);\n\t\t\tif (abs < 1_000) { return `${v}`; }\n\t\t\tconst sign = v < 0 ? '-' : '';\n\t\t\tconst fmt = (x: number): string => {\n\t\t\t\tif (x >= 10) { return `${Math.round(x)}`; }\n\t\t\t\tconst s = x.toFixed(1);\n\t\t\t\treturn s.endsWith('.0') ? s.slice(0, -2) : s;\n\t\t\t};\n\t\t\tif (abs < 1_000_000) { return `${sign}${fmt(abs / 1_000)}k`; }\n\t\t\tif (abs < 1_000_000_000) { return `${sign}${fmt(abs / 1_000_000)}M`; }\n\t\t\treturn `${sign}${fmt(abs / 1_000_000_000)}B`;\n\t\t}\n\t\tcase 'cores':\n\t\t\t// Input is cores-equivalent CPU usage (1.0 = one core fully busy;\n\t\t\t// nproc = box saturated). Display direct, no scaling.\n\t\t\treturn `${v.toFixed(2)} cores`;\n\t\tcase 'bytes-si':\n\t\tcase 'bytes-iec': {\n\t\t\tconst base = formatter === 'bytes-iec' ? 1024 : 1000;\n\t\t\tconst units = formatter === 'bytes-iec'\n\t\t\t\t? ['B', 'KiB', 'MiB', 'GiB', 'TiB']\n\t\t\t\t: ['B', 'KB', 'MB', 'GB', 'TB'];\n\t\t\tlet scaled = v;\n\t\t\tlet i = 0;\n\t\t\twhile (Math.abs(scaled) >= base && i < units.length - 1) {\n\t\t\t\tscaled /= base;\n\t\t\t\ti++;\n\t\t\t}\n\t\t\treturn `${scaled.toFixed(1)} ${units[i]}`;\n\t\t}\n\t\tdefault:\n\t\t\treturn `${v}`;\n\t}\n}\n","// Shared tooltip surface for all analytics chart primitives. Resolves to\n// Studio's --popover token (via --chart-tooltip-bg) so LineChart,\n// StackedAreaChart, and HeatmapMatrix all hover with the same surface\n// the rest of Studio uses for HoverCard/Tooltip, instead of Recharts'\n// default white box or a stray --color-bg-secondary.\n\nimport type { CSSProperties } from 'react';\n\nexport const tooltipContentStyle: CSSProperties = {\n\tbackground: 'var(--chart-tooltip-bg)',\n\tcolor: 'var(--chart-tooltip-fg)',\n\tborder: '1px solid var(--border)',\n\tborderRadius: 6,\n\tboxShadow: '0 4px 12px rgba(0, 0, 0, 0.25)',\n\tpadding: 8,\n\tfontSize: 12,\n};\n\nexport const tooltipLabelStyle: CSSProperties = {\n\tcolor: 'var(--chart-tooltip-fg)',\n\topacity: 0.7,\n\tmarginBottom: 4,\n\tfontSize: 11,\n};\n\nexport const tooltipItemStyle: CSSProperties = {\n\tcolor: 'var(--chart-tooltip-fg)',\n};\n","import {\n\tCartesianGrid,\n\tLegend,\n\tLine,\n\tLineChart as RLineChart,\n\tReferenceLine,\n\tResponsiveContainer,\n\tTooltip,\n\tXAxis,\n\tYAxis,\n} from 'recharts';\nimport { formatAxisTick, formatTooltipTime } from '../lib/time.ts';\nimport type { AxisSpec, SeriesData, Threshold } from '../types/analytics.ts';\nimport { formatValue } from './formatValue.ts';\nimport { tooltipContentStyle, tooltipItemStyle, tooltipLabelStyle } from './tooltipStyle.ts';\n\ninterface Props {\n\tdata: SeriesData;\n\ttheme: 'light' | 'dark';\n\tyAxis?: AxisSpec | { left: AxisSpec; right?: AxisSpec };\n\theight?: number;\n\t/** Optional accessible label override; otherwise composed from series labels. */\n\tariaLabel?: string;\n\t/** Suppress the in-chart Recharts <Legend>. Used by parents (e.g.\n\t * SmallMultiples) that render a shared legend so per-chart legends\n\t * don't crowd out the plot area in small panels. */\n\thideLegend?: boolean;\n\t/** Pin the x-axis to a specific [start, end] millisecond range. When\n\t * set, the axis spans the requested window even if the data is sparse\n\t * inside it — operators expect \"Last 7 days\" to actually show 7 days\n\t * of axis, with the data appearing as a chunk at the right edge. When\n\t * omitted, the axis falls back to dataMin/dataMax of the points. */\n\txDomain?: [number, number];\n\t/** When true, the chart fills its parent's vertical space instead of\n\t * using the fixed `height` prop. Used by the expand-to-fullscreen\n\t * dialog so the chart actually grows to fit; the parent must be a\n\t * flex column with a definite height (e.g. h-[90vh]) for this to\n\t * resolve to a real pixel size. */\n\tfillParent?: boolean;\n}\n\ntype DualAxis = { left: AxisSpec; right?: AxisSpec };\n\nfunction isDualAxis(a: Props['yAxis']): a is DualAxis {\n\treturn !!a && typeof a === 'object' && 'left' in a;\n}\n\n/** Builds a screen-reader-readable summary of the chart contents.\n * Recharts itself emits no a11y semantics — this gives an `<svg>`-equivalent\n * one-shot description so AT users get *something* without a data-table\n * fallback. Better than silent. */\nfunction composeAriaLabel(data: SeriesData): string {\n\tconst seriesNames = data.series.map((s) => s.label);\n\tconst summary = seriesNames.length === 1\n\t\t? `Chart of ${seriesNames[0]}`\n\t\t: `Chart with ${seriesNames.length} series: ${seriesNames.slice(0, 5).join(', ')}${\n\t\t\tseriesNames.length > 5 ? '…' : ''\n\t\t}`;\n\tconst thresholdNote = data.thresholds && data.thresholds.length > 0\n\t\t? `. ${data.thresholds.length} threshold${data.thresholds.length === 1 ? '' : 's'}.`\n\t\t: '';\n\treturn `${summary}${thresholdNote}`;\n}\n\nexport function LineChart(\n\t{ data, theme: _theme, yAxis, height = 240, ariaLabel, hideLegend, xDomain, fillParent }: Props,\n) {\n\tif (data.series.length === 0) {\n\t\treturn (\n\t\t\t<div role=\"status\" aria-live=\"polite\" className=\"text-(--color-text-secondary) text-sm p-4\">\n\t\t\t\tNo data in window\n\t\t\t</div>\n\t\t);\n\t}\n\n\t// Flatten points across series for Recharts' data prop.\n\t// We render one <Line> per series, each with its own `data` prop so axes\n\t// aren't forced to share an X domain per row.\n\tconst isDual = isDualAxis(yAxis);\n\tconst leftAxis = isDual ? yAxis.left : (yAxis as AxisSpec | undefined);\n\tconst rightAxis = isDual ? yAxis.right : undefined;\n\n\tconst colors = ['#58a6ff', '#3fb950', '#f0883e', '#bc8cff', '#f778ba'];\n\n\treturn (\n\t\t<div\n\t\t\trole=\"img\"\n\t\t\taria-label={ariaLabel ?? composeAriaLabel(data)}\n\t\t\tstyle={fillParent\n\t\t\t\t? { width: '100%', height: '100%', minHeight: 0, flex: '1 1 auto', display: 'flex', flexDirection: 'column' }\n\t\t\t\t: { width: '100%', height }}\n\t\t>\n\t\t\t{\n\t\t\t\t/* Inner SVG is decorative once the outer role=img has the\n\t\t\t composed label — Safari/VO otherwise descends and reads\n\t\t\t every <g>/<path> text fragment. */\n\t\t\t}\n\t\t\t<div aria-hidden=\"true\" style={{ width: '100%', height: '100%' }}>\n\t\t\t\t<ResponsiveContainer width=\"100%\" height=\"100%\">\n\t\t\t\t\t<RLineChart margin={{ top: 12, right: 12, bottom: 8, left: 8 }}>\n\t\t\t\t\t\t<CartesianGrid stroke=\"var(--chart-grid)\" strokeDasharray=\"3 3\" />\n\t\t\t\t\t\t<XAxis\n\t\t\t\t\t\t\tdataKey=\"x\"\n\t\t\t\t\t\t\ttype=\"number\"\n\t\t\t\t\t\t\tdomain={xDomain ?? ['dataMin', 'dataMax']}\n\t\t\t\t\t\t\tallowDataOverflow={!!xDomain}\n\t\t\t\t\t\t\tallowDuplicatedCategory={false}\n\t\t\t\t\t\t\ttickFormatter={formatAxisTick}\n\t\t\t\t\t\t\ttick={{ fontSize: 11 }}\n\t\t\t\t\t\t/>\n\t\t\t\t\t\t<YAxis\n\t\t\t\t\t\t\tyAxisId=\"left\"\n\t\t\t\t\t\t\ttickFormatter={(v) => formatValue(v, leftAxis?.formatter, leftAxis?.unit)}\n\t\t\t\t\t\t\tscale={leftAxis?.scale ?? 'linear'}\n\t\t\t\t\t\t\tdomain={leftAxis?.domain as [number | string, number | string] | undefined}\n\t\t\t\t\t\t\ttick={{ fontSize: 11 }}\n\t\t\t\t\t\t\t// Wider than Recharts' ~60px default so e.g. '2400.0 ms'\n\t\t\t\t\t\t\t// or '1.5 GB' doesn't wrap onto two lines per tick.\n\t\t\t\t\t\t\twidth={70}\n\t\t\t\t\t\t/>\n\t\t\t\t\t\t{isDual && rightAxis\n\t\t\t\t\t\t\t? (\n\t\t\t\t\t\t\t\t<YAxis\n\t\t\t\t\t\t\t\t\tyAxisId=\"right\"\n\t\t\t\t\t\t\t\t\torientation=\"right\"\n\t\t\t\t\t\t\t\t\ttickFormatter={(v) => formatValue(v, rightAxis.formatter, rightAxis.unit)}\n\t\t\t\t\t\t\t\t\tscale={rightAxis.scale ?? 'linear'}\n\t\t\t\t\t\t\t\t\tdomain={rightAxis.domain as [number | string, number | string] | undefined}\n\t\t\t\t\t\t\t\t\ttick={{ fontSize: 11 }}\n\t\t\t\t\t\t\t\t\twidth={70}\n\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t)\n\t\t\t\t\t\t\t: null}\n\t\t\t\t\t\t<Tooltip\n\t\t\t\t\t\t\tlabelFormatter={(label) => formatTooltipTime(Number(label))}\n\t\t\t\t\t\t\tformatter={(val, name) => {\n\t\t\t\t\t\t\t\tconst nameStr = String(name);\n\t\t\t\t\t\t\t\tconst series = data.series.find((s) => s.label === nameStr || s.key === nameStr);\n\t\t\t\t\t\t\t\tconst axisSpec = series?.axis === 'right' ? rightAxis : leftAxis;\n\t\t\t\t\t\t\t\treturn [formatValue(Number(val), axisSpec?.formatter, axisSpec?.unit), nameStr];\n\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t\tcontentStyle={tooltipContentStyle}\n\t\t\t\t\t\t\tlabelStyle={tooltipLabelStyle}\n\t\t\t\t\t\t\titemStyle={tooltipItemStyle}\n\t\t\t\t\t\t/>\n\t\t\t\t\t\t{!hideLegend && <Legend />}\n\t\t\t\t\t\t{data.thresholds?.map((t: Threshold, i: number) => {\n\t\t\t\t\t\t\t// Compose a label that carries value + direction so the\n\t\t\t\t\t\t\t// reference line is self-describing instead of relying on\n\t\t\t\t\t\t\t// stroke-color alone (fails WCAG 1.4.1 use-of-color).\n\t\t\t\t\t\t\tconst formatted = formatValue(t.value, leftAxis?.formatter, leftAxis?.unit);\n\t\t\t\t\t\t\tconst directionMark = t.direction === 'above-is-bad' ? '↑ bad' : '↓ bad';\n\t\t\t\t\t\t\tconst labelText = `${t.label} (${formatted}, ${directionMark})`;\n\t\t\t\t\t\t\t// Alternate label position so multiple thresholds (e.g.\n\t\t\t\t\t\t\t// connection's connect+disconnect pair) don't stack on top\n\t\t\t\t\t\t\t// of each other at the same corner.\n\t\t\t\t\t\t\tconst position = i % 2 === 0 ? 'insideTopRight' : 'insideBottomRight';\n\t\t\t\t\t\t\treturn (\n\t\t\t\t\t\t\t\t<ReferenceLine\n\t\t\t\t\t\t\t\t\tkey={`th-${i}`}\n\t\t\t\t\t\t\t\t\tyAxisId=\"left\"\n\t\t\t\t\t\t\t\t\ty={t.value}\n\t\t\t\t\t\t\t\t\tstroke={t.direction === 'above-is-bad' ? 'var(--color-error)' : 'var(--color-success)'}\n\t\t\t\t\t\t\t\t\tstrokeDasharray=\"4 2\"\n\t\t\t\t\t\t\t\t\tlabel={{ value: labelText, position, fontSize: 11 }}\n\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t);\n\t\t\t\t\t\t})}\n\t\t\t\t\t\t{data.series.map((s, idx) => {\n\t\t\t\t\t\t\t// Per-node mode often produces series with 1-2 points\n\t\t\t\t\t\t\t// (sparse data, short window). Recharts won't draw a\n\t\t\t\t\t\t\t// line with a single point, so force a small dot when\n\t\t\t\t\t\t\t// the series is sparse — otherwise the chart looks\n\t\t\t\t\t\t\t// blank even though data is present.\n\t\t\t\t\t\t\tconst showDots = s.points.length <= 5;\n\t\t\t\t\t\t\treturn (\n\t\t\t\t\t\t\t\t<Line\n\t\t\t\t\t\t\t\t\tkey={s.key}\n\t\t\t\t\t\t\t\t\tdata={s.points}\n\t\t\t\t\t\t\t\t\ttype=\"monotone\"\n\t\t\t\t\t\t\t\t\tdataKey=\"y\"\n\t\t\t\t\t\t\t\t\tname={s.label}\n\t\t\t\t\t\t\t\t\tyAxisId={s.axis === 'right' ? 'right' : 'left'}\n\t\t\t\t\t\t\t\t\tstroke={s.color ?? colors[idx % colors.length]}\n\t\t\t\t\t\t\t\t\tstrokeWidth={2}\n\t\t\t\t\t\t\t\t\tstrokeOpacity={s.opacity ?? 1}\n\t\t\t\t\t\t\t\t\tdot={showDots ? { r: 2 } : false}\n\t\t\t\t\t\t\t\t\tconnectNulls={false}\n\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t);\n\t\t\t\t\t\t})}\n\t\t\t\t\t</RLineChart>\n\t\t\t\t</ResponsiveContainer>\n\t\t\t</div>\n\t\t</div>\n\t);\n}\n","import { useMemo } from 'react';\nimport { NodeLegend } from '../charts/NodeLegend.tsx';\nimport { useNodeSelection } from '../hooks/useNodeSelection.ts';\nimport { getNodeColor } from '../lib/nodeColors.ts';\nimport type { AxisSpec, SeriesData } from '../types/analytics.ts';\nimport { LineChart } from './LineChart.tsx';\n\ninterface Panel {\n\ttitle: string;\n\tdata: SeriesData;\n\tyAxis?: AxisSpec | { left: AxisSpec; right?: AxisSpec };\n}\n\ninterface Props {\n\tpanels: Panel[];\n\ttheme: 'light' | 'dark';\n\t/** Minimum width per mini-panel (px). Grid auto-fits. Default 320. */\n\tminPanelWidth?: number;\n\t/** Height per mini-panel (px). Default 240. */\n\tpanelHeight?: number;\n\t/** Pin every mini-panel's x-axis to the same [start, end] window. */\n\txDomain?: [number, number];\n\t/** Fill the parent's vertical space (used by the expand dialog). */\n\tfillParent?: boolean;\n}\n\nfunction extractNode(seriesKey: string): string | null {\n\tconst sep = seriesKey.indexOf('|');\n\treturn sep === -1 ? null : seriesKey.slice(sep + 1);\n}\n\nexport function SmallMultiples(\n\t{ panels, theme, minPanelWidth = 320, panelHeight = 240, xDomain, fillParent }: Props,\n) {\n\t// Union of node ids across all panels' per-node series. Cluster-aggregate\n\t// series (no '|') don't participate in the legend.\n\tconst nodeIds = useMemo(() => {\n\t\tconst set = new Set<string>();\n\t\tfor (const p of panels) {\n\t\t\tfor (const s of p.data.series) {\n\t\t\t\tconst node = extractNode(s.key);\n\t\t\t\tif (node) { set.add(node); }\n\t\t\t}\n\t\t}\n\t\treturn [...set].sort();\n\t}, [panels]);\n\n\tconst { isActive, handleLegendClick } = useNodeSelection(nodeIds);\n\n\tconst filteredPanels = useMemo(() =>\n\t\tpanels.map((p) => ({\n\t\t\t...p,\n\t\t\tdata: {\n\t\t\t\t...p.data,\n\t\t\t\tseries: p.data.series\n\t\t\t\t\t.filter((s) => {\n\t\t\t\t\t\tconst node = extractNode(s.key);\n\t\t\t\t\t\treturn node === null || isActive(node);\n\t\t\t\t\t})\n\t\t\t\t\t.map((s) => {\n\t\t\t\t\t\tconst node = extractNode(s.key);\n\t\t\t\t\t\tif (!node) { return s; }\n\t\t\t\t\t\treturn { ...s, color: s.color ?? getNodeColor(node, nodeIds) };\n\t\t\t\t\t}),\n\t\t\t},\n\t\t})), [panels, isActive, nodeIds]);\n\n\treturn (\n\t\t<div className=\"flex h-full flex-col\">\n\t\t\t<div\n\t\t\t\tclassName=\"flex-1 min-h-0\"\n\t\t\t\tstyle={{\n\t\t\t\t\tdisplay: 'grid',\n\t\t\t\t\tgridTemplateColumns: `repeat(auto-fit, minmax(${minPanelWidth}px, 1fr))`,\n\t\t\t\t\tgap: '1rem',\n\t\t\t\t}}\n\t\t\t>\n\t\t\t\t{filteredPanels.map((panel, idx) => {\n\t\t\t\t\tconst titleId = `sm-panel-${idx}-title`;\n\t\t\t\t\treturn (\n\t\t\t\t\t\t<div key={panel.title}>\n\t\t\t\t\t\t\t<div\n\t\t\t\t\t\t\t\tid={titleId}\n\t\t\t\t\t\t\t\tdata-testid=\"small-multiple-title\"\n\t\t\t\t\t\t\t\tstyle={{ fontSize: 12, fontWeight: 600, marginBottom: 4 }}\n\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t{panel.title}\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t<LineChart\n\t\t\t\t\t\t\t\tdata={panel.data}\n\t\t\t\t\t\t\t\ttheme={theme}\n\t\t\t\t\t\t\t\tyAxis={panel.yAxis}\n\t\t\t\t\t\t\t\txDomain={xDomain}\n\t\t\t\t\t\t\t\theight={panelHeight}\n\t\t\t\t\t\t\t\tfillParent={fillParent}\n\t\t\t\t\t\t\t\tariaLabel={`${panel.title}: chart with ${panel.data.series.length} series`}\n\t\t\t\t\t\t\t\thideLegend\n\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t);\n\t\t\t\t})}\n\t\t\t</div>\n\t\t\t{nodeIds.length > 0 && (\n\t\t\t\t<NodeLegend\n\t\t\t\t\tnodeIds={nodeIds}\n\t\t\t\t\tisActive={isActive}\n\t\t\t\t\tonClickNode={handleLegendClick}\n\t\t\t\t/>\n\t\t\t)}\n\t\t</div>\n\t);\n}\n","/** Sort series by descending magnitude (sum across all points).\n * Largest series first; rendered at bottom of stack by Recharts.\n * Stable sort: ties preserve input order. */\nexport function sortByMagnitude<T extends { points: Array<{ y: number | null }> }>(\n\tseries: readonly T[],\n): T[] {\n\treturn [...series].sort((a, b) => magnitude(b) - magnitude(a));\n}\n\nfunction magnitude(s: { points: Array<{ y: number | null }> }): number {\n\treturn s.points.reduce((sum, p) => sum + (typeof p.y === 'number' ? p.y : 0), 0);\n}\n","import { useMemo } from 'react';\nimport { Area, AreaChart, CartesianGrid, Legend, Line, ResponsiveContainer, Tooltip, XAxis, YAxis } from 'recharts';\nimport { formatAxisTick, formatTooltipTime } from '../lib/time.ts';\nimport type { AxisFormatter, AxisSpec, SeriesData } from '../types/analytics.ts';\nimport { formatValue } from './formatValue.ts';\nimport { sortByMagnitude } from './sortByMagnitude.ts';\nimport { tooltipContentStyle, tooltipLabelStyle } from './tooltipStyle.ts';\n\ninterface Props {\n\tdata: SeriesData;\n\ttheme: 'light' | 'dark';\n\tyAxis?: AxisSpec;\n\theight?: number;\n\t/** Optional accessible label override; otherwise composed from series labels. */\n\tariaLabel?: string;\n\t/** Pin the x-axis to a specific [start, end] millisecond range so the\n\t * axis spans the requested window even when data is sparse. See\n\t * LineChart for the same prop. */\n\txDomain?: [number, number];\n\t/** Fill the parent's vertical space; see LineChart for details. */\n\tfillParent?: boolean;\n\t/** Render the stack as bare lines (no filled area) so each stack\n\t * boundary reads as a distinct line. Useful for panels where bands\n\t * span widely different magnitudes — the filled-area version makes\n\t * the smaller bands hard to see, while the line version preserves\n\t * every series at its actual stacked y-position. */\n\tlineOnly?: boolean;\n}\n\n/** Screen-reader summary; mirrors LineChart.composeAriaLabel. */\nfunction composeAriaLabel(data: SeriesData): string {\n\tconst seriesNames = data.series.map((s) => s.label);\n\tif (seriesNames.length === 0) { return 'Empty stacked area chart'; }\n\treturn `Stacked area chart with ${seriesNames.length} series: ${seriesNames.slice(0, 5).join(', ')}${\n\t\tseriesNames.length > 5 ? '…' : ''\n\t}`;\n}\n\ninterface TooltipPayloadEntry {\n\tdataKey: string;\n\tname: string;\n\tvalue: number | null;\n\tcolor: string;\n}\n\ninterface StackedAreaTooltipProps {\n\tactive?: boolean;\n\tpayload?: readonly TooltipPayloadEntry[];\n\tlabel?: number;\n\tformatter?: AxisFormatter;\n\tunitSuffix?: string;\n}\n\nexport function StackedAreaTooltip({ active, payload, label, formatter, unitSuffix }: StackedAreaTooltipProps) {\n\tif (!active || !payload || payload.length === 0) { return null; }\n\tconst total = payload.reduce((s, p) => s + (typeof p.value === 'number' ? p.value : 0), 0);\n\t// count-si rounds at tick level; use raw 'count' for tooltip total to preserve precision.\n\tconst totalFormatter: AxisFormatter | undefined = formatter === 'count-si' ? 'count' : formatter;\n\treturn (\n\t\t<div style={tooltipContentStyle}>\n\t\t\t<div style={tooltipLabelStyle}>\n\t\t\t\t{label !== undefined ? formatTooltipTime(Number(label)) : ''}\n\t\t\t</div>\n\t\t\t{payload.map((p) => (\n\t\t\t\t<div key={p.dataKey} style={{ display: 'flex', justifyContent: 'space-between', gap: 12 }}>\n\t\t\t\t\t<span style={{ color: p.color }}>{p.name}</span>\n\t\t\t\t\t<span>{formatValue(typeof p.value === 'number' ? p.value : 0, formatter, unitSuffix)}</span>\n\t\t\t\t</div>\n\t\t\t))}\n\t\t\t{payload.length > 1\n\t\t\t\t? (\n\t\t\t\t\t<div\n\t\t\t\t\t\tstyle={{\n\t\t\t\t\t\t\tdisplay: 'flex',\n\t\t\t\t\t\t\tjustifyContent: 'space-between',\n\t\t\t\t\t\t\tgap: 12,\n\t\t\t\t\t\t\tmarginTop: 4,\n\t\t\t\t\t\t\tpaddingTop: 4,\n\t\t\t\t\t\t\tborderTop: '1px solid var(--border)',\n\t\t\t\t\t\t\tfontWeight: 600,\n\t\t\t\t\t\t}}\n\t\t\t\t\t>\n\t\t\t\t\t\t<span>Total</span>\n\t\t\t\t\t\t<span>{formatValue(total, totalFormatter, unitSuffix)}</span>\n\t\t\t\t\t</div>\n\t\t\t\t)\n\t\t\t\t: null}\n\t\t</div>\n\t);\n}\n\nexport function StackedAreaChart(\n\t{ data, theme, yAxis, height = 240, ariaLabel, xDomain, fillParent, lineOnly }: Props,\n) {\n\tconst sortedSeries = useMemo(() => sortByMagnitude(data.series), [data.series]);\n\n\tif (data.series.length === 0) {\n\t\treturn (\n\t\t\t<div role=\"status\" aria-live=\"polite\" className=\"text-(--color-text-secondary) text-sm p-4\">\n\t\t\t\tNo data in window\n\t\t\t</div>\n\t\t);\n\t}\n\n\tconst colors = ['#58a6ff', '#3fb950', '#f0883e', '#bc8cff', '#f778ba', '#79c0ff'];\n\n\t// Merge points by x across series. Series often emit at slightly\n\t// staggered timestamps (e.g. Harper emits per-node records at different\n\t// instants within the period), so a strict equality merge produces rows\n\t// with one populated cell and N-1 nulls — which renders as a sparse,\n\t// mostly-empty stack. Forward-fill carries the last-known value across\n\t// staggered rows so the stack stays continuous.\n\tconst xs = new Set<number>();\n\tfor (const s of data.series) { for (const p of s.points) { xs.add(p.x); } }\n\tif (data.ceiling) { for (const p of data.ceiling.points) { xs.add(p.x); } }\n\n\t// Pre-build x → index lookup per series for O(1) access during forward-fill.\n\tconst seriesPointMaps = data.series.map((s) => {\n\t\tconst m = new Map<number, number | null>();\n\t\tfor (const p of s.points) { m.set(p.x, p.y); }\n\t\treturn m;\n\t});\n\tconst ceilingMap = data.ceiling\n\t\t? new Map<number, number | null>(data.ceiling.points.map((p) => [p.x, p.y]))\n\t\t: null;\n\n\tconst lastSeen: (number | null)[] = data.series.map(() => null);\n\tlet lastCeiling: number | null = null;\n\n\tconst merged: Record<string, number | null>[] = [...xs].sort((a, b) => a - b).map((x) => {\n\t\tconst row: Record<string, number | null> = { x };\n\t\tdata.series.forEach((s, i) => {\n\t\t\tconst m = seriesPointMaps[i];\n\t\t\tif (m.has(x)) { lastSeen[i] = m.get(x) ?? null; }\n\t\t\trow[s.key] = lastSeen[i];\n\t\t});\n\t\tif (ceilingMap && data.ceiling) {\n\t\t\tif (ceilingMap.has(x)) { lastCeiling = ceilingMap.get(x) ?? null; }\n\t\t\trow.__ceiling__ = lastCeiling;\n\t\t}\n\t\treturn row;\n\t});\n\n\tconst resolvedFormatter = yAxis?.formatter;\n\tconst resolvedUnit = yAxis?.unit;\n\tconst fillOpacity = theme === 'dark' ? 0.5 : 0.35;\n\n\treturn (\n\t\t<div\n\t\t\trole=\"img\"\n\t\t\taria-label={ariaLabel ?? composeAriaLabel(data)}\n\t\t\tstyle={fillParent\n\t\t\t\t? { width: '100%', height: '100%', minHeight: 0, flex: '1 1 auto', display: 'flex', flexDirection: 'column' }\n\t\t\t\t: { width: '100%', height }}\n\t\t>\n\t\t\t<div aria-hidden=\"true\" style={{ width: '100%', height: '100%' }}>\n\t\t\t\t<ResponsiveContainer width=\"100%\" height=\"100%\">\n\t\t\t\t\t<AreaChart data={merged}>\n\t\t\t\t\t\t<CartesianGrid stroke=\"var(--chart-grid)\" strokeDasharray=\"3 3\" />\n\t\t\t\t\t\t<XAxis\n\t\t\t\t\t\t\tdataKey=\"x\"\n\t\t\t\t\t\t\ttype=\"number\"\n\t\t\t\t\t\t\tdomain={xDomain ?? ['dataMin', 'dataMax']}\n\t\t\t\t\t\t\tallowDataOverflow={!!xDomain}\n\t\t\t\t\t\t\ttickFormatter={formatAxisTick}\n\t\t\t\t\t\t\ttick={{ fontSize: 11 }}\n\t\t\t\t\t\t/>\n\t\t\t\t\t\t<YAxis\n\t\t\t\t\t\t\ttickFormatter={(v) => formatValue(v, resolvedFormatter, resolvedUnit)}\n\t\t\t\t\t\t\ttick={{ fontSize: 11 }}\n\t\t\t\t\t\t\twidth={70}\n\t\t\t\t\t\t/>\n\t\t\t\t\t\t<Tooltip content={<StackedAreaTooltip formatter={resolvedFormatter} unitSuffix={resolvedUnit} />} />\n\t\t\t\t\t\t<Legend />\n\t\t\t\t\t\t{sortedSeries.map((s, idx) => (\n\t\t\t\t\t\t\t<Area\n\t\t\t\t\t\t\t\tkey={s.key}\n\t\t\t\t\t\t\t\ttype=\"monotone\"\n\t\t\t\t\t\t\t\tdataKey={s.key}\n\t\t\t\t\t\t\t\tname={s.label}\n\t\t\t\t\t\t\t\tstackId=\"1\"\n\t\t\t\t\t\t\t\tstroke={s.color ?? colors[idx % colors.length]}\n\t\t\t\t\t\t\t\tstrokeWidth={lineOnly ? 2 : 1}\n\t\t\t\t\t\t\t\tfill={lineOnly ? 'none' : (s.color ?? colors[idx % colors.length])}\n\t\t\t\t\t\t\t\tfillOpacity={lineOnly ? 0 : fillOpacity}\n\t\t\t\t\t\t\t\tconnectNulls={false}\n\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t))}\n\t\t\t\t\t\t{data.ceiling\n\t\t\t\t\t\t\t? (\n\t\t\t\t\t\t\t\t<Line\n\t\t\t\t\t\t\t\t\ttype=\"monotone\"\n\t\t\t\t\t\t\t\t\tdataKey=\"__ceiling__\"\n\t\t\t\t\t\t\t\t\tname={data.ceiling.label}\n\t\t\t\t\t\t\t\t\tstroke=\"#8b949e\"\n\t\t\t\t\t\t\t\t\tstrokeWidth={2}\n\t\t\t\t\t\t\t\t\tstrokeDasharray=\"6 3\"\n\t\t\t\t\t\t\t\t\tdot={false}\n\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t)\n\t\t\t\t\t\t\t: null}\n\t\t\t\t\t</AreaChart>\n\t\t\t\t</ResponsiveContainer>\n\t\t\t</div>\n\t\t</div>\n\t);\n}\n","// Multi-select chip row for filtering records by a categorical field\n// (`type` for traffic panels, `protocol` for connections, etc.). Default\n// state = all chips active; click solos that chip; Ctrl-click toggles\n// individual chips. Mirrors NodeLegend's interaction model so the panel\n// has a consistent dual-legend pattern (TypeFilterChipRow above / below\n// the chart, NodeLegend at the bottom).\n\nimport { useCallback, useMemo, useState } from 'react';\n\ninterface Props {\n\tvalues: readonly string[];\n\tcolorFor?: (value: string) => string;\n\tariaLabel?: string;\n}\n\nexport interface TypeFilterState {\n\tisActive: (value: string) => boolean;\n\tactiveValues: readonly string[];\n}\n\n/** Hook holding active-set state + the click handler. Component below\n * consumes both. Separating them lets the parent renderer use\n * `isActive` to filter records in the same render pass. */\nexport function useTypeFilter(values: readonly string[]) {\n\tconst [active, setActive] = useState<Set<string> | null>(null);\n\n\tconst isActive = useCallback((v: string) => active === null || active.has(v), [active]);\n\n\tconst handleClick = useCallback((v: string, ctrlKey: boolean) => {\n\t\tsetActive((prev) => {\n\t\t\tif (ctrlKey) {\n\t\t\t\tif (prev === null) { return new Set(values.filter((x) => x !== v)); }\n\t\t\t\tconst next = new Set(prev);\n\t\t\t\tif (next.has(v)) {\n\t\t\t\t\tnext.delete(v);\n\t\t\t\t\tif (next.size === 0) { return null; }\n\t\t\t\t} else {\n\t\t\t\t\tnext.add(v);\n\t\t\t\t\tif (next.size === values.length) { return null; }\n\t\t\t\t}\n\t\t\t\treturn next;\n\t\t\t}\n\t\t\t// Plain click: solo (or reset if already soloed)\n\t\t\tif (prev !== null && prev.size === 1 && prev.has(v)) { return null; }\n\t\t\treturn new Set([v]);\n\t\t});\n\t}, [values]);\n\n\tconst activeValues = useMemo(\n\t\t() => (active === null ? values : values.filter((v) => active.has(v))),\n\t\t[active, values],\n\t);\n\n\treturn { isActive, handleClick, activeValues };\n}\n\ninterface TypeFilterChipRowProps extends Props {\n\tisActive: (v: string) => boolean;\n\tonClick: (v: string, ctrlKey: boolean) => void;\n}\n\nexport function TypeFilterChipRow({\n\tvalues,\n\tisActive,\n\tonClick,\n\tcolorFor,\n\tariaLabel = 'Type filter',\n}: TypeFilterChipRowProps) {\n\tif (values.length === 0) { return null; }\n\treturn (\n\t\t<div\n\t\t\trole=\"group\"\n\t\t\taria-label={ariaLabel}\n\t\t\tclassName=\"flex flex-wrap justify-center gap-x-3 gap-y-1 pt-2 text-[11px]\"\n\t\t>\n\t\t\t{values.map((v) => {\n\t\t\t\tconst active = isActive(v);\n\t\t\t\tconst color = colorFor ? colorFor(v) : 'var(--color-text-secondary)';\n\t\t\t\treturn (\n\t\t\t\t\t<button\n\t\t\t\t\t\tkey={v}\n\t\t\t\t\t\ttype=\"button\"\n\t\t\t\t\t\taria-pressed={active}\n\t\t\t\t\t\tdata-testid=\"type-filter-chip\"\n\t\t\t\t\t\tdata-value={v}\n\t\t\t\t\t\ttitle=\"Click to solo · Ctrl-click to toggle\"\n\t\t\t\t\t\tonClick={(e) => onClick(v, e.ctrlKey || e.metaKey)}\n\t\t\t\t\t\tclassName=\"inline-flex items-center gap-1.5 cursor-pointer border-none bg-transparent p-0\"\n\t\t\t\t\t\tstyle={{ color, opacity: active ? 1 : 0.55 }}\n\t\t\t\t\t>\n\t\t\t\t\t\t<span\n\t\t\t\t\t\t\tclassName=\"inline-block h-[3px] w-3 rounded\"\n\t\t\t\t\t\t\tstyle={{ backgroundColor: color }}\n\t\t\t\t\t\t/>\n\t\t\t\t\t\t<span>{v}</span>\n\t\t\t\t\t</button>\n\t\t\t\t);\n\t\t\t})}\n\t\t</div>\n\t);\n}\n","// Wrapper renderer for type-grouped panels (bytes-sent, bytes-received,\n// mqtt-traffic-sent, mqtt-traffic-received, database-size, connections).\n//\n// Layout (top → bottom):\n// - TypeFilterChipRow: multi-select for typeField values (mqtt /\n// egress / operation / database name / etc.). Click solo, ⌘-click\n// toggle. Chips inherit the same hue as the corresponding stack\n// band so operator can map chip → band visually.\n// - \"Stack by\" segmented control (only when nodes.length > 1):\n// `Type` (default), `Node`, or `Per-node grid` (small multiples).\n// - Chart: stacked area when all selectors are active; bare stacked\n// lines when the operator narrows the selection (no fills swallowing\n// small bands during comparison).\n// - NodeLegend: solo/Ctrl-toggle. Filters which nodes contribute to\n// the stack pre-pipeline.\n\nimport { useMemo, useState } from 'react';\nimport { NodeLegend } from '../charts/NodeLegend.tsx';\nimport { useNodeSelection } from '../hooks/useNodeSelection.ts';\nimport { getTypeColor } from '../lib/colorAllocators/typeColors.ts';\nimport { getNodeColor } from '../lib/nodeColors.ts';\nimport { runPipeline } from '../pipeline/pipeline.ts';\nimport type { AnalyticsDataPoint, MetricSpec, Series, SeriesData, TimeRange } from '../types/analytics.ts';\nimport { SmallMultiples } from './SmallMultiples.tsx';\nimport { StackedAreaChart } from './StackedAreaChart.tsx';\nimport { TypeFilterChipRow, useTypeFilter } from './TypeFilterChipRow.tsx';\n\ninterface Props {\n\t/** Underlying spec (groupBy on `typeField`, primitive `stacked-area`). */\n\tspec: MetricSpec;\n\t/** Field on records that carries the type value (`type` or `protocol`). */\n\ttypeField: string;\n\trecords: AnalyticsDataPoint[];\n\ttimeRange: TimeRange;\n\tnodes: string[];\n\ttheme: 'light' | 'dark';\n\t/** Initial stack-by mode. Defaults to 'type' so the chart matches the\n\t * panel name's \"by type\" framing. The user-facing toggle is rendered\n\t * inside the renderer when nodes.length > 1. */\n\tviewMode?: 'per-node' | 'aggregate';\n\tfillParent?: boolean;\n}\n\ntype StackBy = 'type' | 'node' | 'grid';\n\nexport function TrafficByTypeRenderer({\n\tspec,\n\ttypeField,\n\trecords,\n\ttimeRange,\n\tnodes,\n\ttheme,\n\tviewMode = 'aggregate',\n\tfillParent,\n}: Props) {\n\t// Translate the legacy viewMode prop into the new stackBy state.\n\t// Default 'type' (== 'aggregate'); 'per-node' maps to 'node'.\n\tconst [stackBy, setStackBy] = useState<StackBy>(viewMode === 'per-node' ? 'node' : 'type');\n\n\t// Discover unique type values from records.\n\tconst types = useMemo(() => {\n\t\tconst set = new Set<string>();\n\t\tfor (const r of records) {\n\t\t\tconst v = (r as Record<string, unknown>)[typeField];\n\t\t\tif (typeof v === 'string') { set.add(v); }\n\t\t}\n\t\treturn [...set].sort();\n\t}, [records, typeField]);\n\n\tconst { isActive, handleClick } = useTypeFilter(types);\n\tconst { isActive: isNodeActive, handleLegendClick: handleNodeClick } = useNodeSelection(nodes);\n\n\t// \"All active\" = default state where no chip / node has been soloed.\n\t// Filled bands when summary; bare stacked lines when narrowed.\n\tconst allTypesActive = types.length > 0 && types.every(isActive);\n\tconst allNodesActive = nodes.every((n) => isNodeActive(n));\n\tconst lineOnly = !(allTypesActive && allNodesActive);\n\n\tconst filteredRecords = useMemo(\n\t\t() =>\n\t\t\trecords.filter((r) => {\n\t\t\t\tconst v = (r as Record<string, unknown>)[typeField];\n\t\t\t\tif (typeof v === 'string' && !isActive(v)) { return false; }\n\t\t\t\tif (typeof r.node === 'string' && !isNodeActive(r.node)) { return false; }\n\t\t\t\treturn true;\n\t\t\t}),\n\t\t[records, typeField, isActive, isNodeActive],\n\t);\n\n\t// stackBy === 'node' remaps dimension to 'node'; 'type' keeps the\n\t// spec's natural typeField. 'grid' is handled separately below.\n\tconst runtimeSpec = useMemo<MetricSpec>(() => {\n\t\tif (stackBy !== 'node' || spec.series.kind !== 'groupBy') { return spec; }\n\t\treturn { ...spec, series: { ...spec.series, dimension: 'node' } };\n\t}, [spec, stackBy]);\n\n\tconst data: SeriesData = useMemo(\n\t\t() => runPipeline(runtimeSpec, filteredRecords, timeRange, nodes, { snapToPeriod: true }),\n\t\t[runtimeSpec, filteredRecords, timeRange, nodes],\n\t);\n\n\t// Color each series by its key using the appropriate allocator so the\n\t// same type is the same hue across panels and the chip color matches\n\t// the band color.\n\tconst coloredData: SeriesData = useMemo(() => ({\n\t\t...data,\n\t\tseries: data.series.map((s): Series => ({\n\t\t\t...s,\n\t\t\tcolor: s.color\n\t\t\t\t?? (stackBy === 'node' ? getNodeColor(s.key, nodes) : getTypeColor(s.key, types)),\n\t\t})),\n\t}), [data, stackBy, nodes, types]);\n\n\t// Per-node grid: one mini-chart per active node, each stacked by type.\n\t// Built only when stackBy === 'grid' so we don't run N pipelines for\n\t// every render.\n\tconst gridPanels = useMemo(() => {\n\t\tif (stackBy !== 'grid') { return null; }\n\t\tconst activeNodes = nodes.filter(isNodeActive);\n\t\treturn activeNodes.map((nodeId) => {\n\t\t\tconst nodeRecords = filteredRecords.filter((r) => r.node === nodeId);\n\t\t\tconst seriesData = runPipeline(spec, nodeRecords, timeRange, [nodeId], { snapToPeriod: true });\n\t\t\treturn {\n\t\t\t\ttitle: nodeId,\n\t\t\t\tdata: {\n\t\t\t\t\t...seriesData,\n\t\t\t\t\tseries: seriesData.series.map((s): Series => ({\n\t\t\t\t\t\t...s,\n\t\t\t\t\t\tcolor: s.color ?? getTypeColor(s.key, types),\n\t\t\t\t\t})),\n\t\t\t\t},\n\t\t\t\tyAxis: spec.yAxis,\n\t\t\t};\n\t\t});\n\t}, [stackBy, nodes, isNodeActive, filteredRecords, spec, timeRange, types]);\n\n\tconst showStackByToggle = nodes.length > 1;\n\tconst xDomain: [number, number] = [timeRange.startTime, timeRange.endTime];\n\n\treturn (\n\t\t<div className=\"flex h-full flex-col\">\n\t\t\t<TypeFilterChipRow\n\t\t\t\tvalues={types}\n\t\t\t\tisActive={isActive}\n\t\t\t\tonClick={handleClick}\n\t\t\t\tcolorFor={(v) => getTypeColor(v, types)}\n\t\t\t\tariaLabel={typeField === 'type' ? 'Traffic type' : typeField === 'database' ? 'Database' : 'Protocol'}\n\t\t\t/>\n\t\t\t{showStackByToggle && <StackByToggle stackBy={stackBy} onChange={setStackBy} />}\n\t\t\t<div className=\"min-h-0 flex-1\" style={{ marginTop: 8 }}>\n\t\t\t\t{stackBy === 'grid' && gridPanels\n\t\t\t\t\t? (\n\t\t\t\t\t\t<SmallMultiples\n\t\t\t\t\t\t\tpanels={gridPanels}\n\t\t\t\t\t\t\ttheme={theme}\n\t\t\t\t\t\t\txDomain={xDomain}\n\t\t\t\t\t\t\tfillParent={fillParent}\n\t\t\t\t\t\t/>\n\t\t\t\t\t)\n\t\t\t\t\t: (\n\t\t\t\t\t\t<StackedAreaChart\n\t\t\t\t\t\t\tdata={coloredData}\n\t\t\t\t\t\t\ttheme={theme}\n\t\t\t\t\t\t\tyAxis={spec.yAxis as any}\n\t\t\t\t\t\t\txDomain={xDomain}\n\t\t\t\t\t\t\tfillParent={fillParent}\n\t\t\t\t\t\t\tlineOnly={lineOnly}\n\t\t\t\t\t\t/>\n\t\t\t\t\t)}\n\t\t\t</div>\n\t\t\t{nodes.length > 0 && <NodeLegend nodeIds={nodes} isActive={isNodeActive} onClickNode={handleNodeClick} />}\n\t\t</div>\n\t);\n}\n\ninterface StackByToggleProps {\n\tstackBy: StackBy;\n\tonChange: (s: StackBy) => void;\n}\n\nconst STACK_BY_OPTIONS: ReadonlyArray<{ value: StackBy; label: string }> = [\n\t{ value: 'type', label: 'Type' },\n\t{ value: 'node', label: 'Node' },\n\t{ value: 'grid', label: 'Per-node grid' },\n];\n\nfunction StackByToggle({ stackBy, onChange }: StackByToggleProps) {\n\t// Roving tabindex pattern: only the active radio is in the tab order; arrow\n\t// keys move selection within the group. Matches DimensionChipRow.\n\tconst activeIdx = Math.max(0, STACK_BY_OPTIONS.findIndex((o) => o.value === stackBy));\n\tconst onKeyDown = (e: React.KeyboardEvent<HTMLButtonElement>) => {\n\t\tif (e.key !== 'ArrowRight' && e.key !== 'ArrowLeft' && e.key !== 'ArrowDown' && e.key !== 'ArrowUp') { return; }\n\t\te.preventDefault();\n\t\tconst dir = e.key === 'ArrowRight' || e.key === 'ArrowDown' ? 1 : -1;\n\t\tconst next = (activeIdx + dir + STACK_BY_OPTIONS.length) % STACK_BY_OPTIONS.length;\n\t\tonChange(STACK_BY_OPTIONS[next].value);\n\t};\n\treturn (\n\t\t<div\n\t\t\trole=\"radiogroup\"\n\t\t\taria-label=\"Stack by\"\n\t\t\tclassName=\"flex flex-wrap items-center gap-x-3 gap-y-1 pt-2 text-[11px]\"\n\t\t>\n\t\t\t<span className=\"text-muted-foreground\">Stack by:</span>\n\t\t\t{STACK_BY_OPTIONS.map((opt, idx) => {\n\t\t\t\tconst active = stackBy === opt.value;\n\t\t\t\treturn (\n\t\t\t\t\t<button\n\t\t\t\t\t\tkey={opt.value}\n\t\t\t\t\t\ttype=\"button\"\n\t\t\t\t\t\trole=\"radio\"\n\t\t\t\t\t\taria-checked={active}\n\t\t\t\t\t\ttabIndex={idx === activeIdx ? 0 : -1}\n\t\t\t\t\t\tonKeyDown={onKeyDown}\n\t\t\t\t\t\tdata-testid=\"stack-by-button\"\n\t\t\t\t\t\tdata-value={opt.value}\n\t\t\t\t\t\tonClick={() => onChange(opt.value)}\n\t\t\t\t\t\tclassName=\"inline-flex items-center cursor-pointer border-none bg-transparent p-0 text-(--color-text-secondary)\"\n\t\t\t\t\t\tstyle={{ opacity: active ? 1 : 0.55 }}\n\t\t\t\t\t>\n\t\t\t\t\t\t{opt.label}\n\t\t\t\t\t</button>\n\t\t\t\t);\n\t\t\t})}\n\t\t</div>\n\t);\n}\n","import { TrafficByTypeRenderer } from '../../primitives/TrafficByTypeRenderer.tsx';\nimport type { DerivedMetricSpec, MetricSpec } from '../../types/analytics.ts';\nimport { runPipeline } from '../pipeline.ts';\n\n// Inner spec defaults to per-type stacking. Renderer remaps dimension\n// to 'node' in per-node viewMode so the operator sees cluster total\n// stacked by node — same pattern bytes-received / connections use.\nconst baseSpec: MetricSpec = {\n\ttitle: 'Messages received by type (inner)',\n\tdescription:\n\t\t'Inbound message rate — cluster total across nodes. Internal spec used by mqtt-traffic-received.recompute; not registered.',\n\ttab: 'traffic',\n\tprimaryDimension: 'node',\n\tsubDimension: 'type',\n\tseries: {\n\t\tkind: 'groupBy',\n\t\tdimension: 'type',\n\t\tfield: {\n\t\t\tfield: 'count',\n\t\t\tlabel: 'messages/sec',\n\t\t\ttransform: { kind: 'rate' },\n\t\t},\n\t},\n\ttimestamp: 'time',\n\tbucket: { source: 'period-field', fallbackMs: 60000 },\n\taggregator: { temporal: 'sum', crossNode: 'sum' },\n\tprimitive: 'stacked-area',\n\tyAxis: { unit: ' msg/s', formatter: 'count-si' },\n};\n\nexport const mqttTrafficReceivedDerived: DerivedMetricSpec = {\n\tid: 'mqtt-traffic-received',\n\ttitle: 'Messages received by type',\n\tsubtitle: 'Inbound message rate. Type chips solo / Ctrl-toggle; viewMode flips type/node stack.',\n\ttab: 'traffic',\n\tsourceMetric: 'bytes-received',\n\trecompute: (records, window, nodes, viewMode) => {\n\t\tconst isPerNode = (viewMode ?? 'per-node') === 'per-node';\n\t\tlet spec: MetricSpec = baseSpec;\n\t\tif (isPerNode && baseSpec.series.kind === 'groupBy') {\n\t\t\tspec = { ...baseSpec, series: { ...baseSpec.series, dimension: 'node' } };\n\t\t}\n\t\treturn runPipeline(spec, records, window, nodes, { snapToPeriod: true });\n\t},\n\tRenderer: (props) => <TrafficByTypeRenderer spec={baseSpec} typeField=\"type\" {...props} />,\n\tprimitive: 'stacked-area',\n\tyAxis: { unit: ' msg/s', formatter: 'count-si' },\n};\n","import { TrafficByTypeRenderer } from '../../primitives/TrafficByTypeRenderer.tsx';\nimport type { DerivedMetricSpec, MetricSpec } from '../../types/analytics.ts';\nimport { runPipeline } from '../pipeline.ts';\n\n// Inner spec defaults to per-type stacking. Renderer remaps dimension\n// to 'node' in per-node viewMode so the operator sees cluster total\n// stacked by node — same pattern bytes-sent / connections use.\nconst baseSpec: MetricSpec = {\n\ttitle: 'Messages sent by type (inner)',\n\tdescription:\n\t\t'Outbound message rate — cluster total across nodes. Internal spec used by mqtt-traffic-sent.recompute; not registered.',\n\ttab: 'traffic',\n\tprimaryDimension: 'node',\n\tsubDimension: 'type',\n\tseries: {\n\t\tkind: 'groupBy',\n\t\tdimension: 'type',\n\t\tfield: {\n\t\t\tfield: 'count',\n\t\t\tlabel: 'messages/sec',\n\t\t\ttransform: { kind: 'rate' },\n\t\t},\n\t},\n\ttimestamp: 'time',\n\tbucket: { source: 'period-field', fallbackMs: 60000 },\n\taggregator: { temporal: 'sum', crossNode: 'sum' },\n\tprimitive: 'stacked-area',\n\tyAxis: { unit: ' msg/s', formatter: 'count-si' },\n};\n\nexport const mqttTrafficSentDerived: DerivedMetricSpec = {\n\tid: 'mqtt-traffic-sent',\n\ttitle: 'Messages sent by type',\n\tsubtitle: 'Outbound message rate. Type chips solo / Ctrl-toggle; viewMode flips type/node stack.',\n\ttab: 'traffic',\n\tsourceMetric: 'bytes-sent',\n\trecompute: (records, window, nodes, viewMode) => {\n\t\t// Retained for backward compatibility / direct callers; the\n\t\t// Renderer below is what the dashboard actually uses.\n\t\tconst isPerNode = (viewMode ?? 'per-node') === 'per-node';\n\t\tlet spec: MetricSpec = baseSpec;\n\t\tif (isPerNode && baseSpec.series.kind === 'groupBy') {\n\t\t\tspec = { ...baseSpec, series: { ...baseSpec.series, dimension: 'node' } };\n\t\t}\n\t\treturn runPipeline(spec, records, window, nodes, { snapToPeriod: true });\n\t},\n\tRenderer: (props) => <TrafficByTypeRenderer spec={baseSpec} typeField=\"type\" {...props} />,\n\tprimitive: 'stacked-area',\n\tyAxis: { unit: ' msg/s', formatter: 'count-si' },\n};\n","// Single-select chip row used by the Requests / Database / Health\n// renderers (DimensionSelectorRenderer, ConnectionRenderer, etc.). Visually\n// matches TypeFilterChipRow used by the Traffic tab — minimal inline-flex\n// text + a tiny colored bar — so chip selectors look the same across\n// every analytics dashboard. Behaviorally still a radiogroup: one value\n// active at a time, arrow keys move focus and selection.\n\nimport { type KeyboardEvent, useEffect, useRef } from 'react';\n\ninterface DimensionChipRowProps {\n\t/** Selectable dimension values, in display order. */\n\tdimensionValues: readonly string[];\n\t/** Currently-selected value. */\n\tselected: string;\n\t/** Called on click, Enter/Space, or arrow-key traversal. Per the radiogroup\n\t * pattern, arrow keys move focus AND selection. */\n\tonSelect: (value: string) => void;\n\t/** When provided, renders a non-interactive trailing chip (e.g. 'Other'). */\n\totherKey?: string;\n\t/** Optional palette callback — receives a dimension value and returns a CSS color. */\n\tcolorFor?: (value: string) => string;\n\t/** ARIA label for the radiogroup. */\n\tariaLabel?: string;\n}\n\nconst DEFAULT_COLOR = 'var(--color-text-secondary)';\n\nexport function DimensionChipRow({\n\tdimensionValues,\n\tselected,\n\tonSelect,\n\totherKey,\n\tcolorFor,\n\tariaLabel = 'Dimension selector',\n}: DimensionChipRowProps) {\n\tconst chipRefs = useRef<Array<HTMLButtonElement | null>>([]);\n\n\tuseEffect(() => {\n\t\tchipRefs.current = chipRefs.current.slice(0, dimensionValues.length);\n\t}, [dimensionValues.length]);\n\n\tconst activeIdx = dimensionValues.indexOf(selected);\n\tconst tabbableIdx = activeIdx >= 0 ? activeIdx : 0;\n\n\tfunction handleKeyDown(e: KeyboardEvent<HTMLButtonElement>, idx: number) {\n\t\tif (e.key === 'Enter' || e.key === ' ') {\n\t\t\te.preventDefault();\n\t\t\tonSelect(dimensionValues[idx]);\n\t\t\treturn;\n\t\t}\n\t\tif (\n\t\t\te.key !== 'ArrowLeft'\n\t\t\t&& e.key !== 'ArrowRight'\n\t\t\t&& e.key !== 'ArrowDown'\n\t\t\t&& e.key !== 'ArrowUp'\n\t\t) {\n\t\t\treturn;\n\t\t}\n\t\te.preventDefault();\n\t\tconst n = dimensionValues.length;\n\t\tif (n === 0) { return; }\n\t\tlet next = idx;\n\t\tif (e.key === 'ArrowRight' || e.key === 'ArrowDown') { next = (idx + 1) % n; }\n\t\tif (e.key === 'ArrowLeft' || e.key === 'ArrowUp') { next = (idx - 1 + n) % n; }\n\t\tchipRefs.current[next]?.focus();\n\t\tonSelect(dimensionValues[next]);\n\t}\n\n\tif (dimensionValues.length === 0 && !otherKey) { return null; }\n\n\treturn (\n\t\t<div\n\t\t\trole=\"radiogroup\"\n\t\t\taria-label={ariaLabel}\n\t\t\tdata-testid=\"dimension-chip-row\"\n\t\t\tclassName=\"flex flex-wrap justify-center gap-x-3 gap-y-1 pt-2 text-[11px]\"\n\t\t>\n\t\t\t{dimensionValues.map((value, idx) => {\n\t\t\t\tconst isSelected = value === selected;\n\t\t\t\tconst color = colorFor ? colorFor(value) : DEFAULT_COLOR;\n\t\t\t\treturn (\n\t\t\t\t\t<button\n\t\t\t\t\t\tkey={value}\n\t\t\t\t\t\tref={(el) => {\n\t\t\t\t\t\t\tchipRefs.current[idx] = el;\n\t\t\t\t\t\t}}\n\t\t\t\t\t\ttype=\"button\"\n\t\t\t\t\t\trole=\"radio\"\n\t\t\t\t\t\taria-checked={isSelected}\n\t\t\t\t\t\ttabIndex={idx === tabbableIdx ? 0 : -1}\n\t\t\t\t\t\tdata-testid=\"dimension-chip\"\n\t\t\t\t\t\tdata-value={value}\n\t\t\t\t\t\ttitle=\"Click to select\"\n\t\t\t\t\t\tonKeyDown={(e) => handleKeyDown(e, idx)}\n\t\t\t\t\t\tonClick={() => onSelect(value)}\n\t\t\t\t\t\tclassName=\"inline-flex items-center gap-1.5 cursor-pointer border-none bg-transparent p-0\"\n\t\t\t\t\t\tstyle={{ color, opacity: isSelected ? 1 : 0.55 }}\n\t\t\t\t\t>\n\t\t\t\t\t\t<span\n\t\t\t\t\t\t\tclassName=\"inline-block h-[3px] w-3 rounded\"\n\t\t\t\t\t\t\tstyle={{ backgroundColor: color }}\n\t\t\t\t\t\t/>\n\t\t\t\t\t\t<span>{value}</span>\n\t\t\t\t\t</button>\n\t\t\t\t);\n\t\t\t})}\n\t\t\t{otherKey && (\n\t\t\t\t<span\n\t\t\t\t\tdata-testid=\"dimension-chip\"\n\t\t\t\t\tdata-value={otherKey}\n\t\t\t\t\taria-disabled=\"true\"\n\t\t\t\t\ttitle=\"Aggregate of smaller buckets; not selectable.\"\n\t\t\t\t\tclassName=\"inline-flex items-center gap-1.5 cursor-not-allowed\"\n\t\t\t\t\tstyle={{ color: DEFAULT_COLOR, opacity: 0.4 }}\n\t\t\t\t>\n\t\t\t\t\t<span\n\t\t\t\t\t\tclassName=\"inline-block h-[3px] w-3 rounded\"\n\t\t\t\t\t\tstyle={{ backgroundColor: DEFAULT_COLOR }}\n\t\t\t\t\t/>\n\t\t\t\t\t<span>{otherKey}</span>\n\t\t\t\t</span>\n\t\t\t)}\n\t\t</div>\n\t);\n}\n","// Shared renderer for derived metrics keyed by (path, time) that the\n// operator wants to drill into per-node and filter by path. Used by\n// request-rate (req/s) and error-rate (errored fraction). Each metric\n// supplies a `compute(records, perNode, selectedPath)` callback that\n// emits SeriesData; this component handles all the chip/node UI plus\n// the path-discovery + viewMode threading.\n\nimport { useMemo, useState } from 'react';\nimport { NodeLegend } from '../charts/NodeLegend.tsx';\nimport { useNodeSelection } from '../hooks/useNodeSelection.ts';\nimport { getNodeColor } from '../lib/nodeColors.ts';\nimport type { AnalyticsDataPoint, AxisSpec, SeriesData, Threshold, TimeRange } from '../types/analytics.ts';\nimport { DimensionChipRow } from './DimensionChipRow.tsx';\nimport { LineChart } from './LineChart.tsx';\n\ninterface Props {\n\trecords: AnalyticsDataPoint[];\n\ttimeRange?: TimeRange;\n\tnodes: string[];\n\ttheme: 'light' | 'dark';\n\tviewMode?: 'per-node' | 'aggregate';\n\tyAxis?: AxisSpec | { left: AxisSpec; right?: AxisSpec };\n\tthresholds?: Threshold[];\n\tfillParent?: boolean;\n\t/** Compute the SeriesData for the chosen viewMode + selected path.\n\t * - per-node + path selected: emit one series per node (key is node id)\n\t * - aggregate + path selected: emit one cluster series for that path\n\t * - aggregate + no selection: emit one series per path (cluster aggregate) */\n\tcompute: (\n\t\trecords: AnalyticsDataPoint[],\n\t\toptions: { perNode: boolean; selectedPath: string | null },\n\t) => SeriesData;\n}\n\nfunction shortenNodeLabel(node: string): string {\n\tconst dot = node.indexOf('.');\n\treturn dot === -1 ? node : node.slice(0, dot);\n}\n\nexport function PerPathRateRenderer({\n\trecords,\n\ttimeRange,\n\tnodes,\n\ttheme,\n\tviewMode = 'per-node',\n\tyAxis,\n\tthresholds,\n\tcompute,\n\tfillParent,\n}: Props) {\n\tconst xDomain = timeRange ? [timeRange.startTime, timeRange.endTime] as [number, number] : undefined;\n\tconst perNode = viewMode === 'per-node';\n\n\t// Discover paths from records, ranked by total count so default chip\n\t// selection lands on the highest-traffic path.\n\tconst paths = useMemo(() => {\n\t\tconst totals = new Map<string, number>();\n\t\tfor (const r of records) {\n\t\t\tconst path = (r as Record<string, unknown>).path;\n\t\t\tif (typeof path !== 'string') { continue; }\n\t\t\tconst c = (r as Record<string, unknown>).count;\n\t\t\tconst count = typeof c === 'number' && Number.isFinite(c) ? c : 0;\n\t\t\ttotals.set(path, (totals.get(path) ?? 0) + count);\n\t\t}\n\t\treturn [...totals.entries()].sort((a, b) => b[1] - a[1]).map(([p]) => p);\n\t}, [records]);\n\n\tconst [selected, setSelected] = useState<string>('');\n\tconst effective = paths.includes(selected)\n\t\t? selected\n\t\t// In per-node mode default to the rank-0 path so the operator sees\n\t\t// one path's per-node breakdown immediately. In aggregate mode\n\t\t// default to \"all\" so the operator sees the cluster-by-path stack.\n\t\t: (perNode ? (paths[0] ?? '') : '');\n\n\tconst data = useMemo<SeriesData>(\n\t\t() => compute(records, { perNode, selectedPath: effective || null }),\n\t\t[records, perNode, effective, compute],\n\t);\n\n\tconst { isActive, handleLegendClick } = useNodeSelection(nodes);\n\n\tconst filteredData: SeriesData = useMemo(() => ({\n\t\t...data,\n\t\tthresholds: thresholds ?? data.thresholds,\n\t\tseries: data.series\n\t\t\t.map((s) => {\n\t\t\t\t// In per-node mode the series key IS the node id (no '|' prefix).\n\t\t\t\tif (!perNode) { return s; }\n\t\t\t\treturn { ...s, label: shortenNodeLabel(s.key), color: getNodeColor(s.key, nodes) };\n\t\t\t})\n\t\t\t.filter((s) => !perNode || isActive(s.key)),\n\t}), [data, perNode, nodes, isActive, thresholds]);\n\n\treturn (\n\t\t<div className=\"flex h-full flex-col\">\n\t\t\t{\n\t\t\t\t/* Path selector above the chart (consistent with the rest of the\n\t\t\t dashboards). Node legend stays at the bottom as a color key. */\n\t\t\t}\n\t\t\t{paths.length > 0 && (\n\t\t\t\t<DimensionChipRow\n\t\t\t\t\tdimensionValues={paths}\n\t\t\t\t\tselected={effective}\n\t\t\t\t\tonSelect={setSelected}\n\t\t\t\t\tariaLabel=\"Path\"\n\t\t\t\t/>\n\t\t\t)}\n\t\t\t<div className=\"min-h-0 flex-1\" style={{ marginTop: paths.length > 0 ? 8 : 0 }}>\n\t\t\t\t<LineChart\n\t\t\t\t\tdata={filteredData}\n\t\t\t\t\ttheme={theme}\n\t\t\t\t\tyAxis={yAxis}\n\t\t\t\t\txDomain={xDomain}\n\t\t\t\t\tfillParent={fillParent}\n\t\t\t\t\thideLegend={perNode}\n\t\t\t\t/>\n\t\t\t</div>\n\t\t\t{perNode && nodes.length > 0 && (\n\t\t\t\t<NodeLegend\n\t\t\t\t\tnodeIds={nodes}\n\t\t\t\t\tisActive={isActive}\n\t\t\t\t\tonClickNode={handleLegendClick}\n\t\t\t\t/>\n\t\t\t)}\n\t\t</div>\n\t);\n}\n","import { PerPathRateRenderer } from '../../primitives/PerPathRateRenderer.tsx';\nimport type { AnalyticsDataPoint, DerivedMetricSpec, Series, SeriesData, TimeRange } from '../../types/analytics.ts';\n\n/**\n * Derived: request-rate per (path, time) bucket. Reads raw `count` and `period`\n * columns directly — does NOT delegate to runPipeline / FieldSpec aggregator.\n *\n * Per-bucket rate = Σcount / (period/1000). Across nodes, rates add (sum), so\n * we accumulate `sumCount` over (path, time) across all (method, type, node)\n * records that share the bucket.\n */\nexport function recomputeRequestRate(\n\trecords: AnalyticsDataPoint[],\n\t_window: TimeRange,\n\t_nodes: string[],\n): SeriesData {\n\tconst buckets = new Map<string, Map<number, { sumCount: number; period: number }>>();\n\tfor (const r of records) {\n\t\tconst path = typeof r.path === 'string' ? r.path : null;\n\t\tif (!path) { continue; }\n\t\tif (typeof r.time !== 'number' || !Number.isFinite(r.time)) { continue; }\n\t\tconst count = typeof r.count === 'number' && Number.isFinite(r.count) ? r.count : 0;\n\t\tconst period = typeof r.period === 'number' && r.period > 0 ? r.period : 60000;\n\t\tlet perTime = buckets.get(path);\n\t\tif (!perTime) {\n\t\t\tperTime = new Map();\n\t\t\tbuckets.set(path, perTime);\n\t\t}\n\t\tlet entry = perTime.get(r.time);\n\t\tif (!entry) {\n\t\t\tentry = { sumCount: 0, period };\n\t\t\tperTime.set(r.time, entry);\n\t\t}\n\t\tentry.sumCount += count;\n\t\t// Period assumed identical per-(path, time); first record wins.\n\t}\n\n\tconst series: Series[] = [...buckets.entries()].map(([path, perTime]) => {\n\t\tconst sortedTimes = [...perTime.keys()].sort((a, b) => a - b);\n\t\tconst points = sortedTimes.map((t) => {\n\t\t\tconst e = perTime.get(t)!;\n\t\t\tconst periodSec = e.period / 1000;\n\t\t\tconst y = periodSec > 0 ? e.sumCount / periodSec : null;\n\t\t\treturn { x: t, y, count: e.sumCount };\n\t\t});\n\t\treturn { key: path, label: path, points };\n\t});\n\treturn { series };\n}\n\n/** Per-node + chip-selectable variant. Selected path → one series per\n * node showing that path's rate. No selection → one series per path\n * (cluster aggregate; the original behavior). */\nfunction computeForRenderer(\n\trecords: AnalyticsDataPoint[],\n\t{ perNode, selectedPath }: { perNode: boolean; selectedPath: string | null },\n): SeriesData {\n\tif (perNode && selectedPath) {\n\t\t// Bucket by (node, time) for the selected path only.\n\t\tconst buckets = new Map<string, Map<number, { sumCount: number; period: number }>>();\n\t\tfor (const r of records) {\n\t\t\tif (r.path !== selectedPath) { continue; }\n\t\t\tif (typeof r.time !== 'number' || !Number.isFinite(r.time)) { continue; }\n\t\t\tconst node = typeof r.node === 'string' ? r.node : '_no_node';\n\t\t\tconst count = typeof r.count === 'number' && Number.isFinite(r.count) ? r.count : 0;\n\t\t\tconst period = typeof r.period === 'number' && r.period > 0 ? r.period : 60000;\n\t\t\tlet perTime = buckets.get(node);\n\t\t\tif (!perTime) {\n\t\t\t\tperTime = new Map();\n\t\t\t\tbuckets.set(node, perTime);\n\t\t\t}\n\t\t\tlet entry = perTime.get(r.time);\n\t\t\tif (!entry) {\n\t\t\t\tentry = { sumCount: 0, period };\n\t\t\t\tperTime.set(r.time, entry);\n\t\t\t}\n\t\t\tentry.sumCount += count;\n\t\t}\n\t\tconst series: Series[] = [...buckets.entries()].map(([node, perTime]) => {\n\t\t\tconst sortedTimes = [...perTime.keys()].sort((a, b) => a - b);\n\t\t\tconst points = sortedTimes.map((t) => {\n\t\t\t\tconst e = perTime.get(t)!;\n\t\t\t\tconst periodSec = e.period / 1000;\n\t\t\t\tconst y = periodSec > 0 ? e.sumCount / periodSec : null;\n\t\t\t\treturn { x: t, y, count: e.sumCount };\n\t\t\t});\n\t\t\treturn { key: node, label: node, points };\n\t\t});\n\t\treturn { series };\n\t}\n\n\t// Aggregate path: filter to selected (or use all), produce per-path\n\t// series (the recomputeRequestRate behavior).\n\tconst filtered = selectedPath\n\t\t? records.filter((r) => r.path === selectedPath)\n\t\t: records;\n\treturn recomputeRequestRate(filtered, { startTime: 0, endTime: Number.MAX_SAFE_INTEGER }, []);\n}\n\nexport const requestRateDerived: DerivedMetricSpec = {\n\tid: 'request-rate',\n\ttitle: 'Request rate (req/s)',\n\tsubtitle: 'Per-path req/s. Chip selects path; viewMode flips per-node lines / cluster aggregate.',\n\ttab: 'requests',\n\tsourceMetric: 'duration',\n\trecompute: recomputeRequestRate,\n\tRenderer: ({ records, timeRange, nodes, theme, viewMode }) => (\n\t\t<PerPathRateRenderer\n\t\t\trecords={records}\n\t\t\ttimeRange={timeRange}\n\t\t\tnodes={nodes}\n\t\t\ttheme={theme}\n\t\t\tviewMode={viewMode}\n\t\t\tyAxis={{ unit: '/s', formatter: 'count-si' }}\n\t\t\tcompute={computeForRenderer}\n\t\t/>\n\t),\n\tprimitive: 'line',\n\tyAxis: { unit: '/s', formatter: 'count-si' },\n};\n","import type { AnalyticsDataPoint, DerivedMetricSpec, Series, SeriesData, TimeRange } from '../../types/analytics.ts';\n\n/**\n * Derived metric: per-database transaction-log write rate (bytes/sec).\n *\n * Harper's `database-size` metric carries a `transactionLog` byte counter\n * that is monotonically increasing — it's the cumulative log volume per\n * database. The cumulative number isn't useful on a chart by itself\n * (always rising); the *delta* between consecutive samples per database,\n * normalized by the elapsed time, gives an actionable write-throughput\n * line that mirrors what replication is moving.\n *\n * Implementation:\n * 1. Bucket records by database, then by node (cumulative counters\n * are per-node, so deltas have to be computed within a node).\n * 2. Sort each node's series by time, walk forward computing\n * `(logBytes[i] - logBytes[i-1]) / (time[i] - time[i-1]) * 1000`\n * to get bytes/sec.\n * 3. Sum the resulting per-node rates per `(database, time)` bucket\n * so cluster-wide growth folds into one line per database.\n *\n * Negative or non-finite deltas (counter resets, restarts) are skipped —\n * they'd otherwise put a downward spike on the chart. The first sample\n * per node has no predecessor so it produces no point.\n */\nexport function recomputeTransactionLogGrowth(\n\trecords: AnalyticsDataPoint[],\n\t_window: TimeRange,\n\t_nodes: string[],\n): SeriesData {\n\tconst byDatabase = new Map<string, Map<string, Array<{ time: number; logBytes: number }>>>();\n\tfor (const r of records) {\n\t\tconst database = typeof r.database === 'string' ? r.database : null;\n\t\tif (!database) { continue; }\n\t\tconst time = typeof r.id === 'number'\n\t\t\t? r.id\n\t\t\t: typeof r.time === 'number'\n\t\t\t? r.time\n\t\t\t: null;\n\t\tif (time === null) { continue; }\n\t\tconst node = typeof r.node === 'string' ? r.node : '_no_node';\n\t\tconst logBytes = typeof r.transactionLog === 'number' && Number.isFinite(r.transactionLog)\n\t\t\t? r.transactionLog\n\t\t\t: null;\n\t\tif (logBytes === null) { continue; }\n\t\tlet perNode = byDatabase.get(database);\n\t\tif (!perNode) {\n\t\t\tperNode = new Map();\n\t\t\tbyDatabase.set(database, perNode);\n\t\t}\n\t\tlet samples = perNode.get(node);\n\t\tif (!samples) {\n\t\t\tsamples = [];\n\t\t\tperNode.set(node, samples);\n\t\t}\n\t\tsamples.push({ time, logBytes });\n\t}\n\n\t// Snap the rate's output timestamp to a 60s bucket *only* when rolling per-node\n\t// rates into the cluster sum, so two nodes sampling around the same minute\n\t// boundary (e.g. 1:59:43 + 2:00:03) fold into one point instead of staggering\n\t// across two. Deltas themselves use the raw per-node intervals so accuracy\n\t// is preserved. Sub-bucket sample spacing (test fixtures, in-development\n\t// Harper builds) bypasses snapping so adjacent samples don't collapse to\n\t// the same x and lose their dt.\n\tconst OUTPUT_BUCKET_MS = 60_000;\n\tconst series: Series[] = [];\n\tfor (const [database, perNode] of byDatabase.entries()) {\n\t\tconst ratesByTime = new Map<number, number>();\n\t\tfor (const samples of perNode.values()) {\n\t\t\tsamples.sort((a, b) => a.time - b.time);\n\t\t\tfor (let i = 1; i < samples.length; i++) {\n\t\t\t\tconst prev = samples[i - 1];\n\t\t\t\tconst cur = samples[i];\n\t\t\t\tconst dtMs = cur.time - prev.time;\n\t\t\t\tif (dtMs <= 0) { continue; }\n\t\t\t\tconst dBytes = cur.logBytes - prev.logBytes;\n\t\t\t\tif (!Number.isFinite(dBytes) || dBytes < 0) { continue; }\n\t\t\t\tconst rate = (dBytes * 1000) / dtMs;\n\t\t\t\tif (!Number.isFinite(rate)) { continue; }\n\t\t\t\tconst bucketed = dtMs >= OUTPUT_BUCKET_MS\n\t\t\t\t\t? Math.round(cur.time / OUTPUT_BUCKET_MS) * OUTPUT_BUCKET_MS\n\t\t\t\t\t: cur.time;\n\t\t\t\tratesByTime.set(bucketed, (ratesByTime.get(bucketed) ?? 0) + rate);\n\t\t\t}\n\t\t}\n\t\tconst sortedTimes = [...ratesByTime.keys()].sort((a, b) => a - b);\n\t\tconst points = sortedTimes.map((t) => ({ x: t, y: ratesByTime.get(t)! }));\n\t\tseries.push({ key: database, label: database, points });\n\t}\n\n\treturn { series };\n}\n\nexport const transactionLogGrowthDerived: DerivedMetricSpec = {\n\tid: 'transaction-log-growth',\n\ttitle: 'Transaction log growth',\n\tsubtitle:\n\t\t'Per-database transaction-log write rate (bytes/sec) — derived from `transactionLog` deltas in the database-size response.',\n\ttab: 'storage',\n\tsourceMetric: 'database-size',\n\trecompute: recomputeTransactionLogGrowth,\n\tprimitive: 'line',\n\tyAxis: { unit: '/s', formatter: 'bytes-si' },\n};\n","// Derived metrics recompute from raw source-metric columns. They DO NOT read\n// the source spec's aggregator output; they implement their own Σ-arithmetic\n// to avoid the ratio-of-ratios bug documented in the spec. Step 3 populates\n// this registry with `mqtt-traffic-sent` / `mqtt-traffic-received` (msg/sec\n// views derived from `bytes-sent` / `bytes-received` source records).\n// Step 6A adds `request-rate` (from `duration`) and `error-rate` (from\n// `success`) — the first derived metrics that read raw columns directly\n// instead of delegating to runPipeline.\nimport type { DerivedMetricSpec } from '../../types/analytics.ts';\nimport { errorRateDerived } from './error-rate.tsx';\nimport { mqttTrafficReceivedDerived } from './mqtt-traffic-received.tsx';\nimport { mqttTrafficSentDerived } from './mqtt-traffic-sent.tsx';\nimport { requestRateDerived } from './request-rate.tsx';\nimport { transactionLogGrowthDerived } from './transaction-log-growth.tsx';\n\nexport const derivedRegistry: Record<string, DerivedMetricSpec> = {\n\t'mqtt-traffic-sent': mqttTrafficSentDerived,\n\t'mqtt-traffic-received': mqttTrafficReceivedDerived,\n\t'request-rate': requestRateDerived,\n\t'error-rate': errorRateDerived,\n\t'transaction-log-growth': transactionLogGrowthDerived,\n};\n","import { TrafficByTypeRenderer } from '../primitives/TrafficByTypeRenderer.tsx';\nimport type { AnalyticsDataPoint, MetricSpec, TimeRange } from '../types/analytics.ts';\n\nexport const bytesReceivedSpec: MetricSpec = {\n\ttitle: 'Bytes received by type',\n\tdescription: 'Inbound byte rate (count × mean) — cluster total. Type chips solo / Ctrl-toggle.',\n\ttab: 'traffic',\n\tprimaryDimension: 'node',\n\tsubDimension: 'type',\n\tseries: {\n\t\tkind: 'groupBy',\n\t\tdimension: 'type',\n\t\tfield: {\n\t\t\tfield: {\n\t\t\t\tkind: 'op',\n\t\t\t\top: '*',\n\t\t\t\tleft: { kind: 'ref', field: 'count' },\n\t\t\t\tright: { kind: 'ref', field: 'mean' },\n\t\t\t},\n\t\t\tlabel: 'bytes/sec',\n\t\t\ttransform: { kind: 'rate' },\n\t\t},\n\t},\n\ttimestamp: 'time',\n\tbucket: { source: 'period-field', fallbackMs: 60000 },\n\taggregator: { temporal: 'sum', crossNode: 'sum' },\n\tprimitive: 'stacked-area',\n\tyAxis: { unit: '/s', formatter: 'bytes-si' },\n};\n\ninterface RendererProps {\n\trecords: AnalyticsDataPoint[];\n\ttimeRange: TimeRange;\n\tnodes: string[];\n\ttheme: 'light' | 'dark';\n\tviewMode?: 'per-node' | 'aggregate';\n}\n\nexport function BytesReceivedRenderer(props: RendererProps) {\n\treturn <TrafficByTypeRenderer spec={bytesReceivedSpec} typeField=\"type\" {...props} />;\n}\n","import { TrafficByTypeRenderer } from '../primitives/TrafficByTypeRenderer.tsx';\nimport type { AnalyticsDataPoint, MetricSpec, TimeRange } from '../types/analytics.ts';\n\nexport const bytesSentSpec: MetricSpec = {\n\ttitle: 'Bytes sent by type',\n\tdescription: 'Outbound byte rate (count × mean) — cluster total. Type chips solo / Ctrl-toggle.',\n\ttab: 'traffic',\n\tprimaryDimension: 'node',\n\tsubDimension: 'type',\n\tseries: {\n\t\tkind: 'groupBy',\n\t\tdimension: 'type',\n\t\tfield: {\n\t\t\tfield: {\n\t\t\t\tkind: 'op',\n\t\t\t\top: '*',\n\t\t\t\tleft: { kind: 'ref', field: 'count' },\n\t\t\t\tright: { kind: 'ref', field: 'mean' },\n\t\t\t},\n\t\t\tlabel: 'bytes/sec',\n\t\t\ttransform: { kind: 'rate' },\n\t\t},\n\t},\n\ttimestamp: 'time',\n\tbucket: { source: 'period-field', fallbackMs: 60000 },\n\taggregator: { temporal: 'sum', crossNode: 'sum' },\n\tprimitive: 'stacked-area',\n\tyAxis: { unit: '/s', formatter: 'bytes-si' },\n};\n\ninterface RendererProps {\n\trecords: AnalyticsDataPoint[];\n\ttimeRange: TimeRange;\n\tnodes: string[];\n\ttheme: 'light' | 'dark';\n\tviewMode?: 'per-node' | 'aggregate';\n}\n\nexport function BytesSentRenderer(props: RendererProps) {\n\treturn <TrafficByTypeRenderer spec={bytesSentSpec} typeField=\"type\" {...props} />;\n}\n","// >12-value alternative to DimensionChipRow. Button-triggered popover with a\n// search input + filtered listbox. Mirrors ChipRow's API so callers can swap\n// based on cardinality. Step 6B ships the primitive; first in-tree consumer\n// arrives with db-read/db-write/db-message in a later Step 6 phase.\n\nimport { type KeyboardEvent, useEffect, useId, useRef, useState } from 'react';\n\ninterface DimensionComboboxProps {\n\tdimensionValues: readonly string[];\n\tselected: string;\n\tonSelect: (value: string) => void;\n\totherKey?: string;\n\tcolorFor?: (value: string) => string;\n\tariaLabel?: string;\n}\n\nconst DEFAULT_COLOR = 'var(--color-text-secondary)';\n\nexport function DimensionCombobox({\n\tdimensionValues,\n\tselected,\n\tonSelect,\n\totherKey,\n\tcolorFor,\n\tariaLabel = 'Dimension selector',\n}: DimensionComboboxProps) {\n\tconst [open, setOpen] = useState(false);\n\tconst [query, setQuery] = useState('');\n\tconst [activeIdx, setActiveIdx] = useState(0);\n\tconst listboxId = useId();\n\tconst optionIdPrefix = useId();\n\tconst triggerRef = useRef<HTMLButtonElement | null>(null);\n\tconst searchRef = useRef<HTMLInputElement | null>(null);\n\tconst popoverRef = useRef<HTMLDivElement | null>(null);\n\n\tconst filtered = dimensionValues.filter((v) => v.toLowerCase().includes(query.toLowerCase()));\n\n\tuseEffect(() => {\n\t\tif (open) { searchRef.current?.focus(); }\n\t\telse { setQuery(''); }\n\t}, [open]);\n\n\t// Outside-click / Escape-anywhere dismissal. Without these the popover\n\t// stayed pinned open after clicking elsewhere on the page; clicking the\n\t// trigger itself is allowed to fall through to its own onClick toggle.\n\tuseEffect(() => {\n\t\tif (!open) { return; }\n\t\tconst onPointerDown = (e: PointerEvent) => {\n\t\t\tconst target = e.target as Node | null;\n\t\t\tif (!target) { return; }\n\t\t\tif (popoverRef.current?.contains(target)) { return; }\n\t\t\tif (triggerRef.current?.contains(target)) { return; }\n\t\t\tsetOpen(false);\n\t\t};\n\t\tconst onKeyDown = (e: globalThis.KeyboardEvent) => {\n\t\t\tif (e.key === 'Escape') {\n\t\t\t\tsetOpen(false);\n\t\t\t\ttriggerRef.current?.focus();\n\t\t\t}\n\t\t};\n\t\tdocument.addEventListener('pointerdown', onPointerDown);\n\t\tdocument.addEventListener('keydown', onKeyDown);\n\t\treturn () => {\n\t\t\tdocument.removeEventListener('pointerdown', onPointerDown);\n\t\t\tdocument.removeEventListener('keydown', onKeyDown);\n\t\t};\n\t}, [open]);\n\n\tuseEffect(() => {\n\t\tsetActiveIdx(0);\n\t}, [query]);\n\n\tfunction commit(value: string) {\n\t\tonSelect(value);\n\t\tsetOpen(false);\n\t\ttriggerRef.current?.focus();\n\t}\n\n\tfunction handleSearchKey(e: KeyboardEvent<HTMLInputElement>) {\n\t\tif (e.key === 'Escape') {\n\t\t\te.preventDefault();\n\t\t\tsetOpen(false);\n\t\t\ttriggerRef.current?.focus();\n\t\t\treturn;\n\t\t}\n\t\tif (e.key === 'ArrowDown') {\n\t\t\te.preventDefault();\n\t\t\tsetActiveIdx((i) => Math.min(i + 1, Math.max(0, filtered.length - 1)));\n\t\t\treturn;\n\t\t}\n\t\tif (e.key === 'ArrowUp') {\n\t\t\te.preventDefault();\n\t\t\tsetActiveIdx((i) => Math.max(0, i - 1));\n\t\t\treturn;\n\t\t}\n\t\tif (e.key === 'Enter' && filtered[activeIdx] !== undefined) {\n\t\t\te.preventDefault();\n\t\t\tcommit(filtered[activeIdx]);\n\t\t}\n\t}\n\n\tconst triggerColor = colorFor ? colorFor(selected) : DEFAULT_COLOR;\n\n\treturn (\n\t\t<div className=\"relative pt-3\">\n\t\t\t<button\n\t\t\t\tref={triggerRef}\n\t\t\t\ttype=\"button\"\n\t\t\t\t// WAI-ARIA APG button-pattern combobox: the trigger has\n\t\t\t\t// aria-haspopup='listbox' but is NOT itself role='combobox'.\n\t\t\t\t// The searchbox below is the actual combobox element with\n\t\t\t\t// aria-activedescendant for option highlight tracking.\n\t\t\t\taria-label={ariaLabel}\n\t\t\t\taria-expanded={open}\n\t\t\t\taria-controls={listboxId}\n\t\t\t\taria-haspopup=\"listbox\"\n\t\t\t\tonClick={() => setOpen((v) => !v)}\n\t\t\t\tclassName=\"inline-flex min-h-8 items-center gap-1.5 rounded-full border border-(--color-border) px-2.5 py-1 text-xs text-(--color-text-primary)\"\n\t\t\t>\n\t\t\t\t<span className=\"inline-block h-2 w-2 rounded-full\" style={{ backgroundColor: triggerColor }} />\n\t\t\t\t{selected || '— select —'}\n\t\t\t\t<span aria-hidden>▾</span>\n\t\t\t</button>\n\t\t\t{open && (\n\t\t\t\t<div\n\t\t\t\t\tref={popoverRef}\n\t\t\t\t\tclassName=\"absolute z-10 mt-1 w-72 rounded-md border border-(--color-border) bg-(--color-surface) p-2 shadow-lg\"\n\t\t\t\t>\n\t\t\t\t\t<input\n\t\t\t\t\t\tref={searchRef}\n\t\t\t\t\t\t// WAI-ARIA APG combobox+listbox INPUT pattern: input is\n\t\t\t\t\t\t// the combobox; listbox is its popup; aria-activedescendant\n\t\t\t\t\t\t// announces the highlighted option as arrow keys navigate.\n\t\t\t\t\t\trole=\"combobox\"\n\t\t\t\t\t\ttype=\"text\"\n\t\t\t\t\t\taria-label={`Filter ${ariaLabel}`}\n\t\t\t\t\t\taria-expanded={open}\n\t\t\t\t\t\taria-controls={listboxId}\n\t\t\t\t\t\taria-autocomplete=\"list\"\n\t\t\t\t\t\t// Empty string instead of omitting the attribute so AT\n\t\t\t\t\t\t// caches don't keep a stale id reference between renders.\n\t\t\t\t\t\taria-activedescendant={filtered[activeIdx] !== undefined\n\t\t\t\t\t\t\t? `${optionIdPrefix}-${activeIdx}`\n\t\t\t\t\t\t\t: ''}\n\t\t\t\t\t\tvalue={query}\n\t\t\t\t\t\tonChange={(e) => setQuery(e.target.value)}\n\t\t\t\t\t\tonKeyDown={handleSearchKey}\n\t\t\t\t\t\tplaceholder=\"Filter…\"\n\t\t\t\t\t\tclassName=\"mb-2 w-full rounded border border-(--color-border) px-2 py-1 text-xs\"\n\t\t\t\t\t/>\n\t\t\t\t\t{filtered.length === 0 && (\n\t\t\t\t\t\t<div role=\"status\" aria-live=\"polite\" className=\"px-2 py-1 text-xs text-(--color-text-secondary)\">\n\t\t\t\t\t\t\tNo matches\n\t\t\t\t\t\t</div>\n\t\t\t\t\t)}\n\t\t\t\t\t<ul id={listboxId} role=\"listbox\" className=\"max-h-56 overflow-auto\">\n\t\t\t\t\t\t{filtered.map((value, idx) => {\n\t\t\t\t\t\t\tconst isSelected = value === selected;\n\t\t\t\t\t\t\tconst isActive = idx === activeIdx;\n\t\t\t\t\t\t\tconst color = colorFor ? colorFor(value) : DEFAULT_COLOR;\n\t\t\t\t\t\t\treturn (\n\t\t\t\t\t\t\t\t<li\n\t\t\t\t\t\t\t\t\tkey={value}\n\t\t\t\t\t\t\t\t\tid={`${optionIdPrefix}-${idx}`}\n\t\t\t\t\t\t\t\t\trole=\"option\"\n\t\t\t\t\t\t\t\t\taria-selected={isSelected}\n\t\t\t\t\t\t\t\t\tdata-active={isActive ? 'true' : undefined}\n\t\t\t\t\t\t\t\t\tonMouseDown={(e) => {\n\t\t\t\t\t\t\t\t\t\te.preventDefault();\n\t\t\t\t\t\t\t\t\t\tcommit(value);\n\t\t\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t\t\t\tclassName={`flex cursor-pointer items-center gap-2 rounded px-2 py-1 text-xs ${\n\t\t\t\t\t\t\t\t\t\tisActive ? 'bg-(--color-surface-alt)' : ''\n\t\t\t\t\t\t\t\t\t} ${isSelected ? 'font-semibold' : ''}`}\n\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t<span className=\"inline-block h-2 w-2 rounded-full\" style={{ backgroundColor: color }} />\n\t\t\t\t\t\t\t\t\t<span className=\"truncate\">{value}</span>\n\t\t\t\t\t\t\t\t</li>\n\t\t\t\t\t\t\t);\n\t\t\t\t\t\t})}\n\t\t\t\t\t</ul>\n\t\t\t\t\t{otherKey && (\n\t\t\t\t\t\t<div\n\t\t\t\t\t\t\tclassName=\"mt-2 border-t border-(--color-border) pt-2 text-xs text-(--color-text-secondary)/60\"\n\t\t\t\t\t\t\ttitle=\"Aggregate of smaller buckets; not selectable.\"\n\t\t\t\t\t\t>\n\t\t\t\t\t\t\t{otherKey} (aggregate; not selectable)\n\t\t\t\t\t\t</div>\n\t\t\t\t\t)}\n\t\t\t\t</div>\n\t\t\t)}\n\t\t</div>\n\t);\n}\n","// Shared chip-row + filtered LineChart pattern. Used by duration, success,\n// transfer, response_200, db-{read,write,message}.\n//\n// Two ways to slice the data:\n// 1. Pipeline runs once with `perNode: true` so series come back as\n// one-per-(dim, node). The chip selector still picks one DIMENSION\n// value (path/table/path·method) — filtering keeps every node line\n// for that dimension. Operator immediately sees which node is hot\n// for the selected path/table.\n// 2. When spec.quantileSelector is set, the user can swap the underlying\n// percentile field (p50/p95/p99) via a small button group above the\n// chart. The pipeline re-runs with the chosen field substituted.\n\nimport { useMemo, useState } from 'react';\nimport { NodeLegend } from '../charts/NodeLegend.tsx';\nimport { useNodeSelection } from '../hooks/useNodeSelection.ts';\nimport { getNodeColor } from '../lib/nodeColors.ts';\nimport { runPipeline } from '../pipeline/pipeline.ts';\nimport type { AnalyticsDataPoint, MetricSpec, SeriesData, TimeRange } from '../types/analytics.ts';\nimport { DimensionChipRow } from './DimensionChipRow.tsx';\nimport { DimensionCombobox } from './DimensionCombobox.tsx';\nimport { LineChart } from './LineChart.tsx';\n\nconst OTHER_KEY = 'Other';\nconst CHIP_LIMIT = 12;\n\ninterface Props {\n\tspec: MetricSpec;\n\trecords: AnalyticsDataPoint[];\n\ttimeRange: TimeRange;\n\tnodes: string[];\n\ttheme: 'light' | 'dark';\n\tariaLabel?: string;\n\t/** 'per-node' (default) breaks each chip-selected dimension into one\n\t * line per node; 'aggregate' folds nodes into one cluster series per\n\t * dim. */\n\tviewMode?: 'per-node' | 'aggregate';\n\t/** When true, the chart inside this renderer fills its parent's\n\t * vertical space — used by the expand-to-fullscreen dialog. */\n\tfillParent?: boolean;\n}\n\n/** Last segment of an FQDN as a stable short label.\n * e.g. 'xb6-us-west-1.prod.ibm.harperfabric.com' → 'xb6-us-west-1'.\n * Falls back to the full string if there's no dot. */\nfunction shortenNodeLabel(node: string): string {\n\tconst dot = node.indexOf('.');\n\treturn dot === -1 ? node : node.slice(0, dot);\n}\n\nexport function DimensionSelectorRenderer({\n\tspec,\n\trecords,\n\ttimeRange,\n\tnodes,\n\ttheme,\n\tariaLabel = 'Dimension',\n\tviewMode = 'per-node',\n\tfillParent,\n}: Props) {\n\tconst perNode = viewMode === 'per-node';\n\t// ── Quantile selector state (when spec opts in) ─────────────────────\n\tconst quantileFields = spec.quantileSelector?.fields;\n\tconst [quantile, setQuantile] = useState<string>(\n\t\tspec.quantileSelector?.default ?? '',\n\t);\n\tconst effectiveQuantile = quantileFields?.some((q) => q.field === quantile)\n\t\t? quantile\n\t\t: (spec.quantileSelector?.default ?? '');\n\n\t// Build the runtime spec — substitute the chosen percentile field if a\n\t// quantile selector is active. groupBy specs only.\n\tconst runtimeSpec = useMemo<MetricSpec>(() => {\n\t\tif (!quantileFields || effectiveQuantile === '' || spec.series.kind !== 'groupBy') { return spec; }\n\t\tconst chosen = quantileFields.find((q) => q.field === effectiveQuantile);\n\t\tif (!chosen) { return spec; }\n\t\treturn {\n\t\t\t...spec,\n\t\t\tseries: {\n\t\t\t\t...spec.series,\n\t\t\t\tfield: { ...spec.series.field, field: chosen.field, label: chosen.label },\n\t\t\t},\n\t\t};\n\t}, [spec, quantileFields, effectiveQuantile]);\n\n\tconst fullData = useMemo<SeriesData>(\n\t\t() => runPipeline(runtimeSpec, records, timeRange, nodes, { perNode, snapToPeriod: true }),\n\t\t[runtimeSpec, records, timeRange, nodes, perNode],\n\t);\n\n\t// In perNode mode series keys are `${dim}|${node}`. Build the dimension\n\t// list (the chip-row values) by collecting the prefix part of each key,\n\t// excluding the special OTHER aggregate.\n\tconst dimValues = useMemo(() => {\n\t\tconst seen = new Set<string>();\n\t\tfor (const s of fullData.series) {\n\t\t\tif (s.key === OTHER_KEY) { continue; }\n\t\t\tconst sep = s.key.indexOf('|');\n\t\t\tseen.add(sep === -1 ? s.key : s.key.slice(0, sep));\n\t\t}\n\t\treturn [...seen];\n\t}, [fullData.series]);\n\n\tconst hasOther = fullData.series.some((s) => s.key === OTHER_KEY);\n\tconst [selectedDim, setSelectedDim] = useState<string>(() => dimValues[0] ?? '');\n\tconst effectiveDim = dimValues.includes(selectedDim) ? selectedDim : (dimValues[0] ?? '');\n\n\t// Per-node legend filter — shared across this panel's lines so the\n\t// per-Recharts <Legend> can be hidden and the chart area gets full\n\t// vertical real estate.\n\tconst { isActive, handleLegendClick } = useNodeSelection(nodes);\n\n\t// Filter to (dim|*) prefix; apply per-node coloring so the same node\n\t// keeps the same hue across panels. Then filter by the active-node set.\n\tconst filteredData: SeriesData = useMemo(() => {\n\t\tconst prefix = `${effectiveDim}|`;\n\t\tconst filtered = fullData.series\n\t\t\t.filter((s) => s.key === effectiveDim || s.key.startsWith(prefix))\n\t\t\t.map((s) => {\n\t\t\t\tconst sep = s.key.indexOf('|');\n\t\t\t\tconst node = sep === -1 ? '' : s.key.slice(sep + 1);\n\t\t\t\tif (!node) { return s; }\n\t\t\t\treturn {\n\t\t\t\t\t...s,\n\t\t\t\t\tlabel: shortenNodeLabel(node),\n\t\t\t\t\tcolor: getNodeColor(node, nodes),\n\t\t\t\t};\n\t\t\t})\n\t\t\t.filter((s) => {\n\t\t\t\tconst sep = s.key.indexOf('|');\n\t\t\t\tconst node = sep === -1 ? '' : s.key.slice(sep + 1);\n\t\t\t\treturn node === '' || isActive(node);\n\t\t\t});\n\t\treturn { ...fullData, series: filtered };\n\t}, [fullData, effectiveDim, nodes, isActive]);\n\n\treturn (\n\t\t<div className=\"flex h-full flex-col\">\n\t\t\t{\n\t\t\t\t/* Selectors live above the chart (consistent with TrafficByTypeRenderer):\n\t\t\t quantile (when the spec exposes one) on top, then the dimension\n\t\t\t selector (chip row when ≤ CHIP_LIMIT values, combobox above it).\n\t\t\t The chart fills the remaining space below; the per-node legend\n\t\t\t stays at the bottom because it's a color key, not a selector. */\n\t\t\t}\n\t\t\t{spec.quantileSelector && quantileFields && quantileFields.length > 1 && (\n\t\t\t\t<div\n\t\t\t\t\trole=\"radiogroup\"\n\t\t\t\t\taria-label=\"Quantile\"\n\t\t\t\t\tclassName=\"flex flex-wrap justify-center gap-x-3 gap-y-1 pt-2 text-[11px]\"\n\t\t\t\t>\n\t\t\t\t\t{quantileFields.map((q) => {\n\t\t\t\t\t\tconst active = q.field === effectiveQuantile;\n\t\t\t\t\t\treturn (\n\t\t\t\t\t\t\t<button\n\t\t\t\t\t\t\t\tkey={q.field}\n\t\t\t\t\t\t\t\ttype=\"button\"\n\t\t\t\t\t\t\t\trole=\"radio\"\n\t\t\t\t\t\t\t\taria-checked={active}\n\t\t\t\t\t\t\t\tdata-testid=\"quantile-button\"\n\t\t\t\t\t\t\t\tdata-value={q.field}\n\t\t\t\t\t\t\t\tonClick={() => setQuantile(q.field)}\n\t\t\t\t\t\t\t\tclassName=\"inline-flex items-center cursor-pointer border-none bg-transparent p-0 text-(--color-text-secondary)\"\n\t\t\t\t\t\t\t\tstyle={{ opacity: active ? 1 : 0.3 }}\n\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t{q.label}\n\t\t\t\t\t\t\t</button>\n\t\t\t\t\t\t);\n\t\t\t\t\t})}\n\t\t\t\t</div>\n\t\t\t)}\n\t\t\t{dimValues.length > CHIP_LIMIT\n\t\t\t\t? (\n\t\t\t\t\t<DimensionCombobox\n\t\t\t\t\t\tdimensionValues={dimValues}\n\t\t\t\t\t\tselected={effectiveDim}\n\t\t\t\t\t\tonSelect={setSelectedDim}\n\t\t\t\t\t\totherKey={hasOther ? OTHER_KEY : undefined}\n\t\t\t\t\t\tariaLabel={ariaLabel}\n\t\t\t\t\t/>\n\t\t\t\t)\n\t\t\t\t: (\n\t\t\t\t\t<DimensionChipRow\n\t\t\t\t\t\tdimensionValues={dimValues}\n\t\t\t\t\t\tselected={effectiveDim}\n\t\t\t\t\t\tonSelect={setSelectedDim}\n\t\t\t\t\t\totherKey={hasOther ? OTHER_KEY : undefined}\n\t\t\t\t\t\tariaLabel={ariaLabel}\n\t\t\t\t\t/>\n\t\t\t\t)}\n\t\t\t<div className=\"min-h-0 flex-1\" style={{ marginTop: 8 }}>\n\t\t\t\t<LineChart\n\t\t\t\t\tdata={filteredData}\n\t\t\t\t\ttheme={theme}\n\t\t\t\t\tyAxis={spec.yAxis}\n\t\t\t\t\txDomain={[timeRange.startTime, timeRange.endTime]}\n\t\t\t\t\tfillParent={fillParent}\n\t\t\t\t\thideLegend={perNode}\n\t\t\t\t/>\n\t\t\t</div>\n\t\t\t{perNode && nodes.length > 0 && (\n\t\t\t\t<NodeLegend\n\t\t\t\t\tnodeIds={nodes}\n\t\t\t\t\tisActive={isActive}\n\t\t\t\t\tonClickNode={handleLegendClick}\n\t\t\t\t/>\n\t\t\t)}\n\t\t</div>\n\t);\n}\n","import { DimensionSelectorRenderer } from '../primitives/DimensionSelectorRenderer.tsx';\nimport type { AnalyticsDataPoint, MetricSpec, TimeRange } from '../types/analytics.ts';\n\n// Schema (per Harper get_analytics metric: 'cache-hit'):\n// { time, node, path, period, count, total, ratio }\n//\n// `ratio` is precomputed by Harper as `total / count` (hits / lookups). We\n// plot it directly with a count-weighted-mean cross-bucket so paths with\n// many lookups dominate the cluster line — a path with one lookup and a\n// 100% hit shouldn't drag the average up.\nexport const cacheHitSpec: MetricSpec = {\n\ttitle: 'Cache hit rate',\n\tdescription: 'Per-path cache-hit ratio (count-weighted-mean) — top 10 paths + Other.',\n\ttab: 'requests',\n\tprimaryDimension: 'path',\n\tseries: {\n\t\tkind: 'groupBy',\n\t\tdimension: 'path',\n\t\tfield: { field: 'ratio', label: 'hit ratio' },\n\t\ttopN: 10,\n\t\totherBucket: true,\n\t},\n\ttimestamp: 'time',\n\tbucket: { source: 'period-field', fallbackMs: 60000 },\n\taggregator: { temporal: 'count-weighted-mean', crossNode: 'count-weighted-mean' },\n\t// Same gating as duration / transfer — sample-thin paths grey out\n\t// rather than dragging the chart with one-off readings.\n\tconfidence: { field: 'count', greyBelow: 40, suppressBelow: 100 },\n\tprimitive: 'line',\n\tyAxis: { unit: '', formatter: 'percent', domain: [0, 1] },\n};\n\ninterface RendererProps {\n\trecords: AnalyticsDataPoint[];\n\ttimeRange: TimeRange;\n\tnodes: string[];\n\ttheme: 'light' | 'dark';\n\tviewMode?: 'per-node' | 'aggregate';\n}\n\nexport function CacheHitRenderer(props: RendererProps) {\n\treturn <DimensionSelectorRenderer spec={cacheHitSpec} {...props} ariaLabel=\"Path\" />;\n}\n","// Single source of truth for the percentile field set Harper exposes on\n// quantile-bearing metrics (duration, transfer, db-*, cpu-usage, bytes-*,\n// replication-latency). All 9 percentiles are surfaced to operators so\n// they can inspect the full distribution shape from p1 (best case) to\n// p999 (worst tail). Default selection is p95 — the historical SLO axis.\n\nexport interface QuantileField {\n\t/** Record column to read. Harper uses 'median' for p50. */\n\tfield: 'p1' | 'p10' | 'p25' | 'median' | 'p75' | 'p90' | 'p95' | 'p99' | 'p999';\n\t/** Operator-facing label. */\n\tlabel: string;\n}\n\nexport const QUANTILE_FIELDS: ReadonlyArray<QuantileField> = [\n\t{ field: 'p1', label: 'p1' },\n\t{ field: 'p10', label: 'p10' },\n\t{ field: 'p25', label: 'p25' },\n\t{ field: 'median', label: 'p50' },\n\t{ field: 'p75', label: 'p75' },\n\t{ field: 'p90', label: 'p90' },\n\t{ field: 'p95', label: 'p95' },\n\t{ field: 'p99', label: 'p99' },\n\t{ field: 'p999', label: 'p999' },\n];\n\nexport const QUANTILE_DEFAULT: QuantileField['field'] = 'p95';\n","import { DimensionSelectorRenderer } from '../primitives/DimensionSelectorRenderer.tsx';\nimport type { AnalyticsDataPoint, MetricSpec, TimeRange } from '../types/analytics.ts';\nimport { QUANTILE_DEFAULT, QUANTILE_FIELDS } from './quantileFields.ts';\n\n// Schema (per Harper get_analytics metric: 'cache-resolution'):\n// { time, node, path, method, type, period, count, mean,\n// p1, p10, p25, median, p75, p90, p95, p99, p999 }\n//\n// Time-to-resolve a cache miss, in milliseconds. Same per-path latency\n// distribution shape as `duration` and `transfer` so it shares their\n// renderer pattern: line chart with a path chip selector + a quantile\n// selector (p1…p999, default p95).\nexport const cacheResolutionSpec: MetricSpec = {\n\ttitle: 'Cache miss resolution (p95)',\n\tdescription: 'Per-path time-to-resolve a cache miss (count-weighted-mean) — top 10 paths + Other.',\n\ttab: 'requests',\n\tprimaryDimension: 'path',\n\tsubDimension: 'method',\n\tseries: {\n\t\tkind: 'groupBy',\n\t\tdimension: 'path',\n\t\tfield: { field: 'p95', label: 'p95 resolution (ms)' },\n\t\ttopN: 10,\n\t\totherBucket: true,\n\t},\n\ttimestamp: 'time',\n\tbucket: { source: 'period-field', fallbackMs: 60000 },\n\taggregator: { temporal: 'count-weighted-mean', crossNode: 'count-weighted-mean' },\n\tconfidence: { field: 'count', greyBelow: 40, suppressBelow: 100 },\n\tprimitive: 'line',\n\tyAxis: { unit: '', formatter: 'ms' },\n\tquantileSelector: { fields: QUANTILE_FIELDS, default: QUANTILE_DEFAULT },\n};\n\ninterface RendererProps {\n\trecords: AnalyticsDataPoint[];\n\ttimeRange: TimeRange;\n\tnodes: string[];\n\ttheme: 'light' | 'dark';\n\tviewMode?: 'per-node' | 'aggregate';\n}\n\nexport function CacheResolutionRenderer(props: RendererProps) {\n\treturn <DimensionSelectorRenderer spec={cacheResolutionSpec} {...props} ariaLabel=\"Path\" />;\n}\n","import { useMemo, useState } from 'react';\nimport { NodeLegend } from '../charts/NodeLegend.tsx';\nimport { useNodeSelection } from '../hooks/useNodeSelection.ts';\nimport { getNodeColor } from '../lib/nodeColors.ts';\nimport { DimensionChipRow } from '../primitives/DimensionChipRow.tsx';\nimport { LineChart } from '../primitives/LineChart.tsx';\nimport type { AnalyticsDataPoint, MetricSpec, SeriesData, Threshold, TimeRange } from '../types/analytics.ts';\nimport { runPipeline } from './pipeline.ts';\n\nconst COMPOSITE_FIELD = 'pathMethod';\nconst SEPARATOR = ' · ';\n\nexport const connectionSpec: MetricSpec = {\n\ttitle: 'Connection success ratio',\n\tdescription:\n\t\t'Per-(path, method) connection ratio (count-weighted-mean). MQTT thresholds: connect ≥0.99, disconnect ≥0.2.',\n\ttab: 'traffic',\n\tprimaryDimension: 'path',\n\tsubDimension: 'method',\n\tseries: {\n\t\tkind: 'groupBy',\n\t\tdimension: COMPOSITE_FIELD,\n\t\tfield: { field: 'ratio', label: 'success ratio', transform: { kind: 'ratio' } },\n\t},\n\ttimestamp: 'time',\n\tbucket: { source: 'period-field', fallbackMs: 60000 },\n\taggregator: { temporal: 'count-weighted-mean', crossNode: 'count-weighted-mean' },\n\tconfidence: { field: 'count', greyBelow: 100, suppressBelow: 500 },\n\tprimitive: 'line',\n\tyAxis: { unit: '', formatter: 'percent' },\n\tthresholds: [\n\t\t{\n\t\t\tvalue: 0.99,\n\t\t\tlabel: 'connect',\n\t\t\tdirection: 'below-is-bad',\n\t\t\tminCount: 1000,\n\t\t\tscope: { path: 'mqtt', method: 'connect' },\n\t\t},\n\t\t{\n\t\t\tvalue: 0.20,\n\t\t\tlabel: 'disconnect',\n\t\t\tdirection: 'below-is-bad',\n\t\t\tminCount: 500,\n\t\t\tscope: { path: 'mqtt', method: 'disconnect' },\n\t\t},\n\t],\n};\n\ninterface RendererProps {\n\trecords: AnalyticsDataPoint[];\n\ttimeRange: TimeRange;\n\tnodes: string[];\n\ttheme: 'light' | 'dark';\n\tviewMode?: 'per-node' | 'aggregate';\n\tfillParent?: boolean;\n}\n\nfunction compositeKey(path: unknown, method: unknown): string | null {\n\tif (typeof path !== 'string' && typeof path !== 'number') { return null; }\n\tif (typeof method !== 'string' && typeof method !== 'number') { return null; }\n\treturn `${path}${SEPARATOR}${method}`;\n}\n\nfunction preprocess(records: AnalyticsDataPoint[]): AnalyticsDataPoint[] {\n\tconst out: AnalyticsDataPoint[] = [];\n\tfor (const r of records) {\n\t\tconst path = (r as any).path;\n\t\tconst method = (r as any).method;\n\t\tconst key = compositeKey(path, method);\n\t\tif (key === null) { continue; }\n\t\tconst total = (r as any).total;\n\t\tconst count = (r as any).count;\n\t\tconst nullGap = total === 0 && typeof count === 'number' && count > 0;\n\t\tout.push({\n\t\t\t...r,\n\t\t\t[COMPOSITE_FIELD]: key,\n\t\t\tratio: nullGap ? null : (r as any).ratio,\n\t\t} as any);\n\t}\n\treturn out;\n}\n\nfunction shortenNodeLabel(node: string): string {\n\tconst dot = node.indexOf('.');\n\treturn dot === -1 ? node : node.slice(0, dot);\n}\n\nexport function ConnectionRenderer(\n\t{ records, timeRange, nodes, theme, viewMode = 'per-node', fillParent }: RendererProps,\n) {\n\tconst perNode = viewMode === 'per-node';\n\tconst processed = useMemo(() => preprocess(records), [records]);\n\n\tconst fullData = useMemo<SeriesData>(\n\t\t() => runPipeline(connectionSpec, processed, timeRange, nodes, { perNode, snapToPeriod: true }),\n\t\t[processed, timeRange, nodes, perNode],\n\t);\n\n\t// Per-node series keys are `${pathMethod}|${node}`. The chip selector\n\t// picks one composite (pathMethod) value; filter by prefix.\n\tconst selectable = useMemo(() => {\n\t\tconst seen = new Set<string>();\n\t\tfor (const s of fullData.series) {\n\t\t\tconst sep = s.key.indexOf('|');\n\t\t\tseen.add(sep === -1 ? s.key : s.key.slice(0, sep));\n\t\t}\n\t\treturn [...seen];\n\t}, [fullData.series]);\n\tconst [selected, setSelected] = useState<string>(() => selectable[0] ?? '');\n\tconst effectiveSelected = selectable.includes(selected) ? selected : (selectable[0] ?? '');\n\n\tconst selectedDims = useMemo(() => {\n\t\tif (!effectiveSelected) { return null; }\n\t\tconst [path, method] = effectiveSelected.split(SEPARATOR);\n\t\treturn { path, method } as Record<string, string>;\n\t}, [effectiveSelected]);\n\n\tconst { isActive, handleLegendClick } = useNodeSelection(nodes);\n\n\tconst filteredData: SeriesData = useMemo(() => {\n\t\tfunction thresholdMatches(t: Threshold): boolean {\n\t\t\tif (!selectedDims) { return false; }\n\t\t\tif (!t.scope) { return true; }\n\t\t\tfor (const [dim, want] of Object.entries(t.scope)) {\n\t\t\t\tif (selectedDims[dim] !== want) { return false; }\n\t\t\t}\n\t\t\treturn true;\n\t\t}\n\t\tconst prefix = `${effectiveSelected}|`;\n\t\tconst series = fullData.series\n\t\t\t.filter((s) => s.key === effectiveSelected || s.key.startsWith(prefix))\n\t\t\t.map((s) => {\n\t\t\t\tconst sep = s.key.indexOf('|');\n\t\t\t\tconst node = sep === -1 ? '' : s.key.slice(sep + 1);\n\t\t\t\tif (!node) { return s; }\n\t\t\t\treturn {\n\t\t\t\t\t...s,\n\t\t\t\t\tlabel: shortenNodeLabel(node),\n\t\t\t\t\tcolor: getNodeColor(node, nodes),\n\t\t\t\t};\n\t\t\t})\n\t\t\t.filter((s) => {\n\t\t\t\tconst sep = s.key.indexOf('|');\n\t\t\t\tconst node = sep === -1 ? '' : s.key.slice(sep + 1);\n\t\t\t\treturn node === '' || isActive(node);\n\t\t\t});\n\t\treturn {\n\t\t\t...fullData,\n\t\t\tseries,\n\t\t\tthresholds: (fullData.thresholds ?? []).filter(thresholdMatches),\n\t\t};\n\t}, [fullData, effectiveSelected, selectedDims, nodes, isActive]);\n\n\treturn (\n\t\t<div className=\"flex h-full flex-col\">\n\t\t\t<DimensionChipRow\n\t\t\t\tdimensionValues={selectable}\n\t\t\t\tselected={effectiveSelected}\n\t\t\t\tonSelect={setSelected}\n\t\t\t\tariaLabel=\"Path · method\"\n\t\t\t/>\n\t\t\t<div className=\"min-h-0 flex-1\" style={{ marginTop: 8 }}>\n\t\t\t\t<LineChart\n\t\t\t\t\tdata={filteredData}\n\t\t\t\t\ttheme={theme}\n\t\t\t\t\tyAxis={connectionSpec.yAxis}\n\t\t\t\t\txDomain={[timeRange.startTime, timeRange.endTime]}\n\t\t\t\t\tfillParent={fillParent}\n\t\t\t\t\thideLegend={perNode}\n\t\t\t\t/>\n\t\t\t</div>\n\t\t\t{perNode && nodes.length > 0 && (\n\t\t\t\t<NodeLegend\n\t\t\t\t\tnodeIds={nodes}\n\t\t\t\t\tisActive={isActive}\n\t\t\t\t\tonClickNode={handleLegendClick}\n\t\t\t\t/>\n\t\t\t)}\n\t\t</div>\n\t);\n}\n","import { TrafficByTypeRenderer } from '../primitives/TrafficByTypeRenderer.tsx';\nimport type { AnalyticsDataPoint, MetricSpec, TimeRange } from '../types/analytics.ts';\n\n// Schema note: this Harper build splits \"active sessions\" across two\n// metrics — `mqtt-connections` and `ws-connections` — each carrying a\n// `connections` field with the active-session snapshot. The unified\n// `connections` metric on this build is event-based (connect/disconnect)\n// not snapshot-based, so it isn't a substitute. The dashboard panel\n// (rendered by ConnectionsPanel in TrafficTab) fetches both metrics,\n// tags each row with a synthesized `type` field, and feeds the merged\n// stream into the renderer below.\nexport const connectionsSpec: MetricSpec = {\n\ttitle: 'Connections',\n\tdescription: 'Active sessions by type — chips solo / Ctrl-toggle. viewMode flips type/node stack.',\n\ttab: 'traffic',\n\tprimaryDimension: 'node',\n\tsubDimension: 'type',\n\tseries: {\n\t\tkind: 'groupBy',\n\t\tdimension: 'type',\n\t\tfield: {\n\t\t\tfield: 'connections',\n\t\t\tlabel: 'connections',\n\t\t\taggregator: { temporal: 'max', crossNode: 'sum' },\n\t\t},\n\t},\n\ttimestamp: 'time',\n\tbucket: { source: 'period-field', fallbackMs: 60000 },\n\taggregator: { temporal: 'max', crossNode: 'sum' },\n\tprimitive: 'stacked-area',\n\tyAxis: { unit: '', formatter: 'count-si' },\n};\n\ninterface RendererProps {\n\trecords: AnalyticsDataPoint[];\n\ttimeRange: TimeRange;\n\tnodes: string[];\n\ttheme: 'light' | 'dark';\n\tviewMode?: 'per-node' | 'aggregate';\n\tfillParent?: boolean;\n}\n\nexport function ConnectionsRenderer(props: RendererProps) {\n\treturn <TrafficByTypeRenderer spec={connectionsSpec} typeField=\"type\" {...props} />;\n}\n","import { DimensionSelectorRenderer } from '../primitives/DimensionSelectorRenderer.tsx';\nimport type { AnalyticsDataPoint, MetricSpec, TimeRange } from '../types/analytics.ts';\nimport { QUANTILE_DEFAULT, QUANTILE_FIELDS } from './quantileFields.ts';\n\n// Single-chart with chip selector (path: harper/user) + standard quantile\n// selector (p1..p999, default p95). Replaces the prior 3-panel\n// small-multiples view that locked operators to p50/p95/p99 only.\nexport const cpuUsageSpec: MetricSpec = {\n\ttitle: 'CPU — by scope (harper vs user)',\n\tdescription:\n\t\t'Per-path CPU utilization (count-weighted-mean) — chip selector picks scope; quantile selector picks percentile.',\n\ttab: 'health',\n\tprimaryDimension: 'path',\n\tseries: {\n\t\tkind: 'groupBy',\n\t\tdimension: 'path',\n\t\tfield: { field: 'p95', label: 'CPU %' },\n\t},\n\ttimestamp: 'time',\n\tbucket: { source: 'period-field', fallbackMs: 60000 },\n\taggregator: { temporal: 'count-weighted-mean', crossNode: 'count-weighted-mean' },\n\tconfidence: { field: 'count', greyBelow: 40, suppressBelow: 100 },\n\tprimitive: 'line',\n\tyAxis: { unit: '', formatter: 'percent' },\n\tquantileSelector: { fields: QUANTILE_FIELDS, default: QUANTILE_DEFAULT },\n};\n\ninterface RendererProps {\n\trecords: AnalyticsDataPoint[];\n\ttimeRange: TimeRange;\n\tnodes: string[];\n\ttheme: 'light' | 'dark';\n\tviewMode?: 'per-node' | 'aggregate';\n}\n\nexport function CpuUsageRenderer(props: RendererProps) {\n\treturn <DimensionSelectorRenderer spec={cpuUsageSpec} {...props} ariaLabel=\"Scope\" />;\n}\n","import { TrafficByTypeRenderer } from '../primitives/TrafficByTypeRenderer.tsx';\nimport type { AnalyticsDataPoint, MetricSpec, TimeRange } from '../types/analytics.ts';\n\n// Schema note: Harper emits database-size records with the per-database\n// byte total on the `size` column (analytics-viz's original spec said\n// `used`, which is stale relative to current Harper builds). Each row\n// also carries a `transactionLog` byte counter consumed by the\n// transaction-log-growth derived panel.\n//\n// Rendering: stacked-area with a multi-select chip row above the chart\n// (database names) and a node legend below — the same dual-legend\n// pattern Traffic-tab panels use. Operator can solo / Ctrl-toggle\n// databases or nodes; the chart updates live as records are filtered\n// pre-pipeline.\nexport const databaseSizeSpec: MetricSpec = {\n\ttitle: 'Database size',\n\tdescription: 'Per-database size in bytes — chips solo / Ctrl-toggle databases; node legend filters by node.',\n\ttab: 'storage',\n\tprimaryDimension: 'database',\n\tseries: {\n\t\tkind: 'groupBy',\n\t\tdimension: 'database',\n\t\tfield: { field: 'size', label: 'size (bytes)' },\n\t},\n\ttimestamp: 'id',\n\tbucket: { source: 'period-field', fallbackMs: 60000 },\n\taggregator: { temporal: 'last', crossNode: 'sum' },\n\tprimitive: 'stacked-area',\n\tyAxis: { unit: ' B', formatter: 'bytes-si' },\n};\n\ninterface RendererProps {\n\trecords: AnalyticsDataPoint[];\n\ttimeRange: TimeRange;\n\tnodes: string[];\n\ttheme: 'light' | 'dark';\n\tviewMode?: 'per-node' | 'aggregate';\n\tfillParent?: boolean;\n}\n\n/** Default `viewMode='aggregate'` so the stack reads \"cluster total\n * broken out by database\" — that's the operator's first question for\n * storage. Setting viewMode='per-node' on the panel toggles the stack\n * to \"per-database total broken out by node\" via TrafficByTypeRenderer's\n * built-in remap. */\nexport function DatabaseSizeRenderer(props: RendererProps) {\n\treturn (\n\t\t<TrafficByTypeRenderer\n\t\t\tspec={databaseSizeSpec}\n\t\t\ttypeField=\"database\"\n\t\t\t{...props}\n\t\t\tviewMode={props.viewMode ?? 'aggregate'}\n\t\t/>\n\t);\n}\n","import { DimensionSelectorRenderer } from '../primitives/DimensionSelectorRenderer.tsx';\nimport type { AnalyticsDataPoint, MetricSpec, TimeRange } from '../types/analytics.ts';\nimport { QUANTILE_DEFAULT, QUANTILE_FIELDS } from './quantileFields.ts';\n\nexport const dbMessageSpec: MetricSpec = {\n\ttitle: 'DB message p95',\n\tdescription: 'Per-table DB message p95 (count-weighted-mean) — top 10 tables + Other.',\n\ttab: 'db-activity',\n\tprimaryDimension: 'path',\n\tsubDimension: 'method',\n\tseries: {\n\t\tkind: 'groupBy',\n\t\tdimension: 'path',\n\t\tfield: { field: 'p95', label: 'p95 message (ms)' },\n\t\ttopN: 10,\n\t\totherBucket: true,\n\t},\n\ttimestamp: 'time',\n\tbucket: { source: 'period-field', fallbackMs: 60000 },\n\taggregator: { temporal: 'count-weighted-mean', crossNode: 'count-weighted-mean' },\n\tconfidence: { field: 'count', greyBelow: 40, suppressBelow: 100 },\n\tprimitive: 'line',\n\tyAxis: { unit: '', formatter: 'ms' },\n\tquantileSelector: { fields: QUANTILE_FIELDS, default: QUANTILE_DEFAULT },\n};\n\ninterface RendererProps {\n\trecords: AnalyticsDataPoint[];\n\ttimeRange: TimeRange;\n\tnodes: string[];\n\ttheme: 'light' | 'dark';\n\tviewMode?: 'per-node' | 'aggregate';\n}\n\nexport function DbMessageRenderer(props: RendererProps) {\n\treturn <DimensionSelectorRenderer spec={dbMessageSpec} {...props} ariaLabel=\"Table\" />;\n}\n","import { DimensionSelectorRenderer } from '../primitives/DimensionSelectorRenderer.tsx';\nimport type { AnalyticsDataPoint, MetricSpec, TimeRange } from '../types/analytics.ts';\nimport { QUANTILE_DEFAULT, QUANTILE_FIELDS } from './quantileFields.ts';\n\nexport const dbReadSpec: MetricSpec = {\n\ttitle: 'DB read p95',\n\tdescription: 'Per-table DB read p95 (count-weighted-mean) — top 10 tables + Other.',\n\ttab: 'db-activity',\n\tprimaryDimension: 'path',\n\tsubDimension: 'method',\n\tseries: {\n\t\tkind: 'groupBy',\n\t\tdimension: 'path',\n\t\tfield: { field: 'p95', label: 'p95 read (ms)' },\n\t\ttopN: 10,\n\t\totherBucket: true,\n\t},\n\ttimestamp: 'time',\n\tbucket: { source: 'period-field', fallbackMs: 60000 },\n\taggregator: { temporal: 'count-weighted-mean', crossNode: 'count-weighted-mean' },\n\tconfidence: { field: 'count', greyBelow: 40, suppressBelow: 100 },\n\tprimitive: 'line',\n\tyAxis: { unit: '', formatter: 'ms' },\n\tquantileSelector: { fields: QUANTILE_FIELDS, default: QUANTILE_DEFAULT },\n};\n\ninterface RendererProps {\n\trecords: AnalyticsDataPoint[];\n\ttimeRange: TimeRange;\n\tnodes: string[];\n\ttheme: 'light' | 'dark';\n\tviewMode?: 'per-node' | 'aggregate';\n}\n\nexport function DbReadRenderer(props: RendererProps) {\n\treturn <DimensionSelectorRenderer spec={dbReadSpec} {...props} ariaLabel=\"Table\" />;\n}\n","import { DimensionSelectorRenderer } from '../primitives/DimensionSelectorRenderer.tsx';\nimport type { AnalyticsDataPoint, MetricSpec, TimeRange } from '../types/analytics.ts';\nimport { QUANTILE_DEFAULT, QUANTILE_FIELDS } from './quantileFields.ts';\n\nexport const dbWriteSpec: MetricSpec = {\n\ttitle: 'DB write p95',\n\tdescription: 'Per-table DB write p95 (count-weighted-mean) — top 10 tables + Other.',\n\ttab: 'db-activity',\n\tprimaryDimension: 'path',\n\tsubDimension: 'method',\n\tseries: {\n\t\tkind: 'groupBy',\n\t\tdimension: 'path',\n\t\tfield: { field: 'p95', label: 'p95 write (ms)' },\n\t\ttopN: 10,\n\t\totherBucket: true,\n\t},\n\ttimestamp: 'time',\n\tbucket: { source: 'period-field', fallbackMs: 60000 },\n\taggregator: { temporal: 'count-weighted-mean', crossNode: 'count-weighted-mean' },\n\tconfidence: { field: 'count', greyBelow: 40, suppressBelow: 100 },\n\tprimitive: 'line',\n\tyAxis: { unit: '', formatter: 'ms' },\n\tquantileSelector: { fields: QUANTILE_FIELDS, default: QUANTILE_DEFAULT },\n};\n\ninterface RendererProps {\n\trecords: AnalyticsDataPoint[];\n\ttimeRange: TimeRange;\n\tnodes: string[];\n\ttheme: 'light' | 'dark';\n\tviewMode?: 'per-node' | 'aggregate';\n}\n\nexport function DbWriteRenderer(props: RendererProps) {\n\treturn <DimensionSelectorRenderer spec={dbWriteSpec} {...props} ariaLabel=\"Table\" />;\n}\n","import { DimensionSelectorRenderer } from '../primitives/DimensionSelectorRenderer.tsx';\nimport type { AnalyticsDataPoint, MetricSpec, TimeRange } from '../types/analytics.ts';\nimport { QUANTILE_DEFAULT, QUANTILE_FIELDS } from './quantileFields.ts';\n\nexport const durationSpec: MetricSpec = {\n\ttitle: 'Request duration (p95)',\n\tdescription: 'Per-path request duration p95 (count-weighted-mean) — top 10 paths + Other bucket.',\n\ttab: 'requests',\n\tprimaryDimension: 'path',\n\tsubDimension: 'method',\n\tseries: {\n\t\tkind: 'groupBy',\n\t\tdimension: 'path',\n\t\tfield: { field: 'p95', label: 'p95 duration (ms)' },\n\t\ttopN: 10,\n\t\totherBucket: true,\n\t},\n\ttimestamp: 'time',\n\tbucket: { source: 'period-field', fallbackMs: 60000 },\n\taggregator: { temporal: 'count-weighted-mean', crossNode: 'count-weighted-mean' },\n\tconfidence: { field: 'count', greyBelow: 40, suppressBelow: 100 },\n\tprimitive: 'line',\n\tyAxis: { unit: '', formatter: 'ms' },\n\tquantileSelector: { fields: QUANTILE_FIELDS, default: QUANTILE_DEFAULT },\n};\n\ninterface RendererProps {\n\trecords: AnalyticsDataPoint[];\n\ttimeRange: TimeRange;\n\tnodes: string[];\n\ttheme: 'light' | 'dark';\n\tviewMode?: 'per-node' | 'aggregate';\n}\n\nexport function DurationRenderer(props: RendererProps) {\n\treturn <DimensionSelectorRenderer spec={durationSpec} {...props} ariaLabel=\"Path\" />;\n}\n","import { useMemo, useState } from 'react';\nimport { NodeLegend } from '../charts/NodeLegend.tsx';\nimport { useNodeSelection } from '../hooks/useNodeSelection.ts';\nimport { getNodeColor } from '../lib/nodeColors.ts';\nimport { DimensionChipRow } from '../primitives/DimensionChipRow.tsx';\nimport { LineChart } from '../primitives/LineChart.tsx';\nimport type { AnalyticsDataPoint, AxisSpec, FieldSpec, MetricSpec, SeriesData, TimeRange } from '../types/analytics.ts';\nimport { runPipeline } from './pipeline.ts';\n\n// Field-selector pattern (mirrors memory.tsx). Records expose\n// `active`, `idle`, and `taskQueueLatency`; we surface 4 operator-\n// relevant views as chip-row options, each with its own field\n// projection + y-axis formatter. The prior dual-axis design (one\n// utilization line + one queue-lag line) is replaced by single-field\n// focus so each option uses the full chart real estate with per-node\n// breakdown.\n\ninterface FieldOption {\n\tkey: string;\n\tlabel: string;\n\tfield: FieldSpec['field'];\n\tyAxis: AxisSpec;\n}\n\nconst FIELDS: ReadonlyArray<FieldOption> = [\n\t{\n\t\tkey: 'utilization',\n\t\tlabel: 'Utilization',\n\t\t// active / (active + idle); FieldExpr returns null when divisor is 0.\n\t\tfield: {\n\t\t\tkind: 'op',\n\t\t\top: '/',\n\t\t\tleft: { kind: 'ref', field: 'active' },\n\t\t\tright: {\n\t\t\t\tkind: 'op',\n\t\t\t\top: '+',\n\t\t\t\tleft: { kind: 'ref', field: 'active' },\n\t\t\t\tright: { kind: 'ref', field: 'idle' },\n\t\t\t},\n\t\t},\n\t\tyAxis: { unit: '', formatter: 'percent' },\n\t},\n\t{\n\t\tkey: 'taskQueueLatency',\n\t\tlabel: 'Queue lag',\n\t\tfield: 'taskQueueLatency',\n\t\tyAxis: { unit: '', formatter: 'ms' },\n\t},\n\t{\n\t\tkey: 'active',\n\t\tlabel: 'Active time',\n\t\tfield: 'active',\n\t\tyAxis: { unit: '', formatter: 'ms' },\n\t},\n\t{\n\t\tkey: 'idle',\n\t\tlabel: 'Idle time',\n\t\tfield: 'idle',\n\t\tyAxis: { unit: '', formatter: 'ms' },\n\t},\n];\n\nconst FIELD_KEYS = FIELDS.map((f) => f.key);\n\nexport const mainThreadUtilizationSpec: MetricSpec = {\n\ttitle: 'Main thread utilization',\n\tdescription: 'Per-node main-thread metrics — chip selector picks utilization / queue lag / active time / idle time.',\n\ttab: 'health',\n\tprimaryDimension: 'node',\n\tseries: {\n\t\tkind: 'field',\n\t\tfields: [{ field: FIELDS[0].field, label: FIELDS[0].label }],\n\t},\n\ttimestamp: 'time',\n\tbucket: { source: 'period-field', fallbackMs: 60000 },\n\taggregator: { temporal: 'mean', crossNode: 'mean' },\n\tprimitive: 'line',\n\tyAxis: FIELDS[0].yAxis,\n\tlayout: { colSpan: 2 },\n};\n\ninterface RendererProps {\n\trecords: AnalyticsDataPoint[];\n\ttimeRange: TimeRange;\n\tnodes: string[];\n\ttheme: 'light' | 'dark';\n\tviewMode?: 'per-node' | 'aggregate';\n\tfillParent?: boolean;\n}\n\nfunction shortenNodeLabel(node: string): string {\n\tconst dot = node.indexOf('.');\n\treturn dot === -1 ? node : node.slice(0, dot);\n}\n\nexport function MainThreadRenderer(\n\t{ records, timeRange, nodes, theme, viewMode = 'per-node', fillParent }: RendererProps,\n) {\n\tconst perNode = viewMode === 'per-node';\n\tconst [selectedKey, setSelectedKey] = useState<string>(FIELDS[0].key);\n\tconst effectiveKey = FIELD_KEYS.includes(selectedKey) ? selectedKey : FIELDS[0].key;\n\tconst selected = FIELDS.find((f) => f.key === effectiveKey)!;\n\n\tconst data = useMemo<SeriesData>(() => {\n\t\tconst innerSpec: MetricSpec = {\n\t\t\t...mainThreadUtilizationSpec,\n\t\t\tseries: { kind: 'field', fields: [{ field: selected.field, label: selected.label }] },\n\t\t\tyAxis: selected.yAxis,\n\t\t};\n\t\treturn runPipeline(innerSpec, records, timeRange, nodes, { perNode, snapToPeriod: true });\n\t}, [selected, records, timeRange, nodes, perNode]);\n\n\tconst { isActive, handleLegendClick } = useNodeSelection(nodes);\n\n\tconst filteredData: SeriesData = useMemo(() => ({\n\t\t...data,\n\t\tseries: data.series\n\t\t\t.map((s) => {\n\t\t\t\tconst sep = s.key.indexOf('|');\n\t\t\t\tconst node = sep === -1 ? '' : s.key.slice(sep + 1);\n\t\t\t\tif (!node) { return s; }\n\t\t\t\treturn { ...s, label: shortenNodeLabel(node), color: getNodeColor(node, nodes) };\n\t\t\t})\n\t\t\t.filter((s) => {\n\t\t\t\tconst sep = s.key.indexOf('|');\n\t\t\t\tconst node = sep === -1 ? '' : s.key.slice(sep + 1);\n\t\t\t\treturn node === '' || isActive(node);\n\t\t\t}),\n\t}), [data, nodes, isActive]);\n\n\treturn (\n\t\t<div className=\"flex h-full flex-col\">\n\t\t\t<DimensionChipRow\n\t\t\t\tdimensionValues={FIELD_KEYS.map((k) => FIELDS.find((f) => f.key === k)!.label)}\n\t\t\t\tselected={selected.label}\n\t\t\t\tonSelect={(label) => {\n\t\t\t\t\tconst f = FIELDS.find((x) => x.label === label);\n\t\t\t\t\tif (f) { setSelectedKey(f.key); }\n\t\t\t\t}}\n\t\t\t\tariaLabel=\"Main thread metric\"\n\t\t\t/>\n\t\t\t<div className=\"min-h-0 flex-1\" style={{ marginTop: 8 }}>\n\t\t\t\t<LineChart\n\t\t\t\t\tdata={filteredData}\n\t\t\t\t\ttheme={theme}\n\t\t\t\t\tyAxis={selected.yAxis}\n\t\t\t\t\txDomain={[timeRange.startTime, timeRange.endTime]}\n\t\t\t\t\tfillParent={fillParent}\n\t\t\t\t\thideLegend={perNode}\n\t\t\t\t/>\n\t\t\t</div>\n\t\t\t{perNode && nodes.length > 0 && (\n\t\t\t\t<NodeLegend\n\t\t\t\t\tnodeIds={nodes}\n\t\t\t\t\tisActive={isActive}\n\t\t\t\t\tonClickNode={handleLegendClick}\n\t\t\t\t/>\n\t\t\t)}\n\t\t</div>\n\t);\n}\n","import { useMemo, useState } from 'react';\nimport { NodeLegend } from '../charts/NodeLegend.tsx';\nimport { useNodeSelection } from '../hooks/useNodeSelection.ts';\nimport { getNodeColor } from '../lib/nodeColors.ts';\nimport { DimensionChipRow } from '../primitives/DimensionChipRow.tsx';\nimport { LineChart } from '../primitives/LineChart.tsx';\nimport type { AnalyticsDataPoint, FieldSpec, MetricSpec, SeriesData, TimeRange } from '../types/analytics.ts';\nimport { runPipeline } from './pipeline.ts';\n\n// Field-selector pattern (cousin of DimensionSelectorRenderer): chip-row\n// picks one memory field at a time and the chart shows that one with the\n// full panel real estate, per-node lines, shared NodeLegend below.\n// Replaces the prior 4-panel small-multiples grid that crammed each field\n// into a 280×180 mini-chart with multiple competing per-node lines.\n//\n// Aggregator { temporal: 'last', crossNode: 'mean' } — memory is a gauge.\n// `count` on memory records is THREAD count, not sample volume.\n\nconst MEMORY_FIELDS: ReadonlyArray<FieldSpec> = [\n\t{ field: 'heapUsed', label: 'heap used' },\n\t{ field: 'heapTotal', label: 'heap total' },\n\t{ field: 'external', label: 'external' },\n\t{ field: 'arrayBuffers', label: 'arrayBuffers' },\n];\n\nexport const memorySpec: MetricSpec = {\n\ttitle: 'Process memory',\n\tdescription: 'Per-node V8 memory — chip selector picks the field. Default heap used.',\n\ttab: 'health',\n\tprimaryDimension: 'node',\n\tseries: { kind: 'field', fields: [...MEMORY_FIELDS] },\n\ttimestamp: 'time',\n\tbucket: { source: 'period-field', fallbackMs: 60000 },\n\taggregator: { temporal: 'last', crossNode: 'mean' },\n\tprimitive: 'line',\n\tyAxis: { unit: ' B', formatter: 'bytes-si' },\n};\n\ninterface RendererProps {\n\trecords: AnalyticsDataPoint[];\n\ttimeRange: TimeRange;\n\tnodes: string[];\n\ttheme: 'light' | 'dark';\n\tviewMode?: 'per-node' | 'aggregate';\n\tfillParent?: boolean;\n}\n\nfunction shortenNodeLabel(node: string): string {\n\tconst dot = node.indexOf('.');\n\treturn dot === -1 ? node : node.slice(0, dot);\n}\n\nconst FIELD_LABELS = MEMORY_FIELDS.map((f) => f.label);\n\nexport function MemoryRenderer(\n\t{ records, timeRange, nodes, theme, viewMode = 'per-node', fillParent }: RendererProps,\n) {\n\tconst perNode = viewMode === 'per-node';\n\tconst [selectedLabel, setSelectedLabel] = useState<string>(MEMORY_FIELDS[0].label);\n\tconst effectiveLabel = FIELD_LABELS.includes(selectedLabel) ? selectedLabel : MEMORY_FIELDS[0].label;\n\tconst selectedField = MEMORY_FIELDS.find((f) => f.label === effectiveLabel)!;\n\n\tconst data = useMemo<SeriesData>(() => {\n\t\tconst innerSpec: MetricSpec = {\n\t\t\t...memorySpec,\n\t\t\tseries: { kind: 'field', fields: [selectedField] },\n\t\t};\n\t\treturn runPipeline(innerSpec, records, timeRange, nodes, { perNode, snapToPeriod: true });\n\t}, [selectedField, records, timeRange, nodes, perNode]);\n\n\tconst { isActive, handleLegendClick } = useNodeSelection(nodes);\n\n\tconst filteredData: SeriesData = useMemo(() => ({\n\t\t...data,\n\t\tseries: data.series\n\t\t\t.map((s) => {\n\t\t\t\tconst sep = s.key.indexOf('|');\n\t\t\t\tconst node = sep === -1 ? '' : s.key.slice(sep + 1);\n\t\t\t\tif (!node) { return s; }\n\t\t\t\treturn { ...s, label: shortenNodeLabel(node), color: getNodeColor(node, nodes) };\n\t\t\t})\n\t\t\t.filter((s) => {\n\t\t\t\tconst sep = s.key.indexOf('|');\n\t\t\t\tconst node = sep === -1 ? '' : s.key.slice(sep + 1);\n\t\t\t\treturn node === '' || isActive(node);\n\t\t\t}),\n\t}), [data, nodes, isActive]);\n\n\treturn (\n\t\t<div className=\"flex h-full flex-col\">\n\t\t\t<DimensionChipRow\n\t\t\t\tdimensionValues={FIELD_LABELS}\n\t\t\t\tselected={effectiveLabel}\n\t\t\t\tonSelect={setSelectedLabel}\n\t\t\t\tariaLabel=\"Memory field\"\n\t\t\t/>\n\t\t\t<div className=\"min-h-0 flex-1\" style={{ marginTop: 8 }}>\n\t\t\t\t<LineChart\n\t\t\t\t\tdata={filteredData}\n\t\t\t\t\ttheme={theme}\n\t\t\t\t\tyAxis={memorySpec.yAxis}\n\t\t\t\t\txDomain={[timeRange.startTime, timeRange.endTime]}\n\t\t\t\t\tfillParent={fillParent}\n\t\t\t\t\thideLegend={perNode}\n\t\t\t\t/>\n\t\t\t</div>\n\t\t\t{perNode && nodes.length > 0 && (\n\t\t\t\t<NodeLegend\n\t\t\t\t\tnodeIds={nodes}\n\t\t\t\t\tisActive={isActive}\n\t\t\t\t\tonClickNode={handleLegendClick}\n\t\t\t\t/>\n\t\t\t)}\n\t\t</div>\n\t);\n}\n","/**\n * @internal Exported for unit testing. Production callers should import the\n * re-export from HeatmapMatrix.tsx.\n *\n * Compute responsive cell size (px) for a heatmap grid.\n * - clamp((containerWidth - rowLabelWidth - gap*(colCount-1)) / colCount, min, max)\n * - Returns `min` when container is too narrow; `max` when too wide.\n */\nexport function computeCellSize(\n\tcontainerWidth: number,\n\tcolCount: number,\n\trowLabelWidth: number,\n\tgap: number,\n\tmin: number,\n\tmax: number,\n): number {\n\tif (colCount <= 0) { return max; }\n\tconst usable = containerWidth - rowLabelWidth - gap * Math.max(0, colCount - 1);\n\tconst perCell = Math.floor(usable / colCount);\n\treturn Math.max(min, Math.min(max, perCell));\n}\n","import { useCallback, useEffect, useId, useLayoutEffect, useMemo, useRef, useState } from 'react';\nimport type { KeyboardEvent as ReactKeyboardEvent } from 'react';\nimport type { AxisSpec, HeatmapCell, HeatmapData } from '../types/analytics.ts';\nimport { computeCellSize } from './computeCellSize.ts';\nimport { formatValue } from './formatValue.ts';\nexport { computeCellSize } from './computeCellSize.ts';\n\ninterface Props {\n\tdata: HeatmapData;\n\ttheme: 'light' | 'dark';\n\ttitle?: string;\n\theight?: number;\n}\n\ntype Confidence = 'ok' | 'grey' | 'suppress' | 'absent';\n\n// Light theme: pale → deep red (low → high latency).\nconst LIGHT_STOPS = ['#fef3c7', '#fcd34d', '#f59e0b', '#dc2626', '#7f1d1d'];\n// Dark theme: muted amber → cream.\nconst DARK_STOPS = ['#713f12', '#b45309', '#d97706', '#f59e0b', '#fef3c7'];\n\n// contrast-table (WCAG 2.1 SC 1.4.11 — 3:1 minimum)\n// Outer halo stroke computed dynamically from WCAG relative luminance:\n// luminance > 0.5 → black (#000000); else white (#ffffff).\n//\n// light theme stops (pale → deep red):\n// #fef3c7 L≈0.918 → black\n// #fcd34d L≈0.693 → black\n// #f59e0b L≈0.471 → white\n// #dc2626 L≈0.196 → white\n// #7f1d1d L≈0.064 → white\n//\n// dark theme stops (muted amber → cream):\n// #713f12 L≈0.068 → white\n// #b45309 L≈0.175 → white\n// #d97706 L≈0.330 → white\n// #f59e0b L≈0.471 → white\n// #fef3c7 L≈0.918 → black\n//\n// Halo: inner stroke accent (1px) + outer stroke b/w (1px) = ≥3:1 against any fill.\n\nfunction srgbToLinear(c: number): number {\n\tconst s = c / 255;\n\treturn s <= 0.03928 ? s / 12.92 : Math.pow((s + 0.055) / 1.055, 2.4);\n}\n\nfunction relativeLuminance(hex: string): number {\n\tconst h = hex.replace('#', '');\n\tconst r = parseInt(h.slice(0, 2), 16);\n\tconst g = parseInt(h.slice(2, 4), 16);\n\tconst b = parseInt(h.slice(4, 6), 16);\n\treturn 0.2126 * srgbToLinear(r) + 0.7152 * srgbToLinear(g) + 0.0722 * srgbToLinear(b);\n}\n\nfunction hexToRgb(hex: string): [number, number, number] {\n\tconst h = hex.replace('#', '');\n\treturn [parseInt(h.slice(0, 2), 16), parseInt(h.slice(2, 4), 16), parseInt(h.slice(4, 6), 16)];\n}\n\nfunction rgbToHex(r: number, g: number, b: number): string {\n\tconst h = (v: number) => Math.round(Math.max(0, Math.min(255, v))).toString(16).padStart(2, '0');\n\treturn `#${h(r)}${h(g)}${h(b)}`;\n}\n\nfunction interpolateStops(stops: string[], t: number): string {\n\tif (t <= 0) { return stops[0]; }\n\tif (t >= 1) { return stops[stops.length - 1]; }\n\tconst scaled = t * (stops.length - 1);\n\tconst idx = Math.floor(scaled);\n\tconst frac = scaled - idx;\n\tconst [r1, g1, b1] = hexToRgb(stops[idx]);\n\tconst [r2, g2, b2] = hexToRgb(stops[idx + 1]);\n\treturn rgbToHex(r1 + (r2 - r1) * frac, g1 + (g2 - g1) * frac, b1 + (b2 - b1) * frac);\n}\n\nfunction classifyCell(\n\tcell: HeatmapCell | undefined,\n\tgreyBelow: number,\n\tsuppressBelow: number,\n): Confidence {\n\tif (!cell || cell.value === null || cell.value === undefined) { return 'absent'; }\n\tconst count = cell.count ?? 0;\n\t// grey: greyBelow ≤ count < suppressBelow. suppress: count < greyBelow.\n\tif (count < greyBelow) { return 'suppress'; }\n\tif (count < suppressBelow) { return 'grey'; }\n\treturn 'ok';\n}\n\nfunction truncate(s: string, max: number): string {\n\treturn s.length > max ? s.slice(0, max - 1) + '…' : s;\n}\n\nfunction cellAriaLabel(\n\trow: string,\n\tcol: string,\n\tcell: HeatmapCell | undefined,\n\tconfidence: Confidence,\n\tunit: string,\n\tsuppressBelow: number,\n\tapprox: boolean,\n): string {\n\tconst prefix = `${row} → ${col}: `;\n\tconst approxTag = approx ? ' (approx)' : '';\n\tif (confidence === 'absent') {\n\t\treturn `${prefix}no data`;\n\t}\n\tif (confidence === 'suppress') {\n\t\treturn `${prefix}insufficient samples (n<${suppressBelow}), value hidden`;\n\t}\n\tconst value = cell?.value ?? 0;\n\tconst count = cell?.count ?? 0;\n\tif (confidence === 'grey') {\n\t\treturn `${prefix}${value} ${unit} p95 (approx, low-confidence: ${count} samples below threshold)`;\n\t}\n\treturn `${prefix}${value} ${unit} p95${approxTag}, ${count} samples`;\n}\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Color scale legend — horizontal gradient below the grid.\n// ─────────────────────────────────────────────────────────────────────────────\n\ninterface LegendProps {\n\tstops: string[];\n\tvmin: number;\n\tvmax: number;\n\twidth: number;\n\taxis?: AxisSpec;\n\tapprox: boolean;\n}\n\nfunction HeatmapColorLegend({ stops, vmin, vmax, width, axis, approx }: LegendProps) {\n\tconst gradientId = useId();\n\tconst unit = axis?.unit ?? '';\n\tconst fmt = (v: number) => formatValue(v, axis?.formatter);\n\tconst median = (vmin + vmax) / 2;\n\tconst stripWidth = Math.max(200, Math.min(width, 480));\n\tconst stripHeight = 12;\n\tconst labelPrefix = approx ? 'p95 latency (count-weighted-mean, approx)' : 'p95 latency';\n\tconst label = `${labelPrefix}: ${fmt(vmin)}–${fmt(vmax)} ${unit}. Higher value = worse latency.`;\n\treturn (\n\t\t<svg\n\t\t\trole=\"img\"\n\t\t\taria-label={label}\n\t\t\twidth={stripWidth + 80}\n\t\t\theight={40}\n\t\t\tstyle={{ overflow: 'visible' }}\n\t\t>\n\t\t\t<defs>\n\t\t\t\t<linearGradient id={gradientId} x1=\"0%\" x2=\"100%\" y1=\"0%\" y2=\"0%\">\n\t\t\t\t\t{stops.map((stop, i) => (\n\t\t\t\t\t\t<stop\n\t\t\t\t\t\t\tkey={stop + i}\n\t\t\t\t\t\t\toffset={`${(i / (stops.length - 1)) * 100}%`}\n\t\t\t\t\t\t\tstopColor={stop}\n\t\t\t\t\t\t/>\n\t\t\t\t\t))}\n\t\t\t\t</linearGradient>\n\t\t\t</defs>\n\t\t\t<text\n\t\t\t\tx={0}\n\t\t\t\ty={stripHeight + 2}\n\t\t\t\tfontSize={10}\n\t\t\t\tfill={axis ? 'currentColor' : 'currentColor'}\n\t\t\t\taria-hidden=\"true\"\n\t\t\t>\n\t\t\t\tlow\n\t\t\t</text>\n\t\t\t<rect\n\t\t\t\tx={30}\n\t\t\t\ty={0}\n\t\t\t\twidth={stripWidth}\n\t\t\t\theight={stripHeight}\n\t\t\t\tfill={`url(#${gradientId})`}\n\t\t\t\taria-hidden=\"true\"\n\t\t\t/>\n\t\t\t<text\n\t\t\t\tx={30 + stripWidth + 4}\n\t\t\t\ty={stripHeight + 2}\n\t\t\t\tfontSize={10}\n\t\t\t\tfill=\"currentColor\"\n\t\t\t\taria-hidden=\"true\"\n\t\t\t>\n\t\t\t\thigh\n\t\t\t</text>\n\t\t\t<text x={30} y={stripHeight + 18} fontSize={10} fill=\"currentColor\" aria-hidden=\"true\">\n\t\t\t\t{fmt(vmin)}\n\t\t\t</text>\n\t\t\t<text\n\t\t\t\tx={30 + stripWidth / 2}\n\t\t\t\ty={stripHeight + 18}\n\t\t\t\tfontSize={10}\n\t\t\t\tfill=\"currentColor\"\n\t\t\t\ttextAnchor=\"middle\"\n\t\t\t\taria-hidden=\"true\"\n\t\t\t>\n\t\t\t\t{fmt(median)}\n\t\t\t</text>\n\t\t\t<text\n\t\t\t\tx={30 + stripWidth}\n\t\t\t\ty={stripHeight + 18}\n\t\t\t\tfontSize={10}\n\t\t\t\tfill=\"currentColor\"\n\t\t\t\ttextAnchor=\"end\"\n\t\t\t\taria-hidden=\"true\"\n\t\t\t>\n\t\t\t\t{fmt(vmax)}\n\t\t\t</text>\n\t\t</svg>\n\t);\n}\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Main HeatmapMatrix component\n// ─────────────────────────────────────────────────────────────────────────────\n\nconst MIN_CELL_SIZE = 40;\nconst MAX_CELL_SIZE = 80;\nconst CELL_GAP = 4;\nconst HEADER_HEIGHT = 72; // reserved for rotated column headers\nconst ROW_LABEL_WIDTH = 200;\n\nexport function HeatmapMatrix({ data, theme, title, height }: Props) {\n\tconst titleId = useId();\n\tconst descId = useId();\n\tconst suppressPatternId = useId();\n\n\tconst greyBelow = data.confidence?.greyBelow ?? 0;\n\tconst suppressBelow = data.confidence?.suppressBelow ?? 0;\n\tconst stops = theme === 'dark' ? DARK_STOPS : LIGHT_STOPS;\n\tconst approx = data.approx === true;\n\tconst unit = data.axis?.unit ?? '';\n\n\t// Build a (row,col) -> cell index for O(1) lookup.\n\tconst cellMap = useMemo(() => {\n\t\tconst m = new Map<string, HeatmapCell>();\n\t\tfor (const c of data.cells) { m.set(`${c.row}|${c.col}`, c); }\n\t\treturn m;\n\t}, [data.cells]);\n\n\t// Compute value range for color scaling.\n\tconst [vmin, vmax] = useMemo(() => {\n\t\tif (data.valueRange) { return [data.valueRange.min, data.valueRange.max]; }\n\t\tlet lo = Infinity;\n\t\tlet hi = -Infinity;\n\t\tfor (const c of data.cells) {\n\t\t\tif (c.value !== null && c.value !== undefined && Number.isFinite(c.value)) {\n\t\t\t\tif (c.value < lo) { lo = c.value; }\n\t\t\t\tif (c.value > hi) { hi = c.value; }\n\t\t\t}\n\t\t}\n\t\tif (!Number.isFinite(lo) || !Number.isFinite(hi)) { return [0, 1]; }\n\t\tif (lo === hi) { return [lo, lo + 1]; }\n\t\treturn [lo, hi];\n\t}, [data.cells, data.valueRange]);\n\n\tconst rows = data.rows;\n\tconst cols = data.cols;\n\n\t// Responsive cell sizing: measure wrapper width, clamp to [MIN, MAX].\n\tconst wrapperRef = useRef<HTMLDivElement>(null);\n\tconst [cellSize, setCellSize] = useState<number>(MAX_CELL_SIZE);\n\tconst rafRef = useRef<number | null>(null);\n\n\tuseLayoutEffect(() => {\n\t\tconst w = wrapperRef.current?.clientWidth ?? 0;\n\t\tsetCellSize(\n\t\t\tcomputeCellSize(w, cols.length, ROW_LABEL_WIDTH, CELL_GAP, MIN_CELL_SIZE, MAX_CELL_SIZE),\n\t\t);\n\t}, [cols.length]);\n\n\tuseEffect(() => {\n\t\tconst el = wrapperRef.current;\n\t\tif (!el) { return; }\n\t\tconst ro = new ResizeObserver(() => {\n\t\t\tif (rafRef.current !== null) { cancelAnimationFrame(rafRef.current); }\n\t\t\trafRef.current = requestAnimationFrame(() => {\n\t\t\t\trafRef.current = null;\n\t\t\t\tconst w = el.clientWidth;\n\t\t\t\tsetCellSize(\n\t\t\t\t\tcomputeCellSize(w, cols.length, ROW_LABEL_WIDTH, CELL_GAP, MIN_CELL_SIZE, MAX_CELL_SIZE),\n\t\t\t\t);\n\t\t\t});\n\t\t});\n\t\tro.observe(el);\n\t\treturn () => {\n\t\t\tro.disconnect();\n\t\t\tif (rafRef.current !== null) { cancelAnimationFrame(rafRef.current); }\n\t\t};\n\t}, [cols.length]);\n\n\tconst gridWidth = cols.length * cellSize + (cols.length - 1) * CELL_GAP;\n\tconst gridHeight = rows.length * cellSize + (rows.length - 1) * CELL_GAP;\n\tconst svgWidth = ROW_LABEL_WIDTH + gridWidth + 8;\n\tconst svgHeight = HEADER_HEIGHT + gridHeight + 8;\n\n\t// Roving tabindex state: [rowIdx, colIdx]\n\tconst [active, setActive] = useState<[number, number]>([0, 0]);\n\tconst cellRefs = useRef<Map<string, HTMLElement>>(new Map());\n\n\tconst focusCell = useCallback((r: number, c: number) => {\n\t\tconst el = cellRefs.current.get(`${r}|${c}`);\n\t\tif (el) {\n\t\t\tsetActive([r, c]);\n\t\t\tel.focus();\n\t\t}\n\t}, []);\n\n\tconst handleKeyDown = useCallback(\n\t\t(e: ReactKeyboardEvent<SVGGElement>, r: number, c: number) => {\n\t\t\tconst k = e.key;\n\t\t\tlet handled = false;\n\t\t\tlet nr = r;\n\t\t\tlet nc = c;\n\t\t\tif (k === 'ArrowRight') {\n\t\t\t\thandled = true;\n\t\t\t\tnc = Math.min(cols.length - 1, c + 1);\n\t\t\t} else if (k === 'ArrowLeft') {\n\t\t\t\thandled = true;\n\t\t\t\tnc = Math.max(0, c - 1);\n\t\t\t} else if (k === 'ArrowDown') {\n\t\t\t\thandled = true;\n\t\t\t\tnr = Math.min(rows.length - 1, r + 1);\n\t\t\t} else if (k === 'ArrowUp') {\n\t\t\t\thandled = true;\n\t\t\t\tnr = Math.max(0, r - 1);\n\t\t\t} else if (k === 'Home') {\n\t\t\t\thandled = true;\n\t\t\t\tnc = 0;\n\t\t\t} else if (k === 'End') {\n\t\t\t\thandled = true;\n\t\t\t\tnc = cols.length - 1;\n\t\t\t} else if (k === 'PageUp') {\n\t\t\t\thandled = true;\n\t\t\t\tnr = Math.max(0, r - 5);\n\t\t\t} else if (k === 'PageDown') {\n\t\t\t\thandled = true;\n\t\t\t\tnr = Math.min(rows.length - 1, r + 5);\n\t\t\t}\n\t\t\tif (!handled) { return; }\n\t\t\te.preventDefault();\n\t\t\te.stopPropagation();\n\t\t\tif (nr !== r || nc !== c) { focusCell(nr, nc); }\n\t\t},\n\t\t[rows.length, cols.length, focusCell],\n\t);\n\n\t// Focus halo stroke color based on cell fill luminance.\n\tconst haloOuter = (fill: string): string => {\n\t\tif (!fill.startsWith('#')) { return '#000000'; }\n\t\treturn relativeLuminance(fill) > 0.5 ? '#000000' : '#ffffff';\n\t};\n\n\treturn (\n\t\t<div style={{ position: 'relative' }}>\n\t\t\t{/* Skipped-records status banner */}\n\t\t\t{data.skippedRecordsCount > 0\n\t\t\t\t? (\n\t\t\t\t\t<div\n\t\t\t\t\t\tkey={data.skippedRecordsCount}\n\t\t\t\t\t\trole=\"status\"\n\t\t\t\t\t\taria-atomic=\"true\"\n\t\t\t\t\t\tstyle={{\n\t\t\t\t\t\t\tmarginBottom: 8,\n\t\t\t\t\t\t\tpadding: '4px 8px',\n\t\t\t\t\t\t\tfontSize: 12,\n\t\t\t\t\t\t\tborderLeft: '3px solid var(--color-warning)',\n\t\t\t\t\t\t\tbackground: 'color-mix(in srgb, var(--color-warning) 12%, transparent)',\n\t\t\t\t\t\t\tcolor: 'currentColor',\n\t\t\t\t\t\t}}\n\t\t\t\t\t>\n\t\t\t\t\t\t{`${data.skippedRecordsCount} record(s) omitted — source node unrecognized (cluster node list may be outdated).`}\n\t\t\t\t\t</div>\n\t\t\t\t)\n\t\t\t\t: null}\n\n\t\t\t{/* Visually-hidden title + description */}\n\t\t\t<p\n\t\t\t\tid={titleId}\n\t\t\t\tstyle={{\n\t\t\t\t\tposition: 'absolute',\n\t\t\t\t\twidth: 1,\n\t\t\t\t\theight: 1,\n\t\t\t\t\tpadding: 0,\n\t\t\t\t\tmargin: -1,\n\t\t\t\t\toverflow: 'hidden',\n\t\t\t\t\tclip: 'rect(0, 0, 0, 0)',\n\t\t\t\t\twhiteSpace: 'nowrap',\n\t\t\t\t\tborder: 0,\n\t\t\t\t}}\n\t\t\t>\n\t\t\t\t{title ?? 'Heatmap'}\n\t\t\t</p>\n\t\t\t<p\n\t\t\t\tid={descId}\n\t\t\t\tdata-testid=\"heatmap-desc\"\n\t\t\t\tstyle={{\n\t\t\t\t\tposition: 'absolute',\n\t\t\t\t\twidth: 1,\n\t\t\t\t\theight: 1,\n\t\t\t\t\tpadding: 0,\n\t\t\t\t\tmargin: -1,\n\t\t\t\t\toverflow: 'hidden',\n\t\t\t\t\tclip: 'rect(0, 0, 0, 0)',\n\t\t\t\t\twhiteSpace: 'nowrap',\n\t\t\t\t\tborder: 0,\n\t\t\t\t}}\n\t\t\t>\n\t\t\t\t{(() => {\n\t\t\t\t\tconst descPrefix = approx\n\t\t\t\t\t\t? 'Cells show count-weighted-mean p95 latency (approx).'\n\t\t\t\t\t\t: 'Cells show p95 latency.';\n\t\t\t\t\treturn `${descPrefix} Cells with fewer than ${suppressBelow} samples are blank; ${greyBelow}–${\n\t\t\t\t\t\tsuppressBelow - 1\n\t\t\t\t\t} render grey.`;\n\t\t\t\t})()}\n\t\t\t</p>\n\n\t\t\t<div ref={wrapperRef} style={{ width: '100%', display: 'block', overflowX: 'auto' }}>\n\t\t\t\t<svg\n\t\t\t\t\trole=\"grid\"\n\t\t\t\t\taria-labelledby={titleId}\n\t\t\t\t\taria-describedby={descId}\n\t\t\t\t\twidth={svgWidth}\n\t\t\t\t\theight={height ?? svgHeight}\n\t\t\t\t\tviewBox={`0 0 ${svgWidth} ${svgHeight}`}\n\t\t\t\t\tdata-cell-size={cellSize}\n\t\t\t\t\tstyle={{ overflow: 'visible', display: 'block' }}\n\t\t\t\t>\n\t\t\t\t\t<defs>\n\t\t\t\t\t\t<pattern\n\t\t\t\t\t\t\tid={suppressPatternId}\n\t\t\t\t\t\t\tpatternUnits=\"userSpaceOnUse\"\n\t\t\t\t\t\t\twidth={8}\n\t\t\t\t\t\t\theight={8}\n\t\t\t\t\t\t\tpatternTransform=\"rotate(45)\"\n\t\t\t\t\t\t>\n\t\t\t\t\t\t\t<rect width={8} height={8} fill={theme === 'dark' ? '#1f2937' : '#f3f4f6'} />\n\t\t\t\t\t\t\t<line\n\t\t\t\t\t\t\t\tx1={0}\n\t\t\t\t\t\t\t\ty1={0}\n\t\t\t\t\t\t\t\tx2={0}\n\t\t\t\t\t\t\t\ty2={8}\n\t\t\t\t\t\t\t\tstroke={theme === 'dark' ? '#4b5563' : '#9ca3af'}\n\t\t\t\t\t\t\t\tstrokeWidth={2}\n\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t</pattern>\n\t\t\t\t\t</defs>\n\n\t\t\t\t\t{/* Header row: corner spacer + column headers */}\n\t\t\t\t\t<g role=\"row\">\n\t\t\t\t\t\t{/* corner spacer (no role) */}\n\t\t\t\t\t\t<rect\n\t\t\t\t\t\t\tx={0}\n\t\t\t\t\t\t\ty={0}\n\t\t\t\t\t\t\twidth={ROW_LABEL_WIDTH}\n\t\t\t\t\t\t\theight={HEADER_HEIGHT}\n\t\t\t\t\t\t\tfill=\"transparent\"\n\t\t\t\t\t\t\taria-hidden=\"true\"\n\t\t\t\t\t\t/>\n\t\t\t\t\t\t{cols.map((col, ci) => {\n\t\t\t\t\t\t\tconst cx = ROW_LABEL_WIDTH + ci * (cellSize + CELL_GAP) + cellSize / 2;\n\t\t\t\t\t\t\tconst cy = HEADER_HEIGHT - 8;\n\t\t\t\t\t\t\tconst truncateLength = cellSize < 56 ? 8 : 20;\n\t\t\t\t\t\t\treturn (\n\t\t\t\t\t\t\t\t<g key={col} role=\"columnheader\" aria-label={col}>\n\t\t\t\t\t\t\t\t\t<text\n\t\t\t\t\t\t\t\t\t\tx={cx}\n\t\t\t\t\t\t\t\t\t\ty={cy}\n\t\t\t\t\t\t\t\t\t\tfontSize={11}\n\t\t\t\t\t\t\t\t\t\ttextAnchor=\"end\"\n\t\t\t\t\t\t\t\t\t\ttransform={`rotate(-45, ${cx}, ${cy})`}\n\t\t\t\t\t\t\t\t\t\tfill=\"currentColor\"\n\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\t{truncate(col, truncateLength)}\n\t\t\t\t\t\t\t\t\t\t<title>{col}</title>\n\t\t\t\t\t\t\t\t\t</text>\n\t\t\t\t\t\t\t\t</g>\n\t\t\t\t\t\t\t);\n\t\t\t\t\t\t})}\n\t\t\t\t\t</g>\n\n\t\t\t\t\t{/* Data rows */}\n\t\t\t\t\t{rows.map((row, ri) => {\n\t\t\t\t\t\tconst y = HEADER_HEIGHT + ri * (cellSize + CELL_GAP);\n\t\t\t\t\t\treturn (\n\t\t\t\t\t\t\t<g key={row} role=\"row\">\n\t\t\t\t\t\t\t\t{/* Row header */}\n\t\t\t\t\t\t\t\t<g role=\"rowheader\" aria-label={row}>\n\t\t\t\t\t\t\t\t\t<text\n\t\t\t\t\t\t\t\t\t\tx={ROW_LABEL_WIDTH - 8}\n\t\t\t\t\t\t\t\t\t\ty={y + cellSize / 2 + 4}\n\t\t\t\t\t\t\t\t\t\tfontSize={12}\n\t\t\t\t\t\t\t\t\t\ttextAnchor=\"end\"\n\t\t\t\t\t\t\t\t\t\tfill=\"currentColor\"\n\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\t{truncate(row, 24)}\n\t\t\t\t\t\t\t\t\t\t<title>{row}</title>\n\t\t\t\t\t\t\t\t\t</text>\n\t\t\t\t\t\t\t\t</g>\n\t\t\t\t\t\t\t\t{cols.map((col, ci) => {\n\t\t\t\t\t\t\t\t\tconst cell = cellMap.get(`${row}|${col}`);\n\t\t\t\t\t\t\t\t\tconst confidence = classifyCell(cell, greyBelow, suppressBelow);\n\t\t\t\t\t\t\t\t\tconst cx = ROW_LABEL_WIDTH + ci * (cellSize + CELL_GAP);\n\t\t\t\t\t\t\t\t\tconst cy = y;\n\t\t\t\t\t\t\t\t\tconst aria = cellAriaLabel(row, col, cell, confidence, unit, suppressBelow, approx);\n\n\t\t\t\t\t\t\t\t\tconst isActive = active[0] === ri && active[1] === ci;\n\n\t\t\t\t\t\t\t\t\t// Visual fill\n\t\t\t\t\t\t\t\t\tlet fill = 'transparent';\n\t\t\t\t\t\t\t\t\tlet strokeDasharray: string | undefined;\n\t\t\t\t\t\t\t\t\tlet opacity = 1;\n\t\t\t\t\t\t\t\t\tlet stroke: string | undefined;\n\t\t\t\t\t\t\t\t\tlet rectStrokeWidth = 0;\n\n\t\t\t\t\t\t\t\t\tif (confidence === 'absent') {\n\t\t\t\t\t\t\t\t\t\tfill = 'transparent';\n\t\t\t\t\t\t\t\t\t\tstrokeDasharray = '3 3';\n\t\t\t\t\t\t\t\t\t\tstroke = theme === 'dark' ? '#4b5563' : '#9ca3af';\n\t\t\t\t\t\t\t\t\t\trectStrokeWidth = 1;\n\t\t\t\t\t\t\t\t\t} else if (confidence === 'suppress') {\n\t\t\t\t\t\t\t\t\t\tfill = `url(#${suppressPatternId})`;\n\t\t\t\t\t\t\t\t\t\tstrokeDasharray = '3 3';\n\t\t\t\t\t\t\t\t\t\tstroke = theme === 'dark' ? '#4b5563' : '#9ca3af';\n\t\t\t\t\t\t\t\t\t\trectStrokeWidth = 1;\n\t\t\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\t\t\tconst t = vmax > vmin ? ((cell?.value ?? 0) - vmin) / (vmax - vmin) : 0;\n\t\t\t\t\t\t\t\t\t\tfill = interpolateStops(stops, t);\n\t\t\t\t\t\t\t\t\t\tif (confidence === 'grey') {\n\t\t\t\t\t\t\t\t\t\t\topacity = 0.55;\n\t\t\t\t\t\t\t\t\t\t\tstrokeDasharray = '4 2';\n\t\t\t\t\t\t\t\t\t\t\tstroke = theme === 'dark' ? '#6b7280' : '#9ca3af';\n\t\t\t\t\t\t\t\t\t\t\trectStrokeWidth = 1;\n\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\t\t\tconst haloFill = confidence === 'ok' || confidence === 'grey'\n\t\t\t\t\t\t\t\t\t\t? interpolateStops(stops, vmax > vmin ? ((cell?.value ?? 0) - vmin) / (vmax - vmin) : 0)\n\t\t\t\t\t\t\t\t\t\t: '#808080';\n\t\t\t\t\t\t\t\t\tconst outer = haloOuter(haloFill);\n\n\t\t\t\t\t\t\t\t\tconst setRef = (el: SVGGElement | null) => {\n\t\t\t\t\t\t\t\t\t\tif (el) { cellRefs.current.set(`${ri}|${ci}`, el as unknown as HTMLElement); }\n\t\t\t\t\t\t\t\t\t\telse { cellRefs.current.delete(`${ri}|${ci}`); }\n\t\t\t\t\t\t\t\t\t};\n\n\t\t\t\t\t\t\t\t\treturn (\n\t\t\t\t\t\t\t\t\t\t<g\n\t\t\t\t\t\t\t\t\t\t\tkey={col}\n\t\t\t\t\t\t\t\t\t\t\tref={setRef}\n\t\t\t\t\t\t\t\t\t\t\trole=\"gridcell\"\n\t\t\t\t\t\t\t\t\t\t\taria-label={aria}\n\t\t\t\t\t\t\t\t\t\t\tdata-confidence={confidence}\n\t\t\t\t\t\t\t\t\t\t\ttabIndex={isActive ? 0 : -1}\n\t\t\t\t\t\t\t\t\t\t\tonKeyDown={(e) => handleKeyDown(e, ri, ci)}\n\t\t\t\t\t\t\t\t\t\t\tonFocus={() => setActive([ri, ci])}\n\t\t\t\t\t\t\t\t\t\t\tstyle={{ outline: 'none', cursor: 'default' }}\n\t\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\t\t<rect\n\t\t\t\t\t\t\t\t\t\t\t\tx={cx}\n\t\t\t\t\t\t\t\t\t\t\t\ty={cy}\n\t\t\t\t\t\t\t\t\t\t\t\twidth={cellSize}\n\t\t\t\t\t\t\t\t\t\t\t\theight={cellSize}\n\t\t\t\t\t\t\t\t\t\t\t\trx={4}\n\t\t\t\t\t\t\t\t\t\t\t\try={4}\n\t\t\t\t\t\t\t\t\t\t\t\tstyle={{ fill }}\n\t\t\t\t\t\t\t\t\t\t\t\topacity={opacity}\n\t\t\t\t\t\t\t\t\t\t\t\tstroke={stroke}\n\t\t\t\t\t\t\t\t\t\t\t\tstrokeWidth={rectStrokeWidth}\n\t\t\t\t\t\t\t\t\t\t\t\tstrokeDasharray={strokeDasharray}\n\t\t\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\t\t\t<title>{aria}</title>\n\t\t\t\t\t\t\t\t\t\t\t</rect>\n\t\t\t\t\t\t\t\t\t\t\t{isActive\n\t\t\t\t\t\t\t\t\t\t\t\t? (\n\t\t\t\t\t\t\t\t\t\t\t\t\t<>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t<rect\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tx={cx - 1}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\ty={cy - 1}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\twidth={cellSize + 2}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\theight={cellSize + 2}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\trx={5}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\try={5}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tfill=\"none\"\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tstroke={outer}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tstrokeWidth={1}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tpointerEvents=\"none\"\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t<rect\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tx={cx}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\ty={cy}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\twidth={cellSize}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\theight={cellSize}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\trx={4}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\try={4}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tfill=\"none\"\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tstroke=\"var(--color-accent, #3b82f6)\"\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tstrokeWidth={1}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tpointerEvents=\"none\"\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t\t\t\t\t\t\t</>\n\t\t\t\t\t\t\t\t\t\t\t\t)\n\t\t\t\t\t\t\t\t\t\t\t\t: null}\n\t\t\t\t\t\t\t\t\t\t</g>\n\t\t\t\t\t\t\t\t\t);\n\t\t\t\t\t\t\t\t})}\n\t\t\t\t\t\t\t</g>\n\t\t\t\t\t\t);\n\t\t\t\t\t})}\n\t\t\t\t</svg>\n\t\t\t</div>\n\n\t\t\t{/* Color-scale legend below grid */}\n\t\t\t<div style={{ marginTop: 8 }}>\n\t\t\t\t<HeatmapColorLegend\n\t\t\t\t\tstops={stops}\n\t\t\t\t\tvmin={vmin}\n\t\t\t\t\tvmax={vmax}\n\t\t\t\t\twidth={gridWidth}\n\t\t\t\t\taxis={data.axis}\n\t\t\t\t\tapprox={approx}\n\t\t\t\t/>\n\t\t\t</div>\n\t\t</div>\n\t);\n}\n","export interface ParsedReplicationPath {\n\tsource: string;\n\tdatabase: string;\n\ttable: string;\n}\n\n/** Parse a replication-latency `path` field shaped `<source>.<db>.<table>`.\n *\n * Two-stage parse:\n * 1. **Anchored** — try the longest matching node from `knownNodes`. If\n * a hostname prefix matches exactly, this is precise.\n * 2. **Heuristic fallback** — if no known-node prefix matches, treat the\n * last two dot-segments as `<db>.<table>` and everything before as the\n * source FQDN. Recovers records whose source isn't a destination in\n * the current cluster snapshot (asymmetric replication, decommissioned\n * nodes, gateways that send but never receive). May mis-parse if a\n * database name itself contains dots — accept the trade since the\n * alternative is silently dropping the record.\n *\n * Returns null only when the path is empty, < 3 segments, or has empty\n * trailing segments. */\nexport function parseReplicationPath(\n\tpath: string,\n\tknownNodes: readonly string[],\n): ParsedReplicationPath | null {\n\tif (typeof path !== 'string' || path.length === 0) { return null; }\n\n\tconst sorted = [...knownNodes].sort((a, b) => b.length - a.length);\n\n\tfor (const node of sorted) {\n\t\tif (!path.startsWith(node + '.')) { continue; }\n\t\tconst rest = path.slice(node.length + 1);\n\t\tconst firstDot = rest.indexOf('.');\n\t\tif (firstDot === -1) { return null; }\n\t\tconst database = rest.slice(0, firstDot);\n\t\tconst table = rest.slice(firstDot + 1);\n\t\tif (database.length === 0 || table.length === 0) { return null; }\n\t\treturn { source: node, database, table };\n\t}\n\n\t// Heuristic: split on '.', last two segments are db.table.\n\tconst segments = path.split('.');\n\tif (segments.length < 3) { return null; }\n\tconst table = segments[segments.length - 1];\n\tconst database = segments[segments.length - 2];\n\tconst source = segments.slice(0, -2).join('.');\n\tif (!source || !database || !table) { return null; }\n\treturn { source, database, table };\n}\n","import { type JSX, useMemo, useState } from 'react';\nimport { HeatmapMatrix } from '../primitives/HeatmapMatrix.tsx';\nimport { LineChart } from '../primitives/LineChart.tsx';\nimport type {\n\tAnalyticsDataPoint,\n\tHeatmapCell,\n\tHeatmapData,\n\tMetricSpec,\n\tSeries,\n\tSeriesData,\n\tSeriesPoint,\n\tSpecRegistryRendererProps,\n} from '../types/analytics.ts';\nimport { type AggInput, aggregate } from './aggregators.ts';\nimport { parseReplicationPath } from './pathParser.ts';\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Spec\n// ─────────────────────────────────────────────────────────────────────────────\n\nexport const replicationLatencySpec: MetricSpec = {\n\ttitle: 'Replication latency',\n\tdescription: 'Source → destination p95 latency, count-weighted-mean across the window. Approximate.',\n\ttab: 'replication',\n\tprimaryDimension: 'path',\n\tsubDimension: 'node',\n\tseries: {\n\t\tkind: 'groupBy',\n\t\tdimension: 'path',\n\t\tfield: { field: 'p95', label: 'p95 latency' },\n\t},\n\ttimestamp: 'time',\n\tbucket: { source: 'period-field', fallbackMs: 60000 },\n\taggregator: { temporal: 'count-weighted-mean', crossNode: 'count-weighted-mean' },\n\t// TODO(spec): {greyBelow:40, suppressBelow:100} are calibrated for high-volume clusters.\n\t// Real Harper data with per-record count 2-14 may need re-tuning. See Step 2.5 follow-up.\n\tconfidence: { field: 'count', greyBelow: 40, suppressBelow: 100 },\n\tprimitive: 'heatmap',\n\tyAxis: { unit: '', formatter: 'ms' },\n};\n\nfunction pluralize(n: number, one: string, many: string): string {\n\treturn n === 1 ? one : many;\n}\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Pure pipeline: records -> HeatmapData\n// ─────────────────────────────────────────────────────────────────────────────\n\ninterface ParsedRecord {\n\tsource: string;\n\tdestination: string;\n\tvalue: number;\n\tcount: number;\n\ttime: number;\n}\n\nimport { QUANTILE_FIELDS as REPLICATION_QUANTILE_FIELDS, type QuantileField } from './quantileFields.ts';\nexport { REPLICATION_QUANTILE_FIELDS };\nexport type ReplicationQuantileField = QuantileField['field'];\n\nfunction parseRecords(\n\trecords: AnalyticsDataPoint[],\n\tnodes: readonly string[],\n\tquantileField: ReplicationQuantileField,\n): { parsed: ParsedRecord[]; skipped: number; unrecognizedSources: string[] } {\n\tlet skipped = 0;\n\tconst parsed: ParsedRecord[] = [];\n\tconst unrecognized = new Set<string>();\n\tconst knownSet = new Set(nodes);\n\tfor (const r of records) {\n\t\tconst path = typeof r.path === 'string' ? r.path : '';\n\t\tconst parsedPath = parseReplicationPath(path, nodes);\n\t\tif (!parsedPath) {\n\t\t\tskipped++;\n\t\t\tcontinue;\n\t\t}\n\t\t// pathParser falls back to a heuristic split when no known-node\n\t\t// matches. Track the heuristic-recovered sources so the renderer\n\t\t// can surface them — the operator may want to confirm those are\n\t\t// real peers.\n\t\tif (!knownSet.has(parsedPath.source)) {\n\t\t\tunrecognized.add(parsedPath.source);\n\t\t}\n\t\tconst v = (r as Record<string, unknown>)[quantileField];\n\t\tconst value = typeof v === 'number' ? v : NaN;\n\t\tconst count = typeof r.count === 'number' ? r.count : 0;\n\t\tif (!Number.isFinite(value)) {\n\t\t\tskipped++;\n\t\t\tcontinue;\n\t\t}\n\t\tparsed.push({\n\t\t\tsource: parsedPath.source,\n\t\t\tdestination: r.node,\n\t\t\tvalue,\n\t\t\tcount,\n\t\t\ttime: typeof r.time === 'number' ? r.time : 0,\n\t\t});\n\t}\n\treturn { parsed, skipped, unrecognizedSources: [...unrecognized].sort() };\n}\n\nexport function aggregateReplicationMatrix(\n\trecords: AnalyticsDataPoint[],\n\tnodes: readonly string[],\n\tquantileField: ReplicationQuantileField = 'p95',\n): HeatmapData {\n\tconst { parsed, skipped, unrecognizedSources } = parseRecords(records, nodes, quantileField);\n\n\tif (parsed.length === 0) {\n\t\treturn {\n\t\t\trows: [],\n\t\t\tcols: [],\n\t\t\tcells: [],\n\t\t\taxis: { unit: '', formatter: 'ms' },\n\t\t\tconfidence: { greyBelow: 40, suppressBelow: 100 },\n\t\t\trowAxisLabel: 'Source',\n\t\t\tcolAxisLabel: 'Destination',\n\t\t\tskippedRecordsCount: skipped,\n\t\t\tunrecognizedSources,\n\t\t\tapprox: true,\n\t\t};\n\t}\n\n\t// Group by (source, destination)\n\tconst groups = new Map<string, { items: AggInput[]; totalCount: number }>();\n\tconst sourceSet = new Set<string>();\n\tconst destSet = new Set<string>();\n\tfor (const r of parsed) {\n\t\tsourceSet.add(r.source);\n\t\tdestSet.add(r.destination);\n\t\tconst key = `${r.source}|${r.destination}`;\n\t\tlet g = groups.get(key);\n\t\tif (!g) {\n\t\t\tg = { items: [], totalCount: 0 };\n\t\t\tgroups.set(key, g);\n\t\t}\n\t\tg.items.push({ value: r.value, count: r.count });\n\t\tg.totalCount += r.count;\n\t}\n\n\tconst rows = [...sourceSet].sort();\n\tconst cols = [...destSet].sort();\n\n\tlet approx = false;\n\tconst cells: HeatmapCell[] = [];\n\tfor (const row of rows) {\n\t\tfor (const col of cols) {\n\t\t\tconst g = groups.get(`${row}|${col}`);\n\t\t\tif (g) {\n\t\t\t\tif (g.items.length > 1) { approx = true; }\n\t\t\t\tcells.push({\n\t\t\t\t\trow,\n\t\t\t\t\tcol,\n\t\t\t\t\tvalue: aggregate('count-weighted-mean', g.items),\n\t\t\t\t\tcount: g.totalCount,\n\t\t\t\t});\n\t\t\t} else {\n\t\t\t\tcells.push({ row, col, value: null, count: 0 });\n\t\t\t}\n\t\t}\n\t}\n\n\treturn {\n\t\trows,\n\t\tcols,\n\t\tcells,\n\t\taxis: { unit: '', formatter: 'ms' },\n\t\tconfidence: { greyBelow: 40, suppressBelow: 100 },\n\t\trowAxisLabel: 'Source',\n\t\tcolAxisLabel: 'Destination',\n\t\tskippedRecordsCount: skipped,\n\t\tunrecognizedSources,\n\t\tapprox,\n\t};\n}\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Pure helper: per (source, dest) line series with count-weighted-mean buckets\n// keyed by record.time. Returns approx=true when any time-bucket aggregates\n// more than one source record (e.g. multiple table paths).\n// ─────────────────────────────────────────────────────────────────────────────\n\nexport function bucketLineSeries(\n\trecords: AnalyticsDataPoint[],\n\tsource: string,\n\tdest: string,\n\tnodes: readonly string[],\n\tquantileField: ReplicationQuantileField = 'p95',\n): { points: SeriesPoint[]; approx: boolean } {\n\tconst matching: { time: number; value: number; count: number }[] = [];\n\tfor (const r of records) {\n\t\tif (r.node !== dest) { continue; }\n\t\tconst path = typeof r.path === 'string' ? r.path : '';\n\t\tconst parsed = parseReplicationPath(path, nodes);\n\t\tif (!parsed || parsed.source !== source) { continue; }\n\t\tif (typeof r.time !== 'number') { continue; }\n\t\tconst v = (r as Record<string, unknown>)[quantileField];\n\t\tconst value = typeof v === 'number' ? v : NaN;\n\t\tif (!Number.isFinite(value)) { continue; }\n\t\tconst count = typeof r.count === 'number' ? r.count : 0;\n\t\tmatching.push({ time: r.time, value, count });\n\t}\n\n\tconst bucketsByTime = new Map<number, AggInput[]>();\n\tconst totalCountByTime = new Map<number, number>();\n\tfor (const m of matching) {\n\t\tlet bucket = bucketsByTime.get(m.time);\n\t\tif (!bucket) {\n\t\t\tbucket = [];\n\t\t\tbucketsByTime.set(m.time, bucket);\n\t\t}\n\t\tbucket.push({ value: m.value, count: m.count });\n\t\ttotalCountByTime.set(m.time, (totalCountByTime.get(m.time) ?? 0) + m.count);\n\t}\n\n\tlet approx = false;\n\tconst sortedTimes = [...bucketsByTime.keys()].sort((a, b) => a - b);\n\tconst points: SeriesPoint[] = [];\n\tfor (const time of sortedTimes) {\n\t\tconst bucket = bucketsByTime.get(time)!;\n\t\tif (bucket.length > 1) { approx = true; }\n\t\tpoints.push({\n\t\t\tx: time,\n\t\t\ty: aggregate('count-weighted-mean', bucket),\n\t\t\tcount: totalCountByTime.get(time) ?? 0,\n\t\t});\n\t}\n\n\treturn { points, approx };\n}\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Renderer\n// ─────────────────────────────────────────────────────────────────────────────\n\nconst FALLBACK_MAX_CELLS = 12;\n\nfunction buildLineSeries(\n\trecords: AnalyticsDataPoint[],\n\tnodes: readonly string[],\n\tdata: HeatmapData,\n\tquantileField: ReplicationQuantileField,\n): { seriesData: SeriesData; omittedPairsCount: number } {\n\tconst greyBelow = data.confidence?.greyBelow ?? 0;\n\tconst suppressBelow = data.confidence?.suppressBelow ?? Infinity;\n\n\tconst series: Series[] = [];\n\tlet omittedPairsCount = 0;\n\n\tfor (const cell of data.cells) {\n\t\tconst count = cell.count ?? 0;\n\t\tif (count === 0) {\n\t\t\t// Truly absent pair — skip silently.\n\t\t\tcontinue;\n\t\t}\n\t\tif (count < greyBelow) {\n\t\t\tomittedPairsCount += 1;\n\t\t\tcontinue;\n\t\t}\n\t\tconst out = bucketLineSeries(records, cell.row, cell.col, nodes, quantileField);\n\t\tif (out.points.length === 0) { continue; }\n\t\tconst key = `${cell.row}→${cell.col}`;\n\t\tif (count < suppressBelow) {\n\t\t\t// Grey tier: dim to flag low confidence.\n\t\t\tseries.push({\n\t\t\t\tkey,\n\t\t\t\tlabel: key,\n\t\t\t\tpoints: out.points,\n\t\t\t\tapprox: out.approx,\n\t\t\t\topacity: 0.55,\n\t\t\t});\n\t\t} else {\n\t\t\tseries.push({\n\t\t\t\tkey,\n\t\t\t\tlabel: key,\n\t\t\t\tpoints: out.points,\n\t\t\t\tapprox: out.approx,\n\t\t\t});\n\t\t}\n\t}\n\n\treturn { seriesData: { series }, omittedPairsCount };\n}\n\nexport function ReplicationLatencyRenderer(props: SpecRegistryRendererProps): JSX.Element {\n\tconst { records, nodes, theme, timeRange, fillParent } = props;\n\n\tconst [quantile, setQuantile] = useState<ReplicationQuantileField>('p95');\n\n\tconst data = useMemo(\n\t\t() => aggregateReplicationMatrix(records, nodes, quantile),\n\t\t[records, nodes, quantile],\n\t);\n\n\t// Empty state — still surface skipped-records banner so users see the cause\n\t// when 100% of records had unparseable source nodes.\n\tif (data.rows.length === 0 || data.cols.length === 0) {\n\t\treturn (\n\t\t\t<div>\n\t\t\t\t<RecognitionBanner data={data} theme={theme} />\n\n\t\t\t\t<div>No data in window</div>\n\t\t\t</div>\n\t\t);\n\t}\n\n\tconst cellTotal = data.rows.length * data.cols.length;\n\tconst tooManyCells = cellTotal > FALLBACK_MAX_CELLS;\n\tconst tooFewDimensions = data.rows.length < 2 || data.cols.length < 2;\n\tconst useFallback = tooFewDimensions || tooManyCells;\n\n\tif (useFallback) {\n\t\tconst { seriesData, omittedPairsCount } = buildLineSeries(records, nodes, data, quantile);\n\t\tconst greyBelow = data.confidence?.greyBelow ?? 40;\n\t\tconst message = tooManyCells\n\t\t\t? 'Too many source-destination pairs for a heatmap — showing as lines.'\n\t\t\t: 'Only one source node emitted data in this window. This is typical for clusters with a single write origin — each line below shows latency from that source to one destination.';\n\n\t\tconst allDropped = seriesData.series.length === 0 && omittedPairsCount > 0;\n\t\tconst noData = seriesData.series.length === 0 && omittedPairsCount === 0;\n\n\t\tconst warningStyle = {\n\t\t\tmarginBottom: 8,\n\t\t\tpadding: '4px 8px',\n\t\t\tfontSize: 12,\n\t\t\tborderLeft: '3px solid var(--color-warning, #f59e0b)',\n\t\t\tbackground: theme === 'dark' ? '#1f2937' : '#fffbeb',\n\t\t\tcolor: 'currentColor',\n\t\t} as const;\n\n\t\t// Banners stack at the top in normal document flow; the chart sits\n\t\t// below with explicit vertical space. No flex height-juggling — the\n\t\t// fixed-height LineChart was clipping/overlapping when it competed\n\t\t// with the banner stack for a flex-shared box.\n\t\treturn (\n\t\t\t<div>\n\t\t\t\t<div\n\t\t\t\t\tstyle={{\n\t\t\t\t\t\tfontSize: 11,\n\t\t\t\t\t\topacity: 0.7,\n\t\t\t\t\t\tmarginBottom: 4,\n\t\t\t\t\t}}\n\t\t\t\t>\n\t\t\t\t\tShowing as lines\n\t\t\t\t</div>\n\t\t\t\t{data.skippedRecordsCount > 0\n\t\t\t\t\t? (\n\t\t\t\t\t\t<div\n\t\t\t\t\t\t\tkey={data.skippedRecordsCount}\n\t\t\t\t\t\t\trole=\"status\"\n\t\t\t\t\t\t\taria-atomic=\"true\"\n\t\t\t\t\t\t\tstyle={warningStyle}\n\t\t\t\t\t\t>\n\t\t\t\t\t\t\t{data.unrecognizedSources && data.unrecognizedSources.length > 0\n\t\t\t\t\t\t\t\t? `${data.skippedRecordsCount} record(s) omitted (no value for the selected percentile). Sources recovered via heuristic: ${\n\t\t\t\t\t\t\t\t\tdata.unrecognizedSources.join(', ')\n\t\t\t\t\t\t\t\t}.`\n\t\t\t\t\t\t\t\t: `${data.skippedRecordsCount} record(s) omitted (no value for the selected percentile).`}\n\t\t\t\t\t\t</div>\n\t\t\t\t\t)\n\t\t\t\t\t: null}\n\t\t\t\t{\n\t\t\t\t\t/* Suppress the omitted-pairs banner when all-dropped fires — the\n\t\t\t\t all-dropped banner already cites the count, so showing both is\n\t\t\t\t redundant. */\n\t\t\t\t}\n\t\t\t\t{omittedPairsCount > 0 && !allDropped\n\t\t\t\t\t? (\n\t\t\t\t\t\t<div\n\t\t\t\t\t\t\tkey={omittedPairsCount}\n\t\t\t\t\t\t\trole=\"status\"\n\t\t\t\t\t\t\taria-atomic=\"true\"\n\t\t\t\t\t\t\tstyle={warningStyle}\n\t\t\t\t\t\t>\n\t\t\t\t\t\t\t{`${omittedPairsCount} source-destination ${\n\t\t\t\t\t\t\t\tpluralize(omittedPairsCount, 'pair', 'pairs')\n\t\t\t\t\t\t\t} hidden — fewer than ${greyBelow} samples.`}\n\t\t\t\t\t\t</div>\n\t\t\t\t\t)\n\t\t\t\t\t: null}\n\t\t\t\t<div\n\t\t\t\t\tstyle={{\n\t\t\t\t\t\tmarginBottom: 8,\n\t\t\t\t\t\tpadding: '4px 8px',\n\t\t\t\t\t\tfontSize: 12,\n\t\t\t\t\t\tborderLeft: '3px solid var(--color-info, #3b82f6)',\n\t\t\t\t\t\tbackground: theme === 'dark' ? '#0f172a' : '#eff6ff',\n\t\t\t\t\t\tcolor: 'currentColor',\n\t\t\t\t\t}}\n\t\t\t\t>\n\t\t\t\t\t{message}\n\t\t\t\t</div>\n\t\t\t\t{allDropped\n\t\t\t\t\t? (\n\t\t\t\t\t\t<div\n\t\t\t\t\t\t\trole=\"status\"\n\t\t\t\t\t\t\taria-atomic=\"true\"\n\t\t\t\t\t\t\tstyle={warningStyle}\n\t\t\t\t\t\t>\n\t\t\t\t\t\t\t{`No source-destination pairs cleared the confidence threshold (${greyBelow}+ samples). All ${omittedPairsCount} ${\n\t\t\t\t\t\t\t\tpluralize(omittedPairsCount, 'pair', 'pairs')\n\t\t\t\t\t\t\t} had fewer than ${greyBelow} samples in this window.`}\n\t\t\t\t\t\t</div>\n\t\t\t\t\t)\n\t\t\t\t\t: noData\n\t\t\t\t\t? <div>No data in window</div>\n\t\t\t\t\t: (\n\t\t\t\t\t\t<div style={{ marginTop: 20 }}>\n\t\t\t\t\t\t\t<LineChart\n\t\t\t\t\t\t\t\tdata={seriesData}\n\t\t\t\t\t\t\t\ttheme={theme}\n\t\t\t\t\t\t\t\tyAxis={data.axis}\n\t\t\t\t\t\t\t\theight={320}\n\t\t\t\t\t\t\t\txDomain={[timeRange.startTime, timeRange.endTime]}\n\t\t\t\t\t\t\t\tfillParent={fillParent}\n\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t)}\n\t\t\t</div>\n\t\t);\n\t}\n\n\treturn (\n\t\t<div className=\"flex h-full flex-col\">\n\t\t\t<QuantileSelector value={quantile} onChange={setQuantile} />\n\t\t\t<RecognitionBanner data={data} theme={theme} />\n\t\t\t<div className=\"min-h-0 flex-1\">\n\t\t\t\t<HeatmapMatrix data={data} theme={theme} title=\"Replication latency\" />\n\t\t\t</div>\n\t\t</div>\n\t);\n}\n\ninterface RecognitionBannerProps {\n\tdata: HeatmapData;\n\ttheme: 'light' | 'dark';\n}\n\n/** Renders a single role='status' banner combining skipped-record count\n * and heuristic-recovered source count. Either or both may be present. */\nfunction RecognitionBanner({ data, theme }: RecognitionBannerProps) {\n\tconst skipped = data.skippedRecordsCount;\n\tconst unrecognized = data.unrecognizedSources ?? [];\n\tif (skipped === 0 && unrecognized.length === 0) { return null; }\n\n\tconst parts: string[] = [];\n\tif (skipped > 0) {\n\t\tparts.push(`${skipped} record${skipped === 1 ? '' : 's'} omitted (no value for the selected percentile).`);\n\t}\n\tif (unrecognized.length > 0) {\n\t\tparts.push(\n\t\t\t`Recovered ${unrecognized.length} source${unrecognized.length === 1 ? '' : 's'} not in the cluster snapshot: ${\n\t\t\t\tunrecognized.join(', ')\n\t\t\t}.`,\n\t\t);\n\t}\n\n\treturn (\n\t\t<div\n\t\t\tkey={`${skipped}-${unrecognized.length}`}\n\t\t\trole=\"status\"\n\t\t\taria-atomic=\"true\"\n\t\t\tstyle={{\n\t\t\t\tmarginBottom: 8,\n\t\t\t\tpadding: '4px 8px',\n\t\t\t\tfontSize: 12,\n\t\t\t\tborderLeft: '3px solid var(--color-warning, #f59e0b)',\n\t\t\t\tbackground: theme === 'dark' ? '#1f2937' : '#fffbeb',\n\t\t\t\tcolor: 'currentColor',\n\t\t\t}}\n\t\t>\n\t\t\t{parts.join(' ')}\n\t\t</div>\n\t);\n}\n\ninterface QuantileSelectorProps {\n\tvalue: ReplicationQuantileField;\n\tonChange: (q: ReplicationQuantileField) => void;\n}\n\nfunction QuantileSelector({ value, onChange }: QuantileSelectorProps) {\n\treturn (\n\t\t<div role=\"radiogroup\" aria-label=\"Quantile\" className=\"flex flex-wrap gap-1 pb-2\">\n\t\t\t{REPLICATION_QUANTILE_FIELDS.map((q) => {\n\t\t\t\tconst active = q.field === value;\n\t\t\t\treturn (\n\t\t\t\t\t<button\n\t\t\t\t\t\tkey={q.field}\n\t\t\t\t\t\ttype=\"button\"\n\t\t\t\t\t\trole=\"radio\"\n\t\t\t\t\t\taria-checked={active}\n\t\t\t\t\t\tdata-testid=\"quantile-button\"\n\t\t\t\t\t\tdata-value={q.field}\n\t\t\t\t\t\tonClick={() => onChange(q.field)}\n\t\t\t\t\t\tclassName={`rounded px-2 py-0.5 text-[11px] ${\n\t\t\t\t\t\t\tactive\n\t\t\t\t\t\t\t\t? 'bg-(--color-accent)/20 text-(--color-text-primary) font-semibold'\n\t\t\t\t\t\t\t\t: 'bg-(--color-bg-tertiary) text-(--color-text-secondary) hover:text-(--color-text-primary)'\n\t\t\t\t\t\t}`}\n\t\t\t\t\t>\n\t\t\t\t\t\t{q.label}\n\t\t\t\t\t</button>\n\t\t\t\t);\n\t\t\t})}\n\t\t</div>\n\t);\n}\n","import type { MetricSpec } from '../types/analytics.ts';\n\n// Per-field `crossNode` aggregator overrides (e.g. cpuUtilization's\n// `crossNode: 'max'`) are honored by pipeline.ts as of Step 4.5: the\n// temporal aggregator runs per (time, node), then the crossNode aggregator\n// folds across nodes within each time bucket.\nexport const resourceUsageSpec: MetricSpec = {\n\ttitle: 'Resource usage',\n\tdescription: 'CPU + I/O + page faults + context switches per node — small-multiples view.',\n\ttab: 'health',\n\tprimaryDimension: 'node',\n\tseries: {\n\t\tkind: 'field',\n\t\tfields: [\n\t\t\t{\n\t\t\t\tfield: 'cpuUtilization',\n\t\t\t\t// Cores-equivalent CPU consumption per process. 1.0 = one\n\t\t\t\t// core fully busy; saturating an N-core box means the value\n\t\t\t\t// approaches N. The previous `percent-of-core` transform (×100)\n\t\t\t\t// was cancelled by the cores formatter (÷100); both removed.\n\t\t\t\tlabel: 'Process CPU (cores used)',\n\t\t\t\taggregator: { temporal: 'max', crossNode: 'max' },\n\t\t\t\tyAxis: { unit: '', formatter: 'cores' },\n\t\t\t},\n\t\t\t{\n\t\t\t\tfield: 'fsWrite',\n\t\t\t\tlabel: 'Disk write (B/s)',\n\t\t\t\ttransform: { kind: 'rate' },\n\t\t\t\taggregator: { temporal: 'sum', crossNode: 'sum' },\n\t\t\t\tyAxis: { unit: '/s', formatter: 'bytes-si' },\n\t\t\t},\n\t\t\t{\n\t\t\t\tfield: 'majorPageFault',\n\t\t\t\tlabel: 'Major page faults /s',\n\t\t\t\ttransform: { kind: 'rate' },\n\t\t\t\taggregator: { temporal: 'sum', crossNode: 'max' },\n\t\t\t\tyAxis: { unit: '/s', formatter: 'count-si' },\n\t\t\t},\n\t\t\t{\n\t\t\t\tfield: {\n\t\t\t\t\tkind: 'op',\n\t\t\t\t\top: '+',\n\t\t\t\t\tleft: { kind: 'ref', field: 'voluntaryContextSwitches' },\n\t\t\t\t\tright: { kind: 'ref', field: 'involuntaryContextSwitches' },\n\t\t\t\t},\n\t\t\t\tlabel: 'Context switches /s',\n\t\t\t\ttransform: { kind: 'rate' },\n\t\t\t\taggregator: { temporal: 'sum', crossNode: 'max' },\n\t\t\t\tyAxis: { unit: '/s', formatter: 'count-si' },\n\t\t\t},\n\t\t],\n\t},\n\ttimestamp: 'time',\n\tbucket: { source: 'period-field', fallbackMs: 60000 },\n\taggregator: { temporal: 'sum', crossNode: 'sum' },\n\tprimitive: 'small-multiples',\n\tyAxis: { unit: '', formatter: 'count' },\n\tlayout: { colSpan: 2 },\n};\n","import { DimensionSelectorRenderer } from '../primitives/DimensionSelectorRenderer.tsx';\nimport type { AnalyticsDataPoint, MetricSpec, TimeRange } from '../types/analytics.ts';\n\nexport const response200Spec: MetricSpec = {\n\ttitle: 'HTTP 200 ratio',\n\tdescription: 'Per-path 2xx ratio (mean across nodes; count-weighted across time). Threshold 99.9%, min count 1000.',\n\ttab: 'requests',\n\tprimaryDimension: 'path',\n\tsubDimension: 'method',\n\tseries: {\n\t\tkind: 'groupBy',\n\t\tdimension: 'path',\n\t\t// Harper omits `ratio` on operation/fastify-route records but always\n\t\t// emits total + count. Compute via FieldExpr to avoid silently\n\t\t// dropping ~34% of records. See success.tsx for the matching rationale.\n\t\tfield: {\n\t\t\tfield: {\n\t\t\t\tkind: 'op',\n\t\t\t\top: '/',\n\t\t\t\tleft: { kind: 'ref', field: 'total' },\n\t\t\t\tright: { kind: 'ref', field: 'count' },\n\t\t\t},\n\t\t\tlabel: '200 ratio',\n\t\t},\n\t\ttopN: 10,\n\t\totherBucket: true,\n\t},\n\ttimestamp: 'time',\n\tbucket: { source: 'period-field', fallbackMs: 60000 },\n\taggregator: { temporal: 'mean', crossNode: 'count-weighted-mean' },\n\tconfidence: { field: 'count', greyBelow: 40, suppressBelow: 100 },\n\tprimitive: 'line',\n\tyAxis: { unit: '', formatter: 'percent' },\n\tthresholds: [\n\t\t{ value: 0.999, label: '99.9% SLO', direction: 'below-is-bad', minCount: 1000 },\n\t],\n};\n\ninterface RendererProps {\n\trecords: AnalyticsDataPoint[];\n\ttimeRange: TimeRange;\n\tnodes: string[];\n\ttheme: 'light' | 'dark';\n\tviewMode?: 'per-node' | 'aggregate';\n}\n\nexport function Response200Renderer(props: RendererProps) {\n\treturn <DimensionSelectorRenderer spec={response200Spec} {...props} ariaLabel=\"Path\" />;\n}\n","import type { MetricSpec } from '../types/analytics.ts';\n\nexport const storageVolumeSpec: MetricSpec = {\n\ttitle: 'Storage volume (available)',\n\tdescription: 'Per-node disk available bytes (latest snapshot in window; mean across nodes).',\n\ttab: 'storage',\n\tprimaryDimension: 'node',\n\tseries: {\n\t\tkind: 'groupBy',\n\t\tdimension: 'node',\n\t\tfield: { field: 'available', label: 'available (bytes)' },\n\t},\n\ttimestamp: 'id',\n\tbucket: { source: 'period-field', fallbackMs: 60000 },\n\taggregator: { temporal: 'last', crossNode: 'mean' },\n\tprimitive: 'line',\n\tyAxis: { unit: ' B', formatter: 'bytes-si' },\n};\n","import { DimensionSelectorRenderer } from '../primitives/DimensionSelectorRenderer.tsx';\nimport type { AnalyticsDataPoint, MetricSpec, TimeRange } from '../types/analytics.ts';\n\nexport const successSpec: MetricSpec = {\n\ttitle: 'Request success rate',\n\tdescription: 'Per-path success ratio (count-weighted-mean) — alert when ≥0.001 errors and Σcount ≥1000.',\n\ttab: 'requests',\n\tprimaryDimension: 'path',\n\tsubDimension: 'method',\n\tseries: {\n\t\tkind: 'groupBy',\n\t\tdimension: 'path',\n\t\t// Harper omits `ratio` on operation/fastify-route records (~34% of the\n\t\t// fixture) but always emits total + count. Compute total/count via\n\t\t// FieldExpr instead of reading the optional `ratio` field directly so\n\t\t// those records aren't silently dropped — without this, the displayed\n\t\t// success-rate is biased toward whichever request types Harper happens\n\t\t// to ratio-tag. fieldExpr.ts returns null for count === 0 (div-by-zero\n\t\t// guard), so 0-count buckets still gap correctly.\n\t\tfield: {\n\t\t\tfield: {\n\t\t\t\tkind: 'op',\n\t\t\t\top: '/',\n\t\t\t\tleft: { kind: 'ref', field: 'total' },\n\t\t\t\tright: { kind: 'ref', field: 'count' },\n\t\t\t},\n\t\t\tlabel: 'success ratio',\n\t\t},\n\t\ttopN: 10,\n\t\totherBucket: true,\n\t},\n\ttimestamp: 'time',\n\tbucket: { source: 'period-field', fallbackMs: 60000 },\n\taggregator: { temporal: 'count-weighted-mean', crossNode: 'count-weighted-mean' },\n\tconfidence: { field: 'count', greyBelow: 40, suppressBelow: 100 },\n\tprimitive: 'line',\n\tyAxis: { unit: '', formatter: 'percent' },\n\tthresholds: [\n\t\t{ value: 0.999, label: '99.9% SLO', direction: 'below-is-bad', minCount: 1000 },\n\t],\n};\n\ninterface RendererProps {\n\trecords: AnalyticsDataPoint[];\n\ttimeRange: TimeRange;\n\tnodes: string[];\n\ttheme: 'light' | 'dark';\n\tviewMode?: 'per-node' | 'aggregate';\n}\n\nexport function SuccessRenderer(props: RendererProps) {\n\treturn <DimensionSelectorRenderer spec={successSpec} {...props} ariaLabel=\"Path\" />;\n}\n","import type { MetricSpec } from '../types/analytics.ts';\n\nexport const tlsReusedSpec: MetricSpec = {\n\ttitle: 'TLS session reuse ratio',\n\tdescription: 'Fraction of TLS handshakes resumed via session ticket — higher is better.',\n\ttab: 'traffic',\n\tprimaryDimension: 'node',\n\tseries: {\n\t\tkind: 'groupBy',\n\t\tdimension: 'node',\n\t\tfield: { field: 'ratio', label: 'reuse ratio', transform: { kind: 'ratio' } },\n\t},\n\ttimestamp: 'time',\n\tbucket: { source: 'period-field', fallbackMs: 60000 },\n\taggregator: { temporal: 'count-weighted-mean', crossNode: 'count-weighted-mean' },\n\tconfidence: { field: 'count', greyBelow: 20, suppressBelow: 50 },\n\tprimitive: 'line',\n\tyAxis: { unit: '', formatter: 'percent' },\n\tthresholds: [\n\t\t{ value: 0.5, label: '50% reuse target', direction: 'below-is-bad', minCount: 50 },\n\t],\n};\n","import { DimensionSelectorRenderer } from '../primitives/DimensionSelectorRenderer.tsx';\nimport type { AnalyticsDataPoint, MetricSpec, TimeRange } from '../types/analytics.ts';\nimport { QUANTILE_DEFAULT, QUANTILE_FIELDS } from './quantileFields.ts';\n\nexport const transferSpec: MetricSpec = {\n\ttitle: 'Transfer duration (p95)',\n\tdescription: 'Per-path transfer p95 (count-weighted-mean) — top 10 paths + Other.',\n\ttab: 'requests',\n\tprimaryDimension: 'path',\n\tsubDimension: 'method',\n\tseries: {\n\t\tkind: 'groupBy',\n\t\tdimension: 'path',\n\t\tfield: { field: 'p95', label: 'p95 transfer (ms)' },\n\t\ttopN: 10,\n\t\totherBucket: true,\n\t},\n\ttimestamp: 'time',\n\tbucket: { source: 'period-field', fallbackMs: 60000 },\n\taggregator: { temporal: 'count-weighted-mean', crossNode: 'count-weighted-mean' },\n\tconfidence: { field: 'count', greyBelow: 40, suppressBelow: 100 },\n\tprimitive: 'line',\n\tyAxis: { unit: '', formatter: 'ms' },\n\tquantileSelector: { fields: QUANTILE_FIELDS, default: QUANTILE_DEFAULT },\n};\n\ninterface RendererProps {\n\trecords: AnalyticsDataPoint[];\n\ttimeRange: TimeRange;\n\tnodes: string[];\n\ttheme: 'light' | 'dark';\n\tviewMode?: 'per-node' | 'aggregate';\n}\n\nexport function TransferRenderer(props: RendererProps) {\n\treturn <DimensionSelectorRenderer spec={transferSpec} {...props} ariaLabel=\"Path\" />;\n}\n","import type { MetricSpec } from '../types/analytics.ts';\n\nexport const utilizationSpec: MetricSpec = {\n\ttitle: 'Cluster utilization',\n\tdescription: 'Active / (active + idle) per node — count-weighted-mean across time.',\n\ttab: 'health',\n\tprimaryDimension: 'node',\n\tseries: {\n\t\tkind: 'groupBy',\n\t\tdimension: 'node',\n\t\tfield: { field: 'utilization', label: 'utilization', transform: { kind: 'ratio' } },\n\t},\n\ttimestamp: 'time',\n\tbucket: { source: 'period-field', fallbackMs: 60000 },\n\taggregator: { temporal: 'mean', crossNode: 'max' },\n\tconfidence: { field: 'count', greyBelow: 40, suppressBelow: 100 },\n\tprimitive: 'line',\n\tyAxis: { unit: '', formatter: 'percent' },\n};\n","// Spec registry barrel. Step 1 populates this file with per-metric spec imports\n// and a `specRegistry` map. Step 0 ships the scaffold + known-names export so the\n// allowlist codegen can read something.\n\n// When a spec lands at src/lib/metricSpecs/<kebab>.ts it is added to KNOWN_METRICS\n// here. This list is the source of truth for the backend allowlist generator.\nexport const KNOWN_METRICS = [\n\t'replication-latency',\n\t'resource-usage',\n\t'memory',\n\t'mqtt-connections',\n\t'ws-connections',\n\t'main-thread-utilization',\n\t'bytes-sent',\n\t'bytes-received',\n\t'table-size',\n\t'duration',\n\t'success',\n\t'transfer',\n\t'tls-reused',\n\t'connection',\n\t'cpu-usage',\n\t'db-read',\n\t'db-write',\n\t'db-message',\n\t'response_200',\n\t'utilization',\n\t'database-size',\n\t'storage-volume',\n\t'cache-hit',\n\t'cache-resolution',\n] as const;\n\nexport type KnownMetric = (typeof KNOWN_METRICS)[number];\n\nimport type { SpecRegistryEntry } from '../types/analytics.ts';\nimport { BytesReceivedRenderer, bytesReceivedSpec } from './bytes-received.tsx';\nimport { BytesSentRenderer, bytesSentSpec } from './bytes-sent.tsx';\nimport { CacheHitRenderer, cacheHitSpec } from './cache-hit.tsx';\nimport { CacheResolutionRenderer, cacheResolutionSpec } from './cache-resolution.tsx';\nimport { ConnectionRenderer, connectionSpec } from './connection.tsx';\nimport { ConnectionsRenderer, connectionsSpec } from './connections.tsx';\nimport { CpuUsageRenderer, cpuUsageSpec } from './cpu-usage.tsx';\nimport { DatabaseSizeRenderer, databaseSizeSpec } from './database-size.tsx';\nimport { DbMessageRenderer, dbMessageSpec } from './db-message.tsx';\nimport { DbReadRenderer, dbReadSpec } from './db-read.tsx';\nimport { DbWriteRenderer, dbWriteSpec } from './db-write.tsx';\nimport { DurationRenderer, durationSpec } from './duration.tsx';\nimport { MainThreadRenderer, mainThreadUtilizationSpec } from './main-thread-utilization.tsx';\nimport { MemoryRenderer, memorySpec } from './memory.tsx';\nimport { ReplicationLatencyRenderer, replicationLatencySpec } from './replication-latency.tsx';\nimport { resourceUsageSpec } from './resource-usage.ts';\nimport { Response200Renderer, response200Spec } from './response-200.tsx';\nimport { storageVolumeSpec } from './storage-volume.ts';\nimport { SuccessRenderer, successSpec } from './success.tsx';\nimport { tlsReusedSpec } from './tls-reused.ts';\nimport { TransferRenderer, transferSpec } from './transfer.tsx';\nimport { utilizationSpec } from './utilization.ts';\n\nexport const specRegistry: Record<string, SpecRegistryEntry> = {\n\t'replication-latency': { spec: replicationLatencySpec, Renderer: ReplicationLatencyRenderer },\n\t'bytes-sent': { spec: bytesSentSpec, Renderer: BytesSentRenderer },\n\t'bytes-received': { spec: bytesReceivedSpec, Renderer: BytesReceivedRenderer },\n\t'resource-usage': { spec: resourceUsageSpec },\n\t'connections': { spec: connectionsSpec, Renderer: ConnectionsRenderer },\n\t'duration': { spec: durationSpec, Renderer: DurationRenderer },\n\t'success': { spec: successSpec, Renderer: SuccessRenderer },\n\t'transfer': { spec: transferSpec, Renderer: TransferRenderer },\n\t'tls-reused': { spec: tlsReusedSpec },\n\t'connection': { spec: connectionSpec, Renderer: ConnectionRenderer },\n\t'cpu-usage': { spec: cpuUsageSpec, Renderer: CpuUsageRenderer },\n\t'db-read': { spec: dbReadSpec, Renderer: DbReadRenderer },\n\t'db-write': { spec: dbWriteSpec, Renderer: DbWriteRenderer },\n\t'db-message': { spec: dbMessageSpec, Renderer: DbMessageRenderer },\n\t'response_200': { spec: response200Spec, Renderer: Response200Renderer },\n\t'utilization': { spec: utilizationSpec },\n\t'database-size': { spec: databaseSizeSpec, Renderer: DatabaseSizeRenderer },\n\t'storage-volume': { spec: storageVolumeSpec },\n\t'memory': { spec: memorySpec, Renderer: MemoryRenderer },\n\t'main-thread-utilization': { spec: mainThreadUtilizationSpec, Renderer: MainThreadRenderer },\n\t'cache-hit': { spec: cacheHitSpec, Renderer: CacheHitRenderer },\n\t'cache-resolution': { spec: cacheResolutionSpec, Renderer: CacheResolutionRenderer },\n};\n","import { derivedRegistry } from '../pipeline/derived/index.ts';\nimport { specRegistry } from '../pipeline/index.ts';\nimport type { FieldExpr, FieldSpec, MetricSpec, Transform } from '../types/analytics.ts';\n\n/** Per-derived-metric required-field overrides. Derived specs read raw\n * source-metric columns directly (their `recompute` doesn't go through\n * runPipeline), so a generic spec walker can't infer them. The keys here\n * are derived metric ids (matching `derivedRegistry` keys); values are\n * the source-record fields the recompute reads. */\nconst DERIVED_REQUIRED_FIELDS: Record<string, readonly string[]> = {\n\t'request-rate': ['count', 'period'],\n\t'error-rate': ['count', 'errors'],\n\t// mqtt-traffic-* delegate to runPipeline against bytes-{sent,received}\n\t// inner specs, so they are covered transitively through the source spec\n\t// and the renderer fetches the source metric directly.\n};\n\n/** Walks a MetricSpec and returns the union of Harper field names the\n * pipeline must read to render the *default* view. Used by panel\n * renderers to feed `useAnalyticsRecords({ requiredFields })` so a Harper\n * response missing a load-bearing column surfaces an explicit \"field\n * unavailable\" empty state instead of a blank chart.\n *\n * The bar for \"required\" is intentionally tight: only the fields that\n * must be present for the panel's *initial* render to produce data.\n * Optional drilldowns (quantile alternates the user can pick), labels\n * used purely for grouping, and primary/sub-dimension metadata are\n * excluded — surfacing them as \"missing\" produces false positives that\n * blank an otherwise-fine chart (we hit this with cpu-usage, which has a\n * quantile picker but ships zero quantile fields by default). */\nexport function getSpecRequiredFields(metric: string): readonly string[] {\n\tconst override = DERIVED_REQUIRED_FIELDS[metric];\n\tif (override) { return override; }\n\tconst derived = derivedRegistry[metric];\n\tif (derived) {\n\t\t// Unknown derived without an override: conservative default — require\n\t\t// nothing rather than mis-flag missingFields.\n\t\treturn [];\n\t}\n\tconst entry = specRegistry[metric];\n\tif (!entry?.spec) { return []; }\n\treturn collectFromSpec(entry.spec);\n}\n\nfunction collectFromSpec(spec: MetricSpec): string[] {\n\tconst fields = new Set<string>();\n\tconst series = spec.series;\n\tlet referencesRate = false;\n\n\tif (series.kind === 'field') {\n\t\tfor (const f of series.fields) {\n\t\t\tcollectFromFieldSpec(f, fields);\n\t\t\tif (transformReferencesRate(f.transform)) { referencesRate = true; }\n\t\t}\n\t} else {\n\t\t// groupBy: only the active series.field is required for the chart to\n\t\t// render. The dimension column is also required because the pipeline's\n\t\t// group step reads it; without it every record collapses into a\n\t\t// single bucket.\n\t\tcollectFromFieldSpec(series.field, fields);\n\t\tif (transformReferencesRate(series.field.transform)) { referencesRate = true; }\n\t\t// `dimension: 'node'` is structural — it's read off\n\t\t// AnalyticsDataPoint.node which is part of the contract, not a\n\t\t// schema-drift candidate. Other dimensions (path, type, table,\n\t\t// database, method) are real Harper columns that can drift.\n\t\tif (series.dimension && series.dimension !== 'node') {\n\t\t\tfields.add(series.dimension);\n\t\t}\n\t}\n\n\t// `period` is read for `transform: rate` and for period-snapping. Spec\n\t// authors don't list it explicitly; derive it from rate references.\n\tif (referencesRate) { fields.add('period'); }\n\n\t// NOT required and intentionally excluded:\n\t// - `spec.confidence.field`: confidence gating greys low-count cells but\n\t// a record without it just renders ungated, not blank.\n\t// - `spec.primaryDimension` / `spec.subDimension`: documentation /\n\t// subgroup labels, not always read by the rendering path. Including\n\t// them mis-flags specs whose visible field is something else (we hit\n\t// this with cpu-usage where primaryDimension='path' but the panel\n\t// reads user/harper percentages).\n\t// - `spec.quantileSelector.fields`: alternates the user CAN pick. Only\n\t// the active one matters at any time; flagging all 9–11 alternates\n\t// blanks a perfectly fine default-render whenever a quantile column\n\t// is sparsely populated.\n\n\treturn [...fields];\n}\n\nfunction transformReferencesRate(t: Transform | undefined): boolean {\n\tif (!t) { return false; }\n\tswitch (t.kind) {\n\t\tcase 'rate':\n\t\t\treturn true;\n\t\tcase 'compose':\n\t\t\treturn t.steps.some(transformReferencesRate);\n\t\tdefault:\n\t\t\treturn false;\n\t}\n}\n\nfunction collectFromFieldSpec(f: FieldSpec, out: Set<string>) {\n\tcollectFromExpr(f.field, out);\n}\n\nfunction collectFromExpr(expr: string | FieldExpr, out: Set<string>) {\n\tif (typeof expr === 'string') {\n\t\tout.add(expr);\n\t\treturn;\n\t}\n\tswitch (expr.kind) {\n\t\tcase 'ref':\n\t\t\tout.add(expr.field);\n\t\t\treturn;\n\t\tcase 'const':\n\t\t\treturn;\n\t\tcase 'op':\n\t\t\tcollectFromExpr(expr.left, out);\n\t\t\tcollectFromExpr(expr.right, out);\n\t\t\treturn;\n\t}\n}\n","import type { AnalyticsDataPoint, SeriesData, TimeRange } from '../types/analytics.ts';\nimport { SmallMultiples } from './SmallMultiples.tsx';\n\nconst isDev = import.meta.env?.DEV ?? false;\n\nconst RESERVED_FIELDS = new Set([\n\t'time',\n\t'node',\n\t'id',\n\t'period',\n\t'metric',\n\t// Dimensional / metadata fields that should not be rendered as numeric\n\t// series even when they happen to be numeric (e.g. tls-reused.path = 9926).\n\t'count',\n\t'threadId',\n\t'path',\n\t'method',\n\t'type',\n\t'database',\n\t'table',\n\t'source',\n]);\n\nconst MAX_FALLBACK_PANELS = 8;\n\ninterface Props {\n\tmetric: string;\n\trecords: AnalyticsDataPoint[];\n\twindow: TimeRange;\n\tnodes: string[];\n\ttheme: 'light' | 'dark';\n\t/** Optional inline banner shown above the dev hint — used by callers that\n\t * fell through to FallbackRenderer for a known reason (e.g. legacy chart\n\t * failed to load), so users see the cause. */\n\thint?: string;\n}\n\nfunction inferNumericFields(records: AnalyticsDataPoint[]): string[] {\n\tconst candidates = new Map<string, number>();\n\tfor (const r of records) {\n\t\tfor (const key of Object.keys(r)) {\n\t\t\tif (RESERVED_FIELDS.has(key)) { continue; }\n\t\t\tif (typeof r[key] === 'number' && Number.isFinite(r[key] as number)) {\n\t\t\t\tcandidates.set(key, (candidates.get(key) ?? 0) + 1);\n\t\t\t}\n\t\t}\n\t}\n\t// Keep fields that appeared numeric in at least half the records.\n\tconst half = Math.max(1, Math.floor(records.length / 2));\n\treturn [...candidates.entries()]\n\t\t.filter(([, count]) => count >= half)\n\t\t.map(([key]) => key);\n}\n\nexport function FallbackRenderer({ metric, records, theme, hint }: Props) {\n\tconst fields = inferNumericFields(records);\n\tconst visibleFields = fields.slice(0, MAX_FALLBACK_PANELS);\n\tconst overflow = fields.length - visibleFields.length;\n\n\tconst panels = visibleFields.map((field) => {\n\t\tconst data: SeriesData = {\n\t\t\tseries: [\n\t\t\t\t{\n\t\t\t\t\tkey: field,\n\t\t\t\t\tlabel: field,\n\t\t\t\t\tpoints: records\n\t\t\t\t\t\t.filter((r) => typeof r[field] === 'number')\n\t\t\t\t\t\t.map((r) => ({\n\t\t\t\t\t\t\tx: typeof r.time === 'number' ? r.time : 0,\n\t\t\t\t\t\t\ty: r[field] as number,\n\t\t\t\t\t\t})),\n\t\t\t\t},\n\t\t\t],\n\t\t};\n\t\treturn { title: field, data };\n\t});\n\n\tconst kebab = metric.replace(/_/g, '-');\n\tconst banner = isDev\n\t\t? `Unspecced metric \"${metric}\" — add a spec at src/lib/metricSpecs/${kebab}.ts for a tailored view.`\n\t\t: null;\n\n\treturn (\n\t\t<div>\n\t\t\t{hint && (\n\t\t\t\t<div\n\t\t\t\t\trole=\"status\"\n\t\t\t\t\tstyle={{\n\t\t\t\t\t\tfontSize: 12,\n\t\t\t\t\t\tpadding: '4px 8px',\n\t\t\t\t\t\tmarginBottom: 8,\n\t\t\t\t\t\tbackground: 'color-mix(in srgb, var(--color-text-secondary) 10%, transparent)',\n\t\t\t\t\t\tcolor: 'var(--color-text-secondary)',\n\t\t\t\t\t\tborder: '1px solid color-mix(in srgb, var(--color-text-secondary) 30%, transparent)',\n\t\t\t\t\t\tborderRadius: 4,\n\t\t\t\t\t}}\n\t\t\t\t>\n\t\t\t\t\t{hint}\n\t\t\t\t</div>\n\t\t\t)}\n\t\t\t{banner && (\n\t\t\t\t<div\n\t\t\t\t\trole=\"status\"\n\t\t\t\t\tstyle={{\n\t\t\t\t\t\tfontSize: 12,\n\t\t\t\t\t\tpadding: '4px 8px',\n\t\t\t\t\t\tmarginBottom: 8,\n\t\t\t\t\t\tbackground: 'color-mix(in srgb, var(--color-warning) 15%, transparent)',\n\t\t\t\t\t\tcolor: 'var(--color-warning)',\n\t\t\t\t\t\tborder: '1px solid color-mix(in srgb, var(--color-warning) 40%, transparent)',\n\t\t\t\t\t\tborderRadius: 4,\n\t\t\t\t\t}}\n\t\t\t\t>\n\t\t\t\t\t{banner}\n\t\t\t\t</div>\n\t\t\t)}\n\t\t\t<SmallMultiples panels={panels} theme={theme} />\n\t\t\t{overflow > 0 && (\n\t\t\t\t<div style={{ fontSize: 11, marginTop: 4, opacity: 0.7 }}>\n\t\t\t\t\t{`… and ${overflow} more fields not shown. Add a spec at src/lib/metricSpecs/${kebab}.ts to customize.`}\n\t\t\t\t</div>\n\t\t\t)}\n\t\t</div>\n\t);\n}\n","// Wraps LineChart with a shared NodeLegend below the chart, hiding the\n// in-chart Recharts <Legend>. Used by generic-dispatch line panels\n// (utilization, tls-reused, storage-volume, database-size,\n// main-thread-utilization) so they get the same per-node legend\n// behavior as the chip-selector panels and small-multiples grids.\n\nimport { useMemo } from 'react';\nimport { NodeLegend } from '../charts/NodeLegend.tsx';\nimport { useNodeSelection } from '../hooks/useNodeSelection.ts';\nimport { getNodeColor } from '../lib/nodeColors.ts';\nimport type { AxisSpec, SeriesData } from '../types/analytics.ts';\nimport { LineChart } from './LineChart.tsx';\n\ninterface Props {\n\tdata: SeriesData;\n\ttheme: 'light' | 'dark';\n\tyAxis?: AxisSpec | { left: AxisSpec; right?: AxisSpec };\n\txDomain?: [number, number];\n\tfillParent?: boolean;\n}\n\nfunction extractNode(seriesKey: string): string | null {\n\tconst sep = seriesKey.indexOf('|');\n\tif (sep !== -1) { return seriesKey.slice(sep + 1); }\n\t// Some specs (utilization, tls-reused, storage-volume) groupBy 'node'\n\t// directly — the series key IS the node id, no separator.\n\treturn seriesKey;\n}\n\nexport function LineChartWithNodeLegend({ data, theme, yAxis, xDomain, fillParent }: Props) {\n\t// All series are per-node when this component is used (either via\n\t// pipeline perNode mode or groupBy:'node'). Collect node ids from\n\t// series keys.\n\tconst nodeIds = useMemo(() => {\n\t\tconst set = new Set<string>();\n\t\tfor (const s of data.series) {\n\t\t\tconst node = extractNode(s.key);\n\t\t\tif (node) { set.add(node); }\n\t\t}\n\t\treturn [...set].sort();\n\t}, [data]);\n\n\tconst { isActive, handleLegendClick } = useNodeSelection(nodeIds);\n\n\tconst filteredData = useMemo<SeriesData>(() => ({\n\t\t...data,\n\t\tseries: data.series\n\t\t\t.filter((s) => {\n\t\t\t\tconst node = extractNode(s.key);\n\t\t\t\treturn node === null || isActive(node);\n\t\t\t})\n\t\t\t.map((s) => {\n\t\t\t\tconst node = extractNode(s.key);\n\t\t\t\tif (!node) { return s; }\n\t\t\t\treturn { ...s, color: s.color ?? getNodeColor(node, nodeIds) };\n\t\t\t}),\n\t}), [data, isActive, nodeIds]);\n\n\treturn (\n\t\t<div className=\"flex h-full flex-col\">\n\t\t\t<div className=\"min-h-0 flex-1\">\n\t\t\t\t<LineChart\n\t\t\t\t\tdata={filteredData}\n\t\t\t\t\ttheme={theme}\n\t\t\t\t\tyAxis={yAxis}\n\t\t\t\t\txDomain={xDomain}\n\t\t\t\t\tfillParent={fillParent}\n\t\t\t\t\thideLegend\n\t\t\t\t/>\n\t\t\t</div>\n\t\t\t{nodeIds.length > 0 && (\n\t\t\t\t<NodeLegend\n\t\t\t\t\tnodeIds={nodeIds}\n\t\t\t\t\tisActive={isActive}\n\t\t\t\t\tonClickNode={handleLegendClick}\n\t\t\t\t/>\n\t\t\t)}\n\t\t</div>\n\t);\n}\n","import { Component, type ReactNode } from 'react';\nimport { derivedRegistry } from '../pipeline/derived/index.ts';\nimport { specRegistry } from '../pipeline/index.ts';\nimport { runPipeline } from '../pipeline/pipeline.ts';\nimport type { AnalyticsDataPoint, AxisSpec, MetricSpec, SeriesData, TimeRange } from '../types/analytics.ts';\nimport { FallbackRenderer } from './FallbackRenderer.tsx';\nimport { LineChartWithNodeLegend } from './LineChartWithNodeLegend.tsx';\nimport { SmallMultiples } from './SmallMultiples.tsx';\nimport { StackedAreaChart } from './StackedAreaChart.tsx';\n\nfunction renderPrimitive(\n\tprimitive: MetricSpec['primitive'],\n\tdata: SeriesData,\n\ttheme: 'light' | 'dark',\n\tyAxis: MetricSpec['yAxis'],\n\txDomain?: [number, number],\n\tfillParent?: boolean,\n) {\n\tswitch (primitive) {\n\t\tcase 'line':\n\t\t\treturn (\n\t\t\t\t<LineChartWithNodeLegend data={data} theme={theme} yAxis={yAxis} xDomain={xDomain} fillParent={fillParent} />\n\t\t\t);\n\t\tcase 'stacked-area':\n\t\t\treturn (\n\t\t\t\t<StackedAreaChart\n\t\t\t\t\tdata={data}\n\t\t\t\t\ttheme={theme}\n\t\t\t\t\tyAxis={yAxis as AxisSpec}\n\t\t\t\t\txDomain={xDomain}\n\t\t\t\t\tfillParent={fillParent}\n\t\t\t\t/>\n\t\t\t);\n\t\tcase 'small-multiples':\n\t\t\tthrow new Error('small-multiples is dispatched at the spec branch, not via renderPrimitive');\n\t\tcase 'heatmap':\n\t\t\tthrow new Error('heatmap dispatch is via custom Renderer (replication-latency)');\n\t\tdefault:\n\t\t\tthrow new Error(`Unknown primitive: ${primitive as string}`);\n\t}\n}\n\ninterface MetricErrorBoundaryProps {\n\tchildren: ReactNode;\n\tfallback: ReactNode;\n}\n\nclass MetricErrorBoundary extends Component<MetricErrorBoundaryProps, { failed: boolean }> {\n\tstate = { failed: false };\n\tstatic getDerivedStateFromError() {\n\t\treturn { failed: true };\n\t}\n\tcomponentDidCatch(error: Error) {\n\t\tconsole.error('[MetricRenderer] render failed; falling back:', error);\n\t}\n\trender() {\n\t\treturn this.state.failed ? this.props.fallback : this.props.children;\n\t}\n}\n\ninterface Props {\n\tmetric: string;\n\trecords: AnalyticsDataPoint[];\n\twindow: TimeRange;\n\tnodes: string[];\n\ttheme: 'light' | 'dark';\n\tviewMode?: 'per-node' | 'aggregate';\n\t/** When true, the chart fills its parent's vertical space (used by the\n\t * expand-to-fullscreen dialog). The parent must be a definite-height\n\t * flex column for this to resolve. */\n\tfillParent?: boolean;\n}\n\nexport function MetricRenderer({\n\tmetric,\n\trecords,\n\twindow: timeRange,\n\tnodes,\n\ttheme,\n\tviewMode,\n\tfillParent,\n}: Props) {\n\tconst errorFallback = (\n\t\t<FallbackRenderer\n\t\t\tmetric={metric}\n\t\t\trecords={records}\n\t\t\twindow={timeRange}\n\t\t\tnodes={nodes}\n\t\t\ttheme={theme}\n\t\t\thint=\"Render failed — showing fallback.\"\n\t\t/>\n\t);\n\n\tconst xDomain: [number, number] = [timeRange.startTime, timeRange.endTime];\n\tlet body: ReactNode;\n\tconst derived = derivedRegistry[metric];\n\tif (derived) {\n\t\tif (derived.Renderer) {\n\t\t\tbody = (\n\t\t\t\t<derived.Renderer\n\t\t\t\t\trecords={records}\n\t\t\t\t\ttimeRange={timeRange}\n\t\t\t\t\tnodes={nodes}\n\t\t\t\t\ttheme={theme}\n\t\t\t\t\tviewMode={viewMode}\n\t\t\t\t\tfillParent={fillParent}\n\t\t\t\t/>\n\t\t\t);\n\t\t} else {\n\t\t\tconst seriesData = derived.recompute(records, timeRange, nodes, viewMode);\n\t\t\tbody = renderPrimitive(derived.primitive, seriesData, theme, derived.yAxis, xDomain, fillParent);\n\t\t}\n\t} else {\n\t\tconst entry = specRegistry[metric];\n\t\tif (entry?.Renderer) {\n\t\t\tbody = (\n\t\t\t\t<entry.Renderer\n\t\t\t\t\trecords={records}\n\t\t\t\t\ttimeRange={timeRange}\n\t\t\t\t\tnodes={nodes}\n\t\t\t\t\ttheme={theme}\n\t\t\t\t\tviewMode={viewMode}\n\t\t\t\t\tfillParent={fillParent}\n\t\t\t\t/>\n\t\t\t);\n\t\t} else if (entry?.spec) {\n\t\t\tconst isPerNodeMode = (viewMode ?? 'per-node') === 'per-node';\n\t\t\tif (entry.spec.primitive === 'small-multiples') {\n\t\t\t\tconst outerSpec = entry.spec;\n\t\t\t\tconst series = outerSpec.series;\n\t\t\t\tif (series.kind !== 'field') {\n\t\t\t\t\tthrow new Error(\"small-multiples requires kind='field' series source\");\n\t\t\t\t}\n\t\t\t\tconst panels = series.fields.map((field) => {\n\t\t\t\t\tconst innerSpec: MetricSpec = {\n\t\t\t\t\t\t...outerSpec,\n\t\t\t\t\t\tseries: { kind: 'field', fields: [field] },\n\t\t\t\t\t\taggregator: {\n\t\t\t\t\t\t\ttemporal: field.aggregator?.temporal ?? outerSpec.aggregator.temporal,\n\t\t\t\t\t\t\tcrossNode: field.aggregator?.crossNode ?? outerSpec.aggregator.crossNode,\n\t\t\t\t\t\t},\n\t\t\t\t\t};\n\t\t\t\t\treturn {\n\t\t\t\t\t\ttitle: field.label,\n\t\t\t\t\t\tdata: runPipeline(innerSpec, records, timeRange, nodes, { perNode: isPerNodeMode, snapToPeriod: true }),\n\t\t\t\t\t\tyAxis: field.yAxis ?? outerSpec.yAxis,\n\t\t\t\t\t};\n\t\t\t\t});\n\t\t\t\tbody = <SmallMultiples panels={panels} theme={theme} xDomain={xDomain} fillParent={fillParent} />;\n\t\t\t} else if (\n\t\t\t\tentry.spec.primitive === 'stacked-area'\n\t\t\t\t&& isPerNodeMode\n\t\t\t\t&& entry.spec.series.kind === 'groupBy'\n\t\t\t\t&& entry.spec.series.dimension !== 'node'\n\t\t\t) {\n\t\t\t\tconst remapped: MetricSpec = {\n\t\t\t\t\t...entry.spec,\n\t\t\t\t\tseries: { ...entry.spec.series, dimension: 'node' },\n\t\t\t\t};\n\t\t\t\tconst seriesData = runPipeline(remapped, records, timeRange, nodes, { snapToPeriod: true });\n\t\t\t\tbody = renderPrimitive('stacked-area', seriesData, theme, entry.spec.yAxis, xDomain, fillParent);\n\t\t\t} else if (\n\t\t\t\tentry.spec.primitive === 'line'\n\t\t\t\t&& !isPerNodeMode\n\t\t\t\t&& entry.spec.series.kind === 'groupBy'\n\t\t\t\t&& entry.spec.series.dimension === 'node'\n\t\t\t) {\n\t\t\t\tconst groupSrc = entry.spec.series;\n\t\t\t\tconst inner: MetricSpec = {\n\t\t\t\t\t...entry.spec,\n\t\t\t\t\tseries: { kind: 'field', fields: [{ ...groupSrc.field, label: 'cluster' }] },\n\t\t\t\t};\n\t\t\t\tconst seriesData = runPipeline(inner, records, timeRange, nodes, { snapToPeriod: true });\n\t\t\t\tbody = renderPrimitive(entry.spec.primitive, seriesData, theme, entry.spec.yAxis, xDomain, fillParent);\n\t\t\t} else if (\n\t\t\t\t// `primitive: 'line'` + `groupBy` on a non-node dimension (e.g.\n\t\t\t\t// database-size groupBy 'database'). The default per-node split\n\t\t\t\t// emits one series per (dim, node) and the LineChartWithNodeLegend\n\t\t\t\t// wrapper extracts only the node id from the key — so two\n\t\t\t\t// dimension values on a single-node Harper render as two lines\n\t\t\t\t// sharing the same color and a single legend entry. Run the\n\t\t\t\t// pipeline with perNode=false so series keys are just the\n\t\t\t\t// dimension values; the wrapper then renders one legend chip\n\t\t\t\t// per dimension value (colored via getNodeColor's deterministic\n\t\t\t\t// hash). The crossNode aggregator on the spec folds nodes into\n\t\t\t\t// the cluster-wide line per dimension.\n\t\t\t\tentry.spec.primitive === 'line'\n\t\t\t\t&& entry.spec.series.kind === 'groupBy'\n\t\t\t\t&& entry.spec.series.dimension !== 'node'\n\t\t\t) {\n\t\t\t\tconst seriesData = runPipeline(entry.spec, records, timeRange, nodes, {\n\t\t\t\t\tperNode: false,\n\t\t\t\t\tsnapToPeriod: true,\n\t\t\t\t});\n\t\t\t\tbody = renderPrimitive('line', seriesData, theme, entry.spec.yAxis, xDomain, fillParent);\n\t\t\t} else {\n\t\t\t\tconst seriesData = runPipeline(entry.spec, records, timeRange, nodes, {\n\t\t\t\t\tperNode: isPerNodeMode,\n\t\t\t\t\tsnapToPeriod: true,\n\t\t\t\t});\n\t\t\t\tbody = renderPrimitive(entry.spec.primitive, seriesData, theme, entry.spec.yAxis, xDomain, fillParent);\n\t\t\t}\n\t\t} else {\n\t\t\tbody = <FallbackRenderer metric={metric} records={records} window={timeRange} nodes={nodes} theme={theme} />;\n\t\t}\n\t}\n\n\treturn <MetricErrorBoundary key={metric} fallback={errorFallback}>{body}</MetricErrorBoundary>;\n}\n","import { Component, type ReactNode } from 'react';\n\ninterface Props {\n\tmetric: string;\n\t/** When this changes, the boundary clears its caught-error state so the\n\t * child gets a fresh render attempt. Wire to the time-range stamp so a\n\t * user changing the window retries the panel without a page reload. */\n\tresetKey?: string | number;\n\tchildren: ReactNode;\n}\n\ninterface State {\n\tfailed: boolean;\n\tmessage?: string;\n\tlastResetKey?: string | number;\n}\n\nexport class PanelErrorBoundary extends Component<Props, State> {\n\tstate: State = { failed: false };\n\n\tstatic getDerivedStateFromError(error: unknown): Partial<State> {\n\t\treturn { failed: true, message: error instanceof Error ? error.message : String(error) };\n\t}\n\n\tstatic getDerivedStateFromProps(nextProps: Props, prevState: State): Partial<State> | null {\n\t\tif (prevState.lastResetKey !== nextProps.resetKey) {\n\t\t\treturn { failed: false, message: undefined, lastResetKey: nextProps.resetKey };\n\t\t}\n\t\treturn null;\n\t}\n\n\tcomponentDidCatch(error: Error) {\n\t\tconsole.error(`[panel:${this.props.metric}] render failed`, error);\n\t}\n\n\trender() {\n\t\tif (this.state.failed) {\n\t\t\treturn (\n\t\t\t\t<div className=\"rounded-lg border border-destructive/40 bg-destructive/10 p-4 text-sm text-destructive\">\n\t\t\t\t\t<div className=\"font-medium mb-1\">{`Panel \"${this.props.metric}\" is unavailable`}</div>\n\t\t\t\t\t<div className=\"text-xs opacity-80\">{this.state.message}</div>\n\t\t\t\t</div>\n\t\t\t);\n\t\t}\n\t\treturn this.props.children;\n\t}\n}\n","import { Button } from '@/components/ui/button';\nimport { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';\nimport { type ReactNode, useRef } from 'react';\nimport { ChartCopyButton } from '../components/ChartCopyButton.tsx';\nimport { ChartExpandButton } from '../components/ChartExpandButton.tsx';\nimport { ChartExportButton } from '../components/ChartExportButton.tsx';\nimport { useAnalyticsContext } from '../context/AnalyticsContext.tsx';\nimport { useAnalyticsRecords } from '../hooks/useAnalyticsRecords.ts';\nimport { getSpecRequiredFields } from '../lib/specRequiredFields.ts';\nimport { derivedRegistry } from '../pipeline/derived/index.ts';\nimport { specRegistry } from '../pipeline/index.ts';\nimport { MetricRenderer } from '../primitives/MetricRenderer.tsx';\nimport { PanelErrorBoundary } from './PanelErrorBoundary.tsx';\n\ninterface Props {\n\tmetric: string;\n\ttitleOverride?: string;\n}\n\n/** Renders one Card with a metric chart inside, fetching its own data via\n * the adapter. Used by every analytics tab except Storage's table-size\n * panels (which compose their own custom charts). */\nexport function MetricPanel({ metric, titleOverride }: Props) {\n\tconst { timeRange } = useAnalyticsContext();\n\treturn (\n\t\t<PanelErrorBoundary metric={metric} resetKey={`${timeRange.startTime}-${timeRange.endTime}`}>\n\t\t\t<MetricPanelInner metric={metric} titleOverride={titleOverride} />\n\t\t</PanelErrorBoundary>\n\t);\n}\n\nfunction MetricPanelInner({ metric, titleOverride }: Props) {\n\tconst { timeRange, bucketMs, refreshIntervalMs, theme, instanceParams } = useAnalyticsContext();\n\tconst sourceMetric = derivedRegistry[metric]?.sourceMetric ?? metric;\n\tconst requiredFields = getSpecRequiredFields(metric);\n\tconst { data, isLoading, isError, error, isEmpty, missingFields, refetch } = useAnalyticsRecords({\n\t\tmetric: sourceMetric,\n\t\tstartTime: timeRange.startTime,\n\t\tendTime: timeRange.endTime,\n\t\tinstanceParams,\n\t\trefetchIntervalMs: refreshIntervalMs,\n\t\tbucketMs,\n\t\trequiredFields,\n\t});\n\n\tconst specEntry = specRegistry[metric];\n\tconst derivedEntry = derivedRegistry[metric];\n\tconst title = titleOverride ?? specEntry?.spec?.title ?? derivedEntry?.title ?? metric;\n\tconst description = specEntry?.spec?.description ?? derivedEntry?.subtitle;\n\tconst nodes = collectNodes(data);\n\t// Capture only the chart body, not the whole Card. Otherwise the exported\n\t// PNG includes the title bar's Expand/Copy/Download icons.\n\tconst chartRef = useRef<HTMLDivElement>(null);\n\tconst canExport = !isLoading && !isError && !isEmpty;\n\n\tconst renderChart = (opts: { fillParent: boolean } = { fillParent: false }) => (\n\t\t<MetricRenderer\n\t\t\tmetric={metric}\n\t\t\trecords={data}\n\t\t\twindow={timeRange}\n\t\t\tnodes={nodes}\n\t\t\ttheme={theme}\n\t\t\tfillParent={opts.fillParent}\n\t\t/>\n\t);\n\n\treturn (\n\t\t<Card>\n\t\t\t<CardHeader className=\"flex flex-row items-start justify-between gap-2\">\n\t\t\t\t<div className=\"flex-1 min-w-0\">\n\t\t\t\t\t<CardTitle>{title}</CardTitle>\n\t\t\t\t\t{description && <CardDescription>{description}</CardDescription>}\n\t\t\t\t</div>\n\t\t\t\t{canExport && (\n\t\t\t\t\t<div className=\"flex items-center gap-1 shrink-0\">\n\t\t\t\t\t\t<ChartExpandButton\n\t\t\t\t\t\t\texportSlug={metric}\n\t\t\t\t\t\t\ttitle={title}\n\t\t\t\t\t\t\tdescription={description}\n\t\t\t\t\t\t\trenderChart={renderChart}\n\t\t\t\t\t\t/>\n\t\t\t\t\t\t<ChartCopyButton captureRef={chartRef} exportSlug={metric} />\n\t\t\t\t\t\t<ChartExportButton captureRef={chartRef} exportSlug={metric} />\n\t\t\t\t\t</div>\n\t\t\t\t)}\n\t\t\t</CardHeader>\n\t\t\t<CardContent>\n\t\t\t\t<div ref={chartRef}>\n\t\t\t\t\t<PanelStateOrChart\n\t\t\t\t\t\tisLoading={isLoading}\n\t\t\t\t\t\tisError={isError}\n\t\t\t\t\t\terror={error}\n\t\t\t\t\t\tisEmpty={isEmpty}\n\t\t\t\t\t\tmissingFields={missingFields}\n\t\t\t\t\t\tonRetry={refetch}\n\t\t\t\t\t>\n\t\t\t\t\t\t{renderChart()}\n\t\t\t\t\t</PanelStateOrChart>\n\t\t\t\t</div>\n\t\t\t</CardContent>\n\t\t</Card>\n\t);\n}\n\nfunction PanelStateOrChart({\n\tisLoading,\n\tisError,\n\terror,\n\tisEmpty,\n\tmissingFields,\n\tonRetry,\n\tchildren,\n}: {\n\tisLoading: boolean;\n\tisError: boolean;\n\terror: Error | null;\n\tisEmpty: boolean;\n\tmissingFields: string[];\n\tonRetry: () => void;\n\tchildren: ReactNode;\n}) {\n\tif (isLoading) {\n\t\treturn (\n\t\t\t<div\n\t\t\t\trole=\"status\"\n\t\t\t\taria-live=\"polite\"\n\t\t\t\tclassName=\"h-64 rounded-md bg-muted/30 animate-pulse\"\n\t\t\t\taria-label=\"Loading\"\n\t\t\t/>\n\t\t);\n\t}\n\tif (isError) {\n\t\treturn (\n\t\t\t<div className=\"h-64 rounded-md border border-destructive/40 bg-destructive/10 p-4 text-sm text-destructive flex flex-col items-start justify-center gap-3\">\n\t\t\t\t<div>{`Failed to load: ${error?.message ?? 'unknown error'}`}</div>\n\t\t\t\t<Button variant=\"outline\" size=\"sm\" onClick={onRetry}>Retry</Button>\n\t\t\t</div>\n\t\t);\n\t}\n\tif (isEmpty) {\n\t\treturn (\n\t\t\t<div className=\"h-64 rounded-md border border-border bg-muted/20 p-4 text-sm text-muted-foreground flex items-center justify-center\">\n\t\t\t\t{missingFields.length > 0\n\t\t\t\t\t? `No data — server response is missing required field(s): ${missingFields.join(', ')}.`\n\t\t\t\t\t: 'No data in the selected time range.'}\n\t\t\t</div>\n\t\t);\n\t}\n\treturn <>{children}</>;\n}\n\nfunction collectNodes(rows: { node?: unknown }[]): string[] {\n\tconst set = new Set<string>();\n\tfor (const r of rows) {\n\t\tif (typeof r.node === 'string') { set.add(r.node); }\n\t}\n\treturn [...set].sort();\n}\n","import { MetricPanel } from './MetricPanel.tsx';\n\nconst METRICS = ['db-read', 'db-write', 'db-message'] as const;\n\nexport function DatabaseTab() {\n\treturn (\n\t\t<div className=\"grid grid-cols-1 xl:grid-cols-2 gap-4\">\n\t\t\t{METRICS.map((m) => <MetricPanel key={m} metric={m} />)}\n\t\t</div>\n\t);\n}\n","import { MetricPanel } from './MetricPanel.tsx';\n\nconst METRICS = [\n\t'resource-usage',\n\t'memory',\n\t'main-thread-utilization',\n\t'cpu-usage',\n\t'utilization',\n] as const;\n\nexport function HealthTab() {\n\treturn (\n\t\t<div className=\"grid grid-cols-1 xl:grid-cols-2 gap-4\">\n\t\t\t{METRICS.map((m) => <MetricPanel key={m} metric={m} />)}\n\t\t</div>\n\t);\n}\n","import { cn } from '@/lib/cn';\nimport * as AccordionPrimitive from '@radix-ui/react-accordion';\nimport { ChevronDownIcon } from 'lucide-react';\nimport * as React from 'react';\n\nfunction Accordion({\n\t...props\n}: React.ComponentProps<typeof AccordionPrimitive.Root>) {\n\treturn <AccordionPrimitive.Root data-slot=\"accordion\" {...props} />;\n}\n\nfunction AccordionItem({\n\tclassName,\n\t...props\n}: React.ComponentProps<typeof AccordionPrimitive.Item>) {\n\treturn (\n\t\t<AccordionPrimitive.Item\n\t\t\tdata-slot=\"accordion-item\"\n\t\t\tclassName={cn('border-b last:border-b-0', className)}\n\t\t\t{...props}\n\t\t/>\n\t);\n}\n\nfunction AccordionTrigger({\n\tclassName,\n\tchildren,\n\t...props\n}: React.ComponentProps<typeof AccordionPrimitive.Trigger>) {\n\treturn (\n\t\t<AccordionPrimitive.Header className=\"flex\">\n\t\t\t<AccordionPrimitive.Trigger\n\t\t\t\tdata-slot=\"accordion-trigger\"\n\t\t\t\tclassName={cn(\n\t\t\t\t\t'focus-visible:border-ring focus-visible:ring-ring/50 flex flex-1 items-start justify-between gap-4 rounded-md py-4 text-left text-sm font-medium transition-all outline-none hover:underline focus-visible:ring-[3px] disabled:pointer-events-none disabled:opacity-50 [&[data-state=open]>svg]:rotate-180',\n\t\t\t\t\tclassName,\n\t\t\t\t)}\n\t\t\t\t{...props}\n\t\t\t>\n\t\t\t\t{children}\n\t\t\t\t<ChevronDownIcon className=\"text-muted-foreground pointer-events-none size-4 shrink-0 translate-y-0.5 transition-transform duration-200\" />\n\t\t\t</AccordionPrimitive.Trigger>\n\t\t</AccordionPrimitive.Header>\n\t);\n}\n\nfunction AccordionContent({\n\tclassName,\n\tchildren,\n\t...props\n}: React.ComponentProps<typeof AccordionPrimitive.Content>) {\n\treturn (\n\t\t<AccordionPrimitive.Content\n\t\t\tdata-slot=\"accordion-content\"\n\t\t\tclassName=\"data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down overflow-hidden text-sm\"\n\t\t\t{...props}\n\t\t>\n\t\t\t<div className={cn('pt-0 pb-4', className)}>{children}</div>\n\t\t</AccordionPrimitive.Content>\n\t);\n}\n\nexport { Accordion, AccordionContent, AccordionItem, AccordionTrigger };\n","import { InstanceClientIdConfig } from '@/config/instanceClientConfig';\nimport { queryOptions } from '@tanstack/react-query';\n\ninterface SystemInformationResponse {\n\tsystem: {\n\t\tplatform: 'darwin' | 'linux' | string;\n\t\tdistro: 'macOS' | 'Debian GNU/Linux' | string;\n\t\trelease: string;\n\t\tcodename: string;\n\t\tkernel: string;\n\t\tarch: 'arm64' | 'x64' | string;\n\t\thostname: string;\n\t\tfqdn: string;\n\t\tnode_version: string;\n\t\tnpm_version: string;\n\t};\n\tcpu: {\n\t\tmanufacturer: 'Apple' | 'AMD' | string;\n\t\tbrand: 'M4' | string;\n\t\tvendor: 'Apple' | 'AMD' | string;\n\t\tspeed: number;\n\t\tspeedMin: number;\n\t\tspeedMax: number;\n\t\tcores: number;\n\t\tphysicalCores: number;\n\t\tperformanceCores: number;\n\t\tefficiencyCores: number;\n\t\tprocessors: number;\n\t\tflags: string;\n\t\tvirtualization: boolean;\n\t\tcpu_speed: {\n\t\t\tmin: number;\n\t\t\tmax: number;\n\t\t\tavg: number;\n\t\t\tcores: number[];\n\t\t};\n\t\tcurrent_load: {\n\t\t\tavgLoad: number;\n\t\t\tcurrentLoad: number;\n\t\t\tcurrentLoadUser: number;\n\t\t\tcurrentLoadSystem: number;\n\t\t\tcurrentLoadNice: number;\n\t\t\tcurrentLoadIdle: number;\n\t\t\tcurrentLoadIrq: number;\n\t\t\tcurrentLoadSteal: number;\n\t\t\tcurrentLoadGuest: number;\n\t\t\trawCurrentLoad: number;\n\t\t\trawCurrentLoadUser: number;\n\t\t\trawCurrentLoadSystem: number;\n\t\t\trawCurrentLoadNice: number;\n\t\t\trawCurrentLoadIdle: number;\n\t\t\trawCurrentLoadIrq: number;\n\t\t\trawCurrentLoadSteal: number;\n\t\t\trawCurrentLoadGuest: number;\n\t\t\tcpus: Array<{\n\t\t\t\tload: number;\n\t\t\t\tloadUser: number;\n\t\t\t\tloadSystem: number;\n\t\t\t\tloadNice: number;\n\t\t\t\tloadIdle: number;\n\t\t\t\tloadIrq: number;\n\t\t\t\tloadSteal: number;\n\t\t\t\tloadGuest: number;\n\t\t\t\trawLoadSteal: number;\n\t\t\t\trawLoadGuest: number;\n\t\t\t}>;\n\t\t};\n\t};\n\tmemory: {\n\t\ttotal: number;\n\t\tfree: number;\n\t\tused: number;\n\t\tactive: number;\n\t\tavailable: number;\n\t\treclaimable: number;\n\t\tswaptotal: number;\n\t\tswapused: number;\n\t\tswapfree: number;\n\t\twriteback: unknown;\n\t\tdirty: unknown;\n\t\trss: number;\n\t\theapTotal: number;\n\t\theapUsed: number;\n\t\texternal: number;\n\t\tarrayBuffers: number;\n\t};\n\tdisk: Record<string, unknown>;\n\tnetwork: {\n\t\tdefault_interface: unknown;\n\t\tlatency: Record<string, unknown>;\n\t\tinterfaces: Array<Record<string, unknown>>;\n\t\tstats: Array<Record<string, unknown>>;\n\t\tconnections: Array<Record<string, unknown>>;\n\t};\n\n\t[key: string]: unknown;\n}\n\nexport function getSystemInformationQueryOptions({ entityId, instanceClient }: InstanceClientIdConfig) {\n\treturn queryOptions({\n\t\tqueryKey: [entityId, 'system_information'] as const,\n\t\tqueryFn: async () => {\n\t\t\tconst { data } = await instanceClient.post<SystemInformationResponse>('/', {\n\t\t\t\toperation: 'system_information',\n\t\t\t\tattributes: ['network', 'disk', 'cpu', 'memory', 'system'],\n\t\t\t});\n\t\t\treturn data;\n\t\t},\n\t});\n}\n","import { excludeFalsy } from '@/lib/arrays/excludeFalsy';\nimport { humanFileSize } from '@/lib/humanFileSize';\nimport { translateSecondsToAgo } from '@/lib/translateSecondsToAgo';\n\nconst startOf2025 = new Date(2025, 0).getTime();\nconst oneDayInMs = 24 * 60 * 60 * 1000;\n\ninterface TitleItem {\n\ttitle: string;\n\tdepth: number;\n}\n\ninterface NameValuePairItem {\n\tname: string;\n\tvalue: string;\n\tdepth: number;\n}\n\ntype ItemForDisplay = TitleItem | NameValuePairItem;\n\nexport function crawlData(data: Record<string, unknown>): ItemForDisplay[] {\n\tconst sections: ItemForDisplay[] = [];\n\tfor (const key in data) {\n\t\tconst value = data[key];\n\t\tsections.push(...parseValue(key, value, 0));\n\t}\n\treturn sections;\n}\n\nexport function hasTitle(item: ItemForDisplay): item is TitleItem {\n\treturn !!(item as TitleItem).title;\n}\n\nfunction parseValue(name: string, value: unknown, depth: number, parentName?: string): ItemForDisplay[] {\n\tif (value && Array.isArray(value)) {\n\t\tconst array = value;\n\t\treturn [\n\t\t\tarray.length > 1 && { title: name, depth },\n\t\t\t...value.map((item, index) =>\n\t\t\t\tparseValue(\n\t\t\t\t\tarray.length > 1 ? String(index + 1) : name,\n\t\t\t\t\titem,\n\t\t\t\t\tdepth + 1,\n\t\t\t\t\tname,\n\t\t\t\t)\n\t\t\t).flat(1),\n\t\t].filter(excludeFalsy);\n\t}\n\tif (isObject(value)) {\n\t\tconst obj = value;\n\t\treturn [\n\t\t\t{ title: name, depth },\n\t\t\t...Object.keys(value).map(subKey =>\n\t\t\t\tparseValue(\n\t\t\t\t\tString(subKey),\n\t\t\t\t\tobj[subKey],\n\t\t\t\t\tdepth + 1,\n\t\t\t\t\tname,\n\t\t\t\t)\n\t\t\t).flat(1),\n\t\t];\n\t}\n\tif (name === '__updatedtime__' || name === '__createdtime__') {\n\t\tname = name.replace(/_/g, '').replace('time', '');\n\t}\n\tif (typeof value === 'number') {\n\t\tif (value > startOf2025 && value < Date.now() + oneDayInMs) {\n\t\t\tconst elapsed = Date.now() - value;\n\t\t\tvalue = translateSecondsToAgo(elapsed, value);\n\t\t} else if (parentName === 'memory') {\n\t\t\tvalue = humanFileSize(value);\n\t\t} else if (!name.startsWith('raw') && name.toLowerCase().includes('load')) {\n\t\t\tvalue = Math.round(value * 10) / 10 + '%';\n\t\t}\n\t} else if (typeof value === 'boolean') {\n\t\tvalue = value ? 'Yes' : 'No';\n\t}\n\treturn [\n\t\t{ name, value: String(value), depth },\n\t];\n}\n\nfunction isObject(value: unknown): value is Record<string, unknown> {\n\treturn !!value && typeof value === 'object';\n}\n","import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from '@/components/ui/accordion';\nimport { Card, CardContent } from '@/components/ui/card';\nimport type { InstanceClientIdConfig, InstanceTypeConfig } from '@/config/instanceClientConfig.ts';\nimport { getStatusQueryOptions } from '@/integrations/api/instance/status/getStatus';\nimport { getSystemInformationQueryOptions } from '@/integrations/api/instance/status/getSystemInformation';\nimport { useSuspenseQuery } from '@tanstack/react-query';\nimport { Suspense, useMemo } from 'react';\nimport { crawlData, hasTitle } from '../lib/crawlData.ts';\n\ninterface Props {\n\tinstanceParams: InstanceClientIdConfig & InstanceTypeConfig;\n\tisLocalStudio: boolean;\n}\n\nexport function OverviewTab({ instanceParams, isLocalStudio }: Props) {\n\treturn (\n\t\t<Suspense fallback={<OverviewSkeleton />}>\n\t\t\t{isLocalStudio\n\t\t\t\t? <LocalOverview instanceParams={instanceParams} />\n\t\t\t\t: <CloudOverview instanceParams={instanceParams} />}\n\t\t</Suspense>\n\t);\n}\n\nfunction OverviewSkeleton() {\n\treturn (\n\t\t<div className=\"grid grid-cols-1 md:grid-cols-2 gap-4\">\n\t\t\t{[0, 1, 2, 3].map((i) => <div key={i} className=\"h-40 rounded-lg bg-muted/30 animate-pulse\" />)}\n\t\t</div>\n\t);\n}\n\nfunction LocalOverview({ instanceParams }: { instanceParams: InstanceClientIdConfig & InstanceTypeConfig }) {\n\tconst { data } = useSuspenseQuery(getSystemInformationQueryOptions(instanceParams));\n\treturn <OverviewBody data={data as Record<string, unknown>} />;\n}\n\nfunction CloudOverview({ instanceParams }: { instanceParams: InstanceClientIdConfig & InstanceTypeConfig }) {\n\tconst { data } = useSuspenseQuery(getStatusQueryOptions(instanceParams));\n\treturn <OverviewBody data={data as Record<string, unknown>} />;\n}\n\nexport interface SectionGroup {\n\ttitle: string;\n\trows: { name: string; value: string }[];\n\tsubSections: SectionGroup[];\n}\n\nfunction OverviewBody({ data }: { data: Record<string, unknown> }) {\n\tconst sections = useMemo(() => groupSections(data), [data]);\n\n\treturn (\n\t\t<div className=\"space-y-4\">\n\t\t\t{\n\t\t\t\t/* Default-open the first section only — opening every accordion\n\t\t\t item produces a wall of text on first paint and defeats the\n\t\t\t purpose of the collapsible. */\n\t\t\t}\n\t\t\t<Accordion type=\"multiple\" defaultValue={sections.slice(0, 1).map((s) => s.title)}>\n\t\t\t\t{sections.map((section) => (\n\t\t\t\t\t<AccordionItem key={section.title} value={section.title}>\n\t\t\t\t\t\t<AccordionTrigger className=\"text-base font-semibold\">{section.title}</AccordionTrigger>\n\t\t\t\t\t\t<AccordionContent>\n\t\t\t\t\t\t\t<SectionContent section={section} />\n\t\t\t\t\t\t</AccordionContent>\n\t\t\t\t\t</AccordionItem>\n\t\t\t\t))}\n\t\t\t</Accordion>\n\t\t\t<details className=\"rounded-lg border border-border bg-card\">\n\t\t\t\t<summary className=\"cursor-pointer px-4 py-2 text-sm text-muted-foreground hover:text-foreground\">\n\t\t\t\t\tView raw JSON\n\t\t\t\t</summary>\n\t\t\t\t<pre className=\"px-4 pb-4 text-xs overflow-auto max-h-96\">\n\t\t\t\t\t{JSON.stringify(data, null, 2)}\n\t\t\t\t</pre>\n\t\t\t</details>\n\t\t</div>\n\t);\n}\n\nfunction SectionContent({ section }: { section: SectionGroup }) {\n\treturn (\n\t\t<div className=\"space-y-3\">\n\t\t\t{section.rows.length > 0 && (\n\t\t\t\t<Card>\n\t\t\t\t\t<CardContent className=\"pt-4\">\n\t\t\t\t\t\t<dl className=\"grid grid-cols-1 sm:grid-cols-2 gap-x-6 gap-y-2 text-sm\">\n\t\t\t\t\t\t\t{section.rows.map((row) => (\n\t\t\t\t\t\t\t\t<div key={row.name} className=\"flex justify-between gap-4\">\n\t\t\t\t\t\t\t\t\t<dt className=\"text-muted-foreground\">{row.name}</dt>\n\t\t\t\t\t\t\t\t\t<dd className=\"font-mono text-right truncate\" title={row.value}>{row.value}</dd>\n\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t))}\n\t\t\t\t\t\t</dl>\n\t\t\t\t\t</CardContent>\n\t\t\t\t</Card>\n\t\t\t)}\n\t\t\t{section.subSections.map((sub) => (\n\t\t\t\t<div key={sub.title} className=\"pl-3 border-l border-border\">\n\t\t\t\t\t<div className=\"text-sm font-semibold mb-2 text-muted-foreground\">{sub.title}</div>\n\t\t\t\t\t<SectionContent section={sub} />\n\t\t\t\t</div>\n\t\t\t))}\n\t\t</div>\n\t);\n}\n\nexport function groupSections(data: Record<string, unknown>): SectionGroup[] {\n\tconst items = crawlData(data);\n\ttype GroupWithDepth = SectionGroup & { _depth: number };\n\tconst top: SectionGroup[] = [];\n\tconst stack: GroupWithDepth[] = [];\n\tlet general: SectionGroup | null = null;\n\n\tfor (const item of items) {\n\t\tif (hasTitle(item)) {\n\t\t\twhile (stack.length > 0 && stack[stack.length - 1]._depth >= item.depth) {\n\t\t\t\tstack.pop();\n\t\t\t}\n\t\t\tconst group: GroupWithDepth = {\n\t\t\t\ttitle: item.title,\n\t\t\t\trows: [],\n\t\t\t\tsubSections: [],\n\t\t\t\t_depth: item.depth,\n\t\t\t};\n\t\t\tif (stack.length === 0) {\n\t\t\t\ttop.push(group);\n\t\t\t} else {\n\t\t\t\tstack[stack.length - 1].subSections.push(group);\n\t\t\t}\n\t\t\tstack.push(group);\n\t\t} else {\n\t\t\tconst target = stack[stack.length - 1];\n\t\t\tif (target) {\n\t\t\t\ttarget.rows.push({ name: item.name, value: item.value });\n\t\t\t} else {\n\t\t\t\tif (!general) {\n\t\t\t\t\tgeneral = { title: 'General', rows: [], subSections: [] };\n\t\t\t\t\ttop.unshift(general);\n\t\t\t\t}\n\t\t\t\tgeneral.rows.push({ name: item.name, value: item.value });\n\t\t\t}\n\t\t}\n\t}\n\treturn top;\n}\n","import { MetricPanel } from './MetricPanel.tsx';\n\nexport function ReplicationTab() {\n\treturn (\n\t\t<div className=\"grid grid-cols-1 gap-4\">\n\t\t\t<MetricPanel metric=\"replication-latency\" />\n\t\t</div>\n\t);\n}\n","import { MetricPanel } from './MetricPanel.tsx';\n\nconst METRICS = [\n\t'request-rate',\n\t'error-rate',\n\t'duration',\n\t'success',\n\t'transfer',\n\t'response_200',\n\t'cache-hit',\n\t'cache-resolution',\n] as const;\n\nexport function RequestsTab() {\n\treturn (\n\t\t<div className=\"grid grid-cols-1 xl:grid-cols-2 gap-4\">\n\t\t\t{METRICS.map((m) => <MetricPanel key={m} metric={m} />)}\n\t\t</div>\n\t);\n}\n","/**\n * Categorical palette for tables in the table-size dashboard.\n *\n * Deliberately distinct from NODE_PALETTE in `nodeColors.ts` so the two\n * categorical encodings (nodes and tables) don't visually collide.\n * Chosen to meet WCAG AA contrast on both dark and light backgrounds.\n */\nexport const TABLE_PALETTE = [\n\t'#e45756', // red\n\t'#f58518', // orange\n\t'#eeca3b', // yellow\n\t'#54a24b', // green\n\t'#4c78a8', // blue\n\t'#b279a2', // mauve\n\t'#9d755d', // brown\n\t'#17becf', // cyan\n\t'#72b7b2', // teal\n\t'#bab0ac', // grey\n] as const;\n\n/** Colour for the rolled-up \"Other\" stack — neutral grey so it reads as an aggregate. */\nexport const OTHER_COLOR = '#6b7280';\n\nexport function getTableColor(index: number): string {\n\treturn TABLE_PALETTE[index % TABLE_PALETTE.length];\n}\n","import type { TableSizeRecord, TimeRange } from '../types/analytics.ts';\n\nexport type RankBy = 'bytes' | 'percent';\nexport type EmptyCause = 'upstream-empty' | 'all-other' | null;\n\nexport interface NormalizedRecord {\n\tdatabase: string;\n\ttable: string;\n\t/** Stable \"table key\" used everywhere as \"database.table\". */\n\ttableKey: string;\n\tnode: string;\n\t/** Timestamp in ms (normalized from Harper's `id` field). */\n\ttime: number;\n\tsize: number;\n}\n\nexport interface SnapshotByNodeEntry {\n\tnode: string;\n\t/** Map of tableKey -> bytes, restricted to tables in `tableSet` (plus \"Other\" when applicable). */\n\tstacks: Record<string, number>;\n\t/** Sum of all bytes for this node across ALL tables (not just the top-N). */\n\ttotal: number;\n}\n\nexport interface Snapshot {\n\tbyNode: SnapshotByNodeEntry[];\n\t/** Top-N table keys, in stable display order (rank desc, tie-break alphabetical). */\n\ttableSet: string[];\n\thasOther: boolean;\n\t/** Tables rolled into Other (for tooltips/diagnostics). */\n\totherMembers: string[];\n}\n\nexport interface TrendPoint {\n\ttime: number; // bucket start (ms)\n\tvalues: Record<string, /* node */ number /* bytes */>;\n}\n\nexport interface TableSizeDerived {\n\tsnapshot: Snapshot;\n\ttrend: (selectedTable: string) => TrendPoint[];\n\tdefaultSelection: (rankBy: RankBy) => string | null;\n\temptyCause: EmptyCause;\n\t/** Content signature for memo keys. */\n\tsignature: string;\n}\n\n/** Targeted max number of top-N tables on Panel 1. */\nexport const TOP_N = 8;\n\nexport const OTHER_KEY = '__other__';\n\n/** Threshold below which a table is considered empty/static and excluded from top-N. */\nconst MEANINGFUL_SIZE_THRESHOLD = 4096;\n\n/** Compute bucket width (ms) for trend rendering. */\nexport function computeBucketMs(windowMs: number): number {\n\treturn Math.max(60_000, Math.ceil(windowMs / 90));\n}\n\n/** Build a stable \"database.table\" key. */\nexport function toTableKey(r: { database: string; table: string }): string {\n\treturn `${r.database}.${r.table}`;\n}\n\n/** Normalize raw records: map id→time, sort by time, build tableKey. Does NOT dedup. */\nexport function normalizeRecords(raw: TableSizeRecord[]): NormalizedRecord[] {\n\tconst out: NormalizedRecord[] = raw.map((r) => ({\n\t\tdatabase: r.database,\n\t\ttable: r.table,\n\t\ttableKey: toTableKey(r),\n\t\tnode: r.node,\n\t\ttime: r.id,\n\t\tsize: r.size,\n\t}));\n\t// Stable sort by time ascending; Array.prototype.sort is stable in Node 22+.\n\tout.sort((a, b) => a.time - b.time);\n\treturn out;\n}\n\n/** Drop consecutive unchanged-size repeats per (node, tableKey). */\nexport function dedupRecords(normalized: NormalizedRecord[]): NormalizedRecord[] {\n\tconst lastSize = new Map<string, number>(); // key = `${node}\\0${tableKey}`\n\tconst kept: NormalizedRecord[] = [];\n\tfor (const r of normalized) {\n\t\tconst key = `${r.node}\\0${r.tableKey}`;\n\t\tif (lastSize.get(key) === r.size) { continue; // unchanged repeat\n\t\t }\n\t\tkept.push(r);\n\t\tlastSize.set(key, r.size);\n\t}\n\treturn kept;\n}\n\n/** Rank tables by max-per-node size; return top-N keys and rollup membership. */\nexport function computeTableSet(\n\tnormalized: NormalizedRecord[],\n): { tableSet: string[]; hasOther: boolean; otherMembers: string[] } {\n\t// For each tableKey, compute max size observed on ANY single node in the window.\n\t// Note: use `has`/`undefined` as the \"not yet seen\" sentinel, not `0`, so a\n\t// table whose size is 0 still appears in the key set and flows through to\n\t// otherMembers (rather than being silently dropped).\n\tconst maxPerNode = new Map<string, number>(); // tableKey -> max over nodes of (max over time)\n\tconst perNodeMax = new Map<string, number>(); // `${tableKey}\\0${node}` -> max size\n\tfor (const r of normalized) {\n\t\tconst k = `${r.tableKey}\\0${r.node}`;\n\t\tconst prev = perNodeMax.get(k);\n\t\tif (prev === undefined || r.size > prev) { perNodeMax.set(k, r.size); }\n\t}\n\tfor (const [k, v] of perNodeMax) {\n\t\tconst [tableKey] = k.split('\\0');\n\t\tconst prev = maxPerNode.get(tableKey);\n\t\tif (prev === undefined || v > prev) { maxPerNode.set(tableKey, v); }\n\t}\n\n\t// Keep only meaningful tables.\n\tconst meaningful = [...maxPerNode.entries()].filter(\n\t\t([, v]) => v > MEANINGFUL_SIZE_THRESHOLD,\n\t);\n\n\t// Rank desc, tie-break alphabetical.\n\tmeaningful.sort((a, b) => {\n\t\tif (b[1] !== a[1]) { return b[1] - a[1]; }\n\t\treturn a[0].localeCompare(b[0]);\n\t});\n\n\tconst allMeaningfulKeys = meaningful.map(([k]) => k);\n\n\t// Top-N rule:\n\t// <= TOP_N+1 meaningful tables -> keep all inline; no rollup.\n\t// > TOP_N+1 -> keep top-N; roll up the rest into Other.\n\t// Below-threshold tables always land in Other so they stay discoverable in\n\t// the tooltip / aggregate stack. Without this, on clusters like ours — where\n\t// ~17 static 4 KB tables sit alongside 3 growing ones — the static cohort\n\t// vanishes from the UI entirely.\n\tconst belowThreshold = [...maxPerNode.keys()].filter(\n\t\t(k) => !allMeaningfulKeys.includes(k),\n\t);\n\tbelowThreshold.sort((a, b) => a.localeCompare(b));\n\n\tif (allMeaningfulKeys.length <= TOP_N + 1) {\n\t\treturn {\n\t\t\ttableSet: allMeaningfulKeys,\n\t\t\thasOther: belowThreshold.length > 0,\n\t\t\totherMembers: belowThreshold,\n\t\t};\n\t}\n\n\tconst tableSet = allMeaningfulKeys.slice(0, TOP_N);\n\tconst rolledUpMeaningful = allMeaningfulKeys.slice(TOP_N);\n\tconst otherMembers = [...rolledUpMeaningful, ...belowThreshold];\n\treturn { tableSet, hasOther: otherMembers.length > 0, otherMembers };\n}\n\n/** Build the per-node snapshot rows using the supplied tableSet. */\nexport function computeSnapshot(\n\tnormalized: NormalizedRecord[],\n\ttableSet: string[],\n\thasOther: boolean,\n): SnapshotByNodeEntry[] {\n\t// For each (node, tableKey), find the latest size.\n\tconst latest = new Map<string, { size: number; time: number; tableKey: string; node: string }>();\n\tfor (const r of normalized) {\n\t\tconst k = `${r.node}\\0${r.tableKey}`;\n\t\tconst prev = latest.get(k);\n\t\tif (!prev || r.time >= prev.time) {\n\t\t\tlatest.set(k, { size: r.size, time: r.time, tableKey: r.tableKey, node: r.node });\n\t\t}\n\t}\n\n\tconst top = new Set(tableSet);\n\tconst byNode = new Map<string, SnapshotByNodeEntry>();\n\tfor (const { size, tableKey, node } of latest.values()) {\n\t\tif (!byNode.has(node)) { byNode.set(node, { node, stacks: {}, total: 0 }); }\n\t\tconst entry = byNode.get(node)!;\n\t\tentry.total += size;\n\t\tif (top.has(tableKey)) {\n\t\t\tentry.stacks[tableKey] = size;\n\t\t} else if (hasOther) {\n\t\t\tentry.stacks[OTHER_KEY] = (entry.stacks[OTHER_KEY] ?? 0) + size;\n\t\t}\n\t\t// else: below-threshold table in a no-rollup case — contributes to total only.\n\t}\n\n\t// Sort nodes for stable display order.\n\treturn [...byNode.values()].sort((a, b) => a.node.localeCompare(b.node));\n}\n\n/** Build a factory that returns trend points for a given table. */\nexport function computeTrendFactory(\n\tnormalized: NormalizedRecord[],\n\trange: TimeRange,\n): (selectedTable: string) => TrendPoint[] {\n\tconst bucketMs = computeBucketMs(range.endTime - range.startTime);\n\n\treturn function trend(selectedTable: string): TrendPoint[] {\n\t\t// Collect the latest sample per (bucket, node) for the selected table.\n\t\tconst byBucket = new Map<number, Map<string, { size: number; time: number }>>();\n\t\t// Track each node's last-sample time across the window to truncate trailing buckets.\n\t\tconst lastSampleTime = new Map<string, number>();\n\n\t\tfor (const r of normalized) {\n\t\t\tif (r.tableKey !== selectedTable) { continue; }\n\t\t\tif (r.time < range.startTime || r.time > range.endTime) { continue; }\n\t\t\tconst bucketStart = range.startTime + Math.floor((r.time - range.startTime) / bucketMs) * bucketMs;\n\t\t\tif (!byBucket.has(bucketStart)) { byBucket.set(bucketStart, new Map()); }\n\t\t\tconst nodeMap = byBucket.get(bucketStart)!;\n\t\t\tconst prev = nodeMap.get(r.node);\n\t\t\tif (!prev || r.time >= prev.time) {\n\t\t\t\tnodeMap.set(r.node, { size: r.size, time: r.time });\n\t\t\t}\n\t\t\tconst lastTime = lastSampleTime.get(r.node) ?? 0;\n\t\t\tif (r.time > lastTime) { lastSampleTime.set(r.node, r.time); }\n\t\t}\n\n\t\tconst points: TrendPoint[] = [];\n\t\tconst sortedBuckets = [...byBucket.keys()].sort((a, b) => a - b);\n\t\tfor (const bucketStart of sortedBuckets) {\n\t\t\tconst nodeMap = byBucket.get(bucketStart)!;\n\t\t\tconst values: Record<string, number> = {};\n\t\t\tfor (const [node, { size }] of nodeMap) {\n\t\t\t\t// Drop buckets that start after the node's last sample (truncate trailing).\n\t\t\t\tconst lastTime = lastSampleTime.get(node) ?? 0;\n\t\t\t\tif (bucketStart > lastTime) { continue; }\n\t\t\t\tvalues[node] = size;\n\t\t\t}\n\t\t\tif (Object.keys(values).length > 0) { points.push({ time: bucketStart, values }); }\n\t\t}\n\t\treturn points;\n\t};\n}\n\n/** Compute the default selection (largest delta) for a given ranking. */\nexport function computeDefaultSelection(\n\tnormalized: NormalizedRecord[],\n\trankBy: RankBy,\n): string | null {\n\tif (normalized.length === 0) { return null; }\n\n\t// Group samples by (tableKey, node) to compute per-node min/max.\n\tconst perPair = new Map<string, { min: number; max: number; distinctTimes: Set<number> }>();\n\tfor (const r of normalized) {\n\t\tconst key = `${r.tableKey}\\0${r.node}`;\n\t\tlet agg = perPair.get(key);\n\t\tif (!agg) {\n\t\t\tagg = { min: r.size, max: r.size, distinctTimes: new Set() };\n\t\t\tperPair.set(key, agg);\n\t\t}\n\t\tif (r.size < agg.min) { agg.min = r.size; }\n\t\tif (r.size > agg.max) { agg.max = r.size; }\n\t\tagg.distinctTimes.add(r.time);\n\t}\n\n\t// Per table, find the best (max) per-node delta.\n\ttype Score = { tableKey: string; delta: number; maxSize: number; hasDelta: boolean };\n\tconst perTable = new Map<string, Score>();\n\tfor (const [key, agg] of perPair) {\n\t\tconst [tableKey] = key.split('\\0');\n\t\tconst hasDelta = agg.distinctTimes.size >= 2 && agg.max > agg.min;\n\t\tconst deltaBytes = agg.max - agg.min;\n\t\tconst deltaPct = agg.max > 0 ? deltaBytes / agg.max : 0;\n\t\tconst score = rankBy === 'bytes' ? deltaBytes : deltaPct;\n\n\t\tconst prev = perTable.get(tableKey);\n\t\tif (!prev) {\n\t\t\tperTable.set(tableKey, { tableKey, delta: score, maxSize: agg.max, hasDelta });\n\t\t} else {\n\t\t\tif (score > prev.delta) { prev.delta = score; }\n\t\t\tif (agg.max > prev.maxSize) { prev.maxSize = agg.max; }\n\t\t\tif (hasDelta) { prev.hasDelta = true; }\n\t\t}\n\t}\n\n\tconst all = [...perTable.values()];\n\n\t// Any table with a computable delta?\n\tconst withDelta = all.filter((s) => s.hasDelta);\n\tif (withDelta.length > 0) {\n\t\twithDelta.sort((a, b) => {\n\t\t\tif (b.delta !== a.delta) { return b.delta - a.delta; }\n\t\t\treturn a.tableKey.localeCompare(b.tableKey);\n\t\t});\n\t\treturn withDelta[0].tableKey;\n\t}\n\n\t// Flat window fallback: largest max-size.\n\tall.sort((a, b) => {\n\t\tif (b.maxSize !== a.maxSize) { return b.maxSize - a.maxSize; }\n\t\treturn a.tableKey.localeCompare(b.tableKey);\n\t});\n\treturn all[0]?.tableKey ?? null;\n}\n\n/** Determine the empty-state discriminator. */\nexport function computeEmptyCause(\n\trawCount: number,\n\ttableSet: string[],\n\thasOther: boolean,\n): EmptyCause {\n\tif (rawCount === 0) { return 'upstream-empty'; }\n\tif (tableSet.length === 0 && hasOther) { return 'all-other'; }\n\treturn null;\n}\n\n/** Assemble a `TableSizeDerived` from raw records + current time range. */\nexport function buildDerived(raw: TableSizeRecord[], range: TimeRange): TableSizeDerived {\n\tconst normalized = dedupRecords(normalizeRecords(raw));\n\tconst { tableSet, hasOther, otherMembers } = computeTableSet(normalized);\n\tconst byNode = computeSnapshot(normalized, tableSet, hasOther);\n\tconst trend = computeTrendFactory(normalized, range);\n\tconst emptyCause = computeEmptyCause(raw.length, tableSet, hasOther);\n\n\t// Content signature: window + a cheap digest of the raw input.\n\tconst maxId = raw.reduce((m, r) => (r.id > m ? r.id : m), 0);\n\tconst signature = `${range.startTime}:${range.endTime}:${raw.length}:${maxId}`;\n\n\treturn {\n\t\tsnapshot: { byNode, tableSet, hasOther, otherMembers },\n\t\ttrend,\n\t\tdefaultSelection: (rankBy: RankBy) => computeDefaultSelection(normalized, rankBy),\n\t\temptyCause,\n\t\tsignature,\n\t};\n}\n\nexport interface SelectionResolution {\n\tnextTable: string | null;\n\tnextManual: boolean;\n}\n\n/**\n * Decide what `selectedTable` / `manualSelection` should become given the\n * previous values and the latest derived data. Pure function, so the logic\n * can be unit-tested without a React harness.\n *\n * Rules:\n * - If the user's manual pick is still in `tableSet`, keep it.\n * - Otherwise fall back to `defaultSelection(rankBy)` and clear manual.\n */\nexport function resolveSelection(input: {\n\tprev: string | null;\n\tsnapshot: Snapshot;\n\trankBy: RankBy;\n\tisManual: boolean;\n\tdefaultSelection: (rankBy: RankBy) => string | null;\n}): SelectionResolution {\n\tconst { prev, snapshot, rankBy, isManual, defaultSelection } = input;\n\tconst stillPresent = prev !== null && snapshot.tableSet.includes(prev);\n\tif (isManual && stillPresent) {\n\t\treturn { nextTable: prev, nextManual: true };\n\t}\n\treturn { nextTable: defaultSelection(rankBy), nextManual: false };\n}\n\nexport interface EmptyStateFlags {\n\t/** Render the ChartPanel's generic empty state (no data from upstream). */\n\tisEmpty: boolean;\n\t/** Render the snapshot's \"all tables are small\" inline hint in place of the chart. */\n\tallOtherHint: boolean;\n}\n\n/** Map `emptyCause` to the UI flags both panels consume. Pure for testability. */\nexport function emptyCauseToFlags(cause: EmptyCause): EmptyStateFlags {\n\treturn {\n\t\tisEmpty: cause === 'upstream-empty',\n\t\tallOtherHint: cause === 'all-other',\n\t};\n}\n\n/**\n * Compute the legend growth annotation for a single node across a trend's\n * points. `windowMs` is the panel's requested range (not the samples' span)\n * so the `/hr` rate reflects the user-selected window.\n *\n * Returns an empty string when the annotation wouldn't be meaningful:\n * - fewer than 2 samples\n * - no observed change (delta ≤ 0)\n * - windowMs ≤ 0 (inverted or zero-width range)\n */\nexport function computeGrowthAnnotation(input: {\n\tpoints: Array<{ time: number; values: Record<string, number> }>;\n\tnode: string;\n\twindowMs: number;\n\trankBy: RankBy;\n\tformatBytes: (bytes: number) => string;\n}): string {\n\tconst { points, node, windowMs, rankBy, formatBytes } = input;\n\tconst samples = points\n\t\t.map((p) => p.values[node])\n\t\t.filter((v): v is number => typeof v === 'number');\n\tif (samples.length < 2) { return ''; }\n\tlet min = samples[0];\n\tlet max = samples[0];\n\tfor (const v of samples) {\n\t\tif (v < min) { min = v; }\n\t\tif (v > max) { max = v; }\n\t}\n\tconst delta = max - min;\n\tif (delta <= 0) { return ''; }\n\tif (rankBy === 'percent') {\n\t\tconst pct = max > 0 ? (delta / max) * 100 : 0;\n\t\treturn `+${pct.toFixed(1)}%/window`;\n\t}\n\tif (windowMs <= 0) { return ''; }\n\tconst hours = windowMs / (1000 * 60 * 60);\n\tconst perHr = delta / hours;\n\treturn `+${formatBytes(delta)} (${formatBytes(perHr)}/hr)`;\n}\n","export type Theme = 'light' | 'dark';\n\nconst STORAGE_KEY = 'analytics-viz-theme';\n\nexport function getStoredTheme(): Theme {\n\t// Safari private mode + sandboxed iframes throw on localStorage access.\n\t// Fall back to the OS preference rather than blowing up the whole tab.\n\ttry {\n\t\tconst stored = localStorage.getItem(STORAGE_KEY);\n\t\tif (stored === 'light' || stored === 'dark') { return stored; }\n\t} catch {\n\t\t// fall through to media-query fallback\n\t}\n\treturn window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';\n}\n\nexport function setStoredTheme(theme: Theme): void {\n\ttry {\n\t\tlocalStorage.setItem(STORAGE_KEY, theme);\n\t} catch {\n\t\t// best-effort; silently drop in restricted-storage environments\n\t}\n}\n\nexport function applyTheme(theme: Theme): void {\n\tdocument.documentElement.classList.toggle('dark', theme === 'dark');\n}\n\n/** Reads the studio chart-surface CSS tokens defined in src/index.css.\n * All charts render inside a `Card`, so axis/grid/tooltip colors resolve\n * against `--card`, not the brand-purple `--background`. The hex defaults\n * here are fallbacks for non-DOM environments (tests). */\nexport function getChartColors(_theme: Theme) {\n\treturn {\n\t\taxisColor: 'var(--chart-axis, #6b7280)',\n\t\tgridColor: 'var(--chart-grid, #e5e7eb)',\n\t\ttooltipBg: 'var(--chart-tooltip-bg, #ffffff)',\n\t\ttooltipBorder: 'var(--chart-grid, #d1d5db)',\n\t\ttextColor: 'var(--chart-tooltip-fg, #1f2937)',\n\t};\n}\n","import { type KeyboardEvent, useEffect, useRef } from 'react';\nimport { getTableColor, OTHER_COLOR } from '../lib/tableColors.ts';\nimport { OTHER_KEY } from '../lib/tableSize.ts';\n\ninterface TableSizeChipRowProps {\n\t/** Selectable table keys, in display order. */\n\ttableSet: string[];\n\t/** Whether to render a non-interactive \"Other\" chip. */\n\thasOther: boolean;\n\t/** Currently-selected table key (or null). */\n\tselectedTable: string | null;\n\t/**\n\t * Called when the user picks a chip via click, Enter, Space, or arrow-key\n\t * traversal. Per the ARIA radiogroup pattern, arrow keys move focus AND\n\t * selection — both routes pin `manualSelection=true` in Dashboard so the\n\t * pick survives subsequent data refreshes until the selected table\n\t * disappears from `tableSet`.\n\t */\n\tonSelectTable: (tableKey: string) => void;\n}\n\nexport function TableSizeChipRow({\n\ttableSet,\n\thasOther,\n\tselectedTable,\n\tonSelectTable,\n}: TableSizeChipRowProps) {\n\tconst chipRefs = useRef<Array<HTMLButtonElement | null>>([]);\n\n\tuseEffect(() => {\n\t\tchipRefs.current = chipRefs.current.slice(0, tableSet.length);\n\t}, [tableSet.length]);\n\n\t// Figure out which chip gets tabIndex=0. Default to the selected chip; if\n\t// selectedTable is not in tableSet (e.g. transient refetch gap, or selection\n\t// is `Other`), fall back to the first chip so the radiogroup stays reachable.\n\tconst activeIdx = selectedTable === null ? -1 : tableSet.indexOf(selectedTable);\n\tconst tabbableIdx = activeIdx >= 0 ? activeIdx : 0;\n\n\tfunction handleKeyDown(e: KeyboardEvent<HTMLButtonElement>, idx: number) {\n\t\tif (e.key === 'Enter' || e.key === ' ') {\n\t\t\te.preventDefault();\n\t\t\tonSelectTable(tableSet[idx]);\n\t\t\treturn;\n\t\t}\n\n\t\tif (\n\t\t\te.key !== 'ArrowLeft'\n\t\t\t&& e.key !== 'ArrowRight'\n\t\t\t&& e.key !== 'ArrowDown'\n\t\t\t&& e.key !== 'ArrowUp'\n\t\t) {\n\t\t\treturn;\n\t\t}\n\n\t\te.preventDefault();\n\t\tconst n = tableSet.length;\n\t\tif (n === 0) { return; }\n\t\tlet next = idx;\n\t\tif (e.key === 'ArrowRight' || e.key === 'ArrowDown') { next = (idx + 1) % n; }\n\t\tif (e.key === 'ArrowLeft' || e.key === 'ArrowUp') { next = (idx - 1 + n) % n; }\n\t\tchipRefs.current[next]?.focus();\n\t\tonSelectTable(tableSet[next]);\n\t}\n\n\tif (tableSet.length === 0 && !hasOther) { return null; }\n\n\treturn (\n\t\t<div\n\t\t\trole=\"radiogroup\"\n\t\t\taria-label=\"Table selector\"\n\t\t\tdata-testid=\"table-size-chip-row\"\n\t\t\tclassName=\"flex flex-wrap gap-2 pt-3\"\n\t\t>\n\t\t\t{tableSet.map((tableKey, idx) => {\n\t\t\t\tconst selected = tableKey === selectedTable;\n\t\t\t\tconst color = getTableColor(idx);\n\t\t\t\treturn (\n\t\t\t\t\t<button\n\t\t\t\t\t\tkey={tableKey}\n\t\t\t\t\t\tref={(el) => {\n\t\t\t\t\t\t\tchipRefs.current[idx] = el;\n\t\t\t\t\t\t}}\n\t\t\t\t\t\trole=\"radio\"\n\t\t\t\t\t\taria-checked={selected}\n\t\t\t\t\t\ttabIndex={idx === tabbableIdx ? 0 : -1}\n\t\t\t\t\t\tdata-testid=\"table-size-chip\"\n\t\t\t\t\t\tdata-table={tableKey}\n\t\t\t\t\t\tonKeyDown={(e) => handleKeyDown(e, idx)}\n\t\t\t\t\t\tonClick={() => onSelectTable(tableKey)}\n\t\t\t\t\t\tclassName={`inline-flex min-h-8 items-center gap-1.5 rounded-full border px-2.5 py-1 text-xs ${\n\t\t\t\t\t\t\tselected\n\t\t\t\t\t\t\t\t? 'font-semibold text-(--color-text-primary)'\n\t\t\t\t\t\t\t\t: 'border-(--color-border) text-(--color-text-secondary) hover:text-(--color-text-primary)'\n\t\t\t\t\t\t}`}\n\t\t\t\t\t\tstyle={{ borderColor: selected ? color : undefined }}\n\t\t\t\t\t>\n\t\t\t\t\t\t<span className=\"inline-block h-2 w-2 rounded-full\" style={{ backgroundColor: color }} />\n\t\t\t\t\t\t{tableKey}\n\t\t\t\t\t</button>\n\t\t\t\t);\n\t\t\t})}\n\t\t\t{hasOther && (\n\t\t\t\t<button\n\t\t\t\t\ttype=\"button\"\n\t\t\t\t\taria-disabled=\"true\"\n\t\t\t\t\ttabIndex={-1}\n\t\t\t\t\tdata-testid=\"table-size-chip\"\n\t\t\t\t\tdata-table={OTHER_KEY}\n\t\t\t\t\ttitle=\"Aggregate of smaller tables; not selectable.\"\n\t\t\t\t\tclassName=\"inline-flex min-h-8 items-center gap-1.5 rounded-full border border-dashed border-(--color-border) px-2.5 py-1 text-xs text-(--color-text-secondary)/60 cursor-not-allowed\"\n\t\t\t\t>\n\t\t\t\t\t<span className=\"inline-block h-2 w-2 rounded-full\" style={{ backgroundColor: OTHER_COLOR }} />\n\t\t\t\t\tOther\n\t\t\t\t</button>\n\t\t\t)}\n\t\t</div>\n\t);\n}\n","import { Bar, BarChart, CartesianGrid, Cell, ResponsiveContainer, Tooltip, XAxis, YAxis } from 'recharts';\nimport { useNodeSelection } from '../hooks/useNodeSelection.ts';\nimport { getTableColor, OTHER_COLOR } from '../lib/tableColors.ts';\nimport { OTHER_KEY, type Snapshot } from '../lib/tableSize.ts';\nimport { getChartColors, type Theme } from '../lib/theme.ts';\nimport { formatBytes } from '../lib/time.ts';\nimport type { ViewMode } from '../types/analytics.ts';\nimport { NodeLegend } from './NodeLegend.tsx';\nimport { TableSizeChipRow } from './TableSizeChipRow.tsx';\n\ninterface Props {\n\tsnapshot: Snapshot;\n\t/** Display mode: 'per-node' → Absolute (raw bytes), 'aggregate' → Normalized (percent of cluster max). */\n\tviewMode: ViewMode;\n\ttheme: Theme;\n\t/** Snapshot's own highlight — drives chip `aria-checked` + bar-segment outline. */\n\tselectedTable: string | null;\n\t/** Chip click / Enter / Space / arrow-nav — local to this panel; should NOT\n\t * drive Trend. */\n\tonChipSelect: (tableKey: string) => void;\n\t/** Bar-segment click — drilldown signal: should drive both this panel's\n\t * highlight AND the Trend panel's selection. */\n\tonBarClick: (tableKey: string) => void;\n\t/** Rendered inline when `emptyCause === 'all-other'`. */\n\tallOtherHint?: boolean;\n}\n\ninterface Row {\n\tnode: string;\n\t__total__: number;\n\t/** Aliased values keyed by `t_<idx>` (matching `stackKeys` index) so Recharts'\n\t * string `dataKey` lookup works — table names like `data.events` would\n\t * otherwise be split as object paths. */\n\t[aliasKey: string]: number | string;\n}\n\n/**\n * Both modes produce the same row shape: stacks carry absolute bytes, with\n * `__total__` tracking the node's cross-all-tables total. The mode only\n * changes how the y-axis renders those bytes. This is what gives Normalized\n * its \"visible gap\" behavior: a node missing a top-N table has a shorter\n * bar (not a re-stretched 100%), and a tall node anchors the 100% tick.\n */\nfunction toRows(\n\tsnapshot: Snapshot,\n\tactiveNodes: (n: string) => boolean,\n\tstackKeys: string[],\n): Row[] {\n\treturn snapshot.byNode\n\t\t.filter((n) => activeNodes(n.node))\n\t\t.map((n) => {\n\t\t\tconst aliased: Record<string, number> = {};\n\t\t\tstackKeys.forEach((tableKey, idx) => {\n\t\t\t\taliased[`t_${idx}`] = n.stacks[tableKey] ?? 0;\n\t\t\t});\n\t\t\treturn { node: n.node, ...aliased, __total__: n.total };\n\t\t});\n}\n\nexport function TableSizeSnapshot({\n\tsnapshot,\n\tviewMode,\n\ttheme,\n\tselectedTable,\n\tonChipSelect,\n\tonBarClick,\n\tallOtherHint,\n}: Props) {\n\tconst colors = getChartColors(theme);\n\t// The full cluster node list, used both for the legend and for color\n\t// assignment so the same node gets the same color on both panels.\n\tconst clusterNodeIds = snapshot.byNode.map((n) => n.node);\n\tconst { isActive, handleLegendClick } = useNodeSelection(clusterNodeIds);\n\tconst normalized = viewMode === 'aggregate';\n\n\tif (allOtherHint) {\n\t\treturn (\n\t\t\t<div className=\"flex h-full items-center justify-center text-sm text-(--color-text-secondary)\">\n\t\t\t\tAll tables are small within this window — widen the range to see growth.\n\t\t\t</div>\n\t\t);\n\t}\n\n\tconst stackKeys = [...snapshot.tableSet, ...(snapshot.hasOther ? [OTHER_KEY] : [])];\n\tconst rows = toRows(snapshot, isActive, stackKeys);\n\n\t// In Normalized mode the y-axis is scaled so the tallest (node-total)\n\t// bar hits 100%. Other bars sit shorter and missing segments show up\n\t// as visible gaps rather than renormalized stacks.\n\tconst clusterMaxTotal = rows.reduce((m, r) => Math.max(m, r.__total__), 0) || 1;\n\n\treturn (\n\t\t<div className=\"h-full flex flex-col\">\n\t\t\t<div style={{ width: '100%', height: 300 }}>\n\t\t\t\t<ResponsiveContainer width=\"100%\" height=\"100%\" minWidth={0}>\n\t\t\t\t\t<BarChart data={rows} barCategoryGap=\"20%\">\n\t\t\t\t\t\t<CartesianGrid stroke={colors.gridColor} strokeDasharray=\"3 3\" />\n\t\t\t\t\t\t<XAxis dataKey=\"node\" stroke={colors.axisColor} tick={{ fontSize: 11 }} />\n\t\t\t\t\t\t<YAxis\n\t\t\t\t\t\t\tstroke={colors.axisColor}\n\t\t\t\t\t\t\ttick={{ fontSize: 11 }}\n\t\t\t\t\t\t\ttickFormatter={(v) => {\n\t\t\t\t\t\t\t\tconst n = Number(v);\n\t\t\t\t\t\t\t\tif (normalized) {\n\t\t\t\t\t\t\t\t\treturn `${Math.round((n / clusterMaxTotal) * 100)}%`;\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\treturn formatBytes(n);\n\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t\tdomain={normalized ? [0, clusterMaxTotal] : ['auto', 'auto']}\n\t\t\t\t\t\t/>\n\t\t\t\t\t\t<Tooltip\n\t\t\t\t\t\t\tcontentStyle={{\n\t\t\t\t\t\t\t\tbackgroundColor: colors.tooltipBg,\n\t\t\t\t\t\t\t\tborder: `1px solid ${colors.tooltipBorder}`,\n\t\t\t\t\t\t\t\tborderRadius: 8,\n\t\t\t\t\t\t\t\tfontSize: 12,\n\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t\tformatter={(value, name, ctx) => {\n\t\t\t\t\t\t\t\tconst nameStr = String(name);\n\t\t\t\t\t\t\t\tconst label = nameStr === OTHER_KEY ? 'Other' : nameStr;\n\t\t\t\t\t\t\t\tconst numValue = Number(value);\n\t\t\t\t\t\t\t\tconst total = ((ctx as { payload?: Row })?.payload?.__total__ as number) ?? 0;\n\t\t\t\t\t\t\t\tconst pct = total > 0 ? ((numValue / total) * 100).toFixed(1) : '0';\n\t\t\t\t\t\t\t\treturn [\n\t\t\t\t\t\t\t\t\t`${formatBytes(numValue)} (${pct}% of node total ${formatBytes(total)})`,\n\t\t\t\t\t\t\t\t\tlabel,\n\t\t\t\t\t\t\t\t];\n\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t/>\n\t\t\t\t\t\t{stackKeys.map((tableKey, idx) => {\n\t\t\t\t\t\t\tconst baseColor = tableKey === OTHER_KEY ? OTHER_COLOR : getTableColor(idx);\n\t\t\t\t\t\t\tconst isSelected = tableKey === selectedTable;\n\t\t\t\t\t\t\treturn (\n\t\t\t\t\t\t\t\t<Bar\n\t\t\t\t\t\t\t\t\tkey={tableKey}\n\t\t\t\t\t\t\t\t\t// Aliased dataKey ('t_<idx>') so Recharts' string-path lookup\n\t\t\t\t\t\t\t\t\t// works — table names like 'data.events' contain dots and would\n\t\t\t\t\t\t\t\t\t// otherwise be parsed as object paths. `name` keeps the real\n\t\t\t\t\t\t\t\t\t// table key for tooltips.\n\t\t\t\t\t\t\t\t\tdataKey={`t_${idx}`}\n\t\t\t\t\t\t\t\t\tname={tableKey}\n\t\t\t\t\t\t\t\t\tstackId=\"size\"\n\t\t\t\t\t\t\t\t\tfill={baseColor}\n\t\t\t\t\t\t\t\t\tstroke={isSelected ? baseColor : 'transparent'}\n\t\t\t\t\t\t\t\t\tstrokeWidth={isSelected ? 2 : 0}\n\t\t\t\t\t\t\t\t\tonClick={() => {\n\t\t\t\t\t\t\t\t\t\tif (tableKey !== OTHER_KEY) { onBarClick(tableKey); }\n\t\t\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t\t\t\tstyle={{ cursor: tableKey === OTHER_KEY ? 'not-allowed' : 'pointer' }}\n\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t{rows.map((row) => (\n\t\t\t\t\t\t\t\t\t\t<Cell\n\t\t\t\t\t\t\t\t\t\t\tkey={row.node}\n\t\t\t\t\t\t\t\t\t\t\tdata-testid=\"table-size-segment\"\n\t\t\t\t\t\t\t\t\t\t\tdata-table={tableKey}\n\t\t\t\t\t\t\t\t\t\t\tdata-node={row.node}\n\t\t\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t\t\t))}\n\t\t\t\t\t\t\t\t</Bar>\n\t\t\t\t\t\t\t);\n\t\t\t\t\t\t})}\n\t\t\t\t\t</BarChart>\n\t\t\t\t</ResponsiveContainer>\n\t\t\t</div>\n\t\t\t<NodeLegend nodeIds={clusterNodeIds} isActive={isActive} onClickNode={handleLegendClick} />\n\t\t\t<TableSizeChipRow\n\t\t\t\ttableSet={snapshot.tableSet}\n\t\t\t\thasOther={snapshot.hasOther}\n\t\t\t\tselectedTable={selectedTable}\n\t\t\t\tonSelectTable={onChipSelect}\n\t\t\t/>\n\t\t</div>\n\t);\n}\n","import { useMemo } from 'react';\nimport { CartesianGrid, Line, LineChart, ResponsiveContainer, Tooltip, XAxis, YAxis } from 'recharts';\nimport { useNodeSelection } from '../hooks/useNodeSelection.ts';\nimport { getNodeColor } from '../lib/nodeColors.ts';\nimport { computeGrowthAnnotation, type RankBy, type TableSizeDerived } from '../lib/tableSize.ts';\nimport { getChartColors, type Theme } from '../lib/theme.ts';\nimport { formatAxisTick, formatBytes, formatTooltipTime } from '../lib/time.ts';\nimport type { TimeRange, ViewMode } from '../types/analytics.ts';\nimport { NodeLegend } from './NodeLegend.tsx';\nimport { TableSizeChipRow } from './TableSizeChipRow.tsx';\n\ninterface Props {\n\tderived: TableSizeDerived;\n\tviewMode: ViewMode;\n\ttheme: Theme;\n\tselectedTable: string | null;\n\t/** Trend chip click — local to this panel; should NOT touch the Snapshot. */\n\tonChipSelect: (tableKey: string) => void;\n\t/** Whether the current selectedTable was user-chosen (true) vs auto (false). */\n\tmanualSelection: boolean;\n\t/** Panel's active time range. Used for `/hr` growth annotation so the rate\n\t * is grounded in the user-requested window, not the span of actual samples. */\n\trange: TimeRange;\n\t/** Cluster-wide node list (from snapshot). Ensures node colors match across panels. */\n\tclusterNodeIds: string[];\n\t/** Current ranking preference — controlled by Dashboard (single source of truth). */\n\trankBy: RankBy;\n\t/** User toggled the rank. Dashboard persists to localStorage. */\n\tonRankChange: (r: RankBy) => void;\n}\n\nexport function TableSizeTrend({\n\tderived,\n\tviewMode,\n\ttheme,\n\tselectedTable,\n\tonChipSelect,\n\tmanualSelection,\n\trange,\n\tclusterNodeIds,\n\trankBy,\n\tonRankChange,\n}: Props) {\n\tconst colors = getChartColors(theme);\n\n\tconst points = useMemo(\n\t\t() => (selectedTable ? derived.trend(selectedTable) : []),\n\t\t[derived, selectedTable],\n\t);\n\n\tconst nodesWithData = useMemo(() => {\n\t\tconst s = new Set<string>();\n\t\tfor (const p of points) { for (const n of Object.keys(p.values)) { s.add(n); } }\n\t\treturn [...s].sort();\n\t}, [points]);\n\n\tconst { isActive, handleLegendClick } = useNodeSelection(nodesWithData);\n\n\tconst windowMs = Math.max(0, range.endTime - range.startTime);\n\n\t// Alias node FQDN keys to `n_<idx>` so Recharts' string `dataKey` lookup\n\t// works — node names contain dots and would otherwise be parsed as paths.\n\t// MUST stay above the early `return` below to satisfy Rules of Hooks —\n\t// selectedTable can flip null↔string between renders.\n\tconst nodeAlias = useMemo(() => {\n\t\tconst m = new Map<string, string>();\n\t\tnodesWithData.forEach((node, idx) => m.set(node, `n_${idx}`));\n\t\treturn m;\n\t}, [nodesWithData]);\n\n\tconst chartData = useMemo(() =>\n\t\tpoints.map((p) => {\n\t\t\tconst aliased: Record<string, number | null> = {};\n\t\t\tfor (const [node, alias] of nodeAlias) {\n\t\t\t\taliased[alias] = p.values[node] ?? null;\n\t\t\t}\n\t\t\treturn { time: p.time, ...aliased };\n\t\t}), [points, nodeAlias]);\n\n\tif (!selectedTable) {\n\t\treturn (\n\t\t\t<div className=\"flex h-full items-center justify-center text-sm text-(--color-text-secondary)\">\n\t\t\t\tSelect a table to view trend.\n\t\t\t</div>\n\t\t);\n\t}\n\n\treturn (\n\t\t<div className=\"h-full flex flex-col\">\n\t\t\t{\n\t\t\t\t/* SR-only live region: announces selection changes without adding visible\n\t\t\t duplication of the ChartPanel title (which already interpolates the name). */\n\t\t\t}\n\t\t\t<div\n\t\t\t\taria-live=\"polite\"\n\t\t\t\tdata-testid=\"table-size-trend-title\"\n\t\t\t\tclassName=\"sr-only\"\n\t\t\t>\n\t\t\t\tTrend selection: {selectedTable}\n\t\t\t\t{!manualSelection\n\t\t\t\t\t? ` (auto-selected — ${rankBy === 'bytes' ? 'largest bytes change' : 'largest percent change'})`\n\t\t\t\t\t: ''}\n\t\t\t</div>\n\n\t\t\t{!manualSelection && (\n\t\t\t\t<div className=\"mb-1 text-[10px] text-(--color-text-secondary)\">\n\t\t\t\t\tAuto-selected by {rankBy === 'bytes' ? 'largest bytes change' : 'largest % change'}\n\t\t\t\t</div>\n\t\t\t)}\n\n\t\t\t{/* Rank toggle */}\n\t\t\t<div className=\"mb-2 flex items-center gap-2\">\n\t\t\t\t<span className=\"text-[11px] text-(--color-text-secondary)\">Rank by:</span>\n\t\t\t\t<div\n\t\t\t\t\tclassName=\"inline-flex rounded border border-(--color-border) bg-(--color-bg-tertiary) p-0.5\"\n\t\t\t\t\tdata-testid=\"table-size-rank-toggle\"\n\t\t\t\t>\n\t\t\t\t\t<button\n\t\t\t\t\t\ttype=\"button\"\n\t\t\t\t\t\taria-pressed={rankBy === 'bytes'}\n\t\t\t\t\t\tonClick={() => onRankChange('bytes')}\n\t\t\t\t\t\tclassName={`rounded px-2 py-0.5 text-[11px] ${\n\t\t\t\t\t\t\trankBy === 'bytes'\n\t\t\t\t\t\t\t\t? 'bg-(--color-bg-secondary) text-(--color-text-primary)'\n\t\t\t\t\t\t\t\t: 'text-(--color-text-secondary)'\n\t\t\t\t\t\t}`}\n\t\t\t\t\t>\n\t\t\t\t\t\tBytes changed\n\t\t\t\t\t</button>\n\t\t\t\t\t<button\n\t\t\t\t\t\ttype=\"button\"\n\t\t\t\t\t\taria-pressed={rankBy === 'percent'}\n\t\t\t\t\t\tonClick={() => onRankChange('percent')}\n\t\t\t\t\t\tclassName={`rounded px-2 py-0.5 text-[11px] ${\n\t\t\t\t\t\t\trankBy === 'percent'\n\t\t\t\t\t\t\t\t? 'bg-(--color-bg-secondary) text-(--color-text-primary)'\n\t\t\t\t\t\t\t\t: 'text-(--color-text-secondary)'\n\t\t\t\t\t\t}`}\n\t\t\t\t\t>\n\t\t\t\t\t\t% change\n\t\t\t\t\t</button>\n\t\t\t\t</div>\n\t\t\t</div>\n\n\t\t\t<div style={{ width: '100%', height: 300 }}>\n\t\t\t\t<ResponsiveContainer width=\"100%\" height=\"100%\" minWidth={0}>\n\t\t\t\t\t<LineChart data={chartData}>\n\t\t\t\t\t\t<CartesianGrid stroke={colors.gridColor} strokeDasharray=\"3 3\" />\n\t\t\t\t\t\t<XAxis\n\t\t\t\t\t\t\tdataKey=\"time\"\n\t\t\t\t\t\t\ttype=\"number\"\n\t\t\t\t\t\t\t// Pin to the requested window so a sparse table-size response\n\t\t\t\t\t\t\t// (typically one sample every few minutes) doesn't collapse\n\t\t\t\t\t\t\t// the axis to the data span.\n\t\t\t\t\t\t\tdomain={[range.startTime, range.endTime]}\n\t\t\t\t\t\t\tallowDataOverflow\n\t\t\t\t\t\t\ttickFormatter={formatAxisTick}\n\t\t\t\t\t\t\tstroke={colors.axisColor}\n\t\t\t\t\t\t\ttick={{ fontSize: 11 }}\n\t\t\t\t\t\t\tallowDuplicatedCategory={false}\n\t\t\t\t\t\t/>\n\t\t\t\t\t\t<YAxis\n\t\t\t\t\t\t\tstroke={colors.axisColor}\n\t\t\t\t\t\t\ttick={{ fontSize: 11 }}\n\t\t\t\t\t\t\ttickFormatter={formatBytes}\n\t\t\t\t\t\t\tscale={viewMode === 'per-node' ? 'log' : 'auto'}\n\t\t\t\t\t\t\tdomain={viewMode === 'per-node' ? [1, 'auto'] : ['auto', 'auto']}\n\t\t\t\t\t\t\tallowDataOverflow\n\t\t\t\t\t\t/>\n\t\t\t\t\t\t<Tooltip\n\t\t\t\t\t\t\tcontentStyle={{\n\t\t\t\t\t\t\t\tbackgroundColor: colors.tooltipBg,\n\t\t\t\t\t\t\t\tborder: `1px solid ${colors.tooltipBorder}`,\n\t\t\t\t\t\t\t\tborderRadius: 8,\n\t\t\t\t\t\t\t\tfontSize: 12,\n\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t\tlabelFormatter={(label) => formatTooltipTime(Number(label))}\n\t\t\t\t\t\t\tformatter={(value) => formatBytes(Number(value))}\n\t\t\t\t\t\t/>\n\t\t\t\t\t\t{nodesWithData\n\t\t\t\t\t\t\t.filter((n) => isActive(n))\n\t\t\t\t\t\t\t.map((node) => (\n\t\t\t\t\t\t\t\t<Line\n\t\t\t\t\t\t\t\t\tkey={node}\n\t\t\t\t\t\t\t\t\t// Aliased dataKey ('n_<idx>') so Recharts' string-path lookup\n\t\t\t\t\t\t\t\t\t// works — node FQDNs contain dots and would be parsed as paths.\n\t\t\t\t\t\t\t\t\tdataKey={nodeAlias.get(node)!}\n\t\t\t\t\t\t\t\t\tname={`${node} ${computeGrowthAnnotation({ points, node, windowMs, rankBy, formatBytes })}`.trim()}\n\t\t\t\t\t\t\t\t\t// Use clusterNodeIds (not nodesWithData) so the same node gets\n\t\t\t\t\t\t\t\t\t// the same color on both snapshot and trend panels.\n\t\t\t\t\t\t\t\t\tstroke={getNodeColor(node, clusterNodeIds)}\n\t\t\t\t\t\t\t\t\tstrokeWidth={2}\n\t\t\t\t\t\t\t\t\tdot={false}\n\t\t\t\t\t\t\t\t\ttype=\"monotone\"\n\t\t\t\t\t\t\t\t\tconnectNulls={false}\n\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t))}\n\t\t\t\t\t</LineChart>\n\t\t\t\t</ResponsiveContainer>\n\t\t\t</div>\n\t\t\t<NodeLegend nodeIds={nodesWithData} isActive={isActive} onClickNode={handleLegendClick} />\n\t\t\t<TableSizeChipRow\n\t\t\t\ttableSet={derived.snapshot.tableSet}\n\t\t\t\thasOther={derived.snapshot.hasOther}\n\t\t\t\tselectedTable={selectedTable}\n\t\t\t\tonSelectTable={onChipSelect}\n\t\t\t/>\n\t\t</div>\n\t);\n}\n","import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';\nimport { useMemo, useRef, useState } from 'react';\nimport { TableSizeSnapshot } from '../charts/TableSizeSnapshot.tsx';\nimport { TableSizeTrend } from '../charts/TableSizeTrend.tsx';\nimport { ChartCopyButton } from '../components/ChartCopyButton.tsx';\nimport { ChartExpandButton } from '../components/ChartExpandButton.tsx';\nimport { ChartExportButton } from '../components/ChartExportButton.tsx';\nimport { useAnalyticsContext } from '../context/AnalyticsContext.tsx';\nimport { useAnalyticsRecords } from '../hooks/useAnalyticsRecords.ts';\nimport { buildDerived, type RankBy, type TableSizeDerived } from '../lib/tableSize.ts';\nimport type { TableSizeRecord } from '../types/analytics.ts';\n\nfunction derivedClusterNodes(derived: TableSizeDerived): string[] {\n\treturn derived.snapshot.byNode.map((b) => b.node);\n}\nimport { MetricPanel } from './MetricPanel.tsx';\nimport { PanelErrorBoundary } from './PanelErrorBoundary.tsx';\n\nexport function StorageTab() {\n\treturn (\n\t\t<div className=\"grid grid-cols-1 gap-4\">\n\t\t\t<PanelErrorBoundary metric=\"table-size\">\n\t\t\t\t<TableSizePanels />\n\t\t\t</PanelErrorBoundary>\n\t\t\t<div className=\"grid grid-cols-1 xl:grid-cols-2 gap-4\">\n\t\t\t\t<MetricPanel metric=\"database-size\" />\n\t\t\t\t<MetricPanel metric=\"transaction-log-growth\" />\n\t\t\t\t<MetricPanel metric=\"storage-volume\" />\n\t\t\t</div>\n\t\t</div>\n\t);\n}\n\nfunction TableSizePanels() {\n\tconst { timeRange, bucketMs, refreshIntervalMs, theme, instanceParams } = useAnalyticsContext();\n\tconst { data, isLoading, isError } = useAnalyticsRecords({\n\t\tmetric: 'table-size',\n\t\tstartTime: timeRange.startTime,\n\t\tendTime: timeRange.endTime,\n\t\tinstanceParams,\n\t\trefetchIntervalMs: refreshIntervalMs,\n\t\tbucketMs,\n\t});\n\n\t// Some Harper builds emit the bucket timestamp as `id`, others as `time`.\n\t// Accept either, but synthesize `id` from `time` so downstream\n\t// (buildDerived → tableSize.normalizeRecords) gets the contract it\n\t// expects. Drop rows that have neither — they'd feed NaN timestamps and\n\t// silently produce wrong snapshots.\n\tconst raw = useMemo(() => {\n\t\tconst rows = (data ?? []) as unknown as Array<TableSizeRecord & { time?: number }>;\n\t\tconst cleaned: TableSizeRecord[] = [];\n\t\tlet withoutTimestamp = 0;\n\t\tfor (const r of rows) {\n\t\t\tif (typeof r.id === 'number') {\n\t\t\t\tcleaned.push(r);\n\t\t\t} else if (typeof r.time === 'number') {\n\t\t\t\tcleaned.push({ ...r, id: r.time });\n\t\t\t} else {\n\t\t\t\twithoutTimestamp++;\n\t\t\t}\n\t\t}\n\t\tif (withoutTimestamp > 0) {\n\t\t\tconsole.warn('[table-size] dropped rows missing both id and time', {\n\t\t\t\tdropped: withoutTimestamp,\n\t\t\t\ttotal: rows.length,\n\t\t\t});\n\t\t}\n\t\treturn cleaned;\n\t}, [data]);\n\tconst derived = useMemo(() => buildDerived(raw, timeRange), [raw, timeRange.startTime, timeRange.endTime]);\n\n\tconst rankBy: RankBy = 'bytes';\n\tconst [selected, setSelected] = useState<string | null>(null);\n\tconst snapshotCardRef = useRef<HTMLDivElement>(null);\n\tconst trendCardRef = useRef<HTMLDivElement>(null);\n\tconst effectiveSelection = useMemo(() => {\n\t\tif (selected && derived.snapshot.tableSet.includes(selected)) { return selected; }\n\t\treturn derived.defaultSelection(rankBy);\n\t}, [selected, derived]);\n\n\tif (isLoading) {\n\t\treturn (\n\t\t\t<Card>\n\t\t\t\t<CardHeader>\n\t\t\t\t\t<CardTitle>Table sizes</CardTitle>\n\t\t\t\t</CardHeader>\n\t\t\t\t<CardContent>\n\t\t\t\t\t<div className=\"h-64 rounded-md bg-muted/30 animate-pulse\" aria-label=\"Loading\" />\n\t\t\t\t</CardContent>\n\t\t\t</Card>\n\t\t);\n\t}\n\n\tif (isError) {\n\t\treturn (\n\t\t\t<Card>\n\t\t\t\t<CardHeader>\n\t\t\t\t\t<CardTitle>Table sizes</CardTitle>\n\t\t\t\t\t<CardDescription>Per-table storage breakdown.</CardDescription>\n\t\t\t\t</CardHeader>\n\t\t\t\t<CardContent>\n\t\t\t\t\t<div className=\"h-32 rounded-md border border-destructive/40 bg-destructive/10 p-4 text-sm text-destructive flex items-center justify-center\">\n\t\t\t\t\t\tFailed to load table-size data. Try a different time window or refresh.\n\t\t\t\t\t</div>\n\t\t\t\t</CardContent>\n\t\t\t</Card>\n\t\t);\n\t}\n\n\tif (derived.emptyCause === 'upstream-empty') {\n\t\treturn (\n\t\t\t<Card>\n\t\t\t\t<CardHeader>\n\t\t\t\t\t<CardTitle>Table sizes</CardTitle>\n\t\t\t\t\t<CardDescription>Per-table storage breakdown.</CardDescription>\n\t\t\t\t</CardHeader>\n\t\t\t\t<CardContent>\n\t\t\t\t\t<div className=\"h-32 rounded-md border border-border bg-muted/20 p-4 text-sm text-muted-foreground flex items-center justify-center\">\n\t\t\t\t\t\tNo table-size data in the selected window.\n\t\t\t\t\t</div>\n\t\t\t\t</CardContent>\n\t\t\t</Card>\n\t\t);\n\t}\n\n\t// renderSnapshot/renderTrend accept the opts shape ChartExpandButton passes,\n\t// but TableSizeSnapshot/Trend don't yet support fillParent — they render at\n\t// their native size in both inline and dialog views. Wiring fillParent into\n\t// those charts is a follow-up.\n\tconst renderSnapshot = (_opts?: { fillParent: boolean }) => (\n\t\t<TableSizeSnapshot\n\t\t\tsnapshot={derived.snapshot}\n\t\t\tviewMode=\"per-node\"\n\t\t\ttheme={theme}\n\t\t\tselectedTable={effectiveSelection}\n\t\t\tonChipSelect={setSelected}\n\t\t\tonBarClick={setSelected}\n\t\t\tallOtherHint={derived.emptyCause === 'all-other'}\n\t\t/>\n\t);\n\tconst renderTrend = (_opts?: { fillParent: boolean }) => (\n\t\teffectiveSelection\n\t\t\t? (\n\t\t\t\t<TableSizeTrend\n\t\t\t\t\tderived={derived}\n\t\t\t\t\tviewMode=\"per-node\"\n\t\t\t\t\ttheme={theme}\n\t\t\t\t\tselectedTable={effectiveSelection}\n\t\t\t\t\tonChipSelect={setSelected}\n\t\t\t\t\tmanualSelection={selected !== null}\n\t\t\t\t\trange={timeRange}\n\t\t\t\t\tclusterNodeIds={derivedClusterNodes(derived)}\n\t\t\t\t\trankBy={rankBy}\n\t\t\t\t\tonRankChange={() => {}}\n\t\t\t\t/>\n\t\t\t)\n\t\t\t: (\n\t\t\t\t<div className=\"h-64 rounded-md border border-border bg-muted/20 p-4 text-sm text-muted-foreground flex items-center justify-center\">\n\t\t\t\t\tNo table selected.\n\t\t\t\t</div>\n\t\t\t)\n\t);\n\n\treturn (\n\t\t<div className=\"grid grid-cols-1 xl:grid-cols-2 gap-4\">\n\t\t\t<Card ref={snapshotCardRef}>\n\t\t\t\t<CardHeader className=\"flex flex-row items-start justify-between gap-2\">\n\t\t\t\t\t<div className=\"flex-1 min-w-0\">\n\t\t\t\t\t\t<CardTitle>Table size — snapshot</CardTitle>\n\t\t\t\t\t\t<CardDescription>\n\t\t\t\t\t\t\tBytes per table, per node. Click a segment to pin the trend below.\n\t\t\t\t\t\t</CardDescription>\n\t\t\t\t\t</div>\n\t\t\t\t\t<div className=\"flex items-center gap-1 shrink-0\">\n\t\t\t\t\t\t<ChartExpandButton\n\t\t\t\t\t\t\texportSlug=\"table-size-snapshot\"\n\t\t\t\t\t\t\ttitle=\"Table size — snapshot\"\n\t\t\t\t\t\t\tdescription=\"Bytes per table, per node.\"\n\t\t\t\t\t\t\trenderChart={renderSnapshot}\n\t\t\t\t\t\t/>\n\t\t\t\t\t\t<ChartCopyButton captureRef={snapshotCardRef} exportSlug=\"table-size-snapshot\" />\n\t\t\t\t\t\t<ChartExportButton captureRef={snapshotCardRef} exportSlug=\"table-size-snapshot\" />\n\t\t\t\t\t</div>\n\t\t\t\t</CardHeader>\n\t\t\t\t<CardContent>\n\t\t\t\t\t{renderSnapshot()}\n\t\t\t\t</CardContent>\n\t\t\t</Card>\n\t\t\t<Card ref={trendCardRef}>\n\t\t\t\t<CardHeader className=\"flex flex-row items-start justify-between gap-2\">\n\t\t\t\t\t<div className=\"flex-1 min-w-0\">\n\t\t\t\t\t\t<CardTitle>Table size — trend</CardTitle>\n\t\t\t\t\t\t<CardDescription>\n\t\t\t\t\t\t\t{effectiveSelection\n\t\t\t\t\t\t\t\t? `Growth of ${effectiveSelection} over the selected window.`\n\t\t\t\t\t\t\t\t: 'Pick a table to see its trend.'}\n\t\t\t\t\t\t</CardDescription>\n\t\t\t\t\t</div>\n\t\t\t\t\t{effectiveSelection && (\n\t\t\t\t\t\t<div className=\"flex items-center gap-1 shrink-0\">\n\t\t\t\t\t\t\t<ChartExpandButton\n\t\t\t\t\t\t\t\texportSlug=\"table-size-trend\"\n\t\t\t\t\t\t\t\ttitle={`Table size — trend: ${effectiveSelection}`}\n\t\t\t\t\t\t\t\tdescription={`Growth of ${effectiveSelection} over the selected window.`}\n\t\t\t\t\t\t\t\trenderChart={renderTrend}\n\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t<ChartCopyButton captureRef={trendCardRef} exportSlug=\"table-size-trend\" />\n\t\t\t\t\t\t\t<ChartExportButton captureRef={trendCardRef} exportSlug=\"table-size-trend\" />\n\t\t\t\t\t\t</div>\n\t\t\t\t\t)}\n\t\t\t\t</CardHeader>\n\t\t\t\t<CardContent>\n\t\t\t\t\t{renderTrend()}\n\t\t\t\t</CardContent>\n\t\t\t</Card>\n\t\t</div>\n\t);\n}\n","import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';\nimport { useMemo, useRef } from 'react';\nimport { ChartCopyButton } from '../components/ChartCopyButton.tsx';\nimport { ChartExpandButton } from '../components/ChartExpandButton.tsx';\nimport { ChartExportButton } from '../components/ChartExportButton.tsx';\nimport { useAnalyticsContext } from '../context/AnalyticsContext.tsx';\nimport { useAnalyticsRecords } from '../hooks/useAnalyticsRecords.ts';\nimport { ConnectionsRenderer } from '../pipeline/connections.tsx';\nimport type { AnalyticsDataPoint } from '../types/analytics.ts';\nimport { PanelErrorBoundary } from './PanelErrorBoundary.tsx';\n\n/** Some Harper builds split active-session telemetry across two metrics:\n * `mqtt-connections` (active MQTT sessions) and `ws-connections` (active\n * WebSocket sessions). Each row carries a `connections` field but no\n * protocol discriminator — the metric NAME is the discriminator. We\n * fetch both, tag each row with a synthesized `type` so the renderer's\n * groupBy('type') has something to key on, and pass the merged stream\n * into the standard ConnectionsRenderer.\n *\n * This mirrors analytics-viz's PanelConnections approach (DashboardPanel\n * multi-source merge). It's why this panel doesn't go through the\n * generic MetricPanel — that path is single-metric-per-card. */\nexport function ConnectionsPanel() {\n\tconst { timeRange } = useAnalyticsContext();\n\treturn (\n\t\t<PanelErrorBoundary metric=\"connections\" resetKey={`${timeRange.startTime}-${timeRange.endTime}`}>\n\t\t\t<ConnectionsPanelInner />\n\t\t</PanelErrorBoundary>\n\t);\n}\n\nfunction ConnectionsPanelInner() {\n\tconst { timeRange, bucketMs, refreshIntervalMs, theme, instanceParams } = useAnalyticsContext();\n\t// Chart-only ref; capturing the whole Card would include the action buttons\n\t// in the exported PNG.\n\tconst chartRef = useRef<HTMLDivElement>(null);\n\n\tconst mqtt = useAnalyticsRecords({\n\t\tmetric: 'mqtt-connections',\n\t\tstartTime: timeRange.startTime,\n\t\tendTime: timeRange.endTime,\n\t\tinstanceParams,\n\t\trefetchIntervalMs: refreshIntervalMs,\n\t\tbucketMs,\n\t});\n\tconst ws = useAnalyticsRecords({\n\t\tmetric: 'ws-connections',\n\t\tstartTime: timeRange.startTime,\n\t\tendTime: timeRange.endTime,\n\t\tinstanceParams,\n\t\trefetchIntervalMs: refreshIntervalMs,\n\t\tbucketMs,\n\t});\n\n\tconst isLoading = mqtt.isLoading || ws.isLoading;\n\tconst isError = mqtt.isError || ws.isError;\n\tconst error = mqtt.error || ws.error;\n\n\tconst merged = useMemo<AnalyticsDataPoint[]>(() => {\n\t\tconst out: AnalyticsDataPoint[] = [];\n\t\tfor (const r of mqtt.data) { out.push({ ...r, type: 'mqtt' }); }\n\t\tfor (const r of ws.data) { out.push({ ...r, type: 'ws' }); }\n\t\treturn out;\n\t}, [mqtt.data, ws.data]);\n\n\tconst isEmpty = merged.length === 0;\n\tconst nodes = useMemo(() => {\n\t\tconst set = new Set<string>();\n\t\tfor (const r of merged) {\n\t\t\tif (typeof r.node === 'string') { set.add(r.node); }\n\t\t}\n\t\treturn [...set].sort();\n\t}, [merged]);\n\n\tconst canExport = !isLoading && !isError && !isEmpty;\n\tconst renderChart = (opts: { fillParent: boolean } = { fillParent: false }) => (\n\t\t<ConnectionsRenderer\n\t\t\trecords={merged}\n\t\t\ttimeRange={timeRange}\n\t\t\tnodes={nodes}\n\t\t\ttheme={theme}\n\t\t\tfillParent={opts.fillParent}\n\t\t/>\n\t);\n\n\treturn (\n\t\t<Card>\n\t\t\t<CardHeader className=\"flex flex-row items-start justify-between gap-2\">\n\t\t\t\t<div className=\"flex-1 min-w-0\">\n\t\t\t\t\t<CardTitle>Connections</CardTitle>\n\t\t\t\t\t<CardDescription>Active MQTT + WebSocket sessions — chips solo / Ctrl-toggle.</CardDescription>\n\t\t\t\t</div>\n\t\t\t\t{canExport && (\n\t\t\t\t\t<div className=\"flex items-center gap-1 shrink-0\">\n\t\t\t\t\t\t<ChartExpandButton\n\t\t\t\t\t\t\texportSlug=\"connections\"\n\t\t\t\t\t\t\ttitle=\"Connections\"\n\t\t\t\t\t\t\tdescription=\"Active MQTT + WebSocket sessions.\"\n\t\t\t\t\t\t\trenderChart={renderChart}\n\t\t\t\t\t\t/>\n\t\t\t\t\t\t<ChartCopyButton captureRef={chartRef} exportSlug=\"connections\" />\n\t\t\t\t\t\t<ChartExportButton captureRef={chartRef} exportSlug=\"connections\" />\n\t\t\t\t\t</div>\n\t\t\t\t)}\n\t\t\t</CardHeader>\n\t\t\t<CardContent>\n\t\t\t\t<div ref={chartRef}>\n\t\t\t\t\t{isLoading\n\t\t\t\t\t\t? <div className=\"h-64 rounded-md bg-muted/30 animate-pulse\" aria-label=\"Loading\" />\n\t\t\t\t\t\t: isError\n\t\t\t\t\t\t? (\n\t\t\t\t\t\t\t<div className=\"h-64 rounded-md border border-destructive/40 bg-destructive/10 p-4 text-sm text-destructive\">\n\t\t\t\t\t\t\t\t{`Failed to load: ${error?.message ?? 'unknown error'}`}\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t)\n\t\t\t\t\t\t: isEmpty\n\t\t\t\t\t\t? (\n\t\t\t\t\t\t\t<div className=\"h-64 rounded-md border border-border bg-muted/20 p-4 text-sm text-muted-foreground flex items-center justify-center\">\n\t\t\t\t\t\t\t\tNo active sessions in the selected time range.\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t)\n\t\t\t\t\t\t: renderChart()}\n\t\t\t\t</div>\n\t\t\t</CardContent>\n\t\t</Card>\n\t);\n}\n","import { ConnectionsPanel } from './ConnectionsPanel.tsx';\nimport { MetricPanel } from './MetricPanel.tsx';\n\n// Connections is rendered by a custom panel because it merges two source\n// metrics (mqtt-connections + ws-connections) — the generic MetricPanel\n// is single-metric. The remaining traffic panels go through MetricPanel\n// in the order below.\nconst METRICS = [\n\t'mqtt-traffic-sent',\n\t'mqtt-traffic-received',\n\t'bytes-sent',\n\t'bytes-received',\n\t'tls-reused',\n\t'connection',\n] as const;\n\nexport function TrafficTab() {\n\treturn (\n\t\t<div className=\"grid grid-cols-1 xl:grid-cols-2 gap-4\">\n\t\t\t<ConnectionsPanel />\n\t\t\t{METRICS.map((m) => <MetricPanel key={m} metric={m} />)}\n\t\t</div>\n\t);\n}\n","import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';\nimport { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';\nimport type { InstanceClientIdConfig, InstanceTypeConfig } from '@/config/instanceClientConfig.ts';\nimport { useNavigate, useSearch } from '@tanstack/react-router';\nimport { useTheme } from 'next-themes';\nimport { useCallback, useEffect, useMemo, useState } from 'react';\nimport { AnalyticsOnboardingHint } from './components/AnalyticsOnboardingHint.tsx';\nimport { TimeRangePicker } from './components/TimeRangePicker.tsx';\nimport { type AnalyticsContextValue, AnalyticsProvider } from './context/AnalyticsContext.tsx';\nimport { DEFAULT_PRESET_ID, DEFAULT_REFRESH_MS, getPreset, type TimePresetId } from './context/timePresets.ts';\nimport { useAnalyticsCapability } from './hooks/useAnalyticsCapability.ts';\nimport { DatabaseTab } from './tabs/DatabaseTab.tsx';\nimport { HealthTab } from './tabs/HealthTab.tsx';\nimport { OverviewTab } from './tabs/OverviewTab.tsx';\nimport { ReplicationTab } from './tabs/ReplicationTab.tsx';\nimport { RequestsTab } from './tabs/RequestsTab.tsx';\nimport { StorageTab } from './tabs/StorageTab.tsx';\nimport { TrafficTab } from './tabs/TrafficTab.tsx';\n\ninterface Props {\n\tinstanceParams: InstanceClientIdConfig & InstanceTypeConfig;\n\tisLocalStudio: boolean;\n}\n\nconst TAB_DEFS = [\n\t{ id: 'health', label: 'Health' },\n\t{ id: 'traffic', label: 'Traffic' },\n\t{ id: 'requests', label: 'Requests' },\n\t{ id: 'database', label: 'Database' },\n\t{ id: 'replication', label: 'Replication' },\n\t{ id: 'storage', label: 'Storage' },\n\t{ id: 'overview', label: 'Overview' },\n] as const;\n\ntype TabId = (typeof TAB_DEFS)[number]['id'];\n\nexport function StatusTabs({ instanceParams, isLocalStudio }: Props) {\n\tconst capability = useAnalyticsCapability(instanceParams);\n\n\tif (capability.isLoading) {\n\t\treturn (\n\t\t\t<div role=\"status\" aria-live=\"polite\" className=\"px-4 py-8 text-sm text-muted-foreground\">\n\t\t\t\tChecking analytics availability…\n\t\t\t</div>\n\t\t);\n\t}\n\n\tif (capability.error) {\n\t\treturn (\n\t\t\t<div role=\"alert\" className=\"px-4 py-8 text-sm text-muted-foreground\">\n\t\t\t\t<p className=\"mb-1 font-medium text-foreground\">Analytics unavailable on this instance.</p>\n\t\t\t\t<p>\n\t\t\t\t\tThe Harper instance returned an error from{' '}\n\t\t\t\t\t<code>get_analytics</code>. Check that the instance is reachable and that analytics is enabled, then reload.\n\t\t\t\t</p>\n\t\t\t</div>\n\t\t);\n\t}\n\n\treturn (\n\t\t<StatusTabsInner\n\t\t\tinstanceParams={instanceParams}\n\t\t\tisLocalStudio={isLocalStudio}\n\t\t/>\n\t);\n}\n\nfunction StatusTabsInner({ instanceParams, isLocalStudio }: Props) {\n\tconst navigate = useNavigate();\n\tconst raw: { tab?: string; range?: string; refresh?: string | number } = useSearch({ strict: false });\n\tconst tab: TabId = TAB_DEFS.some((t) => t.id === raw.tab) ? (raw.tab as TabId) : 'health';\n\tconst presetId: TimePresetId = raw.range && VALID_PRESETS.includes(raw.range)\n\t\t? (raw.range as TimePresetId)\n\t\t: DEFAULT_PRESET_ID;\n\tconst refreshMs: number = raw.refresh !== undefined && VALID_REFRESH.includes(Number(raw.refresh))\n\t\t? Number(raw.refresh)\n\t\t: DEFAULT_REFRESH_MS;\n\n\t// Manual refresh ticks bump this to force a fresh window when the user\n\t// clicks the refresh button.\n\tconst [tick, setTick] = useState(0);\n\n\tconst { resolvedTheme } = useTheme();\n\tconst theme = resolvedTheme === 'dark' ? 'dark' : 'light';\n\n\tconst updatePreset = useCallback((id: TimePresetId) => {\n\t\tvoid navigate({ to: '.', search: { tab, range: id, refresh: refreshMs } });\n\t}, [navigate, tab, refreshMs]);\n\n\tconst updateTab = useCallback((id: TabId) => {\n\t\tvoid navigate({ to: '.', search: { tab: id, range: presetId, refresh: refreshMs } });\n\t}, [navigate, presetId, refreshMs]);\n\n\tconst updateRefreshMs = useCallback((ms: number) => {\n\t\tvoid navigate({ to: '.', search: { tab, range: presetId, refresh: ms } });\n\t}, [navigate, tab, presetId]);\n\n\t// Strip our query params on unmount so they don't bleed into sibling\n\t// routes (e.g. navigating from /status to /databases shouldn't carry\n\t// tab/range/refresh forward).\n\tuseEffect(() => {\n\t\treturn () => {\n\t\t\tvoid navigate({ search: undefined, replace: true });\n\t\t};\n\t}, [navigate]);\n\n\tconst ctxValue = useMemo<AnalyticsContextValue>(() => {\n\t\tconst preset = getPreset(presetId);\n\t\tconst endTime = Date.now();\n\t\tconst startTime = endTime - preset.durationMs;\n\t\t// `tick` participates in memo deps so a manual refresh produces a fresh\n\t\t// window even if the user did not change presets.\n\t\tvoid tick;\n\t\treturn {\n\t\t\ttimeRange: { startTime, endTime },\n\t\t\tbucketMs: preset.bucketMs,\n\t\t\trefreshIntervalMs: refreshMs,\n\t\t\ttheme,\n\t\t\tinstanceParams,\n\t\t};\n\t}, [presetId, refreshMs, theme, instanceParams, tick]);\n\n\tconst showTimePicker = tab !== 'overview';\n\tconst picker = showTimePicker\n\t\t? (\n\t\t\t<TimeRangePicker\n\t\t\t\tpresetId={presetId}\n\t\t\t\tonPresetChange={updatePreset}\n\t\t\t\trefreshMs={refreshMs}\n\t\t\t\tonRefreshChange={updateRefreshMs}\n\t\t\t\tonManualRefresh={() => setTick((t) => t + 1)}\n\t\t\t/>\n\t\t)\n\t\t: null;\n\n\treturn (\n\t\t<AnalyticsProvider value={ctxValue}>\n\t\t\t<Tabs value={tab} onValueChange={(v) => updateTab(v as TabId)} className=\"px-4 py-2\">\n\t\t\t\t{\n\t\t\t\t\t/* Hint applies to chart interactions; hide it on Overview which\n\t\t\t\t has none of those affordances. */\n\t\t\t\t}\n\t\t\t\t{tab !== 'overview' && <AnalyticsOnboardingHint />}\n\t\t\t\t{\n\t\t\t\t\t/*\n\t\t\t\t\t * Tab strip layout:\n\t\t\t\t\t * md+ → horizontal Radix tab strip (no wrap, scrolls horizontally\n\t\t\t\t\t * if cramped) on the left; sub-toolbar inside each tab\n\t\t\t\t\t * renders the time-range picker so it stays sticky with\n\t\t\t\t\t * the chart it controls.\n\t\t\t\t\t * <md → Radix Tabs cannot collapse, so we render a Select for\n\t\t\t\t\t * tab navigation; the Tabs.List remains hidden but kept\n\t\t\t\t\t * mounted so TabsContent stays bound to value.\n\t\t\t\t\t */\n\t\t\t\t}\n\t\t\t\t<div className=\"md:hidden mb-3\">\n\t\t\t\t\t<Select value={tab} onValueChange={(v) => updateTab(v as TabId)}>\n\t\t\t\t\t\t<SelectTrigger className=\"w-full\" aria-label=\"Select status tab\">\n\t\t\t\t\t\t\t<SelectValue />\n\t\t\t\t\t\t</SelectTrigger>\n\t\t\t\t\t\t<SelectContent>\n\t\t\t\t\t\t\t{TAB_DEFS.map((t) => <SelectItem key={t.id} value={t.id}>{t.label}</SelectItem>)}\n\t\t\t\t\t\t</SelectContent>\n\t\t\t\t\t</Select>\n\t\t\t\t</div>\n\t\t\t\t<TabsList className=\"hidden md:inline-flex max-w-full overflow-x-auto mb-4\">\n\t\t\t\t\t{TAB_DEFS.map((t) => <TabsTrigger key={t.id} value={t.id}>{t.label}</TabsTrigger>)}\n\t\t\t\t</TabsList>\n\n\t\t\t\t<TabsContent value=\"health\">\n\t\t\t\t\t<TabBody picker={picker}>\n\t\t\t\t\t\t<HealthTab />\n\t\t\t\t\t</TabBody>\n\t\t\t\t</TabsContent>\n\t\t\t\t<TabsContent value=\"traffic\">\n\t\t\t\t\t<TabBody picker={picker}>\n\t\t\t\t\t\t<TrafficTab />\n\t\t\t\t\t</TabBody>\n\t\t\t\t</TabsContent>\n\t\t\t\t<TabsContent value=\"requests\">\n\t\t\t\t\t<TabBody picker={picker}>\n\t\t\t\t\t\t<RequestsTab />\n\t\t\t\t\t</TabBody>\n\t\t\t\t</TabsContent>\n\t\t\t\t<TabsContent value=\"database\">\n\t\t\t\t\t<TabBody picker={picker}>\n\t\t\t\t\t\t<DatabaseTab />\n\t\t\t\t\t</TabBody>\n\t\t\t\t</TabsContent>\n\t\t\t\t<TabsContent value=\"replication\">\n\t\t\t\t\t<TabBody picker={picker}>\n\t\t\t\t\t\t<ReplicationTab />\n\t\t\t\t\t</TabBody>\n\t\t\t\t</TabsContent>\n\t\t\t\t<TabsContent value=\"storage\">\n\t\t\t\t\t<TabBody picker={picker}>\n\t\t\t\t\t\t<StorageTab />\n\t\t\t\t\t</TabBody>\n\t\t\t\t</TabsContent>\n\t\t\t\t<TabsContent value=\"overview\">\n\t\t\t\t\t<OverviewTab instanceParams={instanceParams} isLocalStudio={isLocalStudio} />\n\t\t\t\t</TabsContent>\n\t\t\t</Tabs>\n\t\t</AnalyticsProvider>\n\t);\n}\n\n/** Wrap each chart-bearing tab with a sticky sub-toolbar so the time-range\n * picker stays in view as the user scrolls past long panel grids. The\n * picker is colocated with the data it controls instead of the tab strip\n * so its scope is unambiguous. */\nfunction TabBody({ picker, children }: { picker: React.ReactNode; children: React.ReactNode }) {\n\treturn (\n\t\t<>\n\t\t\t{picker && (\n\t\t\t\t<div className=\"sticky top-0 z-10 -mx-4 px-4 py-2 mb-3 bg-background border-b border-border shadow-sm flex items-center justify-end gap-2\">\n\t\t\t\t\t{picker}\n\t\t\t\t</div>\n\t\t\t)}\n\t\t\t{children}\n\t\t</>\n\t);\n}\n\nconst VALID_PRESETS: readonly string[] = ['1h', '6h', '24h', '7d', '30d'];\nconst VALID_REFRESH: readonly number[] = [0, 30_000, 60_000, 300_000];\n","import { isLocalStudio } from '@/config/constants';\nimport { useInstanceClientIdParams } from '@/config/useInstanceClient.tsx';\nimport { StatusTabs } from '@/features/instance/status/analytics/StatusTabs.tsx';\n\nexport function StatusIndex() {\n\tconst instanceParams = useInstanceClientIdParams();\n\treturn <StatusTabs instanceParams={instanceParams} isLocalStudio={isLocalStudio} />;\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"status-BrfTnnpt.js","names":[],"sources":["../../src/features/instance/status/analytics/components/AnalyticsOnboardingHint.tsx","../../src/features/instance/status/analytics/context/timePresets.ts","../../src/features/instance/status/analytics/hooks/useAnalyticsFreshness.ts","../../src/features/instance/status/analytics/components/TimeRangePicker.tsx","../../src/features/instance/status/analytics/context/AnalyticsContext.tsx","../../src/features/instance/status/analytics/hooks/useAnalyticsCapability.ts","../../src/features/instance/status/analytics/lib/chartExport.ts","../../src/features/instance/status/analytics/components/ChartCopyButton.tsx","../../src/features/instance/status/analytics/components/ChartExportButton.tsx","../../src/features/instance/status/analytics/components/ChartExpandButton.tsx","../../src/features/instance/status/analytics/hooks/useAnalyticsRecords.ts","../../src/features/instance/status/analytics/pipeline/derived/error-rate.tsx","../../src/features/instance/status/analytics/lib/nodeColors.ts","../../src/features/instance/status/analytics/charts/NodeLegend.tsx","../../src/features/instance/status/analytics/hooks/useNodeSelection.ts","../../src/features/instance/status/analytics/lib/colorAllocators/typeColors.ts","../../src/features/instance/status/analytics/pipeline/aggregators.ts","../../src/features/instance/status/analytics/pipeline/approxLabel.ts","../../src/features/instance/status/analytics/pipeline/confidence.ts","../../src/features/instance/status/analytics/pipeline/fieldExpr.ts","../../src/features/instance/status/analytics/pipeline/transforms.ts","../../src/features/instance/status/analytics/pipeline/runTransform.ts","../../src/features/instance/status/analytics/pipeline/pipeline.ts","../../src/features/instance/status/analytics/lib/time.ts","../../src/features/instance/status/analytics/primitives/formatValue.ts","../../src/features/instance/status/analytics/primitives/tooltipStyle.ts","../../src/features/instance/status/analytics/primitives/LineChart.tsx","../../src/features/instance/status/analytics/primitives/SmallMultiples.tsx","../../src/features/instance/status/analytics/primitives/sortByMagnitude.ts","../../src/features/instance/status/analytics/primitives/StackedAreaChart.tsx","../../src/features/instance/status/analytics/primitives/TypeFilterChipRow.tsx","../../src/features/instance/status/analytics/primitives/TrafficByTypeRenderer.tsx","../../src/features/instance/status/analytics/pipeline/derived/mqtt-traffic-received.tsx","../../src/features/instance/status/analytics/pipeline/derived/mqtt-traffic-sent.tsx","../../src/features/instance/status/analytics/primitives/DimensionChipRow.tsx","../../src/features/instance/status/analytics/primitives/PerPathRateRenderer.tsx","../../src/features/instance/status/analytics/pipeline/derived/request-rate.tsx","../../src/features/instance/status/analytics/pipeline/derived/transaction-log-growth.tsx","../../src/features/instance/status/analytics/pipeline/derived/index.ts","../../src/features/instance/status/analytics/pipeline/bytes-received.tsx","../../src/features/instance/status/analytics/pipeline/bytes-sent.tsx","../../src/features/instance/status/analytics/primitives/DimensionCombobox.tsx","../../src/features/instance/status/analytics/primitives/DimensionSelectorRenderer.tsx","../../src/features/instance/status/analytics/pipeline/cache-hit.tsx","../../src/features/instance/status/analytics/pipeline/quantileFields.ts","../../src/features/instance/status/analytics/pipeline/cache-resolution.tsx","../../src/features/instance/status/analytics/pipeline/connection.tsx","../../src/features/instance/status/analytics/pipeline/connections.tsx","../../src/features/instance/status/analytics/pipeline/cpu-usage.tsx","../../src/features/instance/status/analytics/pipeline/database-size.tsx","../../src/features/instance/status/analytics/pipeline/db-message.tsx","../../src/features/instance/status/analytics/pipeline/db-read.tsx","../../src/features/instance/status/analytics/pipeline/db-write.tsx","../../src/features/instance/status/analytics/pipeline/duration.tsx","../../src/features/instance/status/analytics/pipeline/main-thread-utilization.tsx","../../src/features/instance/status/analytics/pipeline/memory.tsx","../../src/features/instance/status/analytics/primitives/computeCellSize.ts","../../src/features/instance/status/analytics/primitives/HeatmapMatrix.tsx","../../src/features/instance/status/analytics/pipeline/pathParser.ts","../../src/features/instance/status/analytics/pipeline/replication-latency.tsx","../../src/features/instance/status/analytics/pipeline/resource-usage.ts","../../src/features/instance/status/analytics/pipeline/response-200.tsx","../../src/features/instance/status/analytics/pipeline/storage-volume.ts","../../src/features/instance/status/analytics/pipeline/success.tsx","../../src/features/instance/status/analytics/pipeline/tls-reused.ts","../../src/features/instance/status/analytics/pipeline/transfer.tsx","../../src/features/instance/status/analytics/pipeline/utilization.ts","../../src/features/instance/status/analytics/pipeline/index.ts","../../src/features/instance/status/analytics/lib/specRequiredFields.ts","../../src/features/instance/status/analytics/primitives/FallbackRenderer.tsx","../../src/features/instance/status/analytics/primitives/LineChartWithNodeLegend.tsx","../../src/features/instance/status/analytics/primitives/MetricRenderer.tsx","../../src/features/instance/status/analytics/tabs/PanelErrorBoundary.tsx","../../src/features/instance/status/analytics/tabs/MetricPanel.tsx","../../src/features/instance/status/analytics/tabs/DatabaseTab.tsx","../../src/features/instance/status/analytics/tabs/HealthTab.tsx","../../src/components/ui/accordion.tsx","../../src/integrations/api/instance/status/getSystemInformation.ts","../../src/features/instance/status/analytics/lib/crawlData.ts","../../src/features/instance/status/analytics/tabs/OverviewTab.tsx","../../src/features/instance/status/analytics/tabs/ReplicationTab.tsx","../../src/features/instance/status/analytics/tabs/RequestsTab.tsx","../../src/features/instance/status/analytics/lib/tableColors.ts","../../src/features/instance/status/analytics/lib/tableSize.ts","../../src/features/instance/status/analytics/lib/theme.ts","../../src/features/instance/status/analytics/charts/TableSizeChipRow.tsx","../../src/features/instance/status/analytics/charts/TableSizeSnapshot.tsx","../../src/features/instance/status/analytics/charts/TableSizeTrend.tsx","../../src/features/instance/status/analytics/tabs/StorageTab.tsx","../../src/features/instance/status/analytics/tabs/ConnectionsPanel.tsx","../../src/features/instance/status/analytics/tabs/TrafficTab.tsx","../../src/features/instance/status/analytics/StatusTabs.tsx","../../src/features/instance/status/index.tsx"],"sourcesContent":["import { Button } from '@/components/ui/button';\nimport { X } from 'lucide-react';\nimport { useEffect, useState } from 'react';\n\n// Versioned key — bump the suffix to re-show the tip after a major UX\n// change (e.g. when we add a new keyboard shortcut or remove an existing\n// one). Past dismissals at older versions are ignored on purpose.\nconst STORAGE_KEY = 'studio:analytics:onboarding-dismissed:v1';\n\n/** First-visit hint explaining the chart interactions that aren't visually\n * discoverable: click a legend entry to solo a node, ⌘/Ctrl-click to\n * multi-select, and click bar segments / heatmap cells for drilldown.\n * Dismissal is persisted to localStorage so it doesn't reappear. */\nexport function AnalyticsOnboardingHint() {\n\tconst [dismissed, setDismissed] = useState<boolean | null>(null);\n\n\tuseEffect(() => {\n\t\ttry {\n\t\t\tsetDismissed(window.localStorage.getItem(STORAGE_KEY) === '1');\n\t\t} catch {\n\t\t\tsetDismissed(true);\n\t\t}\n\t}, []);\n\n\tif (dismissed !== false) { return null; }\n\n\tconst onDismiss = () => {\n\t\ttry {\n\t\t\twindow.localStorage.setItem(STORAGE_KEY, '1');\n\t\t} catch {\n\t\t\t// ignore — dismissal will replay next visit but the hint is harmless\n\t\t}\n\t\tsetDismissed(true);\n\t};\n\n\treturn (\n\t\t<div\n\t\t\trole=\"status\"\n\t\t\tclassName=\"mb-3 flex items-start gap-3 rounded-md border border-border bg-muted/30 px-3 py-2 text-sm text-muted-foreground\"\n\t\t>\n\t\t\t<div className=\"flex-1 min-w-0\">\n\t\t\t\t<span className=\"font-medium text-foreground\">Tip:</span>\n\t\t\t\t{' Click a legend entry to isolate one node, '}\n\t\t\t\t<kbd className=\"rounded border border-border px-1 py-0.5 text-xs\">⌘</kbd>\n\t\t\t\t{' / '}\n\t\t\t\t<kbd className=\"rounded border border-border px-1 py-0.5 text-xs\">Ctrl</kbd>\n\t\t\t\t{'-click to compare a few. Bar segments and heatmap cells are clickable for drilldown.'}\n\t\t\t</div>\n\t\t\t<Button\n\t\t\t\tvariant=\"ghost\"\n\t\t\t\tsize=\"icon\"\n\t\t\t\tonClick={onDismiss}\n\t\t\t\taria-label=\"Dismiss tip\"\n\t\t\t\tclassName=\"-mt-1 h-7 w-7 shrink-0\"\n\t\t\t>\n\t\t\t\t<X className=\"h-4 w-4\" />\n\t\t\t</Button>\n\t\t</div>\n\t);\n}\n","// Bucket-by-window clamps. Per the SRE review: a 30d window with 1m buckets\n// across N nodes and 50 tables is on the order of 11M rows and OOMs the tab.\n// Each preset declares the densest bucket Harper should serve.\n\nexport interface TimePreset {\n\tid: TimePresetId;\n\tlabel: string;\n\tdurationMs: number;\n\tbucketMs: number;\n}\n\nexport type TimePresetId = '1h' | '6h' | '24h' | '7d' | '30d';\n\nconst MIN = 60_000;\nconst HOUR = 60 * MIN;\nconst DAY = 24 * HOUR;\n\nexport const TIME_PRESETS: readonly TimePreset[] = [\n\t{ id: '1h', label: 'Last 1 hour', durationMs: HOUR, bucketMs: 1 * MIN },\n\t{ id: '6h', label: 'Last 6 hours', durationMs: 6 * HOUR, bucketMs: 1 * MIN },\n\t{ id: '24h', label: 'Last 24 hours', durationMs: DAY, bucketMs: 5 * MIN },\n\t{ id: '7d', label: 'Last 7 days', durationMs: 7 * DAY, bucketMs: 15 * MIN },\n\t{ id: '30d', label: 'Last 30 days', durationMs: 30 * DAY, bucketMs: HOUR },\n];\n\nexport const DEFAULT_PRESET_ID: TimePresetId = '1h';\n\nexport function getPreset(id: TimePresetId): TimePreset {\n\tconst p = TIME_PRESETS.find((x) => x.id === id);\n\tif (!p) { throw new Error(`Unknown preset: ${id}`); }\n\treturn p;\n}\n\nexport interface RefreshOption {\n\tlabel: string;\n\tvalue: number;\n}\n\nexport const REFRESH_OPTIONS: readonly RefreshOption[] = [\n\t{ label: 'Off', value: 0 },\n\t{ label: '30s', value: 30_000 },\n\t{ label: '60s', value: 60_000 },\n\t{ label: '5m', value: 300_000 },\n];\n\nexport const DEFAULT_REFRESH_MS = 60_000;\n","import { ANALYTICS_QUERY_KEY_PREFIX } from '@/integrations/api/instance/status/getAnalytics.ts';\nimport { useQueryClient } from '@tanstack/react-query';\nimport { useEffect, useState } from 'react';\n\nconst PREFIX = ANALYTICS_QUERY_KEY_PREFIX;\n\nexport interface AnalyticsFreshness {\n\t/** True while at least one panel-level get_analytics query is fetching. */\n\tisFetching: boolean;\n\t/** ms-since-epoch of the most recent successful fetch on any panel\n\t * query, or null until the first one resolves. */\n\tlastFetchedAt: number | null;\n\t/** Bumps every second so consumers can re-render relative time\n\t * (\"updated Xs ago\") without subscribing themselves. */\n\tnow: number;\n}\n\n/** Watches the React Query cache for activity on the `get_analytics`\n * prefix and exposes a busy flag + most-recent-success timestamp. The\n * TimeRangePicker uses these to show an active/spinning refresh icon\n * and a \"last updated\" relative-time label. */\nexport function useAnalyticsFreshness(): AnalyticsFreshness {\n\tconst client = useQueryClient();\n\tconst [isFetching, setIsFetching] = useState(false);\n\tconst [lastFetchedAt, setLastFetchedAt] = useState<number | null>(null);\n\tconst [now, setNow] = useState(() => Date.now());\n\n\tuseEffect(() => {\n\t\tconst cache = client.getQueryCache();\n\t\tconst isOurs = (q: { queryKey: unknown }) => Array.isArray(q.queryKey) && q.queryKey[0] === PREFIX;\n\t\tconst sync = () => {\n\t\t\tlet fetching = false;\n\t\t\tlet mostRecent: number | null = null;\n\t\t\tfor (const q of cache.getAll()) {\n\t\t\t\tif (!isOurs(q)) { continue; }\n\t\t\t\tif (q.state.fetchStatus === 'fetching') { fetching = true; }\n\t\t\t\tif (q.state.dataUpdatedAt > 0 && (mostRecent === null || q.state.dataUpdatedAt > mostRecent)) {\n\t\t\t\t\tmostRecent = q.state.dataUpdatedAt;\n\t\t\t\t}\n\t\t\t}\n\t\t\t// Only setState when the value actually changed — RQ fires the\n\t\t\t// subscription on every cache event in the entire app, and a\n\t\t\t// no-op setState would still trigger a re-render of the picker.\n\t\t\tsetIsFetching((prev) => (prev === fetching ? prev : fetching));\n\t\t\tsetLastFetchedAt((prev) => (prev === mostRecent ? prev : mostRecent));\n\t\t};\n\t\tsync();\n\t\t// Subscribe filter: we only care about events on `get_analytics_raw`\n\t\t// queries. Filter the *event* (not just the post-aggregate) so we\n\t\t// don't recompute the cache scan for every unrelated query mutation.\n\t\tconst unsub = cache.subscribe((event) => {\n\t\t\tif (!event?.query || !isOurs(event.query)) { return; }\n\t\t\tsync();\n\t\t});\n\t\treturn () => unsub();\n\t}, [client]);\n\n\tuseEffect(() => {\n\t\t// Tick rate adapts to age so we don't burn a 1Hz timer per picker\n\t\t// indefinitely. Updates per-second for the first minute (where \"Xs\"\n\t\t// is changing), every 5s for the next 9 minutes, then every 30s.\n\t\tlet cancelled = false;\n\t\tlet timerId: number | undefined;\n\t\tconst tick = () => {\n\t\t\tif (cancelled) { return; }\n\t\t\tsetNow(Date.now());\n\t\t\tconst age = lastFetchedAt === null ? 0 : Date.now() - lastFetchedAt;\n\t\t\tconst delay = age < 60_000 ? 1000 : age < 600_000 ? 5000 : 30_000;\n\t\t\ttimerId = window.setTimeout(tick, delay);\n\t\t};\n\t\ttimerId = window.setTimeout(tick, 1000);\n\t\treturn () => {\n\t\t\tcancelled = true;\n\t\t\tif (timerId !== undefined) { window.clearTimeout(timerId); }\n\t\t};\n\t}, [lastFetchedAt]);\n\n\treturn { isFetching, lastFetchedAt, now };\n}\n\n/** Format `lastFetchedAt` as a short relative-time label suitable for a\n * toolbar pill: \"just now\" / \"Xs ago\" / \"Xm ago\". Returns null when no\n * fetch has resolved yet. */\nexport function formatRelativeUpdate(lastFetchedAt: number | null, now: number): string | null {\n\tif (lastFetchedAt === null) { return null; }\n\tconst seconds = Math.max(0, Math.floor((now - lastFetchedAt) / 1000));\n\tif (seconds < 5) { return 'just now'; }\n\tif (seconds < 60) { return `${seconds}s ago`; }\n\tconst minutes = Math.floor(seconds / 60);\n\tif (minutes < 60) { return `${minutes}m ago`; }\n\tconst hours = Math.floor(minutes / 60);\n\treturn `${hours}h ago`;\n}\n","import { Button } from '@/components/ui/button';\nimport { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';\nimport { cn } from '@/lib/cn';\nimport { RefreshCw } from 'lucide-react';\nimport { REFRESH_OPTIONS, TIME_PRESETS, type TimePresetId } from '../context/timePresets.ts';\nimport { formatRelativeUpdate, useAnalyticsFreshness } from '../hooks/useAnalyticsFreshness.ts';\n\ninterface Props {\n\tpresetId: TimePresetId;\n\tonPresetChange: (id: TimePresetId) => void;\n\trefreshMs: number;\n\tonRefreshChange: (ms: number) => void;\n\tonManualRefresh: () => void;\n}\n\nexport function TimeRangePicker({\n\tpresetId,\n\tonPresetChange,\n\trefreshMs,\n\tonRefreshChange,\n\tonManualRefresh,\n}: Props) {\n\tconst { isFetching, lastFetchedAt, now } = useAnalyticsFreshness();\n\tconst updatedLabel = formatRelativeUpdate(lastFetchedAt, now);\n\n\treturn (\n\t\t<div className=\"flex items-center gap-2\">\n\t\t\t{updatedLabel && (\n\t\t\t\t<span\n\t\t\t\t\tclassName=\"text-xs text-muted-foreground tabular-nums\"\n\t\t\t\t\t// No aria-live: a self-ticking timestamp causes screen\n\t\t\t\t\t// readers to re-announce every interval. Full ISO time\n\t\t\t\t\t// is exposed via title for hover.\n\t\t\t\t\ttitle={lastFetchedAt ? new Date(lastFetchedAt).toLocaleString() : undefined}\n\t\t\t\t\taria-label={lastFetchedAt ? `Last updated ${new Date(lastFetchedAt).toLocaleString()}` : undefined}\n\t\t\t\t>\n\t\t\t\t\tUpdated {updatedLabel}\n\t\t\t\t</span>\n\t\t\t)}\n\t\t\t<Select value={presetId} onValueChange={(v) => onPresetChange(v as TimePresetId)}>\n\t\t\t\t<SelectTrigger className=\"w-[180px]\">\n\t\t\t\t\t<SelectValue />\n\t\t\t\t</SelectTrigger>\n\t\t\t\t<SelectContent>\n\t\t\t\t\t{TIME_PRESETS.map((p) => <SelectItem key={p.id} value={p.id}>{p.label}</SelectItem>)}\n\t\t\t\t</SelectContent>\n\t\t\t</Select>\n\t\t\t<Select value={String(refreshMs)} onValueChange={(v) => onRefreshChange(Number(v))}>\n\t\t\t\t<SelectTrigger className=\"w-[100px]\">\n\t\t\t\t\t<SelectValue />\n\t\t\t\t</SelectTrigger>\n\t\t\t\t<SelectContent>\n\t\t\t\t\t{REFRESH_OPTIONS.map((o) => <SelectItem key={o.value} value={String(o.value)}>{o.label}</SelectItem>)}\n\t\t\t\t</SelectContent>\n\t\t\t</Select>\n\t\t\t<Button\n\t\t\t\tvariant=\"ghost\"\n\t\t\t\tsize=\"icon\"\n\t\t\t\tonClick={onManualRefresh}\n\t\t\t\tdisabled={isFetching}\n\t\t\t\taria-busy={isFetching}\n\t\t\t\taria-label={isFetching ? 'Refreshing…' : 'Refresh now'}\n\t\t\t\ttitle={isFetching ? 'Refreshing…' : 'Refresh now'}\n\t\t\t>\n\t\t\t\t<RefreshCw className={cn('h-4 w-4', isFetching && 'animate-spin')} />\n\t\t\t</Button>\n\t\t</div>\n\t);\n}\n","import type { InstanceClientIdConfig, InstanceTypeConfig } from '@/config/instanceClientConfig.ts';\nimport { createContext, type ReactNode, useContext, useMemo } from 'react';\nimport type { TimeRange } from '../types/analytics.ts';\n\nexport type AnalyticsTheme = 'light' | 'dark';\n\nexport interface AnalyticsContextValue {\n\ttimeRange: TimeRange;\n\tbucketMs: number;\n\trefreshIntervalMs: number;\n\ttheme: AnalyticsTheme;\n\tinstanceParams: InstanceClientIdConfig & InstanceTypeConfig;\n}\n\nconst Ctx = createContext<AnalyticsContextValue | null>(null);\n\ninterface ProviderProps {\n\tvalue: AnalyticsContextValue;\n\tchildren: ReactNode;\n}\n\nexport function AnalyticsProvider({ value, children }: ProviderProps) {\n\tconst memo = useMemo(() => value, [\n\t\tvalue.timeRange.startTime,\n\t\tvalue.timeRange.endTime,\n\t\tvalue.bucketMs,\n\t\tvalue.refreshIntervalMs,\n\t\tvalue.theme,\n\t\tvalue.instanceParams.entityId,\n\t]);\n\treturn <Ctx.Provider value={memo}>{children}</Ctx.Provider>;\n}\n\nexport function useAnalyticsContext(): AnalyticsContextValue {\n\tconst v = useContext(Ctx);\n\tif (!v) { throw new Error('useAnalyticsContext must be used inside <AnalyticsProvider>'); }\n\treturn v;\n}\n","import type { InstanceClientIdConfig, InstanceTypeConfig } from '@/config/instanceClientConfig.ts';\nimport { useQuery } from '@tanstack/react-query';\n\nexport interface AnalyticsCapability {\n\tsupported: boolean;\n\terror?: Error;\n\tisLoading: boolean;\n\tretry: () => void;\n}\n\n/** Capability-probe metrics tried in priority order. The probe considers\n * `get_analytics` supported if any metric resolves without a transport\n * error (an empty response is fine — it just means the instance is\n * quiet). Querying a list rather than a single metric avoids a false\n * negative on Harper builds where the chosen metric was renamed,\n * disabled, or never emitted. Order from most-likely-emitted to least. */\nconst PROBE_METRICS: readonly string[] = [\n\t'utilization',\n\t'cpu-usage',\n\t'memory',\n\t'main-thread-utilization',\n];\n\nconst PROBE_STALE_TIME_MS = 30 * 60_000;\n\n/** True when the error is plausibly a \"this metric isn't emitted on this\n * build\" signal (HTTP 4xx). False when the error is transport-level\n * (5xx, timeout, network) — those mean the *instance* is unhealthy, so\n * walking to the next metric just compounds load on an already\n * struggling Harper. */\nfunction isMetricNotFoundError(err: unknown): boolean {\n\tconst status = (err as { response?: { status?: number }; status?: number })?.response?.status\n\t\t?? (err as { status?: number })?.status;\n\tif (typeof status !== 'number') { return false; }\n\treturn status >= 400 && status < 500;\n}\n\n/** Probe `get_analytics` once per instance and cache the result for 30\n * minutes. Falls through the metric list ONLY on per-metric 4xx errors\n * (Harper version drift); bails immediately on transport-level errors so\n * a slow / unhealthy Harper isn't hit 4× per attempt. Retries up to twice\n * with exponential backoff for transient blips. The hook exposes a\n * `retry()` function for a Retry button on the fallback view. */\nexport function useAnalyticsCapability(\n\tinstanceParams: InstanceClientIdConfig & InstanceTypeConfig,\n): AnalyticsCapability {\n\tconst query = useQuery({\n\t\tqueryKey: ['analytics-capability', instanceParams.entityId] as const,\n\t\tqueryFn: async () => {\n\t\t\tconst endTime = Date.now();\n\t\t\tconst startTime = endTime - 5 * 60_000;\n\t\t\tlet lastError: unknown = null;\n\t\t\tfor (const metric of PROBE_METRICS) {\n\t\t\t\ttry {\n\t\t\t\t\tawait instanceParams.instanceClient.post('/', {\n\t\t\t\t\t\toperation: 'get_analytics',\n\t\t\t\t\t\tmetric,\n\t\t\t\t\t\tstart_time: startTime,\n\t\t\t\t\t\tend_time: endTime,\n\t\t\t\t\t});\n\t\t\t\t\treturn true;\n\t\t\t\t} catch (err) {\n\t\t\t\t\tlastError = err;\n\t\t\t\t\t// Only walk the list on 4xx (metric-not-found-style).\n\t\t\t\t\t// 5xx / network / timeout = instance-level problem;\n\t\t\t\t\t// re-throw so React Query's outer retry policy decides.\n\t\t\t\t\tif (!isMetricNotFoundError(err)) { throw err; }\n\t\t\t\t}\n\t\t\t}\n\t\t\tthrow lastError instanceof Error ? lastError : new Error('Analytics probe failed for all metrics');\n\t\t},\n\t\tretry: 2,\n\t\tretryDelay: (attempt) => Math.min(1000 * 2 ** attempt, 8000),\n\t\tstaleTime: PROBE_STALE_TIME_MS,\n\t\tgcTime: PROBE_STALE_TIME_MS,\n\t});\n\n\treturn {\n\t\tsupported: query.isSuccess === true,\n\t\terror: query.error as Error | undefined,\n\t\tisLoading: query.isLoading,\n\t\tretry: () => {\n\t\t\tvoid query.refetch();\n\t\t},\n\t};\n}\n","import { toBlob } from 'html-to-image';\n\n/** Resolves the on-screen background color of `el`, walking up the tree\n * until a non-transparent ancestor is found (Card surfaces are\n * rgba(0,0,0,0) by default — falling through to a literal white would\n * produce white-bg PNGs in dark mode). */\nfunction resolveBackground(el: HTMLElement): string {\n\tlet cur: HTMLElement | null = el;\n\twhile (cur) {\n\t\tconst bg = getComputedStyle(cur).backgroundColor;\n\t\tif (bg && bg !== 'rgba(0, 0, 0, 0)' && bg !== 'transparent') { return bg; }\n\t\tcur = cur.parentElement;\n\t}\n\t// Fall back to the document body's resolved background, which respects\n\t// the active theme via Tailwind tokens.\n\tconst bodyBg = typeof document !== 'undefined' ? getComputedStyle(document.body).backgroundColor : '';\n\tif (bodyBg && bodyBg !== 'rgba(0, 0, 0, 0)') { return bodyBg; }\n\treturn '#ffffff';\n}\n\n/** Default pixel ratio for chart exports. 3× yields ~9 megapixels for an\n * 800×1000 panel — enough for slide decks and incident reports without\n * blowing up clipboard payloads. The expanded-view export inherits the\n * same ratio applied against a much larger DOM, so its output is\n * proportionally bigger. */\nexport const DEFAULT_EXPORT_PIXEL_RATIO = 3;\n\n/** Converts a chart's DOM subtree to a PNG blob. backgroundColor walks\n * ancestors so dark-mode Card surfaces capture as dark, not white. */\nexport async function captureChartAsBlob(\n\tchartContainer: HTMLElement,\n\tpixelRatio: number = DEFAULT_EXPORT_PIXEL_RATIO,\n): Promise<Blob> {\n\tconst blob = await toBlob(chartContainer, {\n\t\tpixelRatio,\n\t\tbackgroundColor: resolveBackground(chartContainer),\n\t});\n\tif (!blob) { throw new Error('Failed to capture chart as image'); }\n\treturn blob;\n}\n\nexport async function downloadChart(chartContainer: HTMLElement, filename: string): Promise<void> {\n\tconst blob = await captureChartAsBlob(chartContainer);\n\tconst url = URL.createObjectURL(blob);\n\ttry {\n\t\tconst a = document.createElement('a');\n\t\ta.href = url;\n\t\ta.download = filename;\n\t\ta.click();\n\t} finally {\n\t\tURL.revokeObjectURL(url);\n\t}\n}\n\n/** Capture the chart and write it to the clipboard as a PNG ClipboardItem.\n * Resolves to `true` on success, `false` if either the capture failed or\n * the browser denied clipboard access (Safari + non-secure contexts are\n * the common offenders). The caller decides UX — typically a toast. */\nexport async function copyChartToClipboard(chartContainer: HTMLElement): Promise<boolean> {\n\tif (typeof ClipboardItem === 'undefined' || !navigator.clipboard?.write) {\n\t\treturn false;\n\t}\n\ttry {\n\t\tconst blob = await captureChartAsBlob(chartContainer);\n\t\tawait navigator.clipboard.write([\n\t\t\tnew ClipboardItem({ 'image/png': blob }),\n\t\t]);\n\t\treturn true;\n\t} catch {\n\t\treturn false;\n\t}\n}\n\n/** Slugify a metric/title for use as a download filename. */\nexport function makeExportFilename(prefix: string, range: { startTime: number; endTime: number }): string {\n\tconst safe = prefix.replace(/[^a-z0-9-]+/gi, '-').replace(/(^-|-$)/g, '').toLowerCase();\n\tconst stamp = new Date(range.endTime).toISOString().replace(/[:.]/g, '-');\n\treturn `${safe}-${stamp}.png`;\n}\n","import { Button } from '@/components/ui/button';\nimport { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';\nimport { ClipboardCopy } from 'lucide-react';\nimport { type RefObject, useState } from 'react';\nimport { toast } from 'sonner';\nimport { copyChartToClipboard } from '../lib/chartExport.ts';\n\ninterface Props {\n\t/** The DOM node that should be captured. Usually the chart's outer Card. */\n\tcaptureRef: RefObject<HTMLElement | null>;\n\t/** Slug used in the toast message and the aria-label. */\n\texportSlug: string;\n}\n\n/** Copies the chart's PNG capture to the clipboard. Same capture pipeline\n * as ChartExportButton (so the on-clipboard image matches what would be\n * downloaded), but bypasses the file-system save and lets the user paste\n * directly into Slack, Confluence, an issue tracker, etc.\n *\n * Falls back gracefully when the browser doesn't expose `ClipboardItem`\n * (Safari without permissions, http-served pages) — reports a friendly\n * error toast instead of throwing. */\nexport function ChartCopyButton({ captureRef, exportSlug }: Props) {\n\tconst [busy, setBusy] = useState(false);\n\n\tconst onClick = async () => {\n\t\tif (busy) { return; }\n\t\tconst el = captureRef.current;\n\t\tif (!el) { return; }\n\t\tsetBusy(true);\n\t\ttry {\n\t\t\tconst ok = await copyChartToClipboard(el);\n\t\t\tif (ok) {\n\t\t\t\ttoast.success('Chart copied to clipboard');\n\t\t\t} else {\n\t\t\t\ttoast.error('Could not copy chart', {\n\t\t\t\t\tdescription: 'Your browser blocked the clipboard write. Try Download or check site permissions.',\n\t\t\t\t});\n\t\t\t}\n\t\t} catch (err) {\n\t\t\tconsole.error('[chart-copy] capture failed', err);\n\t\t\ttoast.error('Could not copy chart', {\n\t\t\t\tdescription: err instanceof Error ? err.message : 'Unknown error',\n\t\t\t});\n\t\t} finally {\n\t\t\tsetBusy(false);\n\t\t}\n\t};\n\n\treturn (\n\t\t<Tooltip>\n\t\t\t<TooltipTrigger asChild>\n\t\t\t\t<Button\n\t\t\t\t\tvariant=\"ghost\"\n\t\t\t\t\tsize=\"icon\"\n\t\t\t\t\tonClick={onClick}\n\t\t\t\t\taria-disabled={busy}\n\t\t\t\t\taria-busy={busy}\n\t\t\t\t\taria-label={`Copy ${exportSlug} chart to clipboard`}\n\t\t\t\t>\n\t\t\t\t\t<ClipboardCopy className=\"h-4 w-4\" />\n\t\t\t\t</Button>\n\t\t\t</TooltipTrigger>\n\t\t\t<TooltipContent side=\"top\">Copy to clipboard</TooltipContent>\n\t\t</Tooltip>\n\t);\n}\n","import { Button } from '@/components/ui/button';\nimport { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';\nimport { Download } from 'lucide-react';\nimport { type RefObject, useState } from 'react';\nimport { toast } from 'sonner';\nimport { useAnalyticsContext } from '../context/AnalyticsContext.tsx';\nimport { downloadChart, makeExportFilename } from '../lib/chartExport.ts';\n\ninterface Props {\n\t/** The DOM node that should be captured. Usually the chart's outer Card. */\n\tcaptureRef: RefObject<HTMLElement | null>;\n\t/** Slug used as the filename prefix (e.g. metric id or panel title). */\n\texportSlug: string;\n}\n\n/** Renders a small icon-button that, when clicked, snapshots the referenced\n * element to PNG and triggers a browser download. Filenames include an ISO\n * timestamp of the panel's window end so multiple captures don't collide.\n * Uses Radix Tooltip (keyboard-discoverable) over native title, and shows\n * a sonner toast on success/error so the user gets feedback during the\n * 100–500 ms capture window. */\nexport function ChartExportButton({ captureRef, exportSlug }: Props) {\n\tconst { timeRange } = useAnalyticsContext();\n\tconst [busy, setBusy] = useState(false);\n\n\tconst onClick = async () => {\n\t\tif (busy) { return; }\n\t\tconst el = captureRef.current;\n\t\tif (!el) { return; }\n\t\tsetBusy(true);\n\t\tconst filename = makeExportFilename(exportSlug, timeRange);\n\t\ttry {\n\t\t\tawait downloadChart(el, filename);\n\t\t\ttoast.success(`Saved ${filename}`);\n\t\t} catch (err) {\n\t\t\tconsole.error('[chart-export] capture failed', err);\n\t\t\ttoast.error('Could not export chart', {\n\t\t\t\tdescription: err instanceof Error ? err.message : 'Unknown error',\n\t\t\t});\n\t\t} finally {\n\t\t\tsetBusy(false);\n\t\t}\n\t};\n\n\treturn (\n\t\t<Tooltip>\n\t\t\t<TooltipTrigger asChild>\n\t\t\t\t<Button\n\t\t\t\t\tvariant=\"ghost\"\n\t\t\t\t\tsize=\"icon\"\n\t\t\t\t\tonClick={onClick}\n\t\t\t\t\taria-disabled={busy}\n\t\t\t\t\taria-busy={busy}\n\t\t\t\t\taria-label={`Download ${exportSlug} as PNG`}\n\t\t\t\t>\n\t\t\t\t\t<Download className=\"h-4 w-4\" />\n\t\t\t\t</Button>\n\t\t\t</TooltipTrigger>\n\t\t\t<TooltipContent side=\"top\">Download as PNG</TooltipContent>\n\t\t</Tooltip>\n\t);\n}\n","import { Button } from '@/components/ui/button';\nimport { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from '@/components/ui/dialog';\nimport { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';\nimport { Maximize2 } from 'lucide-react';\nimport { type ReactNode, useRef, useState } from 'react';\nimport { ChartCopyButton } from './ChartCopyButton.tsx';\nimport { ChartExportButton } from './ChartExportButton.tsx';\n\ninterface Props {\n\t/** Slug for the export filename if the user exports from the expanded view. */\n\texportSlug: string;\n\t/** Title shown in the dialog header. */\n\ttitle: string;\n\t/** Optional description shown below the title in the dialog. */\n\tdescription?: ReactNode;\n\t/** A render function that produces the chart's content. Called twice in\n\t * practice — once inline by the parent panel, and once inside the\n\t * expanded dialog. We use a render function (not children) so the\n\t * inner ResponsiveContainer remeasures against the dialog's tall\n\t * parent and grows the chart. The `fillParent` arg lets primitives\n\t * switch from a fixed `height` to filling the dialog's `h-[90vh]`. */\n\trenderChart: (opts: { fillParent: boolean }) => ReactNode;\n}\n\n/** Adds an \"Expand\" icon next to other panel-header actions. Click opens\n * a near-fullscreen Radix Dialog containing the same chart re-rendered\n * at full size, plus its own export button so the capture grabs the\n * bigger DOM (and therefore produces a higher-resolution PNG). */\nexport function ChartExpandButton({ exportSlug, title, description, renderChart }: Props) {\n\tconst [open, setOpen] = useState(false);\n\tconst expandedRef = useRef<HTMLDivElement>(null);\n\n\treturn (\n\t\t<>\n\t\t\t<Tooltip>\n\t\t\t\t<TooltipTrigger asChild>\n\t\t\t\t\t<Button\n\t\t\t\t\t\tvariant=\"ghost\"\n\t\t\t\t\t\tsize=\"icon\"\n\t\t\t\t\t\tonClick={() => setOpen(true)}\n\t\t\t\t\t\taria-label={`Expand ${exportSlug}`}\n\t\t\t\t\t>\n\t\t\t\t\t\t<Maximize2 className=\"h-4 w-4\" />\n\t\t\t\t\t</Button>\n\t\t\t\t</TooltipTrigger>\n\t\t\t\t<TooltipContent side=\"top\">Expand</TooltipContent>\n\t\t\t</Tooltip>\n\t\t\t<Dialog open={open} onOpenChange={setOpen}>\n\t\t\t\t<DialogContent // Override the Card's max-width — we want a near-fullscreen\n\t\t\t\t // canvas. Tailwind's sm:max-w-* utility wins specificity-wise\n\t\t\t\t// in some shadcn dialogs, so pin both.\n\t\t\t\tclassName=\"!max-w-[95vw] sm:!max-w-[95vw] h-[90vh] flex flex-col\">\n\t\t\t\t\t<DialogHeader>\n\t\t\t\t\t\t<div className=\"flex items-start justify-between gap-2 pr-6\">\n\t\t\t\t\t\t\t<div className=\"flex-1 min-w-0\">\n\t\t\t\t\t\t\t\t<DialogTitle>{title}</DialogTitle>\n\t\t\t\t\t\t\t\t{description && <DialogDescription>{description}</DialogDescription>}\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t<div className=\"flex items-center gap-1 shrink-0\">\n\t\t\t\t\t\t\t\t<ChartCopyButton captureRef={expandedRef} exportSlug={`${exportSlug}-expanded`} />\n\t\t\t\t\t\t\t\t<ChartExportButton captureRef={expandedRef} exportSlug={`${exportSlug}-expanded`} />\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t</DialogHeader>\n\t\t\t\t\t{\n\t\t\t\t\t\t/* The expanded chart renders into this region. ref captures\n\t\t\t\t\t everything inside (including the chart's surrounding\n\t\t\t\t\t chips/legend) so the export PNG matches the on-screen view. */\n\t\t\t\t\t}\n\t\t\t\t\t<div ref={expandedRef} className=\"flex-1 min-h-0 overflow-hidden flex flex-col\">\n\t\t\t\t\t\t{renderChart({ fillParent: true })}\n\t\t\t\t\t</div>\n\t\t\t\t</DialogContent>\n\t\t\t</Dialog>\n\t\t</>\n\t);\n}\n","import type { InstanceClientIdConfig, InstanceTypeConfig } from '@/config/instanceClientConfig.ts';\nimport {\n\ttype AnalyticsCondition,\n\tgetRawAnalyticsQueryOptions,\n} from '@/integrations/api/instance/status/getAnalytics.ts';\nimport { keepPreviousData, useQuery } from '@tanstack/react-query';\nimport { useEffect, useMemo, useRef } from 'react';\nimport type { AnalyticsDataPoint } from '../types/analytics.ts';\n\nexport interface UseAnalyticsRecordsArgs {\n\tmetric: string;\n\tstartTime: number;\n\tendTime: number;\n\tconditions?: AnalyticsCondition[];\n\tinstanceParams: InstanceClientIdConfig & InstanceTypeConfig;\n\t/** Polling cadence in ms. Set to 0 to disable. */\n\trefetchIntervalMs?: number;\n\t/** Required keys this metric's spec depends on. Missing keys surface via\n\t * `missingFields`, which the renderer can use to show a precise empty\n\t * state instead of a blank chart. */\n\trequiredFields?: readonly string[];\n\t/** Hint to Harper for the desired bucket size in ms. Honored when the\n\t * server supports it; otherwise it's a client-side soft cap row guard. */\n\tbucketMs?: number;\n}\n\nexport interface UseAnalyticsRecordsResult {\n\tdata: AnalyticsDataPoint[];\n\tisLoading: boolean;\n\tisError: boolean;\n\terror: Error | null;\n\tisEmpty: boolean;\n\t/** Union of keys observed across all returned rows (excluding `time` and\n\t * `node` which are part of AnalyticsDataPoint by contract). */\n\tfieldKeys: Set<string>;\n\t/** Subset of `requiredFields` that did not appear on any row. */\n\tmissingFields: string[];\n\trefetch: () => void;\n}\n\nconst RESERVED = new Set(['time', 'node']);\n\n// Stable empty array so downstream memos keyed on `data` keep referential\n// identity while React Query's response is still undefined. Using a fresh `[]`\n// each render churned every dependent useMemo on every render.\nconst EMPTY: readonly AnalyticsDataPoint[] = Object.freeze([]);\n\n/** Adapter from studio's `get_analytics` operation to the analytics-viz spec\n * pipeline. Passes rows through verbatim, exposes a schema-drift signal so\n * callers can render an explicit \"field unavailable\" state, and applies\n * small jitter to the polling start time so a tab's many concurrent specs\n * do not fire in lockstep on every refresh tick. */\nexport function useAnalyticsRecords({\n\tmetric,\n\tstartTime,\n\tendTime,\n\tconditions,\n\tinstanceParams,\n\trefetchIntervalMs = 60_000,\n\trequiredFields,\n\tbucketMs,\n}: UseAnalyticsRecordsArgs): UseAnalyticsRecordsResult {\n\t// Per-spec startup jitter (0–500 ms) so 5–7 concurrent specs in one tab\n\t// do not refire in the same render frame on auto-refresh.\n\tconst jitterRef = useRef<number | null>(null);\n\tif (jitterRef.current === null) {\n\t\tjitterRef.current = Math.floor(Math.random() * 500);\n\t}\n\n\tconst queryOpts = getRawAnalyticsQueryOptions({\n\t\tmetric,\n\t\tstartTime,\n\t\tendTime,\n\t\tconditions,\n\t\tinstanceParams,\n\t\tbucketMs,\n\t});\n\n\tconst query = useQuery({\n\t\t...queryOpts,\n\t\tstaleTime: refetchIntervalMs > 0 ? refetchIntervalMs : Infinity,\n\t\trefetchInterval: refetchIntervalMs > 0 ? refetchIntervalMs + jitterRef.current : false,\n\t\trefetchOnWindowFocus: false,\n\t\trefetchOnReconnect: false,\n\t\tplaceholderData: keepPreviousData,\n\t});\n\n\t// React Query already pauses interval refetching when the tab is hidden;\n\t// we don't add a visibility-driven refetch ourselves because that turns\n\t// every alt-tab into a synchronized N-panel POST burst on the customer's\n\t// Harper, bypassing staleTime entirely.\n\n\tconst data = (query.data ?? EMPTY) as AnalyticsDataPoint[];\n\n\tconst { fieldKeys, missingFields } = useMemo(() => {\n\t\tconst keys = new Set<string>();\n\t\tfor (const row of data) {\n\t\t\tfor (const k of Object.keys(row)) {\n\t\t\t\tif (!RESERVED.has(k)) { keys.add(k); }\n\t\t\t}\n\t\t}\n\t\t// Schema-drift signal requires *evidence*: at least one row that\n\t\t// carries data but lacks the required field. An empty response is\n\t\t// \"no data in window\" (a quiet-traffic state), not drift; flagging\n\t\t// missing fields there blanks legitimately empty panels with a\n\t\t// misleading error and was the cause of fresh-Harper false\n\t\t// positives on cpu-usage / utilization / etc.\n\t\tconst missing: string[] = [];\n\t\tif (requiredFields && data.length > 0) {\n\t\t\tfor (const f of requiredFields) {\n\t\t\t\tif (!keys.has(f)) { missing.push(f); }\n\t\t\t}\n\t\t}\n\t\treturn { fieldKeys: keys, missingFields: missing };\n\t}, [data, requiredFields]);\n\n\t// Telemetry: warn only when we can be reasonably confident the empty\n\t// result is a schema-drift signal (caller declared required fields and at\n\t// least one is missing) — otherwise legitimate low-traffic windows would\n\t// spam the console for 5–7 panels every refresh tick.\n\tuseEffect(() => {\n\t\tif (!query.isLoading && data.length === 0 && missingFields.length > 0) {\n\t\t\tconsole.warn('[analytics] panel rendered empty with missing fields', {\n\t\t\t\tmetric,\n\t\t\t\tinstanceId: instanceParams.entityId,\n\t\t\t\tmissingFields,\n\t\t\t});\n\t\t}\n\t}, [data.length, query.isLoading, metric, instanceParams.entityId, missingFields]);\n\n\treturn {\n\t\tdata,\n\t\tisLoading: query.isLoading,\n\t\tisError: query.isError,\n\t\terror: query.error as Error | null,\n\t\tisEmpty: data.length === 0,\n\t\tfieldKeys,\n\t\tmissingFields,\n\t\trefetch: query.refetch,\n\t};\n}\n","import type { AnalyticsDataPoint, DerivedMetricSpec, Series, SeriesData, TimeRange } from '../../types/analytics.ts';\n\n/**\n * Derived: per-(path, time) error rate computed from raw `count` and `total`\n * source columns. Σ-arithmetic: errorRate = 1 − Σtotal/Σcount.\n *\n * NOT mean(1 − ratio) — that's the canonical \"ratio-of-ratios\" bug the spec\n * calls out (line 518). Two records with [{count: 1000, total: 990},\n * {count: 10, total: 1}] should yield ≈0.0188 (Σ-correct), not 0.455\n * (mean-of-ratios).\n */\nexport function recomputeErrorRate(\n\trecords: AnalyticsDataPoint[],\n\t_window: TimeRange,\n\t_nodes: string[],\n): SeriesData {\n\tconst buckets = new Map<string, Map<number, { sumCount: number; sumTotal: number }>>();\n\tfor (const r of records) {\n\t\tconst path = typeof r.path === 'string' ? r.path : null;\n\t\tif (!path) { continue; }\n\t\tif (typeof r.time !== 'number' || !Number.isFinite(r.time)) { continue; }\n\t\tconst count = typeof r.count === 'number' && Number.isFinite(r.count) ? r.count : 0;\n\t\tconst total = typeof r.total === 'number' && Number.isFinite(r.total) ? r.total : 0;\n\t\tlet perTime = buckets.get(path);\n\t\tif (!perTime) {\n\t\t\tperTime = new Map();\n\t\t\tbuckets.set(path, perTime);\n\t\t}\n\t\tlet entry = perTime.get(r.time);\n\t\tif (!entry) {\n\t\t\tentry = { sumCount: 0, sumTotal: 0 };\n\t\t\tperTime.set(r.time, entry);\n\t\t}\n\t\tentry.sumCount += count;\n\t\tentry.sumTotal += total;\n\t}\n\n\tconst series: Series[] = [...buckets.entries()].map(([path, perTime]) => {\n\t\tconst sortedTimes = [...perTime.keys()].sort((a, b) => a - b);\n\t\tconst points = sortedTimes.map((t) => {\n\t\t\tconst e = perTime.get(t)!;\n\t\t\tconst y = e.sumCount === 0 ? null : 1 - (e.sumTotal / e.sumCount);\n\t\t\treturn { x: t, y, count: e.sumCount };\n\t\t});\n\t\treturn { key: path, label: path, points };\n\t});\n\n\treturn {\n\t\tseries,\n\t\tthresholds: [\n\t\t\t{ value: 0.001, label: '0.1% error SLO', direction: 'above-is-bad', minCount: 1000 },\n\t\t],\n\t};\n}\n\nexport const errorRateDerived: DerivedMetricSpec = {\n\tid: 'error-rate',\n\ttitle: 'Error rate (≥1000 req)',\n\tsubtitle: 'errored-request fraction — 1 − Σtotal/Σcount',\n\ttab: 'requests',\n\tsourceMetric: 'success',\n\trecompute: recomputeErrorRate,\n\tprimitive: 'line',\n\tyAxis: { unit: '', formatter: 'percent' },\n\t// Threshold lives on the SeriesData returned by `recomputeErrorRate` — the\n\t// LineChart primitive reads thresholds from rendered SeriesData, not from\n\t// the DerivedMetricSpec entry. Single source of truth.\n};\n","export const NODE_PALETTE = [\n\t'#58a6ff', // blue\n\t'#3fb950', // green\n\t'#f0883e', // orange\n\t'#bc8cff', // purple\n\t'#f778ba', // pink\n\t'#79c0ff', // light blue\n\t'#d2a8ff', // lavender\n\t'#ffa657', // amber\n\t'#ff7b72', // red\n\t'#7ee787', // lime\n] as const;\n\nexport function getNodeColor(nodeId: string, allNodeIds: string[]): string {\n\tconst sorted = [...allNodeIds].sort();\n\tconst index = sorted.indexOf(nodeId);\n\treturn NODE_PALETTE[index % NODE_PALETTE.length];\n}\n","import { getNodeColor } from '../lib/nodeColors.ts';\n\ninterface NodeLegendProps {\n\tnodeIds: string[];\n\tisActive: (nodeId: string) => boolean;\n\tonClickNode: (nodeId: string, ctrlKey: boolean) => void;\n\t/** When true, renders all buttons as non-interactive (gray-out only).\n\t * Selection state is preserved — visual / interactivity change only. */\n\tdisabled?: boolean;\n}\n\nexport function NodeLegend({ nodeIds, isActive, onClickNode, disabled }: NodeLegendProps) {\n\treturn (\n\t\t<div\n\t\t\trole=\"group\"\n\t\t\taria-label={disabled ? 'Node filter (unavailable on this tab)' : 'Node filter'}\n\t\t\tclassName=\"flex flex-wrap justify-center gap-x-4 gap-y-1 pt-2 text-[11px]\"\n\t\t>\n\t\t\t{disabled && (\n\t\t\t\t<span className=\"sr-only\" aria-live=\"polite\">\n\t\t\t\t\tPer-node filter is unavailable on this tab. Buttons remain visible to preserve selection state.\n\t\t\t\t</span>\n\t\t\t)}\n\t\t\t{nodeIds.map((node) => {\n\t\t\t\tconst color = getNodeColor(node, nodeIds);\n\t\t\t\tconst active = isActive(node);\n\t\t\t\tconst baseOpacity = active ? 1 : 0.3;\n\t\t\t\tconst buttonStyle = disabled\n\t\t\t\t\t? { color, opacity: 0.5, cursor: 'not-allowed' as const }\n\t\t\t\t\t: { color, opacity: baseOpacity };\n\t\t\t\treturn (\n\t\t\t\t\t<button\n\t\t\t\t\t\tkey={node}\n\t\t\t\t\t\ttype=\"button\"\n\t\t\t\t\t\taria-pressed={active}\n\t\t\t\t\t\t// `aria-disabled` (not `disabled`) keeps the button in the\n\t\t\t\t\t\t// tab order and announces its state to AT, while the click\n\t\t\t\t\t\t// handler still no-ops when disabled. `disabled` would\n\t\t\t\t\t\t// remove the element from focus order entirely, so SR users\n\t\t\t\t\t\t// couldn't discover what the legend means on this tab.\n\t\t\t\t\t\taria-disabled={disabled ? 'true' : undefined}\n\t\t\t\t\t\ttitle={disabled ? \"Per-node filter unavailable on this tab's panels\" : undefined}\n\t\t\t\t\t\tonClick={(e) => {\n\t\t\t\t\t\t\tif (disabled) { return; }\n\t\t\t\t\t\t\tonClickNode(node, e.ctrlKey || e.metaKey);\n\t\t\t\t\t\t}}\n\t\t\t\t\t\tclassName=\"inline-flex items-center gap-1.5 cursor-pointer border-none bg-transparent p-0\"\n\t\t\t\t\t\tstyle={buttonStyle}\n\t\t\t\t\t>\n\t\t\t\t\t\t<span\n\t\t\t\t\t\t\tclassName=\"inline-block h-[3px] w-3 rounded\"\n\t\t\t\t\t\t\tstyle={{ backgroundColor: color }}\n\t\t\t\t\t\t/>\n\t\t\t\t\t\t<span>{node}</span>\n\t\t\t\t\t</button>\n\t\t\t\t);\n\t\t\t})}\n\t\t</div>\n\t);\n}\n","import { useCallback, useState } from 'react';\n\nexport function useNodeSelection(nodeIds: string[]) {\n\tconst [activeNodes, setActiveNodes] = useState<Set<string> | null>(null);\n\n\tconst isActive = useCallback((nodeId: string) => {\n\t\treturn activeNodes === null || activeNodes.has(nodeId);\n\t}, [activeNodes]);\n\n\tconst handleLegendClick = useCallback((nodeId: string, ctrlKey: boolean) => {\n\t\tsetActiveNodes((prev) => {\n\t\t\tif (ctrlKey) {\n\t\t\t\tif (prev === null) {\n\t\t\t\t\treturn new Set(nodeIds.filter((id) => id !== nodeId));\n\t\t\t\t}\n\t\t\t\tconst next = new Set(prev);\n\t\t\t\tif (next.has(nodeId)) {\n\t\t\t\t\tnext.delete(nodeId);\n\t\t\t\t\tif (next.size === 0) { return null; }\n\t\t\t\t} else {\n\t\t\t\t\tnext.add(nodeId);\n\t\t\t\t\tif (next.size === nodeIds.length) { return null; }\n\t\t\t\t}\n\t\t\t\treturn next;\n\t\t\t}\n\t\t\tif (prev !== null && prev.size === 1 && prev.has(nodeId)) {\n\t\t\t\treturn null;\n\t\t\t}\n\t\t\treturn new Set([nodeId]);\n\t\t});\n\t}, [nodeIds]);\n\n\treturn { isActive, handleLegendClick, activeNodes };\n}\n","/** Protocol / type series colors. Six hues spread across the color wheel for\n * accessibility and colorblind-friendliness. Disjoint from NODE_PALETTE and\n * TABLE_PALETTE (enforced in test/colorAllocators/disjoint.test.ts). */\nexport const TYPE_PALETTE: readonly string[] = [\n\t'#0d9488', // teal-600\n\t'#dc2626', // red-600\n\t'#7c3aed', // violet-600\n\t'#d97706', // amber-600\n\t'#db2777', // pink-600\n\t'#0284c7', // sky-600\n];\n\nexport function getTypeColor(typeKey: string, allKeys: readonly string[]): string {\n\tconst sorted = [...allKeys].sort();\n\tconst idx = sorted.indexOf(typeKey);\n\treturn TYPE_PALETTE[(idx < 0 ? 0 : idx) % TYPE_PALETTE.length];\n}\n","// Aggregators run in the data layer, before primitives mount. Percentile\n// implementations use the nearest-rank method: p50 of [10, 20, 30] = 20;\n// p50 of [10, 20] = 10 (the lower of two medians). No interpolation. If a\n// future spec needs interpolated quantiles, add `p50-interp` etc. to the\n// Aggregator union rather than silently changing this behavior.\nimport type { Aggregator } from '../types/analytics.ts';\n\nexport interface AggInput {\n\tvalue: number | null;\n\t/** Required when aggregator === 'count-weighted-mean'; ignored otherwise. */\n\tcount?: number;\n}\n\nexport function aggregate(op: Aggregator, items: AggInput[]): number | null {\n\tconst finite = items.filter(\n\t\t(i): i is AggInput & { value: number } => typeof i.value === 'number' && Number.isFinite(i.value),\n\t);\n\tif (finite.length === 0) { return null; }\n\tconst vals = finite.map((i) => i.value);\n\n\tswitch (op) {\n\t\tcase 'sum':\n\t\t\treturn vals.reduce((a, b) => a + b, 0);\n\t\tcase 'mean':\n\t\t\treturn vals.reduce((a, b) => a + b, 0) / vals.length;\n\t\tcase 'max': {\n\t\t\t// Reducer instead of Math.max(...vals) — argument-spread blows the\n\t\t\t// V8 stack around ~125k elements, and a single (time, dim) bucket\n\t\t\t// can collect tens of thousands of values when many nodes report\n\t\t\t// at high frequency.\n\t\t\tlet m = vals[0];\n\t\t\tfor (let i = 1; i < vals.length; i++) {\n\t\t\t\tif (vals[i] > m) { m = vals[i]; }\n\t\t\t}\n\t\t\treturn m;\n\t\t}\n\t\tcase 'min': {\n\t\t\tlet m = vals[0];\n\t\t\tfor (let i = 1; i < vals.length; i++) {\n\t\t\t\tif (vals[i] < m) { m = vals[i]; }\n\t\t\t}\n\t\t\treturn m;\n\t\t}\n\t\tcase 'last':\n\t\t\treturn vals[vals.length - 1];\n\t\tcase 'p50':\n\t\t\treturn percentile(vals, 0.5);\n\t\tcase 'p95':\n\t\t\treturn percentile(vals, 0.95);\n\t\tcase 'p99':\n\t\t\treturn percentile(vals, 0.99);\n\t\tcase 'count-weighted-mean': {\n\t\t\tlet numer = 0;\n\t\t\tlet denom = 0;\n\t\t\tfor (const item of finite) {\n\t\t\t\tconst c = Number.isFinite(item.count) ? (item.count as number) : 1;\n\t\t\t\tnumer += item.value * c;\n\t\t\t\tdenom += c;\n\t\t\t}\n\t\t\treturn denom === 0 ? null : numer / denom;\n\t\t}\n\t}\n}\n\nfunction percentile(vals: number[], p: number): number {\n\tconst sorted = [...vals].sort((a, b) => a - b);\n\tconst idx = Math.max(1, Math.ceil(p * sorted.length));\n\treturn sorted[idx - 1];\n}\n","import type { Aggregator } from '../types/analytics.ts';\n\nexport function isApproxAggregator(op: Aggregator): boolean {\n\treturn op === 'count-weighted-mean';\n}\n\nexport function labelWithApprox(label: string, op: Aggregator): string {\n\tif (!isApproxAggregator(op)) { return label; }\n\treturn label.endsWith('(approx)') ? label : `${label} (approx)`;\n}\n","import type { MetricSpec } from '../types/analytics.ts';\n\ntype Confidence = NonNullable<MetricSpec['confidence']>;\nexport type ConfidenceClass = 'ok' | 'grey' | 'suppress';\n\n/** Classify a post-aggregation window count.\n *\n * When both thresholds are provided:\n * count >= suppressBelow → 'ok'\n * greyBelow <= count < suppressBelow → 'grey'\n * count < greyBelow → 'suppress'\n *\n * When only suppressBelow is set, the grey tier is disabled: counts below\n * suppressBelow go straight to 'suppress' (greyBelow collapses to\n * suppressBelow, leaving no grey band).\n *\n * When only greyBelow is set, the suppress tier is disabled: counts below\n * greyBelow become 'grey' and never 'suppress'.\n *\n * No rule → 'ok'.\n */\nexport function classifyConfidence(\n\tcount: number | undefined,\n\trule: Pick<Confidence, 'greyBelow' | 'suppressBelow'> | undefined,\n): ConfidenceClass {\n\tif (!rule) { return 'ok'; }\n\tif (count === undefined) { return 'suppress'; }\n\tconst { suppressBelow, greyBelow } = rule;\n\t// Check suppress tier: only if suppressBelow is set and count is below it.\n\tif (suppressBelow !== undefined && count < suppressBelow) {\n\t\t// If greyBelow is also set and count is still >= greyBelow, it's grey\n\t\t// (in the band between greyBelow and suppressBelow). Otherwise suppress.\n\t\tif (greyBelow !== undefined && count >= greyBelow) { return 'grey'; }\n\t\treturn 'suppress';\n\t}\n\t// If we passed the suppress check (or no suppress tier), check grey tier.\n\tif (greyBelow !== undefined && count < greyBelow) { return 'grey'; }\n\treturn 'ok';\n}\n","import type { FieldExpr } from '../types/analytics.ts';\n\nexport function evalFieldExpr(\n\texpr: FieldExpr,\n\trecord: Record<string, unknown>,\n): number | null {\n\tswitch (expr.kind) {\n\t\tcase 'const':\n\t\t\treturn expr.value;\n\t\tcase 'ref': {\n\t\t\tconst v = record[expr.field];\n\t\t\treturn typeof v === 'number' && Number.isFinite(v) ? v : null;\n\t\t}\n\t\tcase 'op': {\n\t\t\tconst l = evalFieldExpr(expr.left, record);\n\t\t\tconst r = evalFieldExpr(expr.right, record);\n\t\t\tif (l === null || r === null) { return null; }\n\t\t\tswitch (expr.op) {\n\t\t\t\tcase '+':\n\t\t\t\t\treturn l + r;\n\t\t\t\tcase '-':\n\t\t\t\t\treturn l - r;\n\t\t\t\tcase '*':\n\t\t\t\t\treturn l * r;\n\t\t\t\tcase '/':\n\t\t\t\t\treturn r === 0 ? null : l / r;\n\t\t\t\tdefault: {\n\t\t\t\t\tconst _exhaustive: never = expr.op;\n\t\t\t\t\tthrow new Error(`unknown op: ${String(_exhaustive)}`);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tdefault: {\n\t\t\tconst _exhaustive: never = expr;\n\t\t\tthrow new Error(`unknown FieldExpr kind: ${(_exhaustive as { kind: string }).kind}`);\n\t\t}\n\t}\n}\n","// Named transforms let specs stay serializable while still supporting\n// non-trivial per-record math (e.g. `cpuUtilization × 100`). Add entries here\n// as specs need them; never accept arbitrary functions from a spec.\nexport const namedTransforms = {\n\t'percent-of-core': (v: number) => v * 100,\n} as const;\n\nexport type NamedTransformKey = keyof typeof namedTransforms;\n","import type { Transform } from '../types/analytics.ts';\nimport { type NamedTransformKey, namedTransforms } from './transforms.ts';\n\n/** Apply a Transform to a scalar. `period` is the record's `period` field in\n * ms; only `rate` consults it. Null input short-circuits to null. */\nexport function runTransform(\n\ttransform: Transform,\n\tvalue: number | null,\n\tperiod: number,\n): number | null {\n\tif (value === null) { return null; }\n\tswitch (transform.kind) {\n\t\tcase 'raw':\n\t\t\treturn value;\n\t\tcase 'scale':\n\t\t\treturn value * transform.factor;\n\t\tcase 'rate':\n\t\t\tif (!Number.isFinite(period) || period <= 0) { return null; }\n\t\t\treturn (value / period) * 1000;\n\t\tcase 'ratio':\n\t\t\treturn value;\n\t\tcase 'compose': {\n\t\t\tlet v: number | null = value;\n\t\t\tfor (const step of transform.steps) {\n\t\t\t\tv = runTransform(step, v, period);\n\t\t\t\tif (v === null) { return null; }\n\t\t\t}\n\t\t\treturn v;\n\t\t}\n\t\tcase 'named': {\n\t\t\tconst fn = namedTransforms[transform.name as NamedTransformKey];\n\t\t\tif (!fn) { throw new Error(`unknown named transform: ${transform.name}`); }\n\t\t\treturn fn(value);\n\t\t}\n\t\tdefault: {\n\t\t\t// Exhaustiveness check — adding a new Transform kind without handling\n\t\t\t// it here will fail typechecking on this assignment.\n\t\t\tconst _exhaustive: never = transform;\n\t\t\tthrow new Error(`unknown transform kind: ${(_exhaustive as { kind: string }).kind}`);\n\t\t}\n\t}\n}\n","// Generic spec pipeline. Both groupBy and field modes emit one SeriesPoint\n// per unique `record.time` per series (per-time bucketing landed in Step 3\n// for groupBy; Step 4 for field).\n//\n// Step 4.5 adds two-pass cross-node aggregation. Within each (dimensionValue,\n// time) bucket (groupBy) or each `time` bucket (field), records are\n// partitioned by `node`. The temporal aggregator runs per `(time, node)`\n// (inner pass), producing one (value, count) per node. Then the crossNode\n// aggregator runs across nodes within `(dim, time)` (outer pass) to yield\n// the final value. The per-node `count` is threaded through to the outer\n// pass's AggInput[] so count-weighted-mean stays well-defined when the\n// crossNode aggregator needs it.\n//\n// Known limitation — count-weighted-mean across nodes: the inner pass\n// invokes the temporal aggregator with values that share the per-node\n// bucket's totalCount as their weight. When a single (node, time) bucket\n// holds multiple records, the inner result is still correct (CWM uses each\n// record's own count internally), but the *weight* attached to each\n// per-node AggInput passed into the outer pass is the sum of all record\n// counts in that node-bucket — i.e., the outer pass weights nodes by total\n// observations, not by the inner-mean's effective sample size. For the\n// shipped specs this is fine: replication-latency does not flow through\n// this pipeline, mqtt-traffic-* is sum/sum (associative). Revisit if a CWM\n// crossNode spec lands.\nimport type {\n\tAggregator,\n\tAnalyticsDataPoint,\n\tFieldExpr,\n\tFieldSpec,\n\tMetricSpec,\n\tSeries,\n\tSeriesData,\n\tSeriesPoint,\n\tTimeRange,\n} from '../types/analytics.ts';\nimport { type AggInput, aggregate } from './aggregators.ts';\nimport { labelWithApprox } from './approxLabel.ts';\nimport { classifyConfidence } from './confidence.ts';\nimport { evalFieldExpr } from './fieldExpr.ts';\nimport { runTransform } from './runTransform.ts';\n\nexport interface RunPipelineOptions {\n\t/** When true, runGroupBy emits one Series per (dim, node) instead of\n\t * collapsing nodes via the crossNode aggregator. The series key is\n\t * `${dim}|${node}` so consumers (e.g. DimensionSelectorRenderer) can\n\t * filter by selected dim while keeping per-node detail. No-op for\n\t * `kind: 'field'` series sources or for the OTHER bucket.\n\t * Used by chip-selector panels (duration, success, transfer, db-*,\n\t * connection, response_200) so the operator can spot a hot node\n\t * instead of reading a cluster-mean line. */\n\tperNode?: boolean;\n\t/** When true, each record's resolved time is snapped to its period\n\t * boundary (`floor(time/period)*period`). Harper emits per-node\n\t * records at slightly offset instants within the same minute; without\n\t * this snap, downstream stacks/lines render with sparse staggered\n\t * rows that look jagged. MetricRenderer enables this for production\n\t * rendering; pipeline tests that use synthetic small-integer times\n\t * leave it off so they keep their distinct buckets. */\n\tsnapToPeriod?: boolean;\n}\n\nexport function runPipeline(\n\tspec: MetricSpec,\n\trecords: AnalyticsDataPoint[],\n\t_window: TimeRange,\n\t_nodes: string[],\n\toptions?: RunPipelineOptions,\n): SeriesData {\n\tif (spec.series.kind === 'field') {\n\t\treturn runFieldSpecs(spec, spec.series.fields, records, options?.perNode ?? false, options?.snapToPeriod ?? false);\n\t}\n\treturn runGroupBy(spec, spec.series, records, options?.perNode ?? false, options?.snapToPeriod ?? false);\n}\n\n/** Snap a record's time to its period boundary so per-node staggering\n * doesn't leak into downstream visuals. Round-to-nearest (not floor) so\n * records arriving a few seconds before the boundary still group with\n * their siblings on the *other* side — Math.floor produced zigzag\n * aggregates because nodes reporting at e.g. 1:59:43, 2:00:03, 2:00:16\n * would split across two buckets. */\nfunction snapToBucketTime(spec: MetricSpec, record: AnalyticsDataPoint, time: number): number {\n\tlet period = 0;\n\tconst p = record.period;\n\tif (typeof p === 'number' && Number.isFinite(p) && p > 0) { period = p; }\n\tif (period <= 0) { period = spec.bucket?.fallbackMs ?? 60_000; }\n\treturn Math.round(time / period) * period;\n}\n\n/** Resolve the timestamp on a record per `spec.timestamp`. Defaults to 'time'.\n * Records like database-size / storage-volume carry `id` (ms since epoch)\n * instead of `time`; `timestamp: 'id'` reads `id`; `'time-or-id'` falls back. */\nfunction resolveTime(spec: MetricSpec, record: AnalyticsDataPoint): number | null {\n\tconst which = spec.timestamp ?? 'time';\n\tif (which === 'time') {\n\t\tconst t = record.time;\n\t\treturn typeof t === 'number' && Number.isFinite(t) ? t : null;\n\t}\n\tif (which === 'id') {\n\t\tconst t = (record as any).id;\n\t\treturn typeof t === 'number' && Number.isFinite(t) ? t : null;\n\t}\n\t// 'time-or-id'\n\tconst t = record.time;\n\tif (typeof t === 'number' && Number.isFinite(t)) { return t; }\n\tconst id = (record as any).id;\n\treturn typeof id === 'number' && Number.isFinite(id) ? id : null;\n}\n\nfunction projectValue(\n\tfieldSpec: FieldSpec,\n\trecord: AnalyticsDataPoint,\n): number | null {\n\tconst raw = typeof fieldSpec.field === 'string'\n\t\t? (typeof record[fieldSpec.field] === 'number' ? (record[fieldSpec.field] as number) : null)\n\t\t: evalFieldExpr(fieldSpec.field as FieldExpr, record);\n\tconst period = typeof record.period === 'number' ? record.period : 0;\n\treturn runTransform(fieldSpec.transform ?? { kind: 'raw' }, raw, period);\n}\n\ninterface NodeBucket {\n\titems: AggInput[]; // per-record {value, count} for this (dim?, time, node)\n\ttotalCount: number; // Σ of record counts within this node-bucket\n}\n\nfunction runGroupBy(\n\tspec: MetricSpec,\n\tsrc: Extract<MetricSpec['series'], { kind: 'groupBy' }>,\n\trecords: AnalyticsDataPoint[],\n\tperNode: boolean,\n\tsnapToPeriod: boolean,\n): SeriesData {\n\t// Step 4.5 structure: dim → time → node → NodeBucket. Per-dimension totals\n\t// are accumulated separately for topN ranking + per-series confidence\n\t// gating.\n\tconst buckets = new Map<string | number, Map<number, Map<string, NodeBucket>>>();\n\tconst dimTotals = new Map<string | number, number>();\n\tconst warnedTimes = new Set<string>();\n\tfor (const r of records) {\n\t\tconst dimVal = r[src.dimension];\n\t\tif (typeof dimVal !== 'string' && typeof dimVal !== 'number') { continue; }\n\t\tconst v = projectValue(src.field, r);\n\t\tif (v === null) { continue; }\n\t\tconst resolvedTime = resolveTime(spec, r);\n\t\tif (resolvedTime === null) {\n\t\t\tconst key = `${String(r[src.dimension])}|${String(r.time)}`;\n\t\t\tif (!warnedTimes.has(key)) {\n\t\t\t\twarnedTimes.add(key);\n\t\t\t\tconsole.warn('[runGroupBy] Dropping record with no resolvable timestamp:', {\n\t\t\t\t\tdimension: r[src.dimension],\n\t\t\t\t\ttime: r.time,\n\t\t\t\t\tid: (r as any).id,\n\t\t\t\t\ttimestamp: spec.timestamp ?? 'time',\n\t\t\t\t});\n\t\t\t}\n\t\t\tcontinue;\n\t\t}\n\t\tconst time = snapToPeriod ? snapToBucketTime(spec, r, resolvedTime) : resolvedTime;\n\t\tconst recordCount = typeof r.count === 'number' && Number.isFinite(r.count) ? r.count : 1;\n\t\tconst node = typeof r.node === 'string' ? r.node : '_no_node';\n\n\t\tdimTotals.set(dimVal, (dimTotals.get(dimVal) ?? 0) + recordCount);\n\n\t\tlet perTime = buckets.get(dimVal);\n\t\tif (!perTime) {\n\t\t\tperTime = new Map();\n\t\t\tbuckets.set(dimVal, perTime);\n\t\t}\n\t\tlet perNodeBucket = perTime.get(time);\n\t\tif (!perNodeBucket) {\n\t\t\tperNodeBucket = new Map();\n\t\t\tperTime.set(time, perNodeBucket);\n\t\t}\n\t\tlet nodeBucket = perNodeBucket.get(node);\n\t\tif (!nodeBucket) {\n\t\t\tnodeBucket = { items: [], totalCount: 0 };\n\t\t\tperNodeBucket.set(node, nodeBucket);\n\t\t}\n\t\tnodeBucket.items.push({ value: v, count: recordCount });\n\t\tnodeBucket.totalCount += recordCount;\n\t}\n\n\tconst tempAgg: Aggregator = src.field.aggregator?.temporal ?? spec.aggregator.temporal;\n\tconst crossAgg: Aggregator = src.field.aggregator?.crossNode ?? spec.aggregator.crossNode;\n\tconst isApprox = tempAgg === 'count-weighted-mean' || crossAgg === 'count-weighted-mean';\n\n\t// Apply topN + otherBucket: rank dimensions by totalCount descending, keep\n\t// the top N, roll the rest into an `Other` aggregate if otherBucket is on.\n\tconst ranked = [...dimTotals.entries()].sort((a, b) => b[1] - a[1]);\n\tconst topN = src.topN ?? Infinity;\n\tconst kept = ranked.slice(0, topN);\n\tconst rest = ranked.slice(topN);\n\n\tconst series: Series[] = [];\n\tlet suppressedSeriesCount = 0;\n\tfor (const [key, total] of kept) {\n\t\tconst confClass = classifyConfidence(\n\t\t\ttotal,\n\t\t\tspec.confidence && {\n\t\t\t\tgreyBelow: spec.confidence.greyBelow,\n\t\t\t\tsuppressBelow: spec.confidence.suppressBelow,\n\t\t\t},\n\t\t);\n\t\tif (confClass === 'suppress') {\n\t\t\tsuppressedSeriesCount++;\n\t\t\tcontinue;\n\t\t}\n\t\tconst perTime = buckets.get(key);\n\t\tif (!perTime) { continue; }\n\t\t// When the spec already groups by node, perNode is redundant — emitting\n\t\t// one series per (node, node) would duplicate. Fall through to the\n\t\t// cluster-aggregate path which, for dimension='node', is naturally\n\t\t// one-series-per-node.\n\t\tconst dimensionIsNode = src.dimension === 'node';\n\t\tif (perNode && !dimensionIsNode) {\n\t\t\t// Emit one Series per (dim, node). Skip the crossNode pass; each\n\t\t\t// node's points come from the inner temporal aggregation alone.\n\t\t\t// Series key is `${dim}|${node}` so renderers can filter by dim\n\t\t\t// prefix; label is the node name.\n\t\t\tconst nodeBuckets = new Map<string, Map<number, NodeBucket>>();\n\t\t\tfor (const [time, byNode] of perTime) {\n\t\t\t\tfor (const [node, nb] of byNode) {\n\t\t\t\t\tlet perTimeForNode = nodeBuckets.get(node);\n\t\t\t\t\tif (!perTimeForNode) {\n\t\t\t\t\t\tperTimeForNode = new Map();\n\t\t\t\t\t\tnodeBuckets.set(node, perTimeForNode);\n\t\t\t\t\t}\n\t\t\t\t\tperTimeForNode.set(time, nb);\n\t\t\t\t}\n\t\t\t}\n\t\t\tfor (const [node, perTimeForNode] of nodeBuckets) {\n\t\t\t\tconst points: SeriesPoint[] = [];\n\t\t\t\tconst sortedTimes = [...perTimeForNode.keys()].sort((a, b) => a - b);\n\t\t\t\tfor (const time of sortedTimes) {\n\t\t\t\t\tconst nb = perTimeForNode.get(time)!;\n\t\t\t\t\tconst y = aggregate(tempAgg, nb.items);\n\t\t\t\t\tpoints.push({ x: time, y, count: nb.totalCount });\n\t\t\t\t}\n\t\t\t\tseries.push({\n\t\t\t\t\tkey: `${String(key)}|${node}`,\n\t\t\t\t\tlabel: labelWithApprox(node, tempAgg),\n\t\t\t\t\tpoints,\n\t\t\t\t\tapprox: isApprox,\n\t\t\t\t});\n\t\t\t}\n\t\t} else {\n\t\t\t// Cluster-aggregate path (default). Two-pass: temporal-per-node,\n\t\t\t// then crossNode across nodes within each time bucket.\n\t\t\tconst points: SeriesPoint[] = [];\n\t\t\tconst sortedTimes = [...perTime.keys()].sort((a, b) => a - b);\n\t\t\tfor (const time of sortedTimes) {\n\t\t\t\tconst byNode = perTime.get(time)!;\n\t\t\t\tconst { y, count } = aggregateTwoPass(tempAgg, crossAgg, byNode);\n\t\t\t\tpoints.push({ x: time, y, count });\n\t\t\t}\n\t\t\tseries.push({\n\t\t\t\tkey: String(key),\n\t\t\t\tlabel: labelWithApprox(String(key), tempAgg),\n\t\t\t\tpoints,\n\t\t\t\tapprox: isApprox,\n\t\t\t});\n\t\t}\n\t}\n\n\t// OTHER bucket: if enabled and there are more buckets beyond topN, aggregate\n\t// them into one. Bucket per-(time, node) across all \"rest\" dimension values,\n\t// then run the same two-pass aggregation.\n\tif (src.otherBucket && rest.length > 0) {\n\t\tconst otherTotal = rest.reduce((acc, [, c]) => acc + c, 0);\n\t\tconst confClass = classifyConfidence(\n\t\t\totherTotal,\n\t\t\tspec.confidence && {\n\t\t\t\tgreyBelow: spec.confidence.greyBelow,\n\t\t\t\tsuppressBelow: spec.confidence.suppressBelow,\n\t\t\t},\n\t\t);\n\t\tif (confClass !== 'suppress') {\n\t\t\tconst otherPerTime = new Map<number, Map<string, NodeBucket>>();\n\t\t\tfor (const [key] of rest) {\n\t\t\t\tconst perTime = buckets.get(key);\n\t\t\t\tif (!perTime) { continue; }\n\t\t\t\tfor (const [time, perNodeBucket] of perTime) {\n\t\t\t\t\tlet mergedPerNode = otherPerTime.get(time);\n\t\t\t\t\tif (!mergedPerNode) {\n\t\t\t\t\t\tmergedPerNode = new Map();\n\t\t\t\t\t\totherPerTime.set(time, mergedPerNode);\n\t\t\t\t\t}\n\t\t\t\t\tfor (const [node, nb] of perNodeBucket) {\n\t\t\t\t\t\tlet merged = mergedPerNode.get(node);\n\t\t\t\t\t\tif (!merged) {\n\t\t\t\t\t\t\tmerged = { items: [], totalCount: 0 };\n\t\t\t\t\t\t\tmergedPerNode.set(node, merged);\n\t\t\t\t\t\t}\n\t\t\t\t\t\tfor (const item of nb.items) { merged.items.push(item); }\n\t\t\t\t\t\tmerged.totalCount += nb.totalCount;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\tconst otherPoints: SeriesPoint[] = [];\n\t\t\tconst sortedTimes = [...otherPerTime.keys()].sort((a, b) => a - b);\n\t\t\tfor (const time of sortedTimes) {\n\t\t\t\tconst byNode = otherPerTime.get(time)!;\n\t\t\t\tconst { y, count } = aggregateTwoPass(tempAgg, crossAgg, byNode);\n\t\t\t\totherPoints.push({ x: time, y, count });\n\t\t\t}\n\t\t\tseries.push({\n\t\t\t\tkey: 'Other',\n\t\t\t\tlabel: labelWithApprox('Other', tempAgg),\n\t\t\t\tpoints: otherPoints,\n\t\t\t\tapprox: isApprox,\n\t\t\t});\n\t\t} else {\n\t\t\tsuppressedSeriesCount++;\n\t\t}\n\t}\n\n\treturn {\n\t\tseries,\n\t\tthresholds: spec.thresholds,\n\t\t...(suppressedSeriesCount > 0 ? { suppressedSeriesCount } : {}),\n\t};\n}\n\nfunction runFieldSpecs(\n\tspec: MetricSpec,\n\tfields: FieldSpec[],\n\trecords: AnalyticsDataPoint[],\n\tperNode: boolean,\n\tsnapToPeriod: boolean,\n): SeriesData {\n\tconst warnedTimes = new Set<string>();\n\tconst seriesArrays: Series[][] = fields.map((f) => {\n\t\t// time → node → NodeBucket\n\t\tconst buckets = new Map<number, Map<string, NodeBucket>>();\n\t\tfor (const r of records) {\n\t\t\tconst resolvedTime = resolveTime(spec, r);\n\t\t\tif (resolvedTime === null) {\n\t\t\t\tconst key = `${f.label}|${String(r.time)}`;\n\t\t\t\tif (!warnedTimes.has(key)) {\n\t\t\t\t\twarnedTimes.add(key);\n\t\t\t\t\tconsole.warn('[runFieldSpecs] Dropping record with no resolvable timestamp:', {\n\t\t\t\t\t\tfield: f.label,\n\t\t\t\t\t\ttime: r.time,\n\t\t\t\t\t\tid: (r as any).id,\n\t\t\t\t\t\ttimestamp: spec.timestamp ?? 'time',\n\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tconst v = projectValue(f, r);\n\t\t\tif (v === null) { continue; }\n\t\t\tconst recordCount = typeof r.count === 'number' && Number.isFinite(r.count) ? r.count : 1;\n\t\t\tconst node = typeof r.node === 'string' ? r.node : '_no_node';\n\t\t\tconst time = snapToPeriod ? snapToBucketTime(spec, r, resolvedTime) : resolvedTime;\n\t\t\tlet byNode = buckets.get(time);\n\t\t\tif (!byNode) {\n\t\t\t\tbyNode = new Map();\n\t\t\t\tbuckets.set(time, byNode);\n\t\t\t}\n\t\t\tlet nodeBucket = byNode.get(node);\n\t\t\tif (!nodeBucket) {\n\t\t\t\tnodeBucket = { items: [], totalCount: 0 };\n\t\t\t\tbyNode.set(node, nodeBucket);\n\t\t\t}\n\t\t\tnodeBucket.items.push({ value: v, count: recordCount });\n\t\t\tnodeBucket.totalCount += recordCount;\n\t\t}\n\t\tconst tempAgg = f.aggregator?.temporal ?? spec.aggregator.temporal;\n\t\tconst crossAgg = f.aggregator?.crossNode ?? spec.aggregator.crossNode;\n\t\tconst isApprox = tempAgg === 'count-weighted-mean' || crossAgg === 'count-weighted-mean';\n\t\tconst fieldKey = typeof f.field === 'string' ? f.field : f.label;\n\n\t\tif (perNode) {\n\t\t\t// Emit one Series per (field, node). Series key is\n\t\t\t// `${fieldKey}|${node}` so callers can identify both axes; label is\n\t\t\t// `${fieldLabel} — ${node}` so legends stay readable.\n\t\t\tconst nodeBuckets = new Map<string, Map<number, NodeBucket>>();\n\t\t\tfor (const [time, byNode] of buckets) {\n\t\t\t\tfor (const [node, nb] of byNode) {\n\t\t\t\t\tlet perTimeForNode = nodeBuckets.get(node);\n\t\t\t\t\tif (!perTimeForNode) {\n\t\t\t\t\t\tperTimeForNode = new Map();\n\t\t\t\t\t\tnodeBuckets.set(node, perTimeForNode);\n\t\t\t\t\t}\n\t\t\t\t\tperTimeForNode.set(time, nb);\n\t\t\t\t}\n\t\t\t}\n\t\t\tconst out: Series[] = [];\n\t\t\tfor (const [node, perTimeForNode] of nodeBuckets) {\n\t\t\t\tconst points: SeriesPoint[] = [];\n\t\t\t\tconst sortedTimes = [...perTimeForNode.keys()].sort((a, b) => a - b);\n\t\t\t\tfor (const time of sortedTimes) {\n\t\t\t\t\tconst nb = perTimeForNode.get(time)!;\n\t\t\t\t\tconst y = aggregate(tempAgg, nb.items);\n\t\t\t\t\tpoints.push({ x: time, y, count: nb.totalCount });\n\t\t\t\t}\n\t\t\t\tout.push({\n\t\t\t\t\tkey: `${fieldKey}|${node}`,\n\t\t\t\t\tlabel: labelWithApprox(`${f.label} — ${node}`, tempAgg),\n\t\t\t\t\taxis: f.axis,\n\t\t\t\t\tpoints,\n\t\t\t\t\tapprox: isApprox,\n\t\t\t\t});\n\t\t\t}\n\t\t\treturn out;\n\t\t}\n\n\t\t// Cluster-aggregate path.\n\t\tconst points: SeriesPoint[] = [];\n\t\tconst sortedTimes = [...buckets.keys()].sort((a, b) => a - b);\n\t\tfor (const t of sortedTimes) {\n\t\t\tconst byNode = buckets.get(t)!;\n\t\t\tconst { y, count } = aggregateTwoPass(tempAgg, crossAgg, byNode);\n\t\t\tpoints.push({ x: t, y, count });\n\t\t}\n\t\treturn [{\n\t\t\tkey: fieldKey,\n\t\t\tlabel: labelWithApprox(f.label, tempAgg),\n\t\t\taxis: f.axis,\n\t\t\tpoints,\n\t\t\tapprox: isApprox,\n\t\t}];\n\t});\n\treturn { series: seriesArrays.flat(), thresholds: spec.thresholds };\n}\n\n/**\n * Two-pass aggregation: temporal (inner, per-node) → crossNode (outer, across\n * nodes within the same time bucket). Returns the final y plus the summed\n * per-node totalCount for the bucket.\n */\nfunction aggregateTwoPass(\n\ttemporal: Aggregator,\n\tcrossNode: Aggregator,\n\tperNode: Map<string, NodeBucket>,\n): { y: number | null; count: number } {\n\tconst perNodeAggs: AggInput[] = [];\n\tlet totalCount = 0;\n\tfor (const [, nodeBucket] of perNode) {\n\t\tconst nodeY = aggregate(temporal, nodeBucket.items);\n\t\tif (typeof nodeY === 'number' && Number.isFinite(nodeY)) {\n\t\t\tperNodeAggs.push({ value: nodeY, count: nodeBucket.totalCount });\n\t\t}\n\t\ttotalCount += nodeBucket.totalCount;\n\t}\n\tconst y = aggregate(crossNode, perNodeAggs);\n\treturn { y, count: totalCount };\n}\n","import type { PresetOption, TimeRange } from '../types/analytics.ts';\n\nexport const TIME_PRESETS: PresetOption[] = [\n\t{ label: '15m', value: '15m', durationMs: 15 * 60 * 1000 },\n\t{ label: '30m', value: '30m', durationMs: 30 * 60 * 1000 },\n\t{ label: '1h', value: '1h', durationMs: 60 * 60 * 1000 },\n\t{ label: '6h', value: '6h', durationMs: 6 * 60 * 60 * 1000 },\n\t{ label: '12h', value: '12h', durationMs: 12 * 60 * 60 * 1000 },\n\t{ label: '1d', value: '1d', durationMs: 24 * 60 * 60 * 1000 },\n\t{ label: '3d', value: '3d', durationMs: 3 * 24 * 60 * 60 * 1000 },\n\t{ label: '1w', value: '1w', durationMs: 7 * 24 * 60 * 60 * 1000 },\n\t{ label: '1mo', value: '1mo', durationMs: 30 * 24 * 60 * 60 * 1000 },\n];\n\nexport function getTimeRangeFromPreset(preset: string, now: number = Date.now()): TimeRange {\n\tconst found = TIME_PRESETS.find((p) => p.value === preset);\n\tif (!found) { throw new Error(`Unknown preset: ${preset}`); }\n\treturn { startTime: now - found.durationMs, endTime: now };\n}\n\nconst axisFormatter = new Intl.DateTimeFormat(undefined, {\n\thour: 'numeric',\n\tminute: '2-digit',\n});\n\nconst tooltipFormatter = new Intl.DateTimeFormat(undefined, {\n\tmonth: 'short',\n\tday: 'numeric',\n\tyear: 'numeric',\n\thour: 'numeric',\n\tminute: '2-digit',\n\tsecond: '2-digit',\n});\n\nconst rangeFormatter = new Intl.DateTimeFormat(undefined, {\n\tmonth: 'short',\n\tday: 'numeric',\n\thour: 'numeric',\n\tminute: '2-digit',\n});\n\nconst tzFormatter = new Intl.DateTimeFormat(undefined, {\n\ttimeZoneName: 'short',\n});\n\nexport function formatAxisTick(timestamp: number): string {\n\treturn axisFormatter.format(new Date(timestamp));\n}\n\nexport function formatTooltipTime(timestamp: number): string {\n\treturn tooltipFormatter.format(new Date(timestamp));\n}\n\nexport function formatTimeRange(startTime: number, endTime: number): string {\n\tconst start = rangeFormatter.format(new Date(startTime));\n\tconst end = rangeFormatter.format(new Date(endTime));\n\treturn `${start} – ${end}`;\n}\n\nexport function getTimezoneAbbr(): string {\n\tconst parts = tzFormatter.formatToParts(new Date());\n\tconst tz = parts.find((p) => p.type === 'timeZoneName');\n\treturn tz?.value ?? 'UTC';\n}\n\nexport function formatBytes(bytes: number): string {\n\tif (bytes === 0) { return '0 B'; }\n\tconst units = ['B', 'KB', 'MB', 'GB', 'TB'];\n\tconst k = 1000; // SI\n\tconst i = Math.floor(Math.log(bytes) / Math.log(k));\n\tconst value = bytes / k ** i;\n\treturn `${value.toFixed(value < 10 ? 1 : 0)} ${units[i]}`;\n}\n\nexport function formatBytesPerMin(bytes: number): string {\n\tif (bytes === 0) { return '0 B/min'; }\n\tconst units = ['B/min', 'KB/min', 'MB/min', 'GB/min', 'TB/min'];\n\tconst k = 1000;\n\tconst i = Math.floor(Math.log(bytes) / Math.log(k));\n\tconst value = bytes / k ** i;\n\treturn `${value.toFixed(value < 10 ? 1 : 0)} ${units[i]}`;\n}\n","import type { AxisSpec } from '../types/analytics.ts';\n\nexport function formatValue(\n\tv: number,\n\tformatter?: AxisSpec['formatter'],\n\tunitSuffix?: string,\n): string {\n\tif (v === null || v === undefined || !Number.isFinite(v)) { return '—'; }\n\tconst base = formatBase(v, formatter);\n\t// unitSuffix is meant to *compose* with the formatter's unit, not\n\t// duplicate it. Specs should set unitSuffix to '' when the formatter\n\t// already includes the right unit (e.g. formatter: 'ms' → spec sets\n\t// unit: ''), and to a modifier like '/s' when adding rate context\n\t// (formatter: 'bytes-si' + unit: '/s' → \"MB/s\").\n\treturn unitSuffix ? `${base}${unitSuffix}` : base;\n}\n\nfunction formatBase(v: number, formatter?: AxisSpec['formatter']): string {\n\tswitch (formatter) {\n\t\tcase 'percent':\n\t\t\treturn `${(v * 100).toFixed(1)}%`;\n\t\tcase 'ms':\n\t\t\treturn `${v.toFixed(1)} ms`;\n\t\tcase 'count':\n\t\t\treturn `${v.toFixed(0)}`;\n\t\tcase 'count-si': {\n\t\t\tconst abs = Math.abs(v);\n\t\t\tif (abs < 1_000) { return `${v}`; }\n\t\t\tconst sign = v < 0 ? '-' : '';\n\t\t\tconst fmt = (x: number): string => {\n\t\t\t\tif (x >= 10) { return `${Math.round(x)}`; }\n\t\t\t\tconst s = x.toFixed(1);\n\t\t\t\treturn s.endsWith('.0') ? s.slice(0, -2) : s;\n\t\t\t};\n\t\t\tif (abs < 1_000_000) { return `${sign}${fmt(abs / 1_000)}k`; }\n\t\t\tif (abs < 1_000_000_000) { return `${sign}${fmt(abs / 1_000_000)}M`; }\n\t\t\treturn `${sign}${fmt(abs / 1_000_000_000)}B`;\n\t\t}\n\t\tcase 'cores':\n\t\t\t// Input is cores-equivalent CPU usage (1.0 = one core fully busy;\n\t\t\t// nproc = box saturated). Display direct, no scaling.\n\t\t\treturn `${v.toFixed(2)} cores`;\n\t\tcase 'bytes-si':\n\t\tcase 'bytes-iec': {\n\t\t\tconst base = formatter === 'bytes-iec' ? 1024 : 1000;\n\t\t\tconst units = formatter === 'bytes-iec'\n\t\t\t\t? ['B', 'KiB', 'MiB', 'GiB', 'TiB']\n\t\t\t\t: ['B', 'KB', 'MB', 'GB', 'TB'];\n\t\t\tlet scaled = v;\n\t\t\tlet i = 0;\n\t\t\twhile (Math.abs(scaled) >= base && i < units.length - 1) {\n\t\t\t\tscaled /= base;\n\t\t\t\ti++;\n\t\t\t}\n\t\t\treturn `${scaled.toFixed(1)} ${units[i]}`;\n\t\t}\n\t\tdefault:\n\t\t\treturn `${v}`;\n\t}\n}\n","// Shared tooltip surface for all analytics chart primitives. Resolves to\n// Studio's --popover token (via --chart-tooltip-bg) so LineChart,\n// StackedAreaChart, and HeatmapMatrix all hover with the same surface\n// the rest of Studio uses for HoverCard/Tooltip, instead of Recharts'\n// default white box or a stray --color-bg-secondary.\n\nimport type { CSSProperties } from 'react';\n\nexport const tooltipContentStyle: CSSProperties = {\n\tbackground: 'var(--chart-tooltip-bg)',\n\tcolor: 'var(--chart-tooltip-fg)',\n\tborder: '1px solid var(--border)',\n\tborderRadius: 6,\n\tboxShadow: '0 4px 12px rgba(0, 0, 0, 0.25)',\n\tpadding: 8,\n\tfontSize: 12,\n};\n\nexport const tooltipLabelStyle: CSSProperties = {\n\tcolor: 'var(--chart-tooltip-fg)',\n\topacity: 0.7,\n\tmarginBottom: 4,\n\tfontSize: 11,\n};\n\nexport const tooltipItemStyle: CSSProperties = {\n\tcolor: 'var(--chart-tooltip-fg)',\n};\n","import {\n\tCartesianGrid,\n\tLegend,\n\tLine,\n\tLineChart as RLineChart,\n\tReferenceLine,\n\tResponsiveContainer,\n\tTooltip,\n\tXAxis,\n\tYAxis,\n} from 'recharts';\nimport { formatAxisTick, formatTooltipTime } from '../lib/time.ts';\nimport type { AxisSpec, SeriesData, Threshold } from '../types/analytics.ts';\nimport { formatValue } from './formatValue.ts';\nimport { tooltipContentStyle, tooltipItemStyle, tooltipLabelStyle } from './tooltipStyle.ts';\n\ninterface Props {\n\tdata: SeriesData;\n\ttheme: 'light' | 'dark';\n\tyAxis?: AxisSpec | { left: AxisSpec; right?: AxisSpec };\n\theight?: number;\n\t/** Optional accessible label override; otherwise composed from series labels. */\n\tariaLabel?: string;\n\t/** Suppress the in-chart Recharts <Legend>. Used by parents (e.g.\n\t * SmallMultiples) that render a shared legend so per-chart legends\n\t * don't crowd out the plot area in small panels. */\n\thideLegend?: boolean;\n\t/** Pin the x-axis to a specific [start, end] millisecond range. When\n\t * set, the axis spans the requested window even if the data is sparse\n\t * inside it — operators expect \"Last 7 days\" to actually show 7 days\n\t * of axis, with the data appearing as a chunk at the right edge. When\n\t * omitted, the axis falls back to dataMin/dataMax of the points. */\n\txDomain?: [number, number];\n\t/** When true, the chart fills its parent's vertical space instead of\n\t * using the fixed `height` prop. Used by the expand-to-fullscreen\n\t * dialog so the chart actually grows to fit; the parent must be a\n\t * flex column with a definite height (e.g. h-[90vh]) for this to\n\t * resolve to a real pixel size. */\n\tfillParent?: boolean;\n}\n\ntype DualAxis = { left: AxisSpec; right?: AxisSpec };\n\nfunction isDualAxis(a: Props['yAxis']): a is DualAxis {\n\treturn !!a && typeof a === 'object' && 'left' in a;\n}\n\n/** Builds a screen-reader-readable summary of the chart contents.\n * Recharts itself emits no a11y semantics — this gives an `<svg>`-equivalent\n * one-shot description so AT users get *something* without a data-table\n * fallback. Better than silent. */\nfunction composeAriaLabel(data: SeriesData): string {\n\tconst seriesNames = data.series.map((s) => s.label);\n\tconst summary = seriesNames.length === 1\n\t\t? `Chart of ${seriesNames[0]}`\n\t\t: `Chart with ${seriesNames.length} series: ${seriesNames.slice(0, 5).join(', ')}${\n\t\t\tseriesNames.length > 5 ? '…' : ''\n\t\t}`;\n\tconst thresholdNote = data.thresholds && data.thresholds.length > 0\n\t\t? `. ${data.thresholds.length} threshold${data.thresholds.length === 1 ? '' : 's'}.`\n\t\t: '';\n\treturn `${summary}${thresholdNote}`;\n}\n\nexport function LineChart(\n\t{ data, theme: _theme, yAxis, height = 240, ariaLabel, hideLegend, xDomain, fillParent }: Props,\n) {\n\tif (data.series.length === 0) {\n\t\treturn (\n\t\t\t<div role=\"status\" aria-live=\"polite\" className=\"text-(--color-text-secondary) text-sm p-4\">\n\t\t\t\tNo data in window\n\t\t\t</div>\n\t\t);\n\t}\n\n\t// Flatten points across series for Recharts' data prop.\n\t// We render one <Line> per series, each with its own `data` prop so axes\n\t// aren't forced to share an X domain per row.\n\tconst isDual = isDualAxis(yAxis);\n\tconst leftAxis = isDual ? yAxis.left : (yAxis as AxisSpec | undefined);\n\tconst rightAxis = isDual ? yAxis.right : undefined;\n\n\tconst colors = ['#58a6ff', '#3fb950', '#f0883e', '#bc8cff', '#f778ba'];\n\n\treturn (\n\t\t<div\n\t\t\trole=\"img\"\n\t\t\taria-label={ariaLabel ?? composeAriaLabel(data)}\n\t\t\tstyle={fillParent\n\t\t\t\t? { width: '100%', height: '100%', minHeight: 0, flex: '1 1 auto', display: 'flex', flexDirection: 'column' }\n\t\t\t\t: { width: '100%', height }}\n\t\t>\n\t\t\t{\n\t\t\t\t/* Inner SVG is decorative once the outer role=img has the\n\t\t\t composed label — Safari/VO otherwise descends and reads\n\t\t\t every <g>/<path> text fragment. */\n\t\t\t}\n\t\t\t<div aria-hidden=\"true\" style={{ width: '100%', height: '100%' }}>\n\t\t\t\t<ResponsiveContainer width=\"100%\" height=\"100%\">\n\t\t\t\t\t<RLineChart margin={{ top: 12, right: 12, bottom: 8, left: 8 }}>\n\t\t\t\t\t\t<CartesianGrid stroke=\"var(--chart-grid)\" strokeDasharray=\"3 3\" />\n\t\t\t\t\t\t<XAxis\n\t\t\t\t\t\t\tdataKey=\"x\"\n\t\t\t\t\t\t\ttype=\"number\"\n\t\t\t\t\t\t\tdomain={xDomain ?? ['dataMin', 'dataMax']}\n\t\t\t\t\t\t\tallowDataOverflow={!!xDomain}\n\t\t\t\t\t\t\tallowDuplicatedCategory={false}\n\t\t\t\t\t\t\ttickFormatter={formatAxisTick}\n\t\t\t\t\t\t\ttick={{ fontSize: 11 }}\n\t\t\t\t\t\t/>\n\t\t\t\t\t\t<YAxis\n\t\t\t\t\t\t\tyAxisId=\"left\"\n\t\t\t\t\t\t\ttickFormatter={(v) => formatValue(v, leftAxis?.formatter, leftAxis?.unit)}\n\t\t\t\t\t\t\tscale={leftAxis?.scale ?? 'linear'}\n\t\t\t\t\t\t\tdomain={leftAxis?.domain as [number | string, number | string] | undefined}\n\t\t\t\t\t\t\ttick={{ fontSize: 11 }}\n\t\t\t\t\t\t\t// Wider than Recharts' ~60px default so e.g. '2400.0 ms'\n\t\t\t\t\t\t\t// or '1.5 GB' doesn't wrap onto two lines per tick.\n\t\t\t\t\t\t\twidth={70}\n\t\t\t\t\t\t/>\n\t\t\t\t\t\t{isDual && rightAxis\n\t\t\t\t\t\t\t? (\n\t\t\t\t\t\t\t\t<YAxis\n\t\t\t\t\t\t\t\t\tyAxisId=\"right\"\n\t\t\t\t\t\t\t\t\torientation=\"right\"\n\t\t\t\t\t\t\t\t\ttickFormatter={(v) => formatValue(v, rightAxis.formatter, rightAxis.unit)}\n\t\t\t\t\t\t\t\t\tscale={rightAxis.scale ?? 'linear'}\n\t\t\t\t\t\t\t\t\tdomain={rightAxis.domain as [number | string, number | string] | undefined}\n\t\t\t\t\t\t\t\t\ttick={{ fontSize: 11 }}\n\t\t\t\t\t\t\t\t\twidth={70}\n\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t)\n\t\t\t\t\t\t\t: null}\n\t\t\t\t\t\t<Tooltip\n\t\t\t\t\t\t\tlabelFormatter={(label) => formatTooltipTime(Number(label))}\n\t\t\t\t\t\t\tformatter={(val, name) => {\n\t\t\t\t\t\t\t\tconst nameStr = String(name);\n\t\t\t\t\t\t\t\tconst series = data.series.find((s) => s.label === nameStr || s.key === nameStr);\n\t\t\t\t\t\t\t\tconst axisSpec = series?.axis === 'right' ? rightAxis : leftAxis;\n\t\t\t\t\t\t\t\treturn [formatValue(Number(val), axisSpec?.formatter, axisSpec?.unit), nameStr];\n\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t\tcontentStyle={tooltipContentStyle}\n\t\t\t\t\t\t\tlabelStyle={tooltipLabelStyle}\n\t\t\t\t\t\t\titemStyle={tooltipItemStyle}\n\t\t\t\t\t\t/>\n\t\t\t\t\t\t{!hideLegend && <Legend />}\n\t\t\t\t\t\t{data.thresholds?.map((t: Threshold, i: number) => {\n\t\t\t\t\t\t\t// Compose a label that carries value + direction so the\n\t\t\t\t\t\t\t// reference line is self-describing instead of relying on\n\t\t\t\t\t\t\t// stroke-color alone (fails WCAG 1.4.1 use-of-color).\n\t\t\t\t\t\t\tconst formatted = formatValue(t.value, leftAxis?.formatter, leftAxis?.unit);\n\t\t\t\t\t\t\tconst directionMark = t.direction === 'above-is-bad' ? '↑ bad' : '↓ bad';\n\t\t\t\t\t\t\tconst labelText = `${t.label} (${formatted}, ${directionMark})`;\n\t\t\t\t\t\t\t// Alternate label position so multiple thresholds (e.g.\n\t\t\t\t\t\t\t// connection's connect+disconnect pair) don't stack on top\n\t\t\t\t\t\t\t// of each other at the same corner.\n\t\t\t\t\t\t\tconst position = i % 2 === 0 ? 'insideTopRight' : 'insideBottomRight';\n\t\t\t\t\t\t\treturn (\n\t\t\t\t\t\t\t\t<ReferenceLine\n\t\t\t\t\t\t\t\t\tkey={`th-${i}`}\n\t\t\t\t\t\t\t\t\tyAxisId=\"left\"\n\t\t\t\t\t\t\t\t\ty={t.value}\n\t\t\t\t\t\t\t\t\tstroke={t.direction === 'above-is-bad' ? 'var(--color-error)' : 'var(--color-success)'}\n\t\t\t\t\t\t\t\t\tstrokeDasharray=\"4 2\"\n\t\t\t\t\t\t\t\t\tlabel={{ value: labelText, position, fontSize: 11 }}\n\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t);\n\t\t\t\t\t\t})}\n\t\t\t\t\t\t{data.series.map((s, idx) => {\n\t\t\t\t\t\t\t// Per-node mode often produces series with 1-2 points\n\t\t\t\t\t\t\t// (sparse data, short window). Recharts won't draw a\n\t\t\t\t\t\t\t// line with a single point, so force a small dot when\n\t\t\t\t\t\t\t// the series is sparse — otherwise the chart looks\n\t\t\t\t\t\t\t// blank even though data is present.\n\t\t\t\t\t\t\tconst showDots = s.points.length <= 5;\n\t\t\t\t\t\t\treturn (\n\t\t\t\t\t\t\t\t<Line\n\t\t\t\t\t\t\t\t\tkey={s.key}\n\t\t\t\t\t\t\t\t\tdata={s.points}\n\t\t\t\t\t\t\t\t\ttype=\"monotone\"\n\t\t\t\t\t\t\t\t\tdataKey=\"y\"\n\t\t\t\t\t\t\t\t\tname={s.label}\n\t\t\t\t\t\t\t\t\tyAxisId={s.axis === 'right' ? 'right' : 'left'}\n\t\t\t\t\t\t\t\t\tstroke={s.color ?? colors[idx % colors.length]}\n\t\t\t\t\t\t\t\t\tstrokeWidth={2}\n\t\t\t\t\t\t\t\t\tstrokeOpacity={s.opacity ?? 1}\n\t\t\t\t\t\t\t\t\tdot={showDots ? { r: 2 } : false}\n\t\t\t\t\t\t\t\t\tconnectNulls={false}\n\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t);\n\t\t\t\t\t\t})}\n\t\t\t\t\t</RLineChart>\n\t\t\t\t</ResponsiveContainer>\n\t\t\t</div>\n\t\t</div>\n\t);\n}\n","import { useMemo } from 'react';\nimport { NodeLegend } from '../charts/NodeLegend.tsx';\nimport { useNodeSelection } from '../hooks/useNodeSelection.ts';\nimport { getNodeColor } from '../lib/nodeColors.ts';\nimport type { AxisSpec, SeriesData } from '../types/analytics.ts';\nimport { LineChart } from './LineChart.tsx';\n\ninterface Panel {\n\ttitle: string;\n\tdata: SeriesData;\n\tyAxis?: AxisSpec | { left: AxisSpec; right?: AxisSpec };\n}\n\ninterface Props {\n\tpanels: Panel[];\n\ttheme: 'light' | 'dark';\n\t/** Minimum width per mini-panel (px). Grid auto-fits. Default 320. */\n\tminPanelWidth?: number;\n\t/** Height per mini-panel (px). Default 240. */\n\tpanelHeight?: number;\n\t/** Pin every mini-panel's x-axis to the same [start, end] window. */\n\txDomain?: [number, number];\n\t/** Fill the parent's vertical space (used by the expand dialog). */\n\tfillParent?: boolean;\n}\n\nfunction extractNode(seriesKey: string): string | null {\n\tconst sep = seriesKey.indexOf('|');\n\treturn sep === -1 ? null : seriesKey.slice(sep + 1);\n}\n\nexport function SmallMultiples(\n\t{ panels, theme, minPanelWidth = 320, panelHeight = 240, xDomain, fillParent }: Props,\n) {\n\t// Union of node ids across all panels' per-node series. Cluster-aggregate\n\t// series (no '|') don't participate in the legend.\n\tconst nodeIds = useMemo(() => {\n\t\tconst set = new Set<string>();\n\t\tfor (const p of panels) {\n\t\t\tfor (const s of p.data.series) {\n\t\t\t\tconst node = extractNode(s.key);\n\t\t\t\tif (node) { set.add(node); }\n\t\t\t}\n\t\t}\n\t\treturn [...set].sort();\n\t}, [panels]);\n\n\tconst { isActive, handleLegendClick } = useNodeSelection(nodeIds);\n\n\tconst filteredPanels = useMemo(() =>\n\t\tpanels.map((p) => ({\n\t\t\t...p,\n\t\t\tdata: {\n\t\t\t\t...p.data,\n\t\t\t\tseries: p.data.series\n\t\t\t\t\t.filter((s) => {\n\t\t\t\t\t\tconst node = extractNode(s.key);\n\t\t\t\t\t\treturn node === null || isActive(node);\n\t\t\t\t\t})\n\t\t\t\t\t.map((s) => {\n\t\t\t\t\t\tconst node = extractNode(s.key);\n\t\t\t\t\t\tif (!node) { return s; }\n\t\t\t\t\t\treturn { ...s, color: s.color ?? getNodeColor(node, nodeIds) };\n\t\t\t\t\t}),\n\t\t\t},\n\t\t})), [panels, isActive, nodeIds]);\n\n\treturn (\n\t\t<div className=\"flex h-full flex-col\">\n\t\t\t<div\n\t\t\t\tclassName=\"flex-1 min-h-0\"\n\t\t\t\tstyle={{\n\t\t\t\t\tdisplay: 'grid',\n\t\t\t\t\tgridTemplateColumns: `repeat(auto-fit, minmax(${minPanelWidth}px, 1fr))`,\n\t\t\t\t\tgap: '1rem',\n\t\t\t\t}}\n\t\t\t>\n\t\t\t\t{filteredPanels.map((panel, idx) => {\n\t\t\t\t\tconst titleId = `sm-panel-${idx}-title`;\n\t\t\t\t\treturn (\n\t\t\t\t\t\t<div key={panel.title}>\n\t\t\t\t\t\t\t<div\n\t\t\t\t\t\t\t\tid={titleId}\n\t\t\t\t\t\t\t\tdata-testid=\"small-multiple-title\"\n\t\t\t\t\t\t\t\tstyle={{ fontSize: 12, fontWeight: 600, marginBottom: 4 }}\n\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t{panel.title}\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t<LineChart\n\t\t\t\t\t\t\t\tdata={panel.data}\n\t\t\t\t\t\t\t\ttheme={theme}\n\t\t\t\t\t\t\t\tyAxis={panel.yAxis}\n\t\t\t\t\t\t\t\txDomain={xDomain}\n\t\t\t\t\t\t\t\theight={panelHeight}\n\t\t\t\t\t\t\t\tfillParent={fillParent}\n\t\t\t\t\t\t\t\tariaLabel={`${panel.title}: chart with ${panel.data.series.length} series`}\n\t\t\t\t\t\t\t\thideLegend\n\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t);\n\t\t\t\t})}\n\t\t\t</div>\n\t\t\t{nodeIds.length > 0 && (\n\t\t\t\t<NodeLegend\n\t\t\t\t\tnodeIds={nodeIds}\n\t\t\t\t\tisActive={isActive}\n\t\t\t\t\tonClickNode={handleLegendClick}\n\t\t\t\t/>\n\t\t\t)}\n\t\t</div>\n\t);\n}\n","/** Sort series by descending magnitude (sum across all points).\n * Largest series first; rendered at bottom of stack by Recharts.\n * Stable sort: ties preserve input order. */\nexport function sortByMagnitude<T extends { points: Array<{ y: number | null }> }>(\n\tseries: readonly T[],\n): T[] {\n\treturn [...series].sort((a, b) => magnitude(b) - magnitude(a));\n}\n\nfunction magnitude(s: { points: Array<{ y: number | null }> }): number {\n\treturn s.points.reduce((sum, p) => sum + (typeof p.y === 'number' ? p.y : 0), 0);\n}\n","import { useMemo } from 'react';\nimport { Area, AreaChart, CartesianGrid, Legend, Line, ResponsiveContainer, Tooltip, XAxis, YAxis } from 'recharts';\nimport { formatAxisTick, formatTooltipTime } from '../lib/time.ts';\nimport type { AxisFormatter, AxisSpec, SeriesData } from '../types/analytics.ts';\nimport { formatValue } from './formatValue.ts';\nimport { sortByMagnitude } from './sortByMagnitude.ts';\nimport { tooltipContentStyle, tooltipLabelStyle } from './tooltipStyle.ts';\n\ninterface Props {\n\tdata: SeriesData;\n\ttheme: 'light' | 'dark';\n\tyAxis?: AxisSpec;\n\theight?: number;\n\t/** Optional accessible label override; otherwise composed from series labels. */\n\tariaLabel?: string;\n\t/** Pin the x-axis to a specific [start, end] millisecond range so the\n\t * axis spans the requested window even when data is sparse. See\n\t * LineChart for the same prop. */\n\txDomain?: [number, number];\n\t/** Fill the parent's vertical space; see LineChart for details. */\n\tfillParent?: boolean;\n\t/** Render the stack as bare lines (no filled area) so each stack\n\t * boundary reads as a distinct line. Useful for panels where bands\n\t * span widely different magnitudes — the filled-area version makes\n\t * the smaller bands hard to see, while the line version preserves\n\t * every series at its actual stacked y-position. */\n\tlineOnly?: boolean;\n}\n\n/** Screen-reader summary; mirrors LineChart.composeAriaLabel. */\nfunction composeAriaLabel(data: SeriesData): string {\n\tconst seriesNames = data.series.map((s) => s.label);\n\tif (seriesNames.length === 0) { return 'Empty stacked area chart'; }\n\treturn `Stacked area chart with ${seriesNames.length} series: ${seriesNames.slice(0, 5).join(', ')}${\n\t\tseriesNames.length > 5 ? '…' : ''\n\t}`;\n}\n\ninterface TooltipPayloadEntry {\n\tdataKey: string;\n\tname: string;\n\tvalue: number | null;\n\tcolor: string;\n}\n\ninterface StackedAreaTooltipProps {\n\tactive?: boolean;\n\tpayload?: readonly TooltipPayloadEntry[];\n\tlabel?: number;\n\tformatter?: AxisFormatter;\n\tunitSuffix?: string;\n}\n\nexport function StackedAreaTooltip({ active, payload, label, formatter, unitSuffix }: StackedAreaTooltipProps) {\n\tif (!active || !payload || payload.length === 0) { return null; }\n\tconst total = payload.reduce((s, p) => s + (typeof p.value === 'number' ? p.value : 0), 0);\n\t// count-si rounds at tick level; use raw 'count' for tooltip total to preserve precision.\n\tconst totalFormatter: AxisFormatter | undefined = formatter === 'count-si' ? 'count' : formatter;\n\treturn (\n\t\t<div style={tooltipContentStyle}>\n\t\t\t<div style={tooltipLabelStyle}>\n\t\t\t\t{label !== undefined ? formatTooltipTime(Number(label)) : ''}\n\t\t\t</div>\n\t\t\t{payload.map((p) => (\n\t\t\t\t<div key={p.dataKey} style={{ display: 'flex', justifyContent: 'space-between', gap: 12 }}>\n\t\t\t\t\t<span style={{ color: p.color }}>{p.name}</span>\n\t\t\t\t\t<span>{formatValue(typeof p.value === 'number' ? p.value : 0, formatter, unitSuffix)}</span>\n\t\t\t\t</div>\n\t\t\t))}\n\t\t\t{payload.length > 1\n\t\t\t\t? (\n\t\t\t\t\t<div\n\t\t\t\t\t\tstyle={{\n\t\t\t\t\t\t\tdisplay: 'flex',\n\t\t\t\t\t\t\tjustifyContent: 'space-between',\n\t\t\t\t\t\t\tgap: 12,\n\t\t\t\t\t\t\tmarginTop: 4,\n\t\t\t\t\t\t\tpaddingTop: 4,\n\t\t\t\t\t\t\tborderTop: '1px solid var(--border)',\n\t\t\t\t\t\t\tfontWeight: 600,\n\t\t\t\t\t\t}}\n\t\t\t\t\t>\n\t\t\t\t\t\t<span>Total</span>\n\t\t\t\t\t\t<span>{formatValue(total, totalFormatter, unitSuffix)}</span>\n\t\t\t\t\t</div>\n\t\t\t\t)\n\t\t\t\t: null}\n\t\t</div>\n\t);\n}\n\nexport function StackedAreaChart(\n\t{ data, theme, yAxis, height = 240, ariaLabel, xDomain, fillParent, lineOnly }: Props,\n) {\n\tconst sortedSeries = useMemo(() => sortByMagnitude(data.series), [data.series]);\n\n\tif (data.series.length === 0) {\n\t\treturn (\n\t\t\t<div role=\"status\" aria-live=\"polite\" className=\"text-(--color-text-secondary) text-sm p-4\">\n\t\t\t\tNo data in window\n\t\t\t</div>\n\t\t);\n\t}\n\n\tconst colors = ['#58a6ff', '#3fb950', '#f0883e', '#bc8cff', '#f778ba', '#79c0ff'];\n\n\t// Merge points by x across series. Series often emit at slightly\n\t// staggered timestamps (e.g. Harper emits per-node records at different\n\t// instants within the period), so a strict equality merge produces rows\n\t// with one populated cell and N-1 nulls — which renders as a sparse,\n\t// mostly-empty stack. Forward-fill carries the last-known value across\n\t// staggered rows so the stack stays continuous.\n\tconst xs = new Set<number>();\n\tfor (const s of data.series) { for (const p of s.points) { xs.add(p.x); } }\n\tif (data.ceiling) { for (const p of data.ceiling.points) { xs.add(p.x); } }\n\n\t// Pre-build x → index lookup per series for O(1) access during forward-fill.\n\tconst seriesPointMaps = data.series.map((s) => {\n\t\tconst m = new Map<number, number | null>();\n\t\tfor (const p of s.points) { m.set(p.x, p.y); }\n\t\treturn m;\n\t});\n\tconst ceilingMap = data.ceiling\n\t\t? new Map<number, number | null>(data.ceiling.points.map((p) => [p.x, p.y]))\n\t\t: null;\n\n\tconst lastSeen: (number | null)[] = data.series.map(() => null);\n\tlet lastCeiling: number | null = null;\n\n\tconst merged: Record<string, number | null>[] = [...xs].sort((a, b) => a - b).map((x) => {\n\t\tconst row: Record<string, number | null> = { x };\n\t\tdata.series.forEach((s, i) => {\n\t\t\tconst m = seriesPointMaps[i];\n\t\t\tif (m.has(x)) { lastSeen[i] = m.get(x) ?? null; }\n\t\t\trow[s.key] = lastSeen[i];\n\t\t});\n\t\tif (ceilingMap && data.ceiling) {\n\t\t\tif (ceilingMap.has(x)) { lastCeiling = ceilingMap.get(x) ?? null; }\n\t\t\trow.__ceiling__ = lastCeiling;\n\t\t}\n\t\treturn row;\n\t});\n\n\tconst resolvedFormatter = yAxis?.formatter;\n\tconst resolvedUnit = yAxis?.unit;\n\tconst fillOpacity = theme === 'dark' ? 0.5 : 0.35;\n\n\treturn (\n\t\t<div\n\t\t\trole=\"img\"\n\t\t\taria-label={ariaLabel ?? composeAriaLabel(data)}\n\t\t\tstyle={fillParent\n\t\t\t\t? { width: '100%', height: '100%', minHeight: 0, flex: '1 1 auto', display: 'flex', flexDirection: 'column' }\n\t\t\t\t: { width: '100%', height }}\n\t\t>\n\t\t\t<div aria-hidden=\"true\" style={{ width: '100%', height: '100%' }}>\n\t\t\t\t<ResponsiveContainer width=\"100%\" height=\"100%\">\n\t\t\t\t\t<AreaChart data={merged}>\n\t\t\t\t\t\t<CartesianGrid stroke=\"var(--chart-grid)\" strokeDasharray=\"3 3\" />\n\t\t\t\t\t\t<XAxis\n\t\t\t\t\t\t\tdataKey=\"x\"\n\t\t\t\t\t\t\ttype=\"number\"\n\t\t\t\t\t\t\tdomain={xDomain ?? ['dataMin', 'dataMax']}\n\t\t\t\t\t\t\tallowDataOverflow={!!xDomain}\n\t\t\t\t\t\t\ttickFormatter={formatAxisTick}\n\t\t\t\t\t\t\ttick={{ fontSize: 11 }}\n\t\t\t\t\t\t/>\n\t\t\t\t\t\t<YAxis\n\t\t\t\t\t\t\ttickFormatter={(v) => formatValue(v, resolvedFormatter, resolvedUnit)}\n\t\t\t\t\t\t\ttick={{ fontSize: 11 }}\n\t\t\t\t\t\t\twidth={70}\n\t\t\t\t\t\t/>\n\t\t\t\t\t\t<Tooltip content={<StackedAreaTooltip formatter={resolvedFormatter} unitSuffix={resolvedUnit} />} />\n\t\t\t\t\t\t<Legend />\n\t\t\t\t\t\t{sortedSeries.map((s, idx) => (\n\t\t\t\t\t\t\t<Area\n\t\t\t\t\t\t\t\tkey={s.key}\n\t\t\t\t\t\t\t\ttype=\"monotone\"\n\t\t\t\t\t\t\t\tdataKey={s.key}\n\t\t\t\t\t\t\t\tname={s.label}\n\t\t\t\t\t\t\t\tstackId=\"1\"\n\t\t\t\t\t\t\t\tstroke={s.color ?? colors[idx % colors.length]}\n\t\t\t\t\t\t\t\tstrokeWidth={lineOnly ? 2 : 1}\n\t\t\t\t\t\t\t\tfill={lineOnly ? 'none' : (s.color ?? colors[idx % colors.length])}\n\t\t\t\t\t\t\t\tfillOpacity={lineOnly ? 0 : fillOpacity}\n\t\t\t\t\t\t\t\tconnectNulls={false}\n\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t))}\n\t\t\t\t\t\t{data.ceiling\n\t\t\t\t\t\t\t? (\n\t\t\t\t\t\t\t\t<Line\n\t\t\t\t\t\t\t\t\ttype=\"monotone\"\n\t\t\t\t\t\t\t\t\tdataKey=\"__ceiling__\"\n\t\t\t\t\t\t\t\t\tname={data.ceiling.label}\n\t\t\t\t\t\t\t\t\tstroke=\"#8b949e\"\n\t\t\t\t\t\t\t\t\tstrokeWidth={2}\n\t\t\t\t\t\t\t\t\tstrokeDasharray=\"6 3\"\n\t\t\t\t\t\t\t\t\tdot={false}\n\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t)\n\t\t\t\t\t\t\t: null}\n\t\t\t\t\t</AreaChart>\n\t\t\t\t</ResponsiveContainer>\n\t\t\t</div>\n\t\t</div>\n\t);\n}\n","// Multi-select chip row for filtering records by a categorical field\n// (`type` for traffic panels, `protocol` for connections, etc.). Default\n// state = all chips active; click solos that chip; Ctrl-click toggles\n// individual chips. Mirrors NodeLegend's interaction model so the panel\n// has a consistent dual-legend pattern (TypeFilterChipRow above / below\n// the chart, NodeLegend at the bottom).\n\nimport { useCallback, useMemo, useState } from 'react';\n\ninterface Props {\n\tvalues: readonly string[];\n\tcolorFor?: (value: string) => string;\n\tariaLabel?: string;\n}\n\nexport interface TypeFilterState {\n\tisActive: (value: string) => boolean;\n\tactiveValues: readonly string[];\n}\n\n/** Hook holding active-set state + the click handler. Component below\n * consumes both. Separating them lets the parent renderer use\n * `isActive` to filter records in the same render pass. */\nexport function useTypeFilter(values: readonly string[]) {\n\tconst [active, setActive] = useState<Set<string> | null>(null);\n\n\tconst isActive = useCallback((v: string) => active === null || active.has(v), [active]);\n\n\tconst handleClick = useCallback((v: string, ctrlKey: boolean) => {\n\t\tsetActive((prev) => {\n\t\t\tif (ctrlKey) {\n\t\t\t\tif (prev === null) { return new Set(values.filter((x) => x !== v)); }\n\t\t\t\tconst next = new Set(prev);\n\t\t\t\tif (next.has(v)) {\n\t\t\t\t\tnext.delete(v);\n\t\t\t\t\tif (next.size === 0) { return null; }\n\t\t\t\t} else {\n\t\t\t\t\tnext.add(v);\n\t\t\t\t\tif (next.size === values.length) { return null; }\n\t\t\t\t}\n\t\t\t\treturn next;\n\t\t\t}\n\t\t\t// Plain click: solo (or reset if already soloed)\n\t\t\tif (prev !== null && prev.size === 1 && prev.has(v)) { return null; }\n\t\t\treturn new Set([v]);\n\t\t});\n\t}, [values]);\n\n\tconst activeValues = useMemo(\n\t\t() => (active === null ? values : values.filter((v) => active.has(v))),\n\t\t[active, values],\n\t);\n\n\treturn { isActive, handleClick, activeValues };\n}\n\ninterface TypeFilterChipRowProps extends Props {\n\tisActive: (v: string) => boolean;\n\tonClick: (v: string, ctrlKey: boolean) => void;\n}\n\nexport function TypeFilterChipRow({\n\tvalues,\n\tisActive,\n\tonClick,\n\tcolorFor,\n\tariaLabel = 'Type filter',\n}: TypeFilterChipRowProps) {\n\tif (values.length === 0) { return null; }\n\treturn (\n\t\t<div\n\t\t\trole=\"group\"\n\t\t\taria-label={ariaLabel}\n\t\t\tclassName=\"flex flex-wrap justify-center gap-x-3 gap-y-1 pt-2 text-[11px]\"\n\t\t>\n\t\t\t{values.map((v) => {\n\t\t\t\tconst active = isActive(v);\n\t\t\t\tconst color = colorFor ? colorFor(v) : 'var(--color-text-secondary)';\n\t\t\t\treturn (\n\t\t\t\t\t<button\n\t\t\t\t\t\tkey={v}\n\t\t\t\t\t\ttype=\"button\"\n\t\t\t\t\t\taria-pressed={active}\n\t\t\t\t\t\tdata-testid=\"type-filter-chip\"\n\t\t\t\t\t\tdata-value={v}\n\t\t\t\t\t\ttitle=\"Click to solo · Ctrl-click to toggle\"\n\t\t\t\t\t\tonClick={(e) => onClick(v, e.ctrlKey || e.metaKey)}\n\t\t\t\t\t\tclassName=\"inline-flex items-center gap-1.5 cursor-pointer border-none bg-transparent p-0\"\n\t\t\t\t\t\tstyle={{ color, opacity: active ? 1 : 0.55 }}\n\t\t\t\t\t>\n\t\t\t\t\t\t<span\n\t\t\t\t\t\t\tclassName=\"inline-block h-[3px] w-3 rounded\"\n\t\t\t\t\t\t\tstyle={{ backgroundColor: color }}\n\t\t\t\t\t\t/>\n\t\t\t\t\t\t<span>{v}</span>\n\t\t\t\t\t</button>\n\t\t\t\t);\n\t\t\t})}\n\t\t</div>\n\t);\n}\n","// Wrapper renderer for type-grouped panels (bytes-sent, bytes-received,\n// mqtt-traffic-sent, mqtt-traffic-received, database-size, connections).\n//\n// Layout (top → bottom):\n// - TypeFilterChipRow: multi-select for typeField values (mqtt /\n// egress / operation / database name / etc.). Click solo, ⌘-click\n// toggle. Chips inherit the same hue as the corresponding stack\n// band so operator can map chip → band visually.\n// - \"Stack by\" segmented control (only when nodes.length > 1):\n// `Type` (default), `Node`, or `Per-node grid` (small multiples).\n// - Chart: stacked area when all selectors are active; bare stacked\n// lines when the operator narrows the selection (no fills swallowing\n// small bands during comparison).\n// - NodeLegend: solo/Ctrl-toggle. Filters which nodes contribute to\n// the stack pre-pipeline.\n\nimport { useMemo, useState } from 'react';\nimport { NodeLegend } from '../charts/NodeLegend.tsx';\nimport { useNodeSelection } from '../hooks/useNodeSelection.ts';\nimport { getTypeColor } from '../lib/colorAllocators/typeColors.ts';\nimport { getNodeColor } from '../lib/nodeColors.ts';\nimport { runPipeline } from '../pipeline/pipeline.ts';\nimport type { AnalyticsDataPoint, MetricSpec, Series, SeriesData, TimeRange } from '../types/analytics.ts';\nimport { SmallMultiples } from './SmallMultiples.tsx';\nimport { StackedAreaChart } from './StackedAreaChart.tsx';\nimport { TypeFilterChipRow, useTypeFilter } from './TypeFilterChipRow.tsx';\n\ninterface Props {\n\t/** Underlying spec (groupBy on `typeField`, primitive `stacked-area`). */\n\tspec: MetricSpec;\n\t/** Field on records that carries the type value (`type` or `protocol`). */\n\ttypeField: string;\n\trecords: AnalyticsDataPoint[];\n\ttimeRange: TimeRange;\n\tnodes: string[];\n\ttheme: 'light' | 'dark';\n\t/** Initial stack-by mode. Defaults to 'type' so the chart matches the\n\t * panel name's \"by type\" framing. The user-facing toggle is rendered\n\t * inside the renderer when nodes.length > 1. */\n\tviewMode?: 'per-node' | 'aggregate';\n\tfillParent?: boolean;\n}\n\ntype StackBy = 'type' | 'node' | 'grid';\n\nexport function TrafficByTypeRenderer({\n\tspec,\n\ttypeField,\n\trecords,\n\ttimeRange,\n\tnodes,\n\ttheme,\n\tviewMode = 'aggregate',\n\tfillParent,\n}: Props) {\n\t// Translate the legacy viewMode prop into the new stackBy state.\n\t// Default 'type' (== 'aggregate'); 'per-node' maps to 'node'.\n\tconst [stackBy, setStackBy] = useState<StackBy>(viewMode === 'per-node' ? 'node' : 'type');\n\n\t// Discover unique type values from records.\n\tconst types = useMemo(() => {\n\t\tconst set = new Set<string>();\n\t\tfor (const r of records) {\n\t\t\tconst v = (r as Record<string, unknown>)[typeField];\n\t\t\tif (typeof v === 'string') { set.add(v); }\n\t\t}\n\t\treturn [...set].sort();\n\t}, [records, typeField]);\n\n\tconst { isActive, handleClick } = useTypeFilter(types);\n\tconst { isActive: isNodeActive, handleLegendClick: handleNodeClick } = useNodeSelection(nodes);\n\n\t// \"All active\" = default state where no chip / node has been soloed.\n\t// Filled bands when summary; bare stacked lines when narrowed.\n\tconst allTypesActive = types.length > 0 && types.every(isActive);\n\tconst allNodesActive = nodes.every((n) => isNodeActive(n));\n\tconst lineOnly = !(allTypesActive && allNodesActive);\n\n\tconst filteredRecords = useMemo(\n\t\t() =>\n\t\t\trecords.filter((r) => {\n\t\t\t\tconst v = (r as Record<string, unknown>)[typeField];\n\t\t\t\tif (typeof v === 'string' && !isActive(v)) { return false; }\n\t\t\t\tif (typeof r.node === 'string' && !isNodeActive(r.node)) { return false; }\n\t\t\t\treturn true;\n\t\t\t}),\n\t\t[records, typeField, isActive, isNodeActive],\n\t);\n\n\t// stackBy === 'node' remaps dimension to 'node'; 'type' keeps the\n\t// spec's natural typeField. 'grid' is handled separately below.\n\tconst runtimeSpec = useMemo<MetricSpec>(() => {\n\t\tif (stackBy !== 'node' || spec.series.kind !== 'groupBy') { return spec; }\n\t\treturn { ...spec, series: { ...spec.series, dimension: 'node' } };\n\t}, [spec, stackBy]);\n\n\tconst data: SeriesData = useMemo(\n\t\t() => runPipeline(runtimeSpec, filteredRecords, timeRange, nodes, { snapToPeriod: true }),\n\t\t[runtimeSpec, filteredRecords, timeRange, nodes],\n\t);\n\n\t// Color each series by its key using the appropriate allocator so the\n\t// same type is the same hue across panels and the chip color matches\n\t// the band color.\n\tconst coloredData: SeriesData = useMemo(() => ({\n\t\t...data,\n\t\tseries: data.series.map((s): Series => ({\n\t\t\t...s,\n\t\t\tcolor: s.color\n\t\t\t\t?? (stackBy === 'node' ? getNodeColor(s.key, nodes) : getTypeColor(s.key, types)),\n\t\t})),\n\t}), [data, stackBy, nodes, types]);\n\n\t// Per-node grid: one mini-chart per active node, each stacked by type.\n\t// Built only when stackBy === 'grid' so we don't run N pipelines for\n\t// every render.\n\tconst gridPanels = useMemo(() => {\n\t\tif (stackBy !== 'grid') { return null; }\n\t\tconst activeNodes = nodes.filter(isNodeActive);\n\t\treturn activeNodes.map((nodeId) => {\n\t\t\tconst nodeRecords = filteredRecords.filter((r) => r.node === nodeId);\n\t\t\tconst seriesData = runPipeline(spec, nodeRecords, timeRange, [nodeId], { snapToPeriod: true });\n\t\t\treturn {\n\t\t\t\ttitle: nodeId,\n\t\t\t\tdata: {\n\t\t\t\t\t...seriesData,\n\t\t\t\t\tseries: seriesData.series.map((s): Series => ({\n\t\t\t\t\t\t...s,\n\t\t\t\t\t\tcolor: s.color ?? getTypeColor(s.key, types),\n\t\t\t\t\t})),\n\t\t\t\t},\n\t\t\t\tyAxis: spec.yAxis,\n\t\t\t};\n\t\t});\n\t}, [stackBy, nodes, isNodeActive, filteredRecords, spec, timeRange, types]);\n\n\tconst showStackByToggle = nodes.length > 1;\n\tconst xDomain: [number, number] = [timeRange.startTime, timeRange.endTime];\n\n\treturn (\n\t\t<div className=\"flex h-full flex-col\">\n\t\t\t<TypeFilterChipRow\n\t\t\t\tvalues={types}\n\t\t\t\tisActive={isActive}\n\t\t\t\tonClick={handleClick}\n\t\t\t\tcolorFor={(v) => getTypeColor(v, types)}\n\t\t\t\tariaLabel={typeField === 'type' ? 'Traffic type' : typeField === 'database' ? 'Database' : 'Protocol'}\n\t\t\t/>\n\t\t\t{showStackByToggle && <StackByToggle stackBy={stackBy} onChange={setStackBy} />}\n\t\t\t<div className=\"min-h-0 flex-1\" style={{ marginTop: 8 }}>\n\t\t\t\t{stackBy === 'grid' && gridPanels\n\t\t\t\t\t? (\n\t\t\t\t\t\t<SmallMultiples\n\t\t\t\t\t\t\tpanels={gridPanels}\n\t\t\t\t\t\t\ttheme={theme}\n\t\t\t\t\t\t\txDomain={xDomain}\n\t\t\t\t\t\t\tfillParent={fillParent}\n\t\t\t\t\t\t/>\n\t\t\t\t\t)\n\t\t\t\t\t: (\n\t\t\t\t\t\t<StackedAreaChart\n\t\t\t\t\t\t\tdata={coloredData}\n\t\t\t\t\t\t\ttheme={theme}\n\t\t\t\t\t\t\tyAxis={spec.yAxis as any}\n\t\t\t\t\t\t\txDomain={xDomain}\n\t\t\t\t\t\t\tfillParent={fillParent}\n\t\t\t\t\t\t\tlineOnly={lineOnly}\n\t\t\t\t\t\t/>\n\t\t\t\t\t)}\n\t\t\t</div>\n\t\t\t{nodes.length > 0 && <NodeLegend nodeIds={nodes} isActive={isNodeActive} onClickNode={handleNodeClick} />}\n\t\t</div>\n\t);\n}\n\ninterface StackByToggleProps {\n\tstackBy: StackBy;\n\tonChange: (s: StackBy) => void;\n}\n\nconst STACK_BY_OPTIONS: ReadonlyArray<{ value: StackBy; label: string }> = [\n\t{ value: 'type', label: 'Type' },\n\t{ value: 'node', label: 'Node' },\n\t{ value: 'grid', label: 'Per-node grid' },\n];\n\nfunction StackByToggle({ stackBy, onChange }: StackByToggleProps) {\n\t// Roving tabindex pattern: only the active radio is in the tab order; arrow\n\t// keys move selection within the group. Matches DimensionChipRow.\n\tconst activeIdx = Math.max(0, STACK_BY_OPTIONS.findIndex((o) => o.value === stackBy));\n\tconst onKeyDown = (e: React.KeyboardEvent<HTMLButtonElement>) => {\n\t\tif (e.key !== 'ArrowRight' && e.key !== 'ArrowLeft' && e.key !== 'ArrowDown' && e.key !== 'ArrowUp') { return; }\n\t\te.preventDefault();\n\t\tconst dir = e.key === 'ArrowRight' || e.key === 'ArrowDown' ? 1 : -1;\n\t\tconst next = (activeIdx + dir + STACK_BY_OPTIONS.length) % STACK_BY_OPTIONS.length;\n\t\tonChange(STACK_BY_OPTIONS[next].value);\n\t};\n\treturn (\n\t\t<div\n\t\t\trole=\"radiogroup\"\n\t\t\taria-label=\"Stack by\"\n\t\t\tclassName=\"flex flex-wrap items-center gap-x-3 gap-y-1 pt-2 text-[11px]\"\n\t\t>\n\t\t\t<span className=\"text-muted-foreground\">Stack by:</span>\n\t\t\t{STACK_BY_OPTIONS.map((opt, idx) => {\n\t\t\t\tconst active = stackBy === opt.value;\n\t\t\t\treturn (\n\t\t\t\t\t<button\n\t\t\t\t\t\tkey={opt.value}\n\t\t\t\t\t\ttype=\"button\"\n\t\t\t\t\t\trole=\"radio\"\n\t\t\t\t\t\taria-checked={active}\n\t\t\t\t\t\ttabIndex={idx === activeIdx ? 0 : -1}\n\t\t\t\t\t\tonKeyDown={onKeyDown}\n\t\t\t\t\t\tdata-testid=\"stack-by-button\"\n\t\t\t\t\t\tdata-value={opt.value}\n\t\t\t\t\t\tonClick={() => onChange(opt.value)}\n\t\t\t\t\t\tclassName=\"inline-flex items-center cursor-pointer border-none bg-transparent p-0 text-(--color-text-secondary)\"\n\t\t\t\t\t\tstyle={{ opacity: active ? 1 : 0.55 }}\n\t\t\t\t\t>\n\t\t\t\t\t\t{opt.label}\n\t\t\t\t\t</button>\n\t\t\t\t);\n\t\t\t})}\n\t\t</div>\n\t);\n}\n","import { TrafficByTypeRenderer } from '../../primitives/TrafficByTypeRenderer.tsx';\nimport type { DerivedMetricSpec, MetricSpec } from '../../types/analytics.ts';\nimport { runPipeline } from '../pipeline.ts';\n\n// Inner spec defaults to per-type stacking. Renderer remaps dimension\n// to 'node' in per-node viewMode so the operator sees cluster total\n// stacked by node — same pattern bytes-received / connections use.\nconst baseSpec: MetricSpec = {\n\ttitle: 'Messages received by type (inner)',\n\tdescription:\n\t\t'Inbound message rate — cluster total across nodes. Internal spec used by mqtt-traffic-received.recompute; not registered.',\n\ttab: 'traffic',\n\tprimaryDimension: 'node',\n\tsubDimension: 'type',\n\tseries: {\n\t\tkind: 'groupBy',\n\t\tdimension: 'type',\n\t\tfield: {\n\t\t\tfield: 'count',\n\t\t\tlabel: 'messages/sec',\n\t\t\ttransform: { kind: 'rate' },\n\t\t},\n\t},\n\ttimestamp: 'time',\n\tbucket: { source: 'period-field', fallbackMs: 60000 },\n\taggregator: { temporal: 'sum', crossNode: 'sum' },\n\tprimitive: 'stacked-area',\n\tyAxis: { unit: ' msg/s', formatter: 'count-si' },\n};\n\nexport const mqttTrafficReceivedDerived: DerivedMetricSpec = {\n\tid: 'mqtt-traffic-received',\n\ttitle: 'Messages received by type',\n\tsubtitle: 'Inbound message rate. Type chips solo / Ctrl-toggle; viewMode flips type/node stack.',\n\ttab: 'traffic',\n\tsourceMetric: 'bytes-received',\n\trecompute: (records, window, nodes, viewMode) => {\n\t\tconst isPerNode = (viewMode ?? 'per-node') === 'per-node';\n\t\tlet spec: MetricSpec = baseSpec;\n\t\tif (isPerNode && baseSpec.series.kind === 'groupBy') {\n\t\t\tspec = { ...baseSpec, series: { ...baseSpec.series, dimension: 'node' } };\n\t\t}\n\t\treturn runPipeline(spec, records, window, nodes, { snapToPeriod: true });\n\t},\n\tRenderer: (props) => <TrafficByTypeRenderer spec={baseSpec} typeField=\"type\" {...props} />,\n\tprimitive: 'stacked-area',\n\tyAxis: { unit: ' msg/s', formatter: 'count-si' },\n};\n","import { TrafficByTypeRenderer } from '../../primitives/TrafficByTypeRenderer.tsx';\nimport type { DerivedMetricSpec, MetricSpec } from '../../types/analytics.ts';\nimport { runPipeline } from '../pipeline.ts';\n\n// Inner spec defaults to per-type stacking. Renderer remaps dimension\n// to 'node' in per-node viewMode so the operator sees cluster total\n// stacked by node — same pattern bytes-sent / connections use.\nconst baseSpec: MetricSpec = {\n\ttitle: 'Messages sent by type (inner)',\n\tdescription:\n\t\t'Outbound message rate — cluster total across nodes. Internal spec used by mqtt-traffic-sent.recompute; not registered.',\n\ttab: 'traffic',\n\tprimaryDimension: 'node',\n\tsubDimension: 'type',\n\tseries: {\n\t\tkind: 'groupBy',\n\t\tdimension: 'type',\n\t\tfield: {\n\t\t\tfield: 'count',\n\t\t\tlabel: 'messages/sec',\n\t\t\ttransform: { kind: 'rate' },\n\t\t},\n\t},\n\ttimestamp: 'time',\n\tbucket: { source: 'period-field', fallbackMs: 60000 },\n\taggregator: { temporal: 'sum', crossNode: 'sum' },\n\tprimitive: 'stacked-area',\n\tyAxis: { unit: ' msg/s', formatter: 'count-si' },\n};\n\nexport const mqttTrafficSentDerived: DerivedMetricSpec = {\n\tid: 'mqtt-traffic-sent',\n\ttitle: 'Messages sent by type',\n\tsubtitle: 'Outbound message rate. Type chips solo / Ctrl-toggle; viewMode flips type/node stack.',\n\ttab: 'traffic',\n\tsourceMetric: 'bytes-sent',\n\trecompute: (records, window, nodes, viewMode) => {\n\t\t// Retained for backward compatibility / direct callers; the\n\t\t// Renderer below is what the dashboard actually uses.\n\t\tconst isPerNode = (viewMode ?? 'per-node') === 'per-node';\n\t\tlet spec: MetricSpec = baseSpec;\n\t\tif (isPerNode && baseSpec.series.kind === 'groupBy') {\n\t\t\tspec = { ...baseSpec, series: { ...baseSpec.series, dimension: 'node' } };\n\t\t}\n\t\treturn runPipeline(spec, records, window, nodes, { snapToPeriod: true });\n\t},\n\tRenderer: (props) => <TrafficByTypeRenderer spec={baseSpec} typeField=\"type\" {...props} />,\n\tprimitive: 'stacked-area',\n\tyAxis: { unit: ' msg/s', formatter: 'count-si' },\n};\n","// Single-select chip row used by the Requests / Database / Health\n// renderers (DimensionSelectorRenderer, ConnectionRenderer, etc.). Visually\n// matches TypeFilterChipRow used by the Traffic tab — minimal inline-flex\n// text + a tiny colored bar — so chip selectors look the same across\n// every analytics dashboard. Behaviorally still a radiogroup: one value\n// active at a time, arrow keys move focus and selection.\n\nimport { type KeyboardEvent, useEffect, useRef } from 'react';\n\ninterface DimensionChipRowProps {\n\t/** Selectable dimension values, in display order. */\n\tdimensionValues: readonly string[];\n\t/** Currently-selected value. */\n\tselected: string;\n\t/** Called on click, Enter/Space, or arrow-key traversal. Per the radiogroup\n\t * pattern, arrow keys move focus AND selection. */\n\tonSelect: (value: string) => void;\n\t/** When provided, renders a non-interactive trailing chip (e.g. 'Other'). */\n\totherKey?: string;\n\t/** Optional palette callback — receives a dimension value and returns a CSS color. */\n\tcolorFor?: (value: string) => string;\n\t/** ARIA label for the radiogroup. */\n\tariaLabel?: string;\n}\n\nconst DEFAULT_COLOR = 'var(--color-text-secondary)';\n\nexport function DimensionChipRow({\n\tdimensionValues,\n\tselected,\n\tonSelect,\n\totherKey,\n\tcolorFor,\n\tariaLabel = 'Dimension selector',\n}: DimensionChipRowProps) {\n\tconst chipRefs = useRef<Array<HTMLButtonElement | null>>([]);\n\n\tuseEffect(() => {\n\t\tchipRefs.current = chipRefs.current.slice(0, dimensionValues.length);\n\t}, [dimensionValues.length]);\n\n\tconst activeIdx = dimensionValues.indexOf(selected);\n\tconst tabbableIdx = activeIdx >= 0 ? activeIdx : 0;\n\n\tfunction handleKeyDown(e: KeyboardEvent<HTMLButtonElement>, idx: number) {\n\t\tif (e.key === 'Enter' || e.key === ' ') {\n\t\t\te.preventDefault();\n\t\t\tonSelect(dimensionValues[idx]);\n\t\t\treturn;\n\t\t}\n\t\tif (\n\t\t\te.key !== 'ArrowLeft'\n\t\t\t&& e.key !== 'ArrowRight'\n\t\t\t&& e.key !== 'ArrowDown'\n\t\t\t&& e.key !== 'ArrowUp'\n\t\t) {\n\t\t\treturn;\n\t\t}\n\t\te.preventDefault();\n\t\tconst n = dimensionValues.length;\n\t\tif (n === 0) { return; }\n\t\tlet next = idx;\n\t\tif (e.key === 'ArrowRight' || e.key === 'ArrowDown') { next = (idx + 1) % n; }\n\t\tif (e.key === 'ArrowLeft' || e.key === 'ArrowUp') { next = (idx - 1 + n) % n; }\n\t\tchipRefs.current[next]?.focus();\n\t\tonSelect(dimensionValues[next]);\n\t}\n\n\tif (dimensionValues.length === 0 && !otherKey) { return null; }\n\n\treturn (\n\t\t<div\n\t\t\trole=\"radiogroup\"\n\t\t\taria-label={ariaLabel}\n\t\t\tdata-testid=\"dimension-chip-row\"\n\t\t\tclassName=\"flex flex-wrap justify-center gap-x-3 gap-y-1 pt-2 text-[11px]\"\n\t\t>\n\t\t\t{dimensionValues.map((value, idx) => {\n\t\t\t\tconst isSelected = value === selected;\n\t\t\t\tconst color = colorFor ? colorFor(value) : DEFAULT_COLOR;\n\t\t\t\treturn (\n\t\t\t\t\t<button\n\t\t\t\t\t\tkey={value}\n\t\t\t\t\t\tref={(el) => {\n\t\t\t\t\t\t\tchipRefs.current[idx] = el;\n\t\t\t\t\t\t}}\n\t\t\t\t\t\ttype=\"button\"\n\t\t\t\t\t\trole=\"radio\"\n\t\t\t\t\t\taria-checked={isSelected}\n\t\t\t\t\t\ttabIndex={idx === tabbableIdx ? 0 : -1}\n\t\t\t\t\t\tdata-testid=\"dimension-chip\"\n\t\t\t\t\t\tdata-value={value}\n\t\t\t\t\t\ttitle=\"Click to select\"\n\t\t\t\t\t\tonKeyDown={(e) => handleKeyDown(e, idx)}\n\t\t\t\t\t\tonClick={() => onSelect(value)}\n\t\t\t\t\t\tclassName=\"inline-flex items-center gap-1.5 cursor-pointer border-none bg-transparent p-0\"\n\t\t\t\t\t\tstyle={{ color, opacity: isSelected ? 1 : 0.55 }}\n\t\t\t\t\t>\n\t\t\t\t\t\t<span\n\t\t\t\t\t\t\tclassName=\"inline-block h-[3px] w-3 rounded\"\n\t\t\t\t\t\t\tstyle={{ backgroundColor: color }}\n\t\t\t\t\t\t/>\n\t\t\t\t\t\t<span>{value}</span>\n\t\t\t\t\t</button>\n\t\t\t\t);\n\t\t\t})}\n\t\t\t{otherKey && (\n\t\t\t\t<span\n\t\t\t\t\tdata-testid=\"dimension-chip\"\n\t\t\t\t\tdata-value={otherKey}\n\t\t\t\t\taria-disabled=\"true\"\n\t\t\t\t\ttitle=\"Aggregate of smaller buckets; not selectable.\"\n\t\t\t\t\tclassName=\"inline-flex items-center gap-1.5 cursor-not-allowed\"\n\t\t\t\t\tstyle={{ color: DEFAULT_COLOR, opacity: 0.4 }}\n\t\t\t\t>\n\t\t\t\t\t<span\n\t\t\t\t\t\tclassName=\"inline-block h-[3px] w-3 rounded\"\n\t\t\t\t\t\tstyle={{ backgroundColor: DEFAULT_COLOR }}\n\t\t\t\t\t/>\n\t\t\t\t\t<span>{otherKey}</span>\n\t\t\t\t</span>\n\t\t\t)}\n\t\t</div>\n\t);\n}\n","// Shared renderer for derived metrics keyed by (path, time) that the\n// operator wants to drill into per-node and filter by path. Used by\n// request-rate (req/s) and error-rate (errored fraction). Each metric\n// supplies a `compute(records, perNode, selectedPath)` callback that\n// emits SeriesData; this component handles all the chip/node UI plus\n// the path-discovery + viewMode threading.\n\nimport { useMemo, useState } from 'react';\nimport { NodeLegend } from '../charts/NodeLegend.tsx';\nimport { useNodeSelection } from '../hooks/useNodeSelection.ts';\nimport { getNodeColor } from '../lib/nodeColors.ts';\nimport type { AnalyticsDataPoint, AxisSpec, SeriesData, Threshold, TimeRange } from '../types/analytics.ts';\nimport { DimensionChipRow } from './DimensionChipRow.tsx';\nimport { LineChart } from './LineChart.tsx';\n\ninterface Props {\n\trecords: AnalyticsDataPoint[];\n\ttimeRange?: TimeRange;\n\tnodes: string[];\n\ttheme: 'light' | 'dark';\n\tviewMode?: 'per-node' | 'aggregate';\n\tyAxis?: AxisSpec | { left: AxisSpec; right?: AxisSpec };\n\tthresholds?: Threshold[];\n\tfillParent?: boolean;\n\t/** Compute the SeriesData for the chosen viewMode + selected path.\n\t * - per-node + path selected: emit one series per node (key is node id)\n\t * - aggregate + path selected: emit one cluster series for that path\n\t * - aggregate + no selection: emit one series per path (cluster aggregate) */\n\tcompute: (\n\t\trecords: AnalyticsDataPoint[],\n\t\toptions: { perNode: boolean; selectedPath: string | null },\n\t) => SeriesData;\n}\n\nfunction shortenNodeLabel(node: string): string {\n\tconst dot = node.indexOf('.');\n\treturn dot === -1 ? node : node.slice(0, dot);\n}\n\nexport function PerPathRateRenderer({\n\trecords,\n\ttimeRange,\n\tnodes,\n\ttheme,\n\tviewMode = 'per-node',\n\tyAxis,\n\tthresholds,\n\tcompute,\n\tfillParent,\n}: Props) {\n\tconst xDomain = timeRange ? [timeRange.startTime, timeRange.endTime] as [number, number] : undefined;\n\tconst perNode = viewMode === 'per-node';\n\n\t// Discover paths from records, ranked by total count so default chip\n\t// selection lands on the highest-traffic path.\n\tconst paths = useMemo(() => {\n\t\tconst totals = new Map<string, number>();\n\t\tfor (const r of records) {\n\t\t\tconst path = (r as Record<string, unknown>).path;\n\t\t\tif (typeof path !== 'string') { continue; }\n\t\t\tconst c = (r as Record<string, unknown>).count;\n\t\t\tconst count = typeof c === 'number' && Number.isFinite(c) ? c : 0;\n\t\t\ttotals.set(path, (totals.get(path) ?? 0) + count);\n\t\t}\n\t\treturn [...totals.entries()].sort((a, b) => b[1] - a[1]).map(([p]) => p);\n\t}, [records]);\n\n\tconst [selected, setSelected] = useState<string>('');\n\tconst effective = paths.includes(selected)\n\t\t? selected\n\t\t// In per-node mode default to the rank-0 path so the operator sees\n\t\t// one path's per-node breakdown immediately. In aggregate mode\n\t\t// default to \"all\" so the operator sees the cluster-by-path stack.\n\t\t: (perNode ? (paths[0] ?? '') : '');\n\n\tconst data = useMemo<SeriesData>(\n\t\t() => compute(records, { perNode, selectedPath: effective || null }),\n\t\t[records, perNode, effective, compute],\n\t);\n\n\tconst { isActive, handleLegendClick } = useNodeSelection(nodes);\n\n\tconst filteredData: SeriesData = useMemo(() => ({\n\t\t...data,\n\t\tthresholds: thresholds ?? data.thresholds,\n\t\tseries: data.series\n\t\t\t.map((s) => {\n\t\t\t\t// In per-node mode the series key IS the node id (no '|' prefix).\n\t\t\t\tif (!perNode) { return s; }\n\t\t\t\treturn { ...s, label: shortenNodeLabel(s.key), color: getNodeColor(s.key, nodes) };\n\t\t\t})\n\t\t\t.filter((s) => !perNode || isActive(s.key)),\n\t}), [data, perNode, nodes, isActive, thresholds]);\n\n\treturn (\n\t\t<div className=\"flex h-full flex-col\">\n\t\t\t{\n\t\t\t\t/* Path selector above the chart (consistent with the rest of the\n\t\t\t dashboards). Node legend stays at the bottom as a color key. */\n\t\t\t}\n\t\t\t{paths.length > 0 && (\n\t\t\t\t<DimensionChipRow\n\t\t\t\t\tdimensionValues={paths}\n\t\t\t\t\tselected={effective}\n\t\t\t\t\tonSelect={setSelected}\n\t\t\t\t\tariaLabel=\"Path\"\n\t\t\t\t/>\n\t\t\t)}\n\t\t\t<div className=\"min-h-0 flex-1\" style={{ marginTop: paths.length > 0 ? 8 : 0 }}>\n\t\t\t\t<LineChart\n\t\t\t\t\tdata={filteredData}\n\t\t\t\t\ttheme={theme}\n\t\t\t\t\tyAxis={yAxis}\n\t\t\t\t\txDomain={xDomain}\n\t\t\t\t\tfillParent={fillParent}\n\t\t\t\t\thideLegend={perNode}\n\t\t\t\t/>\n\t\t\t</div>\n\t\t\t{perNode && nodes.length > 0 && (\n\t\t\t\t<NodeLegend\n\t\t\t\t\tnodeIds={nodes}\n\t\t\t\t\tisActive={isActive}\n\t\t\t\t\tonClickNode={handleLegendClick}\n\t\t\t\t/>\n\t\t\t)}\n\t\t</div>\n\t);\n}\n","import { PerPathRateRenderer } from '../../primitives/PerPathRateRenderer.tsx';\nimport type { AnalyticsDataPoint, DerivedMetricSpec, Series, SeriesData, TimeRange } from '../../types/analytics.ts';\n\n/**\n * Derived: request-rate per (path, time) bucket. Reads raw `count` and `period`\n * columns directly — does NOT delegate to runPipeline / FieldSpec aggregator.\n *\n * Per-bucket rate = Σcount / (period/1000). Across nodes, rates add (sum), so\n * we accumulate `sumCount` over (path, time) across all (method, type, node)\n * records that share the bucket.\n */\nexport function recomputeRequestRate(\n\trecords: AnalyticsDataPoint[],\n\t_window: TimeRange,\n\t_nodes: string[],\n): SeriesData {\n\tconst buckets = new Map<string, Map<number, { sumCount: number; period: number }>>();\n\tfor (const r of records) {\n\t\tconst path = typeof r.path === 'string' ? r.path : null;\n\t\tif (!path) { continue; }\n\t\tif (typeof r.time !== 'number' || !Number.isFinite(r.time)) { continue; }\n\t\tconst count = typeof r.count === 'number' && Number.isFinite(r.count) ? r.count : 0;\n\t\tconst period = typeof r.period === 'number' && r.period > 0 ? r.period : 60000;\n\t\tlet perTime = buckets.get(path);\n\t\tif (!perTime) {\n\t\t\tperTime = new Map();\n\t\t\tbuckets.set(path, perTime);\n\t\t}\n\t\tlet entry = perTime.get(r.time);\n\t\tif (!entry) {\n\t\t\tentry = { sumCount: 0, period };\n\t\t\tperTime.set(r.time, entry);\n\t\t}\n\t\tentry.sumCount += count;\n\t\t// Period assumed identical per-(path, time); first record wins.\n\t}\n\n\tconst series: Series[] = [...buckets.entries()].map(([path, perTime]) => {\n\t\tconst sortedTimes = [...perTime.keys()].sort((a, b) => a - b);\n\t\tconst points = sortedTimes.map((t) => {\n\t\t\tconst e = perTime.get(t)!;\n\t\t\tconst periodSec = e.period / 1000;\n\t\t\tconst y = periodSec > 0 ? e.sumCount / periodSec : null;\n\t\t\treturn { x: t, y, count: e.sumCount };\n\t\t});\n\t\treturn { key: path, label: path, points };\n\t});\n\treturn { series };\n}\n\n/** Per-node + chip-selectable variant. Selected path → one series per\n * node showing that path's rate. No selection → one series per path\n * (cluster aggregate; the original behavior). */\nfunction computeForRenderer(\n\trecords: AnalyticsDataPoint[],\n\t{ perNode, selectedPath }: { perNode: boolean; selectedPath: string | null },\n): SeriesData {\n\tif (perNode && selectedPath) {\n\t\t// Bucket by (node, time) for the selected path only.\n\t\tconst buckets = new Map<string, Map<number, { sumCount: number; period: number }>>();\n\t\tfor (const r of records) {\n\t\t\tif (r.path !== selectedPath) { continue; }\n\t\t\tif (typeof r.time !== 'number' || !Number.isFinite(r.time)) { continue; }\n\t\t\tconst node = typeof r.node === 'string' ? r.node : '_no_node';\n\t\t\tconst count = typeof r.count === 'number' && Number.isFinite(r.count) ? r.count : 0;\n\t\t\tconst period = typeof r.period === 'number' && r.period > 0 ? r.period : 60000;\n\t\t\tlet perTime = buckets.get(node);\n\t\t\tif (!perTime) {\n\t\t\t\tperTime = new Map();\n\t\t\t\tbuckets.set(node, perTime);\n\t\t\t}\n\t\t\tlet entry = perTime.get(r.time);\n\t\t\tif (!entry) {\n\t\t\t\tentry = { sumCount: 0, period };\n\t\t\t\tperTime.set(r.time, entry);\n\t\t\t}\n\t\t\tentry.sumCount += count;\n\t\t}\n\t\tconst series: Series[] = [...buckets.entries()].map(([node, perTime]) => {\n\t\t\tconst sortedTimes = [...perTime.keys()].sort((a, b) => a - b);\n\t\t\tconst points = sortedTimes.map((t) => {\n\t\t\t\tconst e = perTime.get(t)!;\n\t\t\t\tconst periodSec = e.period / 1000;\n\t\t\t\tconst y = periodSec > 0 ? e.sumCount / periodSec : null;\n\t\t\t\treturn { x: t, y, count: e.sumCount };\n\t\t\t});\n\t\t\treturn { key: node, label: node, points };\n\t\t});\n\t\treturn { series };\n\t}\n\n\t// Aggregate path: filter to selected (or use all), produce per-path\n\t// series (the recomputeRequestRate behavior).\n\tconst filtered = selectedPath\n\t\t? records.filter((r) => r.path === selectedPath)\n\t\t: records;\n\treturn recomputeRequestRate(filtered, { startTime: 0, endTime: Number.MAX_SAFE_INTEGER }, []);\n}\n\nexport const requestRateDerived: DerivedMetricSpec = {\n\tid: 'request-rate',\n\ttitle: 'Request rate (req/s)',\n\tsubtitle: 'Per-path req/s. Chip selects path; viewMode flips per-node lines / cluster aggregate.',\n\ttab: 'requests',\n\tsourceMetric: 'duration',\n\trecompute: recomputeRequestRate,\n\tRenderer: ({ records, timeRange, nodes, theme, viewMode }) => (\n\t\t<PerPathRateRenderer\n\t\t\trecords={records}\n\t\t\ttimeRange={timeRange}\n\t\t\tnodes={nodes}\n\t\t\ttheme={theme}\n\t\t\tviewMode={viewMode}\n\t\t\tyAxis={{ unit: '/s', formatter: 'count-si' }}\n\t\t\tcompute={computeForRenderer}\n\t\t/>\n\t),\n\tprimitive: 'line',\n\tyAxis: { unit: '/s', formatter: 'count-si' },\n};\n","import type { AnalyticsDataPoint, DerivedMetricSpec, Series, SeriesData, TimeRange } from '../../types/analytics.ts';\n\n/**\n * Derived metric: per-database transaction-log write rate (bytes/sec).\n *\n * Harper's `database-size` metric carries a `transactionLog` byte counter\n * that is monotonically increasing — it's the cumulative log volume per\n * database. The cumulative number isn't useful on a chart by itself\n * (always rising); the *delta* between consecutive samples per database,\n * normalized by the elapsed time, gives an actionable write-throughput\n * line that mirrors what replication is moving.\n *\n * Implementation:\n * 1. Bucket records by database, then by node (cumulative counters\n * are per-node, so deltas have to be computed within a node).\n * 2. Sort each node's series by time, walk forward computing\n * `(logBytes[i] - logBytes[i-1]) / (time[i] - time[i-1]) * 1000`\n * to get bytes/sec.\n * 3. Sum the resulting per-node rates per `(database, time)` bucket\n * so cluster-wide growth folds into one line per database.\n *\n * Negative or non-finite deltas (counter resets, restarts) are skipped —\n * they'd otherwise put a downward spike on the chart. The first sample\n * per node has no predecessor so it produces no point.\n */\nexport function recomputeTransactionLogGrowth(\n\trecords: AnalyticsDataPoint[],\n\t_window: TimeRange,\n\t_nodes: string[],\n): SeriesData {\n\tconst byDatabase = new Map<string, Map<string, Array<{ time: number; logBytes: number }>>>();\n\tfor (const r of records) {\n\t\tconst database = typeof r.database === 'string' ? r.database : null;\n\t\tif (!database) { continue; }\n\t\tconst time = typeof r.id === 'number'\n\t\t\t? r.id\n\t\t\t: typeof r.time === 'number'\n\t\t\t? r.time\n\t\t\t: null;\n\t\tif (time === null) { continue; }\n\t\tconst node = typeof r.node === 'string' ? r.node : '_no_node';\n\t\tconst logBytes = typeof r.transactionLog === 'number' && Number.isFinite(r.transactionLog)\n\t\t\t? r.transactionLog\n\t\t\t: null;\n\t\tif (logBytes === null) { continue; }\n\t\tlet perNode = byDatabase.get(database);\n\t\tif (!perNode) {\n\t\t\tperNode = new Map();\n\t\t\tbyDatabase.set(database, perNode);\n\t\t}\n\t\tlet samples = perNode.get(node);\n\t\tif (!samples) {\n\t\t\tsamples = [];\n\t\t\tperNode.set(node, samples);\n\t\t}\n\t\tsamples.push({ time, logBytes });\n\t}\n\n\t// Snap the rate's output timestamp to a 60s bucket *only* when rolling per-node\n\t// rates into the cluster sum, so two nodes sampling around the same minute\n\t// boundary (e.g. 1:59:43 + 2:00:03) fold into one point instead of staggering\n\t// across two. Deltas themselves use the raw per-node intervals so accuracy\n\t// is preserved. Sub-bucket sample spacing (test fixtures, in-development\n\t// Harper builds) bypasses snapping so adjacent samples don't collapse to\n\t// the same x and lose their dt.\n\tconst OUTPUT_BUCKET_MS = 60_000;\n\tconst series: Series[] = [];\n\tfor (const [database, perNode] of byDatabase.entries()) {\n\t\tconst ratesByTime = new Map<number, number>();\n\t\tfor (const samples of perNode.values()) {\n\t\t\tsamples.sort((a, b) => a.time - b.time);\n\t\t\tfor (let i = 1; i < samples.length; i++) {\n\t\t\t\tconst prev = samples[i - 1];\n\t\t\t\tconst cur = samples[i];\n\t\t\t\tconst dtMs = cur.time - prev.time;\n\t\t\t\tif (dtMs <= 0) { continue; }\n\t\t\t\tconst dBytes = cur.logBytes - prev.logBytes;\n\t\t\t\tif (!Number.isFinite(dBytes) || dBytes < 0) { continue; }\n\t\t\t\tconst rate = (dBytes * 1000) / dtMs;\n\t\t\t\tif (!Number.isFinite(rate)) { continue; }\n\t\t\t\tconst bucketed = dtMs >= OUTPUT_BUCKET_MS\n\t\t\t\t\t? Math.round(cur.time / OUTPUT_BUCKET_MS) * OUTPUT_BUCKET_MS\n\t\t\t\t\t: cur.time;\n\t\t\t\tratesByTime.set(bucketed, (ratesByTime.get(bucketed) ?? 0) + rate);\n\t\t\t}\n\t\t}\n\t\tconst sortedTimes = [...ratesByTime.keys()].sort((a, b) => a - b);\n\t\tconst points = sortedTimes.map((t) => ({ x: t, y: ratesByTime.get(t)! }));\n\t\tseries.push({ key: database, label: database, points });\n\t}\n\n\treturn { series };\n}\n\nexport const transactionLogGrowthDerived: DerivedMetricSpec = {\n\tid: 'transaction-log-growth',\n\ttitle: 'Transaction log growth',\n\tsubtitle:\n\t\t'Per-database transaction-log write rate (bytes/sec) — derived from `transactionLog` deltas in the database-size response.',\n\ttab: 'storage',\n\tsourceMetric: 'database-size',\n\trecompute: recomputeTransactionLogGrowth,\n\tprimitive: 'line',\n\tyAxis: { unit: '/s', formatter: 'bytes-si' },\n};\n","// Derived metrics recompute from raw source-metric columns. They DO NOT read\n// the source spec's aggregator output; they implement their own Σ-arithmetic\n// to avoid the ratio-of-ratios bug documented in the spec. Step 3 populates\n// this registry with `mqtt-traffic-sent` / `mqtt-traffic-received` (msg/sec\n// views derived from `bytes-sent` / `bytes-received` source records).\n// Step 6A adds `request-rate` (from `duration`) and `error-rate` (from\n// `success`) — the first derived metrics that read raw columns directly\n// instead of delegating to runPipeline.\nimport type { DerivedMetricSpec } from '../../types/analytics.ts';\nimport { errorRateDerived } from './error-rate.tsx';\nimport { mqttTrafficReceivedDerived } from './mqtt-traffic-received.tsx';\nimport { mqttTrafficSentDerived } from './mqtt-traffic-sent.tsx';\nimport { requestRateDerived } from './request-rate.tsx';\nimport { transactionLogGrowthDerived } from './transaction-log-growth.tsx';\n\nexport const derivedRegistry: Record<string, DerivedMetricSpec> = {\n\t'mqtt-traffic-sent': mqttTrafficSentDerived,\n\t'mqtt-traffic-received': mqttTrafficReceivedDerived,\n\t'request-rate': requestRateDerived,\n\t'error-rate': errorRateDerived,\n\t'transaction-log-growth': transactionLogGrowthDerived,\n};\n","import { TrafficByTypeRenderer } from '../primitives/TrafficByTypeRenderer.tsx';\nimport type { AnalyticsDataPoint, MetricSpec, TimeRange } from '../types/analytics.ts';\n\nexport const bytesReceivedSpec: MetricSpec = {\n\ttitle: 'Bytes received by type',\n\tdescription: 'Inbound byte rate (count × mean) — cluster total. Type chips solo / Ctrl-toggle.',\n\ttab: 'traffic',\n\tprimaryDimension: 'node',\n\tsubDimension: 'type',\n\tseries: {\n\t\tkind: 'groupBy',\n\t\tdimension: 'type',\n\t\tfield: {\n\t\t\tfield: {\n\t\t\t\tkind: 'op',\n\t\t\t\top: '*',\n\t\t\t\tleft: { kind: 'ref', field: 'count' },\n\t\t\t\tright: { kind: 'ref', field: 'mean' },\n\t\t\t},\n\t\t\tlabel: 'bytes/sec',\n\t\t\ttransform: { kind: 'rate' },\n\t\t},\n\t},\n\ttimestamp: 'time',\n\tbucket: { source: 'period-field', fallbackMs: 60000 },\n\taggregator: { temporal: 'sum', crossNode: 'sum' },\n\tprimitive: 'stacked-area',\n\tyAxis: { unit: '/s', formatter: 'bytes-si' },\n};\n\ninterface RendererProps {\n\trecords: AnalyticsDataPoint[];\n\ttimeRange: TimeRange;\n\tnodes: string[];\n\ttheme: 'light' | 'dark';\n\tviewMode?: 'per-node' | 'aggregate';\n}\n\nexport function BytesReceivedRenderer(props: RendererProps) {\n\treturn <TrafficByTypeRenderer spec={bytesReceivedSpec} typeField=\"type\" {...props} />;\n}\n","import { TrafficByTypeRenderer } from '../primitives/TrafficByTypeRenderer.tsx';\nimport type { AnalyticsDataPoint, MetricSpec, TimeRange } from '../types/analytics.ts';\n\nexport const bytesSentSpec: MetricSpec = {\n\ttitle: 'Bytes sent by type',\n\tdescription: 'Outbound byte rate (count × mean) — cluster total. Type chips solo / Ctrl-toggle.',\n\ttab: 'traffic',\n\tprimaryDimension: 'node',\n\tsubDimension: 'type',\n\tseries: {\n\t\tkind: 'groupBy',\n\t\tdimension: 'type',\n\t\tfield: {\n\t\t\tfield: {\n\t\t\t\tkind: 'op',\n\t\t\t\top: '*',\n\t\t\t\tleft: { kind: 'ref', field: 'count' },\n\t\t\t\tright: { kind: 'ref', field: 'mean' },\n\t\t\t},\n\t\t\tlabel: 'bytes/sec',\n\t\t\ttransform: { kind: 'rate' },\n\t\t},\n\t},\n\ttimestamp: 'time',\n\tbucket: { source: 'period-field', fallbackMs: 60000 },\n\taggregator: { temporal: 'sum', crossNode: 'sum' },\n\tprimitive: 'stacked-area',\n\tyAxis: { unit: '/s', formatter: 'bytes-si' },\n};\n\ninterface RendererProps {\n\trecords: AnalyticsDataPoint[];\n\ttimeRange: TimeRange;\n\tnodes: string[];\n\ttheme: 'light' | 'dark';\n\tviewMode?: 'per-node' | 'aggregate';\n}\n\nexport function BytesSentRenderer(props: RendererProps) {\n\treturn <TrafficByTypeRenderer spec={bytesSentSpec} typeField=\"type\" {...props} />;\n}\n","// >12-value alternative to DimensionChipRow. Button-triggered popover with a\n// search input + filtered listbox. Mirrors ChipRow's API so callers can swap\n// based on cardinality. Step 6B ships the primitive; first in-tree consumer\n// arrives with db-read/db-write/db-message in a later Step 6 phase.\n\nimport { type KeyboardEvent, useEffect, useId, useRef, useState } from 'react';\n\ninterface DimensionComboboxProps {\n\tdimensionValues: readonly string[];\n\tselected: string;\n\tonSelect: (value: string) => void;\n\totherKey?: string;\n\tcolorFor?: (value: string) => string;\n\tariaLabel?: string;\n}\n\nconst DEFAULT_COLOR = 'var(--color-text-secondary)';\n\nexport function DimensionCombobox({\n\tdimensionValues,\n\tselected,\n\tonSelect,\n\totherKey,\n\tcolorFor,\n\tariaLabel = 'Dimension selector',\n}: DimensionComboboxProps) {\n\tconst [open, setOpen] = useState(false);\n\tconst [query, setQuery] = useState('');\n\tconst [activeIdx, setActiveIdx] = useState(0);\n\tconst listboxId = useId();\n\tconst optionIdPrefix = useId();\n\tconst triggerRef = useRef<HTMLButtonElement | null>(null);\n\tconst searchRef = useRef<HTMLInputElement | null>(null);\n\tconst popoverRef = useRef<HTMLDivElement | null>(null);\n\n\tconst filtered = dimensionValues.filter((v) => v.toLowerCase().includes(query.toLowerCase()));\n\n\tuseEffect(() => {\n\t\tif (open) { searchRef.current?.focus(); }\n\t\telse { setQuery(''); }\n\t}, [open]);\n\n\t// Outside-click / Escape-anywhere dismissal. Without these the popover\n\t// stayed pinned open after clicking elsewhere on the page; clicking the\n\t// trigger itself is allowed to fall through to its own onClick toggle.\n\tuseEffect(() => {\n\t\tif (!open) { return; }\n\t\tconst onPointerDown = (e: PointerEvent) => {\n\t\t\tconst target = e.target as Node | null;\n\t\t\tif (!target) { return; }\n\t\t\tif (popoverRef.current?.contains(target)) { return; }\n\t\t\tif (triggerRef.current?.contains(target)) { return; }\n\t\t\tsetOpen(false);\n\t\t};\n\t\tconst onKeyDown = (e: globalThis.KeyboardEvent) => {\n\t\t\tif (e.key === 'Escape') {\n\t\t\t\tsetOpen(false);\n\t\t\t\ttriggerRef.current?.focus();\n\t\t\t}\n\t\t};\n\t\tdocument.addEventListener('pointerdown', onPointerDown);\n\t\tdocument.addEventListener('keydown', onKeyDown);\n\t\treturn () => {\n\t\t\tdocument.removeEventListener('pointerdown', onPointerDown);\n\t\t\tdocument.removeEventListener('keydown', onKeyDown);\n\t\t};\n\t}, [open]);\n\n\tuseEffect(() => {\n\t\tsetActiveIdx(0);\n\t}, [query]);\n\n\tfunction commit(value: string) {\n\t\tonSelect(value);\n\t\tsetOpen(false);\n\t\ttriggerRef.current?.focus();\n\t}\n\n\tfunction handleSearchKey(e: KeyboardEvent<HTMLInputElement>) {\n\t\tif (e.key === 'Escape') {\n\t\t\te.preventDefault();\n\t\t\tsetOpen(false);\n\t\t\ttriggerRef.current?.focus();\n\t\t\treturn;\n\t\t}\n\t\tif (e.key === 'ArrowDown') {\n\t\t\te.preventDefault();\n\t\t\tsetActiveIdx((i) => Math.min(i + 1, Math.max(0, filtered.length - 1)));\n\t\t\treturn;\n\t\t}\n\t\tif (e.key === 'ArrowUp') {\n\t\t\te.preventDefault();\n\t\t\tsetActiveIdx((i) => Math.max(0, i - 1));\n\t\t\treturn;\n\t\t}\n\t\tif (e.key === 'Enter' && filtered[activeIdx] !== undefined) {\n\t\t\te.preventDefault();\n\t\t\tcommit(filtered[activeIdx]);\n\t\t}\n\t}\n\n\tconst triggerColor = colorFor ? colorFor(selected) : DEFAULT_COLOR;\n\n\treturn (\n\t\t<div className=\"relative pt-3\">\n\t\t\t<button\n\t\t\t\tref={triggerRef}\n\t\t\t\ttype=\"button\"\n\t\t\t\t// WAI-ARIA APG button-pattern combobox: the trigger has\n\t\t\t\t// aria-haspopup='listbox' but is NOT itself role='combobox'.\n\t\t\t\t// The searchbox below is the actual combobox element with\n\t\t\t\t// aria-activedescendant for option highlight tracking.\n\t\t\t\taria-label={ariaLabel}\n\t\t\t\taria-expanded={open}\n\t\t\t\taria-controls={listboxId}\n\t\t\t\taria-haspopup=\"listbox\"\n\t\t\t\tonClick={() => setOpen((v) => !v)}\n\t\t\t\tclassName=\"inline-flex min-h-8 items-center gap-1.5 rounded-full border border-(--color-border) px-2.5 py-1 text-xs text-(--color-text-primary)\"\n\t\t\t>\n\t\t\t\t<span className=\"inline-block h-2 w-2 rounded-full\" style={{ backgroundColor: triggerColor }} />\n\t\t\t\t{selected || '— select —'}\n\t\t\t\t<span aria-hidden>▾</span>\n\t\t\t</button>\n\t\t\t{open && (\n\t\t\t\t<div\n\t\t\t\t\tref={popoverRef}\n\t\t\t\t\tclassName=\"absolute z-10 mt-1 w-72 rounded-md border border-(--color-border) bg-(--color-surface) p-2 shadow-lg\"\n\t\t\t\t>\n\t\t\t\t\t<input\n\t\t\t\t\t\tref={searchRef}\n\t\t\t\t\t\t// WAI-ARIA APG combobox+listbox INPUT pattern: input is\n\t\t\t\t\t\t// the combobox; listbox is its popup; aria-activedescendant\n\t\t\t\t\t\t// announces the highlighted option as arrow keys navigate.\n\t\t\t\t\t\trole=\"combobox\"\n\t\t\t\t\t\ttype=\"text\"\n\t\t\t\t\t\taria-label={`Filter ${ariaLabel}`}\n\t\t\t\t\t\taria-expanded={open}\n\t\t\t\t\t\taria-controls={listboxId}\n\t\t\t\t\t\taria-autocomplete=\"list\"\n\t\t\t\t\t\t// Empty string instead of omitting the attribute so AT\n\t\t\t\t\t\t// caches don't keep a stale id reference between renders.\n\t\t\t\t\t\taria-activedescendant={filtered[activeIdx] !== undefined\n\t\t\t\t\t\t\t? `${optionIdPrefix}-${activeIdx}`\n\t\t\t\t\t\t\t: ''}\n\t\t\t\t\t\tvalue={query}\n\t\t\t\t\t\tonChange={(e) => setQuery(e.target.value)}\n\t\t\t\t\t\tonKeyDown={handleSearchKey}\n\t\t\t\t\t\tplaceholder=\"Filter…\"\n\t\t\t\t\t\tclassName=\"mb-2 w-full rounded border border-(--color-border) px-2 py-1 text-xs\"\n\t\t\t\t\t/>\n\t\t\t\t\t{filtered.length === 0 && (\n\t\t\t\t\t\t<div role=\"status\" aria-live=\"polite\" className=\"px-2 py-1 text-xs text-(--color-text-secondary)\">\n\t\t\t\t\t\t\tNo matches\n\t\t\t\t\t\t</div>\n\t\t\t\t\t)}\n\t\t\t\t\t<ul id={listboxId} role=\"listbox\" className=\"max-h-56 overflow-auto\">\n\t\t\t\t\t\t{filtered.map((value, idx) => {\n\t\t\t\t\t\t\tconst isSelected = value === selected;\n\t\t\t\t\t\t\tconst isActive = idx === activeIdx;\n\t\t\t\t\t\t\tconst color = colorFor ? colorFor(value) : DEFAULT_COLOR;\n\t\t\t\t\t\t\treturn (\n\t\t\t\t\t\t\t\t<li\n\t\t\t\t\t\t\t\t\tkey={value}\n\t\t\t\t\t\t\t\t\tid={`${optionIdPrefix}-${idx}`}\n\t\t\t\t\t\t\t\t\trole=\"option\"\n\t\t\t\t\t\t\t\t\taria-selected={isSelected}\n\t\t\t\t\t\t\t\t\tdata-active={isActive ? 'true' : undefined}\n\t\t\t\t\t\t\t\t\tonMouseDown={(e) => {\n\t\t\t\t\t\t\t\t\t\te.preventDefault();\n\t\t\t\t\t\t\t\t\t\tcommit(value);\n\t\t\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t\t\t\tclassName={`flex cursor-pointer items-center gap-2 rounded px-2 py-1 text-xs ${\n\t\t\t\t\t\t\t\t\t\tisActive ? 'bg-(--color-surface-alt)' : ''\n\t\t\t\t\t\t\t\t\t} ${isSelected ? 'font-semibold' : ''}`}\n\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t<span className=\"inline-block h-2 w-2 rounded-full\" style={{ backgroundColor: color }} />\n\t\t\t\t\t\t\t\t\t<span className=\"truncate\">{value}</span>\n\t\t\t\t\t\t\t\t</li>\n\t\t\t\t\t\t\t);\n\t\t\t\t\t\t})}\n\t\t\t\t\t</ul>\n\t\t\t\t\t{otherKey && (\n\t\t\t\t\t\t<div\n\t\t\t\t\t\t\tclassName=\"mt-2 border-t border-(--color-border) pt-2 text-xs text-(--color-text-secondary)/60\"\n\t\t\t\t\t\t\ttitle=\"Aggregate of smaller buckets; not selectable.\"\n\t\t\t\t\t\t>\n\t\t\t\t\t\t\t{otherKey} (aggregate; not selectable)\n\t\t\t\t\t\t</div>\n\t\t\t\t\t)}\n\t\t\t\t</div>\n\t\t\t)}\n\t\t</div>\n\t);\n}\n","// Shared chip-row + filtered LineChart pattern. Used by duration, success,\n// transfer, response_200, db-{read,write,message}.\n//\n// Two ways to slice the data:\n// 1. Pipeline runs once with `perNode: true` so series come back as\n// one-per-(dim, node). The chip selector still picks one DIMENSION\n// value (path/table/path·method) — filtering keeps every node line\n// for that dimension. Operator immediately sees which node is hot\n// for the selected path/table.\n// 2. When spec.quantileSelector is set, the user can swap the underlying\n// percentile field (p50/p95/p99) via a small button group above the\n// chart. The pipeline re-runs with the chosen field substituted.\n\nimport { useMemo, useState } from 'react';\nimport { NodeLegend } from '../charts/NodeLegend.tsx';\nimport { useNodeSelection } from '../hooks/useNodeSelection.ts';\nimport { getNodeColor } from '../lib/nodeColors.ts';\nimport { runPipeline } from '../pipeline/pipeline.ts';\nimport type { AnalyticsDataPoint, MetricSpec, SeriesData, TimeRange } from '../types/analytics.ts';\nimport { DimensionChipRow } from './DimensionChipRow.tsx';\nimport { DimensionCombobox } from './DimensionCombobox.tsx';\nimport { LineChart } from './LineChart.tsx';\n\nconst OTHER_KEY = 'Other';\nconst CHIP_LIMIT = 12;\n\ninterface Props {\n\tspec: MetricSpec;\n\trecords: AnalyticsDataPoint[];\n\ttimeRange: TimeRange;\n\tnodes: string[];\n\ttheme: 'light' | 'dark';\n\tariaLabel?: string;\n\t/** 'per-node' (default) breaks each chip-selected dimension into one\n\t * line per node; 'aggregate' folds nodes into one cluster series per\n\t * dim. */\n\tviewMode?: 'per-node' | 'aggregate';\n\t/** When true, the chart inside this renderer fills its parent's\n\t * vertical space — used by the expand-to-fullscreen dialog. */\n\tfillParent?: boolean;\n}\n\n/** Last segment of an FQDN as a stable short label.\n * e.g. 'xb6-us-west-1.prod.ibm.harperfabric.com' → 'xb6-us-west-1'.\n * Falls back to the full string if there's no dot. */\nfunction shortenNodeLabel(node: string): string {\n\tconst dot = node.indexOf('.');\n\treturn dot === -1 ? node : node.slice(0, dot);\n}\n\nexport function DimensionSelectorRenderer({\n\tspec,\n\trecords,\n\ttimeRange,\n\tnodes,\n\ttheme,\n\tariaLabel = 'Dimension',\n\tviewMode = 'per-node',\n\tfillParent,\n}: Props) {\n\tconst perNode = viewMode === 'per-node';\n\t// ── Quantile selector state (when spec opts in) ─────────────────────\n\tconst quantileFields = spec.quantileSelector?.fields;\n\tconst [quantile, setQuantile] = useState<string>(\n\t\tspec.quantileSelector?.default ?? '',\n\t);\n\tconst effectiveQuantile = quantileFields?.some((q) => q.field === quantile)\n\t\t? quantile\n\t\t: (spec.quantileSelector?.default ?? '');\n\n\t// Build the runtime spec — substitute the chosen percentile field if a\n\t// quantile selector is active. groupBy specs only.\n\tconst runtimeSpec = useMemo<MetricSpec>(() => {\n\t\tif (!quantileFields || effectiveQuantile === '' || spec.series.kind !== 'groupBy') { return spec; }\n\t\tconst chosen = quantileFields.find((q) => q.field === effectiveQuantile);\n\t\tif (!chosen) { return spec; }\n\t\treturn {\n\t\t\t...spec,\n\t\t\tseries: {\n\t\t\t\t...spec.series,\n\t\t\t\tfield: { ...spec.series.field, field: chosen.field, label: chosen.label },\n\t\t\t},\n\t\t};\n\t}, [spec, quantileFields, effectiveQuantile]);\n\n\tconst fullData = useMemo<SeriesData>(\n\t\t() => runPipeline(runtimeSpec, records, timeRange, nodes, { perNode, snapToPeriod: true }),\n\t\t[runtimeSpec, records, timeRange, nodes, perNode],\n\t);\n\n\t// In perNode mode series keys are `${dim}|${node}`. Build the dimension\n\t// list (the chip-row values) by collecting the prefix part of each key,\n\t// excluding the special OTHER aggregate.\n\tconst dimValues = useMemo(() => {\n\t\tconst seen = new Set<string>();\n\t\tfor (const s of fullData.series) {\n\t\t\tif (s.key === OTHER_KEY) { continue; }\n\t\t\tconst sep = s.key.indexOf('|');\n\t\t\tseen.add(sep === -1 ? s.key : s.key.slice(0, sep));\n\t\t}\n\t\treturn [...seen];\n\t}, [fullData.series]);\n\n\tconst hasOther = fullData.series.some((s) => s.key === OTHER_KEY);\n\tconst [selectedDim, setSelectedDim] = useState<string>(() => dimValues[0] ?? '');\n\tconst effectiveDim = dimValues.includes(selectedDim) ? selectedDim : (dimValues[0] ?? '');\n\n\t// Per-node legend filter — shared across this panel's lines so the\n\t// per-Recharts <Legend> can be hidden and the chart area gets full\n\t// vertical real estate.\n\tconst { isActive, handleLegendClick } = useNodeSelection(nodes);\n\n\t// Filter to (dim|*) prefix; apply per-node coloring so the same node\n\t// keeps the same hue across panels. Then filter by the active-node set.\n\tconst filteredData: SeriesData = useMemo(() => {\n\t\tconst prefix = `${effectiveDim}|`;\n\t\tconst filtered = fullData.series\n\t\t\t.filter((s) => s.key === effectiveDim || s.key.startsWith(prefix))\n\t\t\t.map((s) => {\n\t\t\t\tconst sep = s.key.indexOf('|');\n\t\t\t\tconst node = sep === -1 ? '' : s.key.slice(sep + 1);\n\t\t\t\tif (!node) { return s; }\n\t\t\t\treturn {\n\t\t\t\t\t...s,\n\t\t\t\t\tlabel: shortenNodeLabel(node),\n\t\t\t\t\tcolor: getNodeColor(node, nodes),\n\t\t\t\t};\n\t\t\t})\n\t\t\t.filter((s) => {\n\t\t\t\tconst sep = s.key.indexOf('|');\n\t\t\t\tconst node = sep === -1 ? '' : s.key.slice(sep + 1);\n\t\t\t\treturn node === '' || isActive(node);\n\t\t\t});\n\t\treturn { ...fullData, series: filtered };\n\t}, [fullData, effectiveDim, nodes, isActive]);\n\n\treturn (\n\t\t<div className=\"flex h-full flex-col\">\n\t\t\t{\n\t\t\t\t/* Selectors live above the chart (consistent with TrafficByTypeRenderer):\n\t\t\t quantile (when the spec exposes one) on top, then the dimension\n\t\t\t selector (chip row when ≤ CHIP_LIMIT values, combobox above it).\n\t\t\t The chart fills the remaining space below; the per-node legend\n\t\t\t stays at the bottom because it's a color key, not a selector. */\n\t\t\t}\n\t\t\t{spec.quantileSelector && quantileFields && quantileFields.length > 1 && (\n\t\t\t\t<div\n\t\t\t\t\trole=\"radiogroup\"\n\t\t\t\t\taria-label=\"Quantile\"\n\t\t\t\t\tclassName=\"flex flex-wrap justify-center gap-x-3 gap-y-1 pt-2 text-[11px]\"\n\t\t\t\t>\n\t\t\t\t\t{quantileFields.map((q) => {\n\t\t\t\t\t\tconst active = q.field === effectiveQuantile;\n\t\t\t\t\t\treturn (\n\t\t\t\t\t\t\t<button\n\t\t\t\t\t\t\t\tkey={q.field}\n\t\t\t\t\t\t\t\ttype=\"button\"\n\t\t\t\t\t\t\t\trole=\"radio\"\n\t\t\t\t\t\t\t\taria-checked={active}\n\t\t\t\t\t\t\t\tdata-testid=\"quantile-button\"\n\t\t\t\t\t\t\t\tdata-value={q.field}\n\t\t\t\t\t\t\t\tonClick={() => setQuantile(q.field)}\n\t\t\t\t\t\t\t\tclassName=\"inline-flex items-center cursor-pointer border-none bg-transparent p-0 text-(--color-text-secondary)\"\n\t\t\t\t\t\t\t\tstyle={{ opacity: active ? 1 : 0.3 }}\n\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t{q.label}\n\t\t\t\t\t\t\t</button>\n\t\t\t\t\t\t);\n\t\t\t\t\t})}\n\t\t\t\t</div>\n\t\t\t)}\n\t\t\t{dimValues.length > CHIP_LIMIT\n\t\t\t\t? (\n\t\t\t\t\t<DimensionCombobox\n\t\t\t\t\t\tdimensionValues={dimValues}\n\t\t\t\t\t\tselected={effectiveDim}\n\t\t\t\t\t\tonSelect={setSelectedDim}\n\t\t\t\t\t\totherKey={hasOther ? OTHER_KEY : undefined}\n\t\t\t\t\t\tariaLabel={ariaLabel}\n\t\t\t\t\t/>\n\t\t\t\t)\n\t\t\t\t: (\n\t\t\t\t\t<DimensionChipRow\n\t\t\t\t\t\tdimensionValues={dimValues}\n\t\t\t\t\t\tselected={effectiveDim}\n\t\t\t\t\t\tonSelect={setSelectedDim}\n\t\t\t\t\t\totherKey={hasOther ? OTHER_KEY : undefined}\n\t\t\t\t\t\tariaLabel={ariaLabel}\n\t\t\t\t\t/>\n\t\t\t\t)}\n\t\t\t<div className=\"min-h-0 flex-1\" style={{ marginTop: 8 }}>\n\t\t\t\t<LineChart\n\t\t\t\t\tdata={filteredData}\n\t\t\t\t\ttheme={theme}\n\t\t\t\t\tyAxis={spec.yAxis}\n\t\t\t\t\txDomain={[timeRange.startTime, timeRange.endTime]}\n\t\t\t\t\tfillParent={fillParent}\n\t\t\t\t\thideLegend={perNode}\n\t\t\t\t/>\n\t\t\t</div>\n\t\t\t{perNode && nodes.length > 0 && (\n\t\t\t\t<NodeLegend\n\t\t\t\t\tnodeIds={nodes}\n\t\t\t\t\tisActive={isActive}\n\t\t\t\t\tonClickNode={handleLegendClick}\n\t\t\t\t/>\n\t\t\t)}\n\t\t</div>\n\t);\n}\n","import { DimensionSelectorRenderer } from '../primitives/DimensionSelectorRenderer.tsx';\nimport type { AnalyticsDataPoint, MetricSpec, TimeRange } from '../types/analytics.ts';\n\n// Schema (per Harper get_analytics metric: 'cache-hit'):\n// { time, node, path, period, count, total, ratio }\n//\n// `ratio` is precomputed by Harper as `total / count` (hits / lookups). We\n// plot it directly with a count-weighted-mean cross-bucket so paths with\n// many lookups dominate the cluster line — a path with one lookup and a\n// 100% hit shouldn't drag the average up.\nexport const cacheHitSpec: MetricSpec = {\n\ttitle: 'Cache hit rate',\n\tdescription: 'Per-path cache-hit ratio (count-weighted-mean) — top 10 paths + Other.',\n\ttab: 'requests',\n\tprimaryDimension: 'path',\n\tseries: {\n\t\tkind: 'groupBy',\n\t\tdimension: 'path',\n\t\tfield: { field: 'ratio', label: 'hit ratio' },\n\t\ttopN: 10,\n\t\totherBucket: true,\n\t},\n\ttimestamp: 'time',\n\tbucket: { source: 'period-field', fallbackMs: 60000 },\n\taggregator: { temporal: 'count-weighted-mean', crossNode: 'count-weighted-mean' },\n\t// Same gating as duration / transfer — sample-thin paths grey out\n\t// rather than dragging the chart with one-off readings.\n\tconfidence: { field: 'count', greyBelow: 40, suppressBelow: 100 },\n\tprimitive: 'line',\n\tyAxis: { unit: '', formatter: 'percent', domain: [0, 1] },\n};\n\ninterface RendererProps {\n\trecords: AnalyticsDataPoint[];\n\ttimeRange: TimeRange;\n\tnodes: string[];\n\ttheme: 'light' | 'dark';\n\tviewMode?: 'per-node' | 'aggregate';\n}\n\nexport function CacheHitRenderer(props: RendererProps) {\n\treturn <DimensionSelectorRenderer spec={cacheHitSpec} {...props} ariaLabel=\"Path\" />;\n}\n","// Single source of truth for the percentile field set Harper exposes on\n// quantile-bearing metrics (duration, transfer, db-*, cpu-usage, bytes-*,\n// replication-latency). All 9 percentiles are surfaced to operators so\n// they can inspect the full distribution shape from p1 (best case) to\n// p999 (worst tail). Default selection is p95 — the historical SLO axis.\n\nexport interface QuantileField {\n\t/** Record column to read. Harper uses 'median' for p50. */\n\tfield: 'p1' | 'p10' | 'p25' | 'median' | 'p75' | 'p90' | 'p95' | 'p99' | 'p999';\n\t/** Operator-facing label. */\n\tlabel: string;\n}\n\nexport const QUANTILE_FIELDS: ReadonlyArray<QuantileField> = [\n\t{ field: 'p1', label: 'p1' },\n\t{ field: 'p10', label: 'p10' },\n\t{ field: 'p25', label: 'p25' },\n\t{ field: 'median', label: 'p50' },\n\t{ field: 'p75', label: 'p75' },\n\t{ field: 'p90', label: 'p90' },\n\t{ field: 'p95', label: 'p95' },\n\t{ field: 'p99', label: 'p99' },\n\t{ field: 'p999', label: 'p999' },\n];\n\nexport const QUANTILE_DEFAULT: QuantileField['field'] = 'p95';\n","import { DimensionSelectorRenderer } from '../primitives/DimensionSelectorRenderer.tsx';\nimport type { AnalyticsDataPoint, MetricSpec, TimeRange } from '../types/analytics.ts';\nimport { QUANTILE_DEFAULT, QUANTILE_FIELDS } from './quantileFields.ts';\n\n// Schema (per Harper get_analytics metric: 'cache-resolution'):\n// { time, node, path, method, type, period, count, mean,\n// p1, p10, p25, median, p75, p90, p95, p99, p999 }\n//\n// Time-to-resolve a cache miss, in milliseconds. Same per-path latency\n// distribution shape as `duration` and `transfer` so it shares their\n// renderer pattern: line chart with a path chip selector + a quantile\n// selector (p1…p999, default p95).\nexport const cacheResolutionSpec: MetricSpec = {\n\ttitle: 'Cache miss resolution (p95)',\n\tdescription: 'Per-path time-to-resolve a cache miss (count-weighted-mean) — top 10 paths + Other.',\n\ttab: 'requests',\n\tprimaryDimension: 'path',\n\tsubDimension: 'method',\n\tseries: {\n\t\tkind: 'groupBy',\n\t\tdimension: 'path',\n\t\tfield: { field: 'p95', label: 'p95 resolution (ms)' },\n\t\ttopN: 10,\n\t\totherBucket: true,\n\t},\n\ttimestamp: 'time',\n\tbucket: { source: 'period-field', fallbackMs: 60000 },\n\taggregator: { temporal: 'count-weighted-mean', crossNode: 'count-weighted-mean' },\n\tconfidence: { field: 'count', greyBelow: 40, suppressBelow: 100 },\n\tprimitive: 'line',\n\tyAxis: { unit: '', formatter: 'ms' },\n\tquantileSelector: { fields: QUANTILE_FIELDS, default: QUANTILE_DEFAULT },\n};\n\ninterface RendererProps {\n\trecords: AnalyticsDataPoint[];\n\ttimeRange: TimeRange;\n\tnodes: string[];\n\ttheme: 'light' | 'dark';\n\tviewMode?: 'per-node' | 'aggregate';\n}\n\nexport function CacheResolutionRenderer(props: RendererProps) {\n\treturn <DimensionSelectorRenderer spec={cacheResolutionSpec} {...props} ariaLabel=\"Path\" />;\n}\n","import { useMemo, useState } from 'react';\nimport { NodeLegend } from '../charts/NodeLegend.tsx';\nimport { useNodeSelection } from '../hooks/useNodeSelection.ts';\nimport { getNodeColor } from '../lib/nodeColors.ts';\nimport { DimensionChipRow } from '../primitives/DimensionChipRow.tsx';\nimport { LineChart } from '../primitives/LineChart.tsx';\nimport type { AnalyticsDataPoint, MetricSpec, SeriesData, Threshold, TimeRange } from '../types/analytics.ts';\nimport { runPipeline } from './pipeline.ts';\n\nconst COMPOSITE_FIELD = 'pathMethod';\nconst SEPARATOR = ' · ';\n\nexport const connectionSpec: MetricSpec = {\n\ttitle: 'Connection success ratio',\n\tdescription:\n\t\t'Per-(path, method) connection ratio (count-weighted-mean). MQTT thresholds: connect ≥0.99, disconnect ≥0.2.',\n\ttab: 'traffic',\n\tprimaryDimension: 'path',\n\tsubDimension: 'method',\n\tseries: {\n\t\tkind: 'groupBy',\n\t\tdimension: COMPOSITE_FIELD,\n\t\tfield: { field: 'ratio', label: 'success ratio', transform: { kind: 'ratio' } },\n\t},\n\ttimestamp: 'time',\n\tbucket: { source: 'period-field', fallbackMs: 60000 },\n\taggregator: { temporal: 'count-weighted-mean', crossNode: 'count-weighted-mean' },\n\tconfidence: { field: 'count', greyBelow: 100, suppressBelow: 500 },\n\tprimitive: 'line',\n\tyAxis: { unit: '', formatter: 'percent' },\n\tthresholds: [\n\t\t{\n\t\t\tvalue: 0.99,\n\t\t\tlabel: 'connect',\n\t\t\tdirection: 'below-is-bad',\n\t\t\tminCount: 1000,\n\t\t\tscope: { path: 'mqtt', method: 'connect' },\n\t\t},\n\t\t{\n\t\t\tvalue: 0.20,\n\t\t\tlabel: 'disconnect',\n\t\t\tdirection: 'below-is-bad',\n\t\t\tminCount: 500,\n\t\t\tscope: { path: 'mqtt', method: 'disconnect' },\n\t\t},\n\t],\n};\n\ninterface RendererProps {\n\trecords: AnalyticsDataPoint[];\n\ttimeRange: TimeRange;\n\tnodes: string[];\n\ttheme: 'light' | 'dark';\n\tviewMode?: 'per-node' | 'aggregate';\n\tfillParent?: boolean;\n}\n\nfunction compositeKey(path: unknown, method: unknown): string | null {\n\tif (typeof path !== 'string' && typeof path !== 'number') { return null; }\n\tif (typeof method !== 'string' && typeof method !== 'number') { return null; }\n\treturn `${path}${SEPARATOR}${method}`;\n}\n\nfunction preprocess(records: AnalyticsDataPoint[]): AnalyticsDataPoint[] {\n\tconst out: AnalyticsDataPoint[] = [];\n\tfor (const r of records) {\n\t\tconst path = (r as any).path;\n\t\tconst method = (r as any).method;\n\t\tconst key = compositeKey(path, method);\n\t\tif (key === null) { continue; }\n\t\tconst total = (r as any).total;\n\t\tconst count = (r as any).count;\n\t\tconst nullGap = total === 0 && typeof count === 'number' && count > 0;\n\t\tout.push({\n\t\t\t...r,\n\t\t\t[COMPOSITE_FIELD]: key,\n\t\t\tratio: nullGap ? null : (r as any).ratio,\n\t\t} as any);\n\t}\n\treturn out;\n}\n\nfunction shortenNodeLabel(node: string): string {\n\tconst dot = node.indexOf('.');\n\treturn dot === -1 ? node : node.slice(0, dot);\n}\n\nexport function ConnectionRenderer(\n\t{ records, timeRange, nodes, theme, viewMode = 'per-node', fillParent }: RendererProps,\n) {\n\tconst perNode = viewMode === 'per-node';\n\tconst processed = useMemo(() => preprocess(records), [records]);\n\n\tconst fullData = useMemo<SeriesData>(\n\t\t() => runPipeline(connectionSpec, processed, timeRange, nodes, { perNode, snapToPeriod: true }),\n\t\t[processed, timeRange, nodes, perNode],\n\t);\n\n\t// Per-node series keys are `${pathMethod}|${node}`. The chip selector\n\t// picks one composite (pathMethod) value; filter by prefix.\n\tconst selectable = useMemo(() => {\n\t\tconst seen = new Set<string>();\n\t\tfor (const s of fullData.series) {\n\t\t\tconst sep = s.key.indexOf('|');\n\t\t\tseen.add(sep === -1 ? s.key : s.key.slice(0, sep));\n\t\t}\n\t\treturn [...seen];\n\t}, [fullData.series]);\n\tconst [selected, setSelected] = useState<string>(() => selectable[0] ?? '');\n\tconst effectiveSelected = selectable.includes(selected) ? selected : (selectable[0] ?? '');\n\n\tconst selectedDims = useMemo(() => {\n\t\tif (!effectiveSelected) { return null; }\n\t\tconst [path, method] = effectiveSelected.split(SEPARATOR);\n\t\treturn { path, method } as Record<string, string>;\n\t}, [effectiveSelected]);\n\n\tconst { isActive, handleLegendClick } = useNodeSelection(nodes);\n\n\tconst filteredData: SeriesData = useMemo(() => {\n\t\tfunction thresholdMatches(t: Threshold): boolean {\n\t\t\tif (!selectedDims) { return false; }\n\t\t\tif (!t.scope) { return true; }\n\t\t\tfor (const [dim, want] of Object.entries(t.scope)) {\n\t\t\t\tif (selectedDims[dim] !== want) { return false; }\n\t\t\t}\n\t\t\treturn true;\n\t\t}\n\t\tconst prefix = `${effectiveSelected}|`;\n\t\tconst series = fullData.series\n\t\t\t.filter((s) => s.key === effectiveSelected || s.key.startsWith(prefix))\n\t\t\t.map((s) => {\n\t\t\t\tconst sep = s.key.indexOf('|');\n\t\t\t\tconst node = sep === -1 ? '' : s.key.slice(sep + 1);\n\t\t\t\tif (!node) { return s; }\n\t\t\t\treturn {\n\t\t\t\t\t...s,\n\t\t\t\t\tlabel: shortenNodeLabel(node),\n\t\t\t\t\tcolor: getNodeColor(node, nodes),\n\t\t\t\t};\n\t\t\t})\n\t\t\t.filter((s) => {\n\t\t\t\tconst sep = s.key.indexOf('|');\n\t\t\t\tconst node = sep === -1 ? '' : s.key.slice(sep + 1);\n\t\t\t\treturn node === '' || isActive(node);\n\t\t\t});\n\t\treturn {\n\t\t\t...fullData,\n\t\t\tseries,\n\t\t\tthresholds: (fullData.thresholds ?? []).filter(thresholdMatches),\n\t\t};\n\t}, [fullData, effectiveSelected, selectedDims, nodes, isActive]);\n\n\treturn (\n\t\t<div className=\"flex h-full flex-col\">\n\t\t\t<DimensionChipRow\n\t\t\t\tdimensionValues={selectable}\n\t\t\t\tselected={effectiveSelected}\n\t\t\t\tonSelect={setSelected}\n\t\t\t\tariaLabel=\"Path · method\"\n\t\t\t/>\n\t\t\t<div className=\"min-h-0 flex-1\" style={{ marginTop: 8 }}>\n\t\t\t\t<LineChart\n\t\t\t\t\tdata={filteredData}\n\t\t\t\t\ttheme={theme}\n\t\t\t\t\tyAxis={connectionSpec.yAxis}\n\t\t\t\t\txDomain={[timeRange.startTime, timeRange.endTime]}\n\t\t\t\t\tfillParent={fillParent}\n\t\t\t\t\thideLegend={perNode}\n\t\t\t\t/>\n\t\t\t</div>\n\t\t\t{perNode && nodes.length > 0 && (\n\t\t\t\t<NodeLegend\n\t\t\t\t\tnodeIds={nodes}\n\t\t\t\t\tisActive={isActive}\n\t\t\t\t\tonClickNode={handleLegendClick}\n\t\t\t\t/>\n\t\t\t)}\n\t\t</div>\n\t);\n}\n","import { TrafficByTypeRenderer } from '../primitives/TrafficByTypeRenderer.tsx';\nimport type { AnalyticsDataPoint, MetricSpec, TimeRange } from '../types/analytics.ts';\n\n// Schema note: this Harper build splits \"active sessions\" across two\n// metrics — `mqtt-connections` and `ws-connections` — each carrying a\n// `connections` field with the active-session snapshot. The unified\n// `connections` metric on this build is event-based (connect/disconnect)\n// not snapshot-based, so it isn't a substitute. The dashboard panel\n// (rendered by ConnectionsPanel in TrafficTab) fetches both metrics,\n// tags each row with a synthesized `type` field, and feeds the merged\n// stream into the renderer below.\nexport const connectionsSpec: MetricSpec = {\n\ttitle: 'Connections',\n\tdescription: 'Active sessions by type — chips solo / Ctrl-toggle. viewMode flips type/node stack.',\n\ttab: 'traffic',\n\tprimaryDimension: 'node',\n\tsubDimension: 'type',\n\tseries: {\n\t\tkind: 'groupBy',\n\t\tdimension: 'type',\n\t\tfield: {\n\t\t\tfield: 'connections',\n\t\t\tlabel: 'connections',\n\t\t\taggregator: { temporal: 'max', crossNode: 'sum' },\n\t\t},\n\t},\n\ttimestamp: 'time',\n\tbucket: { source: 'period-field', fallbackMs: 60000 },\n\taggregator: { temporal: 'max', crossNode: 'sum' },\n\tprimitive: 'stacked-area',\n\tyAxis: { unit: '', formatter: 'count-si' },\n};\n\ninterface RendererProps {\n\trecords: AnalyticsDataPoint[];\n\ttimeRange: TimeRange;\n\tnodes: string[];\n\ttheme: 'light' | 'dark';\n\tviewMode?: 'per-node' | 'aggregate';\n\tfillParent?: boolean;\n}\n\nexport function ConnectionsRenderer(props: RendererProps) {\n\treturn <TrafficByTypeRenderer spec={connectionsSpec} typeField=\"type\" {...props} />;\n}\n","import { DimensionSelectorRenderer } from '../primitives/DimensionSelectorRenderer.tsx';\nimport type { AnalyticsDataPoint, MetricSpec, TimeRange } from '../types/analytics.ts';\nimport { QUANTILE_DEFAULT, QUANTILE_FIELDS } from './quantileFields.ts';\n\n// Single-chart with chip selector (path: harper/user) + standard quantile\n// selector (p1..p999, default p95). Replaces the prior 3-panel\n// small-multiples view that locked operators to p50/p95/p99 only.\nexport const cpuUsageSpec: MetricSpec = {\n\ttitle: 'CPU — by scope (harper vs user)',\n\tdescription:\n\t\t'Per-path CPU utilization (count-weighted-mean) — chip selector picks scope; quantile selector picks percentile.',\n\ttab: 'health',\n\tprimaryDimension: 'path',\n\tseries: {\n\t\tkind: 'groupBy',\n\t\tdimension: 'path',\n\t\tfield: { field: 'p95', label: 'CPU %' },\n\t},\n\ttimestamp: 'time',\n\tbucket: { source: 'period-field', fallbackMs: 60000 },\n\taggregator: { temporal: 'count-weighted-mean', crossNode: 'count-weighted-mean' },\n\tconfidence: { field: 'count', greyBelow: 40, suppressBelow: 100 },\n\tprimitive: 'line',\n\tyAxis: { unit: '', formatter: 'percent' },\n\tquantileSelector: { fields: QUANTILE_FIELDS, default: QUANTILE_DEFAULT },\n};\n\ninterface RendererProps {\n\trecords: AnalyticsDataPoint[];\n\ttimeRange: TimeRange;\n\tnodes: string[];\n\ttheme: 'light' | 'dark';\n\tviewMode?: 'per-node' | 'aggregate';\n}\n\nexport function CpuUsageRenderer(props: RendererProps) {\n\treturn <DimensionSelectorRenderer spec={cpuUsageSpec} {...props} ariaLabel=\"Scope\" />;\n}\n","import { TrafficByTypeRenderer } from '../primitives/TrafficByTypeRenderer.tsx';\nimport type { AnalyticsDataPoint, MetricSpec, TimeRange } from '../types/analytics.ts';\n\n// Schema note: Harper emits database-size records with the per-database\n// byte total on the `size` column (analytics-viz's original spec said\n// `used`, which is stale relative to current Harper builds). Each row\n// also carries a `transactionLog` byte counter consumed by the\n// transaction-log-growth derived panel.\n//\n// Rendering: stacked-area with a multi-select chip row above the chart\n// (database names) and a node legend below — the same dual-legend\n// pattern Traffic-tab panels use. Operator can solo / Ctrl-toggle\n// databases or nodes; the chart updates live as records are filtered\n// pre-pipeline.\nexport const databaseSizeSpec: MetricSpec = {\n\ttitle: 'Database size',\n\tdescription: 'Per-database size in bytes — chips solo / Ctrl-toggle databases; node legend filters by node.',\n\ttab: 'storage',\n\tprimaryDimension: 'database',\n\tseries: {\n\t\tkind: 'groupBy',\n\t\tdimension: 'database',\n\t\tfield: { field: 'size', label: 'size (bytes)' },\n\t},\n\ttimestamp: 'id',\n\tbucket: { source: 'period-field', fallbackMs: 60000 },\n\taggregator: { temporal: 'last', crossNode: 'sum' },\n\tprimitive: 'stacked-area',\n\tyAxis: { unit: ' B', formatter: 'bytes-si' },\n};\n\ninterface RendererProps {\n\trecords: AnalyticsDataPoint[];\n\ttimeRange: TimeRange;\n\tnodes: string[];\n\ttheme: 'light' | 'dark';\n\tviewMode?: 'per-node' | 'aggregate';\n\tfillParent?: boolean;\n}\n\n/** Default `viewMode='aggregate'` so the stack reads \"cluster total\n * broken out by database\" — that's the operator's first question for\n * storage. Setting viewMode='per-node' on the panel toggles the stack\n * to \"per-database total broken out by node\" via TrafficByTypeRenderer's\n * built-in remap. */\nexport function DatabaseSizeRenderer(props: RendererProps) {\n\treturn (\n\t\t<TrafficByTypeRenderer\n\t\t\tspec={databaseSizeSpec}\n\t\t\ttypeField=\"database\"\n\t\t\t{...props}\n\t\t\tviewMode={props.viewMode ?? 'aggregate'}\n\t\t/>\n\t);\n}\n","import { DimensionSelectorRenderer } from '../primitives/DimensionSelectorRenderer.tsx';\nimport type { AnalyticsDataPoint, MetricSpec, TimeRange } from '../types/analytics.ts';\nimport { QUANTILE_DEFAULT, QUANTILE_FIELDS } from './quantileFields.ts';\n\nexport const dbMessageSpec: MetricSpec = {\n\ttitle: 'DB message p95',\n\tdescription: 'Per-table DB message p95 (count-weighted-mean) — top 10 tables + Other.',\n\ttab: 'db-activity',\n\tprimaryDimension: 'path',\n\tsubDimension: 'method',\n\tseries: {\n\t\tkind: 'groupBy',\n\t\tdimension: 'path',\n\t\tfield: { field: 'p95', label: 'p95 message (ms)' },\n\t\ttopN: 10,\n\t\totherBucket: true,\n\t},\n\ttimestamp: 'time',\n\tbucket: { source: 'period-field', fallbackMs: 60000 },\n\taggregator: { temporal: 'count-weighted-mean', crossNode: 'count-weighted-mean' },\n\tconfidence: { field: 'count', greyBelow: 40, suppressBelow: 100 },\n\tprimitive: 'line',\n\tyAxis: { unit: '', formatter: 'ms' },\n\tquantileSelector: { fields: QUANTILE_FIELDS, default: QUANTILE_DEFAULT },\n};\n\ninterface RendererProps {\n\trecords: AnalyticsDataPoint[];\n\ttimeRange: TimeRange;\n\tnodes: string[];\n\ttheme: 'light' | 'dark';\n\tviewMode?: 'per-node' | 'aggregate';\n}\n\nexport function DbMessageRenderer(props: RendererProps) {\n\treturn <DimensionSelectorRenderer spec={dbMessageSpec} {...props} ariaLabel=\"Table\" />;\n}\n","import { DimensionSelectorRenderer } from '../primitives/DimensionSelectorRenderer.tsx';\nimport type { AnalyticsDataPoint, MetricSpec, TimeRange } from '../types/analytics.ts';\nimport { QUANTILE_DEFAULT, QUANTILE_FIELDS } from './quantileFields.ts';\n\nexport const dbReadSpec: MetricSpec = {\n\ttitle: 'DB read p95',\n\tdescription: 'Per-table DB read p95 (count-weighted-mean) — top 10 tables + Other.',\n\ttab: 'db-activity',\n\tprimaryDimension: 'path',\n\tsubDimension: 'method',\n\tseries: {\n\t\tkind: 'groupBy',\n\t\tdimension: 'path',\n\t\tfield: { field: 'p95', label: 'p95 read (ms)' },\n\t\ttopN: 10,\n\t\totherBucket: true,\n\t},\n\ttimestamp: 'time',\n\tbucket: { source: 'period-field', fallbackMs: 60000 },\n\taggregator: { temporal: 'count-weighted-mean', crossNode: 'count-weighted-mean' },\n\tconfidence: { field: 'count', greyBelow: 40, suppressBelow: 100 },\n\tprimitive: 'line',\n\tyAxis: { unit: '', formatter: 'ms' },\n\tquantileSelector: { fields: QUANTILE_FIELDS, default: QUANTILE_DEFAULT },\n};\n\ninterface RendererProps {\n\trecords: AnalyticsDataPoint[];\n\ttimeRange: TimeRange;\n\tnodes: string[];\n\ttheme: 'light' | 'dark';\n\tviewMode?: 'per-node' | 'aggregate';\n}\n\nexport function DbReadRenderer(props: RendererProps) {\n\treturn <DimensionSelectorRenderer spec={dbReadSpec} {...props} ariaLabel=\"Table\" />;\n}\n","import { DimensionSelectorRenderer } from '../primitives/DimensionSelectorRenderer.tsx';\nimport type { AnalyticsDataPoint, MetricSpec, TimeRange } from '../types/analytics.ts';\nimport { QUANTILE_DEFAULT, QUANTILE_FIELDS } from './quantileFields.ts';\n\nexport const dbWriteSpec: MetricSpec = {\n\ttitle: 'DB write p95',\n\tdescription: 'Per-table DB write p95 (count-weighted-mean) — top 10 tables + Other.',\n\ttab: 'db-activity',\n\tprimaryDimension: 'path',\n\tsubDimension: 'method',\n\tseries: {\n\t\tkind: 'groupBy',\n\t\tdimension: 'path',\n\t\tfield: { field: 'p95', label: 'p95 write (ms)' },\n\t\ttopN: 10,\n\t\totherBucket: true,\n\t},\n\ttimestamp: 'time',\n\tbucket: { source: 'period-field', fallbackMs: 60000 },\n\taggregator: { temporal: 'count-weighted-mean', crossNode: 'count-weighted-mean' },\n\tconfidence: { field: 'count', greyBelow: 40, suppressBelow: 100 },\n\tprimitive: 'line',\n\tyAxis: { unit: '', formatter: 'ms' },\n\tquantileSelector: { fields: QUANTILE_FIELDS, default: QUANTILE_DEFAULT },\n};\n\ninterface RendererProps {\n\trecords: AnalyticsDataPoint[];\n\ttimeRange: TimeRange;\n\tnodes: string[];\n\ttheme: 'light' | 'dark';\n\tviewMode?: 'per-node' | 'aggregate';\n}\n\nexport function DbWriteRenderer(props: RendererProps) {\n\treturn <DimensionSelectorRenderer spec={dbWriteSpec} {...props} ariaLabel=\"Table\" />;\n}\n","import { DimensionSelectorRenderer } from '../primitives/DimensionSelectorRenderer.tsx';\nimport type { AnalyticsDataPoint, MetricSpec, TimeRange } from '../types/analytics.ts';\nimport { QUANTILE_DEFAULT, QUANTILE_FIELDS } from './quantileFields.ts';\n\nexport const durationSpec: MetricSpec = {\n\ttitle: 'Request duration (p95)',\n\tdescription: 'Per-path request duration p95 (count-weighted-mean) — top 10 paths + Other bucket.',\n\ttab: 'requests',\n\tprimaryDimension: 'path',\n\tsubDimension: 'method',\n\tseries: {\n\t\tkind: 'groupBy',\n\t\tdimension: 'path',\n\t\tfield: { field: 'p95', label: 'p95 duration (ms)' },\n\t\ttopN: 10,\n\t\totherBucket: true,\n\t},\n\ttimestamp: 'time',\n\tbucket: { source: 'period-field', fallbackMs: 60000 },\n\taggregator: { temporal: 'count-weighted-mean', crossNode: 'count-weighted-mean' },\n\tconfidence: { field: 'count', greyBelow: 40, suppressBelow: 100 },\n\tprimitive: 'line',\n\tyAxis: { unit: '', formatter: 'ms' },\n\tquantileSelector: { fields: QUANTILE_FIELDS, default: QUANTILE_DEFAULT },\n};\n\ninterface RendererProps {\n\trecords: AnalyticsDataPoint[];\n\ttimeRange: TimeRange;\n\tnodes: string[];\n\ttheme: 'light' | 'dark';\n\tviewMode?: 'per-node' | 'aggregate';\n}\n\nexport function DurationRenderer(props: RendererProps) {\n\treturn <DimensionSelectorRenderer spec={durationSpec} {...props} ariaLabel=\"Path\" />;\n}\n","import { useMemo, useState } from 'react';\nimport { NodeLegend } from '../charts/NodeLegend.tsx';\nimport { useNodeSelection } from '../hooks/useNodeSelection.ts';\nimport { getNodeColor } from '../lib/nodeColors.ts';\nimport { DimensionChipRow } from '../primitives/DimensionChipRow.tsx';\nimport { LineChart } from '../primitives/LineChart.tsx';\nimport type { AnalyticsDataPoint, AxisSpec, FieldSpec, MetricSpec, SeriesData, TimeRange } from '../types/analytics.ts';\nimport { runPipeline } from './pipeline.ts';\n\n// Field-selector pattern (mirrors memory.tsx). Records expose\n// `active`, `idle`, and `taskQueueLatency`; we surface 4 operator-\n// relevant views as chip-row options, each with its own field\n// projection + y-axis formatter. The prior dual-axis design (one\n// utilization line + one queue-lag line) is replaced by single-field\n// focus so each option uses the full chart real estate with per-node\n// breakdown.\n\ninterface FieldOption {\n\tkey: string;\n\tlabel: string;\n\tfield: FieldSpec['field'];\n\tyAxis: AxisSpec;\n}\n\nconst FIELDS: ReadonlyArray<FieldOption> = [\n\t{\n\t\tkey: 'utilization',\n\t\tlabel: 'Utilization',\n\t\t// active / (active + idle); FieldExpr returns null when divisor is 0.\n\t\tfield: {\n\t\t\tkind: 'op',\n\t\t\top: '/',\n\t\t\tleft: { kind: 'ref', field: 'active' },\n\t\t\tright: {\n\t\t\t\tkind: 'op',\n\t\t\t\top: '+',\n\t\t\t\tleft: { kind: 'ref', field: 'active' },\n\t\t\t\tright: { kind: 'ref', field: 'idle' },\n\t\t\t},\n\t\t},\n\t\tyAxis: { unit: '', formatter: 'percent' },\n\t},\n\t{\n\t\tkey: 'taskQueueLatency',\n\t\tlabel: 'Queue lag',\n\t\tfield: 'taskQueueLatency',\n\t\tyAxis: { unit: '', formatter: 'ms' },\n\t},\n\t{\n\t\tkey: 'active',\n\t\tlabel: 'Active time',\n\t\tfield: 'active',\n\t\tyAxis: { unit: '', formatter: 'ms' },\n\t},\n\t{\n\t\tkey: 'idle',\n\t\tlabel: 'Idle time',\n\t\tfield: 'idle',\n\t\tyAxis: { unit: '', formatter: 'ms' },\n\t},\n];\n\nconst FIELD_KEYS = FIELDS.map((f) => f.key);\n\nexport const mainThreadUtilizationSpec: MetricSpec = {\n\ttitle: 'Main thread utilization',\n\tdescription: 'Per-node main-thread metrics — chip selector picks utilization / queue lag / active time / idle time.',\n\ttab: 'health',\n\tprimaryDimension: 'node',\n\tseries: {\n\t\tkind: 'field',\n\t\tfields: [{ field: FIELDS[0].field, label: FIELDS[0].label }],\n\t},\n\ttimestamp: 'time',\n\tbucket: { source: 'period-field', fallbackMs: 60000 },\n\taggregator: { temporal: 'mean', crossNode: 'mean' },\n\tprimitive: 'line',\n\tyAxis: FIELDS[0].yAxis,\n\tlayout: { colSpan: 2 },\n};\n\ninterface RendererProps {\n\trecords: AnalyticsDataPoint[];\n\ttimeRange: TimeRange;\n\tnodes: string[];\n\ttheme: 'light' | 'dark';\n\tviewMode?: 'per-node' | 'aggregate';\n\tfillParent?: boolean;\n}\n\nfunction shortenNodeLabel(node: string): string {\n\tconst dot = node.indexOf('.');\n\treturn dot === -1 ? node : node.slice(0, dot);\n}\n\nexport function MainThreadRenderer(\n\t{ records, timeRange, nodes, theme, viewMode = 'per-node', fillParent }: RendererProps,\n) {\n\tconst perNode = viewMode === 'per-node';\n\tconst [selectedKey, setSelectedKey] = useState<string>(FIELDS[0].key);\n\tconst effectiveKey = FIELD_KEYS.includes(selectedKey) ? selectedKey : FIELDS[0].key;\n\tconst selected = FIELDS.find((f) => f.key === effectiveKey)!;\n\n\tconst data = useMemo<SeriesData>(() => {\n\t\tconst innerSpec: MetricSpec = {\n\t\t\t...mainThreadUtilizationSpec,\n\t\t\tseries: { kind: 'field', fields: [{ field: selected.field, label: selected.label }] },\n\t\t\tyAxis: selected.yAxis,\n\t\t};\n\t\treturn runPipeline(innerSpec, records, timeRange, nodes, { perNode, snapToPeriod: true });\n\t}, [selected, records, timeRange, nodes, perNode]);\n\n\tconst { isActive, handleLegendClick } = useNodeSelection(nodes);\n\n\tconst filteredData: SeriesData = useMemo(() => ({\n\t\t...data,\n\t\tseries: data.series\n\t\t\t.map((s) => {\n\t\t\t\tconst sep = s.key.indexOf('|');\n\t\t\t\tconst node = sep === -1 ? '' : s.key.slice(sep + 1);\n\t\t\t\tif (!node) { return s; }\n\t\t\t\treturn { ...s, label: shortenNodeLabel(node), color: getNodeColor(node, nodes) };\n\t\t\t})\n\t\t\t.filter((s) => {\n\t\t\t\tconst sep = s.key.indexOf('|');\n\t\t\t\tconst node = sep === -1 ? '' : s.key.slice(sep + 1);\n\t\t\t\treturn node === '' || isActive(node);\n\t\t\t}),\n\t}), [data, nodes, isActive]);\n\n\treturn (\n\t\t<div className=\"flex h-full flex-col\">\n\t\t\t<DimensionChipRow\n\t\t\t\tdimensionValues={FIELD_KEYS.map((k) => FIELDS.find((f) => f.key === k)!.label)}\n\t\t\t\tselected={selected.label}\n\t\t\t\tonSelect={(label) => {\n\t\t\t\t\tconst f = FIELDS.find((x) => x.label === label);\n\t\t\t\t\tif (f) { setSelectedKey(f.key); }\n\t\t\t\t}}\n\t\t\t\tariaLabel=\"Main thread metric\"\n\t\t\t/>\n\t\t\t<div className=\"min-h-0 flex-1\" style={{ marginTop: 8 }}>\n\t\t\t\t<LineChart\n\t\t\t\t\tdata={filteredData}\n\t\t\t\t\ttheme={theme}\n\t\t\t\t\tyAxis={selected.yAxis}\n\t\t\t\t\txDomain={[timeRange.startTime, timeRange.endTime]}\n\t\t\t\t\tfillParent={fillParent}\n\t\t\t\t\thideLegend={perNode}\n\t\t\t\t/>\n\t\t\t</div>\n\t\t\t{perNode && nodes.length > 0 && (\n\t\t\t\t<NodeLegend\n\t\t\t\t\tnodeIds={nodes}\n\t\t\t\t\tisActive={isActive}\n\t\t\t\t\tonClickNode={handleLegendClick}\n\t\t\t\t/>\n\t\t\t)}\n\t\t</div>\n\t);\n}\n","import { useMemo, useState } from 'react';\nimport { NodeLegend } from '../charts/NodeLegend.tsx';\nimport { useNodeSelection } from '../hooks/useNodeSelection.ts';\nimport { getNodeColor } from '../lib/nodeColors.ts';\nimport { DimensionChipRow } from '../primitives/DimensionChipRow.tsx';\nimport { LineChart } from '../primitives/LineChart.tsx';\nimport type { AnalyticsDataPoint, FieldSpec, MetricSpec, SeriesData, TimeRange } from '../types/analytics.ts';\nimport { runPipeline } from './pipeline.ts';\n\n// Field-selector pattern (cousin of DimensionSelectorRenderer): chip-row\n// picks one memory field at a time and the chart shows that one with the\n// full panel real estate, per-node lines, shared NodeLegend below.\n// Replaces the prior 4-panel small-multiples grid that crammed each field\n// into a 280×180 mini-chart with multiple competing per-node lines.\n//\n// Aggregator { temporal: 'last', crossNode: 'mean' } — memory is a gauge.\n// `count` on memory records is THREAD count, not sample volume.\n\nconst MEMORY_FIELDS: ReadonlyArray<FieldSpec> = [\n\t{ field: 'heapUsed', label: 'heap used' },\n\t{ field: 'heapTotal', label: 'heap total' },\n\t{ field: 'external', label: 'external' },\n\t{ field: 'arrayBuffers', label: 'arrayBuffers' },\n];\n\nexport const memorySpec: MetricSpec = {\n\ttitle: 'Process memory',\n\tdescription: 'Per-node V8 memory — chip selector picks the field. Default heap used.',\n\ttab: 'health',\n\tprimaryDimension: 'node',\n\tseries: { kind: 'field', fields: [...MEMORY_FIELDS] },\n\ttimestamp: 'time',\n\tbucket: { source: 'period-field', fallbackMs: 60000 },\n\taggregator: { temporal: 'last', crossNode: 'mean' },\n\tprimitive: 'line',\n\tyAxis: { unit: ' B', formatter: 'bytes-si' },\n};\n\ninterface RendererProps {\n\trecords: AnalyticsDataPoint[];\n\ttimeRange: TimeRange;\n\tnodes: string[];\n\ttheme: 'light' | 'dark';\n\tviewMode?: 'per-node' | 'aggregate';\n\tfillParent?: boolean;\n}\n\nfunction shortenNodeLabel(node: string): string {\n\tconst dot = node.indexOf('.');\n\treturn dot === -1 ? node : node.slice(0, dot);\n}\n\nconst FIELD_LABELS = MEMORY_FIELDS.map((f) => f.label);\n\nexport function MemoryRenderer(\n\t{ records, timeRange, nodes, theme, viewMode = 'per-node', fillParent }: RendererProps,\n) {\n\tconst perNode = viewMode === 'per-node';\n\tconst [selectedLabel, setSelectedLabel] = useState<string>(MEMORY_FIELDS[0].label);\n\tconst effectiveLabel = FIELD_LABELS.includes(selectedLabel) ? selectedLabel : MEMORY_FIELDS[0].label;\n\tconst selectedField = MEMORY_FIELDS.find((f) => f.label === effectiveLabel)!;\n\n\tconst data = useMemo<SeriesData>(() => {\n\t\tconst innerSpec: MetricSpec = {\n\t\t\t...memorySpec,\n\t\t\tseries: { kind: 'field', fields: [selectedField] },\n\t\t};\n\t\treturn runPipeline(innerSpec, records, timeRange, nodes, { perNode, snapToPeriod: true });\n\t}, [selectedField, records, timeRange, nodes, perNode]);\n\n\tconst { isActive, handleLegendClick } = useNodeSelection(nodes);\n\n\tconst filteredData: SeriesData = useMemo(() => ({\n\t\t...data,\n\t\tseries: data.series\n\t\t\t.map((s) => {\n\t\t\t\tconst sep = s.key.indexOf('|');\n\t\t\t\tconst node = sep === -1 ? '' : s.key.slice(sep + 1);\n\t\t\t\tif (!node) { return s; }\n\t\t\t\treturn { ...s, label: shortenNodeLabel(node), color: getNodeColor(node, nodes) };\n\t\t\t})\n\t\t\t.filter((s) => {\n\t\t\t\tconst sep = s.key.indexOf('|');\n\t\t\t\tconst node = sep === -1 ? '' : s.key.slice(sep + 1);\n\t\t\t\treturn node === '' || isActive(node);\n\t\t\t}),\n\t}), [data, nodes, isActive]);\n\n\treturn (\n\t\t<div className=\"flex h-full flex-col\">\n\t\t\t<DimensionChipRow\n\t\t\t\tdimensionValues={FIELD_LABELS}\n\t\t\t\tselected={effectiveLabel}\n\t\t\t\tonSelect={setSelectedLabel}\n\t\t\t\tariaLabel=\"Memory field\"\n\t\t\t/>\n\t\t\t<div className=\"min-h-0 flex-1\" style={{ marginTop: 8 }}>\n\t\t\t\t<LineChart\n\t\t\t\t\tdata={filteredData}\n\t\t\t\t\ttheme={theme}\n\t\t\t\t\tyAxis={memorySpec.yAxis}\n\t\t\t\t\txDomain={[timeRange.startTime, timeRange.endTime]}\n\t\t\t\t\tfillParent={fillParent}\n\t\t\t\t\thideLegend={perNode}\n\t\t\t\t/>\n\t\t\t</div>\n\t\t\t{perNode && nodes.length > 0 && (\n\t\t\t\t<NodeLegend\n\t\t\t\t\tnodeIds={nodes}\n\t\t\t\t\tisActive={isActive}\n\t\t\t\t\tonClickNode={handleLegendClick}\n\t\t\t\t/>\n\t\t\t)}\n\t\t</div>\n\t);\n}\n","/**\n * @internal Exported for unit testing. Production callers should import the\n * re-export from HeatmapMatrix.tsx.\n *\n * Compute responsive cell size (px) for a heatmap grid.\n * - clamp((containerWidth - rowLabelWidth - gap*(colCount-1)) / colCount, min, max)\n * - Returns `min` when container is too narrow; `max` when too wide.\n */\nexport function computeCellSize(\n\tcontainerWidth: number,\n\tcolCount: number,\n\trowLabelWidth: number,\n\tgap: number,\n\tmin: number,\n\tmax: number,\n): number {\n\tif (colCount <= 0) { return max; }\n\tconst usable = containerWidth - rowLabelWidth - gap * Math.max(0, colCount - 1);\n\tconst perCell = Math.floor(usable / colCount);\n\treturn Math.max(min, Math.min(max, perCell));\n}\n","import { useCallback, useEffect, useId, useLayoutEffect, useMemo, useRef, useState } from 'react';\nimport type { KeyboardEvent as ReactKeyboardEvent } from 'react';\nimport type { AxisSpec, HeatmapCell, HeatmapData } from '../types/analytics.ts';\nimport { computeCellSize } from './computeCellSize.ts';\nimport { formatValue } from './formatValue.ts';\nexport { computeCellSize } from './computeCellSize.ts';\n\ninterface Props {\n\tdata: HeatmapData;\n\ttheme: 'light' | 'dark';\n\ttitle?: string;\n\theight?: number;\n}\n\ntype Confidence = 'ok' | 'grey' | 'suppress' | 'absent';\n\n// Light theme: pale → deep red (low → high latency).\nconst LIGHT_STOPS = ['#fef3c7', '#fcd34d', '#f59e0b', '#dc2626', '#7f1d1d'];\n// Dark theme: muted amber → cream.\nconst DARK_STOPS = ['#713f12', '#b45309', '#d97706', '#f59e0b', '#fef3c7'];\n\n// contrast-table (WCAG 2.1 SC 1.4.11 — 3:1 minimum)\n// Outer halo stroke computed dynamically from WCAG relative luminance:\n// luminance > 0.5 → black (#000000); else white (#ffffff).\n//\n// light theme stops (pale → deep red):\n// #fef3c7 L≈0.918 → black\n// #fcd34d L≈0.693 → black\n// #f59e0b L≈0.471 → white\n// #dc2626 L≈0.196 → white\n// #7f1d1d L≈0.064 → white\n//\n// dark theme stops (muted amber → cream):\n// #713f12 L≈0.068 → white\n// #b45309 L≈0.175 → white\n// #d97706 L≈0.330 → white\n// #f59e0b L≈0.471 → white\n// #fef3c7 L≈0.918 → black\n//\n// Halo: inner stroke accent (1px) + outer stroke b/w (1px) = ≥3:1 against any fill.\n\nfunction srgbToLinear(c: number): number {\n\tconst s = c / 255;\n\treturn s <= 0.03928 ? s / 12.92 : Math.pow((s + 0.055) / 1.055, 2.4);\n}\n\nfunction relativeLuminance(hex: string): number {\n\tconst h = hex.replace('#', '');\n\tconst r = parseInt(h.slice(0, 2), 16);\n\tconst g = parseInt(h.slice(2, 4), 16);\n\tconst b = parseInt(h.slice(4, 6), 16);\n\treturn 0.2126 * srgbToLinear(r) + 0.7152 * srgbToLinear(g) + 0.0722 * srgbToLinear(b);\n}\n\nfunction hexToRgb(hex: string): [number, number, number] {\n\tconst h = hex.replace('#', '');\n\treturn [parseInt(h.slice(0, 2), 16), parseInt(h.slice(2, 4), 16), parseInt(h.slice(4, 6), 16)];\n}\n\nfunction rgbToHex(r: number, g: number, b: number): string {\n\tconst h = (v: number) => Math.round(Math.max(0, Math.min(255, v))).toString(16).padStart(2, '0');\n\treturn `#${h(r)}${h(g)}${h(b)}`;\n}\n\nfunction interpolateStops(stops: string[], t: number): string {\n\tif (t <= 0) { return stops[0]; }\n\tif (t >= 1) { return stops[stops.length - 1]; }\n\tconst scaled = t * (stops.length - 1);\n\tconst idx = Math.floor(scaled);\n\tconst frac = scaled - idx;\n\tconst [r1, g1, b1] = hexToRgb(stops[idx]);\n\tconst [r2, g2, b2] = hexToRgb(stops[idx + 1]);\n\treturn rgbToHex(r1 + (r2 - r1) * frac, g1 + (g2 - g1) * frac, b1 + (b2 - b1) * frac);\n}\n\nfunction classifyCell(\n\tcell: HeatmapCell | undefined,\n\tgreyBelow: number,\n\tsuppressBelow: number,\n): Confidence {\n\tif (!cell || cell.value === null || cell.value === undefined) { return 'absent'; }\n\tconst count = cell.count ?? 0;\n\t// grey: greyBelow ≤ count < suppressBelow. suppress: count < greyBelow.\n\tif (count < greyBelow) { return 'suppress'; }\n\tif (count < suppressBelow) { return 'grey'; }\n\treturn 'ok';\n}\n\nfunction truncate(s: string, max: number): string {\n\treturn s.length > max ? s.slice(0, max - 1) + '…' : s;\n}\n\nfunction cellAriaLabel(\n\trow: string,\n\tcol: string,\n\tcell: HeatmapCell | undefined,\n\tconfidence: Confidence,\n\tunit: string,\n\tsuppressBelow: number,\n\tapprox: boolean,\n): string {\n\tconst prefix = `${row} → ${col}: `;\n\tconst approxTag = approx ? ' (approx)' : '';\n\tif (confidence === 'absent') {\n\t\treturn `${prefix}no data`;\n\t}\n\tif (confidence === 'suppress') {\n\t\treturn `${prefix}insufficient samples (n<${suppressBelow}), value hidden`;\n\t}\n\tconst value = cell?.value ?? 0;\n\tconst count = cell?.count ?? 0;\n\tif (confidence === 'grey') {\n\t\treturn `${prefix}${value} ${unit} p95 (approx, low-confidence: ${count} samples below threshold)`;\n\t}\n\treturn `${prefix}${value} ${unit} p95${approxTag}, ${count} samples`;\n}\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Color scale legend — horizontal gradient below the grid.\n// ─────────────────────────────────────────────────────────────────────────────\n\ninterface LegendProps {\n\tstops: string[];\n\tvmin: number;\n\tvmax: number;\n\twidth: number;\n\taxis?: AxisSpec;\n\tapprox: boolean;\n}\n\nfunction HeatmapColorLegend({ stops, vmin, vmax, width, axis, approx }: LegendProps) {\n\tconst gradientId = useId();\n\tconst unit = axis?.unit ?? '';\n\tconst fmt = (v: number) => formatValue(v, axis?.formatter);\n\tconst median = (vmin + vmax) / 2;\n\tconst stripWidth = Math.max(200, Math.min(width, 480));\n\tconst stripHeight = 12;\n\tconst labelPrefix = approx ? 'p95 latency (count-weighted-mean, approx)' : 'p95 latency';\n\tconst label = `${labelPrefix}: ${fmt(vmin)}–${fmt(vmax)} ${unit}. Higher value = worse latency.`;\n\treturn (\n\t\t<svg\n\t\t\trole=\"img\"\n\t\t\taria-label={label}\n\t\t\twidth={stripWidth + 80}\n\t\t\theight={40}\n\t\t\tstyle={{ overflow: 'visible' }}\n\t\t>\n\t\t\t<defs>\n\t\t\t\t<linearGradient id={gradientId} x1=\"0%\" x2=\"100%\" y1=\"0%\" y2=\"0%\">\n\t\t\t\t\t{stops.map((stop, i) => (\n\t\t\t\t\t\t<stop\n\t\t\t\t\t\t\tkey={stop + i}\n\t\t\t\t\t\t\toffset={`${(i / (stops.length - 1)) * 100}%`}\n\t\t\t\t\t\t\tstopColor={stop}\n\t\t\t\t\t\t/>\n\t\t\t\t\t))}\n\t\t\t\t</linearGradient>\n\t\t\t</defs>\n\t\t\t<text\n\t\t\t\tx={0}\n\t\t\t\ty={stripHeight + 2}\n\t\t\t\tfontSize={10}\n\t\t\t\tfill={axis ? 'currentColor' : 'currentColor'}\n\t\t\t\taria-hidden=\"true\"\n\t\t\t>\n\t\t\t\tlow\n\t\t\t</text>\n\t\t\t<rect\n\t\t\t\tx={30}\n\t\t\t\ty={0}\n\t\t\t\twidth={stripWidth}\n\t\t\t\theight={stripHeight}\n\t\t\t\tfill={`url(#${gradientId})`}\n\t\t\t\taria-hidden=\"true\"\n\t\t\t/>\n\t\t\t<text\n\t\t\t\tx={30 + stripWidth + 4}\n\t\t\t\ty={stripHeight + 2}\n\t\t\t\tfontSize={10}\n\t\t\t\tfill=\"currentColor\"\n\t\t\t\taria-hidden=\"true\"\n\t\t\t>\n\t\t\t\thigh\n\t\t\t</text>\n\t\t\t<text x={30} y={stripHeight + 18} fontSize={10} fill=\"currentColor\" aria-hidden=\"true\">\n\t\t\t\t{fmt(vmin)}\n\t\t\t</text>\n\t\t\t<text\n\t\t\t\tx={30 + stripWidth / 2}\n\t\t\t\ty={stripHeight + 18}\n\t\t\t\tfontSize={10}\n\t\t\t\tfill=\"currentColor\"\n\t\t\t\ttextAnchor=\"middle\"\n\t\t\t\taria-hidden=\"true\"\n\t\t\t>\n\t\t\t\t{fmt(median)}\n\t\t\t</text>\n\t\t\t<text\n\t\t\t\tx={30 + stripWidth}\n\t\t\t\ty={stripHeight + 18}\n\t\t\t\tfontSize={10}\n\t\t\t\tfill=\"currentColor\"\n\t\t\t\ttextAnchor=\"end\"\n\t\t\t\taria-hidden=\"true\"\n\t\t\t>\n\t\t\t\t{fmt(vmax)}\n\t\t\t</text>\n\t\t</svg>\n\t);\n}\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Main HeatmapMatrix component\n// ─────────────────────────────────────────────────────────────────────────────\n\nconst MIN_CELL_SIZE = 40;\nconst MAX_CELL_SIZE = 80;\nconst CELL_GAP = 4;\nconst HEADER_HEIGHT = 72; // reserved for rotated column headers\nconst ROW_LABEL_WIDTH = 200;\n\nexport function HeatmapMatrix({ data, theme, title, height }: Props) {\n\tconst titleId = useId();\n\tconst descId = useId();\n\tconst suppressPatternId = useId();\n\n\tconst greyBelow = data.confidence?.greyBelow ?? 0;\n\tconst suppressBelow = data.confidence?.suppressBelow ?? 0;\n\tconst stops = theme === 'dark' ? DARK_STOPS : LIGHT_STOPS;\n\tconst approx = data.approx === true;\n\tconst unit = data.axis?.unit ?? '';\n\n\t// Build a (row,col) -> cell index for O(1) lookup.\n\tconst cellMap = useMemo(() => {\n\t\tconst m = new Map<string, HeatmapCell>();\n\t\tfor (const c of data.cells) { m.set(`${c.row}|${c.col}`, c); }\n\t\treturn m;\n\t}, [data.cells]);\n\n\t// Compute value range for color scaling.\n\tconst [vmin, vmax] = useMemo(() => {\n\t\tif (data.valueRange) { return [data.valueRange.min, data.valueRange.max]; }\n\t\tlet lo = Infinity;\n\t\tlet hi = -Infinity;\n\t\tfor (const c of data.cells) {\n\t\t\tif (c.value !== null && c.value !== undefined && Number.isFinite(c.value)) {\n\t\t\t\tif (c.value < lo) { lo = c.value; }\n\t\t\t\tif (c.value > hi) { hi = c.value; }\n\t\t\t}\n\t\t}\n\t\tif (!Number.isFinite(lo) || !Number.isFinite(hi)) { return [0, 1]; }\n\t\tif (lo === hi) { return [lo, lo + 1]; }\n\t\treturn [lo, hi];\n\t}, [data.cells, data.valueRange]);\n\n\tconst rows = data.rows;\n\tconst cols = data.cols;\n\n\t// Responsive cell sizing: measure wrapper width, clamp to [MIN, MAX].\n\tconst wrapperRef = useRef<HTMLDivElement>(null);\n\tconst [cellSize, setCellSize] = useState<number>(MAX_CELL_SIZE);\n\tconst rafRef = useRef<number | null>(null);\n\n\tuseLayoutEffect(() => {\n\t\tconst w = wrapperRef.current?.clientWidth ?? 0;\n\t\tsetCellSize(\n\t\t\tcomputeCellSize(w, cols.length, ROW_LABEL_WIDTH, CELL_GAP, MIN_CELL_SIZE, MAX_CELL_SIZE),\n\t\t);\n\t}, [cols.length]);\n\n\tuseEffect(() => {\n\t\tconst el = wrapperRef.current;\n\t\tif (!el) { return; }\n\t\tconst ro = new ResizeObserver(() => {\n\t\t\tif (rafRef.current !== null) { cancelAnimationFrame(rafRef.current); }\n\t\t\trafRef.current = requestAnimationFrame(() => {\n\t\t\t\trafRef.current = null;\n\t\t\t\tconst w = el.clientWidth;\n\t\t\t\tsetCellSize(\n\t\t\t\t\tcomputeCellSize(w, cols.length, ROW_LABEL_WIDTH, CELL_GAP, MIN_CELL_SIZE, MAX_CELL_SIZE),\n\t\t\t\t);\n\t\t\t});\n\t\t});\n\t\tro.observe(el);\n\t\treturn () => {\n\t\t\tro.disconnect();\n\t\t\tif (rafRef.current !== null) { cancelAnimationFrame(rafRef.current); }\n\t\t};\n\t}, [cols.length]);\n\n\tconst gridWidth = cols.length * cellSize + (cols.length - 1) * CELL_GAP;\n\tconst gridHeight = rows.length * cellSize + (rows.length - 1) * CELL_GAP;\n\tconst svgWidth = ROW_LABEL_WIDTH + gridWidth + 8;\n\tconst svgHeight = HEADER_HEIGHT + gridHeight + 8;\n\n\t// Roving tabindex state: [rowIdx, colIdx]\n\tconst [active, setActive] = useState<[number, number]>([0, 0]);\n\tconst cellRefs = useRef<Map<string, HTMLElement>>(new Map());\n\n\tconst focusCell = useCallback((r: number, c: number) => {\n\t\tconst el = cellRefs.current.get(`${r}|${c}`);\n\t\tif (el) {\n\t\t\tsetActive([r, c]);\n\t\t\tel.focus();\n\t\t}\n\t}, []);\n\n\tconst handleKeyDown = useCallback(\n\t\t(e: ReactKeyboardEvent<SVGGElement>, r: number, c: number) => {\n\t\t\tconst k = e.key;\n\t\t\tlet handled = false;\n\t\t\tlet nr = r;\n\t\t\tlet nc = c;\n\t\t\tif (k === 'ArrowRight') {\n\t\t\t\thandled = true;\n\t\t\t\tnc = Math.min(cols.length - 1, c + 1);\n\t\t\t} else if (k === 'ArrowLeft') {\n\t\t\t\thandled = true;\n\t\t\t\tnc = Math.max(0, c - 1);\n\t\t\t} else if (k === 'ArrowDown') {\n\t\t\t\thandled = true;\n\t\t\t\tnr = Math.min(rows.length - 1, r + 1);\n\t\t\t} else if (k === 'ArrowUp') {\n\t\t\t\thandled = true;\n\t\t\t\tnr = Math.max(0, r - 1);\n\t\t\t} else if (k === 'Home') {\n\t\t\t\thandled = true;\n\t\t\t\tnc = 0;\n\t\t\t} else if (k === 'End') {\n\t\t\t\thandled = true;\n\t\t\t\tnc = cols.length - 1;\n\t\t\t} else if (k === 'PageUp') {\n\t\t\t\thandled = true;\n\t\t\t\tnr = Math.max(0, r - 5);\n\t\t\t} else if (k === 'PageDown') {\n\t\t\t\thandled = true;\n\t\t\t\tnr = Math.min(rows.length - 1, r + 5);\n\t\t\t}\n\t\t\tif (!handled) { return; }\n\t\t\te.preventDefault();\n\t\t\te.stopPropagation();\n\t\t\tif (nr !== r || nc !== c) { focusCell(nr, nc); }\n\t\t},\n\t\t[rows.length, cols.length, focusCell],\n\t);\n\n\t// Focus halo stroke color based on cell fill luminance.\n\tconst haloOuter = (fill: string): string => {\n\t\tif (!fill.startsWith('#')) { return '#000000'; }\n\t\treturn relativeLuminance(fill) > 0.5 ? '#000000' : '#ffffff';\n\t};\n\n\treturn (\n\t\t<div style={{ position: 'relative' }}>\n\t\t\t{/* Skipped-records status banner */}\n\t\t\t{data.skippedRecordsCount > 0\n\t\t\t\t? (\n\t\t\t\t\t<div\n\t\t\t\t\t\tkey={data.skippedRecordsCount}\n\t\t\t\t\t\trole=\"status\"\n\t\t\t\t\t\taria-atomic=\"true\"\n\t\t\t\t\t\tstyle={{\n\t\t\t\t\t\t\tmarginBottom: 8,\n\t\t\t\t\t\t\tpadding: '4px 8px',\n\t\t\t\t\t\t\tfontSize: 12,\n\t\t\t\t\t\t\tborderLeft: '3px solid var(--color-warning)',\n\t\t\t\t\t\t\tbackground: 'color-mix(in srgb, var(--color-warning) 12%, transparent)',\n\t\t\t\t\t\t\tcolor: 'currentColor',\n\t\t\t\t\t\t}}\n\t\t\t\t\t>\n\t\t\t\t\t\t{`${data.skippedRecordsCount} record(s) omitted — source node unrecognized (cluster node list may be outdated).`}\n\t\t\t\t\t</div>\n\t\t\t\t)\n\t\t\t\t: null}\n\n\t\t\t{/* Visually-hidden title + description */}\n\t\t\t<p\n\t\t\t\tid={titleId}\n\t\t\t\tstyle={{\n\t\t\t\t\tposition: 'absolute',\n\t\t\t\t\twidth: 1,\n\t\t\t\t\theight: 1,\n\t\t\t\t\tpadding: 0,\n\t\t\t\t\tmargin: -1,\n\t\t\t\t\toverflow: 'hidden',\n\t\t\t\t\tclip: 'rect(0, 0, 0, 0)',\n\t\t\t\t\twhiteSpace: 'nowrap',\n\t\t\t\t\tborder: 0,\n\t\t\t\t}}\n\t\t\t>\n\t\t\t\t{title ?? 'Heatmap'}\n\t\t\t</p>\n\t\t\t<p\n\t\t\t\tid={descId}\n\t\t\t\tdata-testid=\"heatmap-desc\"\n\t\t\t\tstyle={{\n\t\t\t\t\tposition: 'absolute',\n\t\t\t\t\twidth: 1,\n\t\t\t\t\theight: 1,\n\t\t\t\t\tpadding: 0,\n\t\t\t\t\tmargin: -1,\n\t\t\t\t\toverflow: 'hidden',\n\t\t\t\t\tclip: 'rect(0, 0, 0, 0)',\n\t\t\t\t\twhiteSpace: 'nowrap',\n\t\t\t\t\tborder: 0,\n\t\t\t\t}}\n\t\t\t>\n\t\t\t\t{(() => {\n\t\t\t\t\tconst descPrefix = approx\n\t\t\t\t\t\t? 'Cells show count-weighted-mean p95 latency (approx).'\n\t\t\t\t\t\t: 'Cells show p95 latency.';\n\t\t\t\t\treturn `${descPrefix} Cells with fewer than ${suppressBelow} samples are blank; ${greyBelow}–${\n\t\t\t\t\t\tsuppressBelow - 1\n\t\t\t\t\t} render grey.`;\n\t\t\t\t})()}\n\t\t\t</p>\n\n\t\t\t<div ref={wrapperRef} style={{ width: '100%', display: 'block', overflowX: 'auto' }}>\n\t\t\t\t<svg\n\t\t\t\t\trole=\"grid\"\n\t\t\t\t\taria-labelledby={titleId}\n\t\t\t\t\taria-describedby={descId}\n\t\t\t\t\twidth={svgWidth}\n\t\t\t\t\theight={height ?? svgHeight}\n\t\t\t\t\tviewBox={`0 0 ${svgWidth} ${svgHeight}`}\n\t\t\t\t\tdata-cell-size={cellSize}\n\t\t\t\t\tstyle={{ overflow: 'visible', display: 'block' }}\n\t\t\t\t>\n\t\t\t\t\t<defs>\n\t\t\t\t\t\t<pattern\n\t\t\t\t\t\t\tid={suppressPatternId}\n\t\t\t\t\t\t\tpatternUnits=\"userSpaceOnUse\"\n\t\t\t\t\t\t\twidth={8}\n\t\t\t\t\t\t\theight={8}\n\t\t\t\t\t\t\tpatternTransform=\"rotate(45)\"\n\t\t\t\t\t\t>\n\t\t\t\t\t\t\t<rect width={8} height={8} fill={theme === 'dark' ? '#1f2937' : '#f3f4f6'} />\n\t\t\t\t\t\t\t<line\n\t\t\t\t\t\t\t\tx1={0}\n\t\t\t\t\t\t\t\ty1={0}\n\t\t\t\t\t\t\t\tx2={0}\n\t\t\t\t\t\t\t\ty2={8}\n\t\t\t\t\t\t\t\tstroke={theme === 'dark' ? '#4b5563' : '#9ca3af'}\n\t\t\t\t\t\t\t\tstrokeWidth={2}\n\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t</pattern>\n\t\t\t\t\t</defs>\n\n\t\t\t\t\t{/* Header row: corner spacer + column headers */}\n\t\t\t\t\t<g role=\"row\">\n\t\t\t\t\t\t{/* corner spacer (no role) */}\n\t\t\t\t\t\t<rect\n\t\t\t\t\t\t\tx={0}\n\t\t\t\t\t\t\ty={0}\n\t\t\t\t\t\t\twidth={ROW_LABEL_WIDTH}\n\t\t\t\t\t\t\theight={HEADER_HEIGHT}\n\t\t\t\t\t\t\tfill=\"transparent\"\n\t\t\t\t\t\t\taria-hidden=\"true\"\n\t\t\t\t\t\t/>\n\t\t\t\t\t\t{cols.map((col, ci) => {\n\t\t\t\t\t\t\tconst cx = ROW_LABEL_WIDTH + ci * (cellSize + CELL_GAP) + cellSize / 2;\n\t\t\t\t\t\t\tconst cy = HEADER_HEIGHT - 8;\n\t\t\t\t\t\t\tconst truncateLength = cellSize < 56 ? 8 : 20;\n\t\t\t\t\t\t\treturn (\n\t\t\t\t\t\t\t\t<g key={col} role=\"columnheader\" aria-label={col}>\n\t\t\t\t\t\t\t\t\t<text\n\t\t\t\t\t\t\t\t\t\tx={cx}\n\t\t\t\t\t\t\t\t\t\ty={cy}\n\t\t\t\t\t\t\t\t\t\tfontSize={11}\n\t\t\t\t\t\t\t\t\t\ttextAnchor=\"end\"\n\t\t\t\t\t\t\t\t\t\ttransform={`rotate(-45, ${cx}, ${cy})`}\n\t\t\t\t\t\t\t\t\t\tfill=\"currentColor\"\n\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\t{truncate(col, truncateLength)}\n\t\t\t\t\t\t\t\t\t\t<title>{col}</title>\n\t\t\t\t\t\t\t\t\t</text>\n\t\t\t\t\t\t\t\t</g>\n\t\t\t\t\t\t\t);\n\t\t\t\t\t\t})}\n\t\t\t\t\t</g>\n\n\t\t\t\t\t{/* Data rows */}\n\t\t\t\t\t{rows.map((row, ri) => {\n\t\t\t\t\t\tconst y = HEADER_HEIGHT + ri * (cellSize + CELL_GAP);\n\t\t\t\t\t\treturn (\n\t\t\t\t\t\t\t<g key={row} role=\"row\">\n\t\t\t\t\t\t\t\t{/* Row header */}\n\t\t\t\t\t\t\t\t<g role=\"rowheader\" aria-label={row}>\n\t\t\t\t\t\t\t\t\t<text\n\t\t\t\t\t\t\t\t\t\tx={ROW_LABEL_WIDTH - 8}\n\t\t\t\t\t\t\t\t\t\ty={y + cellSize / 2 + 4}\n\t\t\t\t\t\t\t\t\t\tfontSize={12}\n\t\t\t\t\t\t\t\t\t\ttextAnchor=\"end\"\n\t\t\t\t\t\t\t\t\t\tfill=\"currentColor\"\n\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\t{truncate(row, 24)}\n\t\t\t\t\t\t\t\t\t\t<title>{row}</title>\n\t\t\t\t\t\t\t\t\t</text>\n\t\t\t\t\t\t\t\t</g>\n\t\t\t\t\t\t\t\t{cols.map((col, ci) => {\n\t\t\t\t\t\t\t\t\tconst cell = cellMap.get(`${row}|${col}`);\n\t\t\t\t\t\t\t\t\tconst confidence = classifyCell(cell, greyBelow, suppressBelow);\n\t\t\t\t\t\t\t\t\tconst cx = ROW_LABEL_WIDTH + ci * (cellSize + CELL_GAP);\n\t\t\t\t\t\t\t\t\tconst cy = y;\n\t\t\t\t\t\t\t\t\tconst aria = cellAriaLabel(row, col, cell, confidence, unit, suppressBelow, approx);\n\n\t\t\t\t\t\t\t\t\tconst isActive = active[0] === ri && active[1] === ci;\n\n\t\t\t\t\t\t\t\t\t// Visual fill\n\t\t\t\t\t\t\t\t\tlet fill = 'transparent';\n\t\t\t\t\t\t\t\t\tlet strokeDasharray: string | undefined;\n\t\t\t\t\t\t\t\t\tlet opacity = 1;\n\t\t\t\t\t\t\t\t\tlet stroke: string | undefined;\n\t\t\t\t\t\t\t\t\tlet rectStrokeWidth = 0;\n\n\t\t\t\t\t\t\t\t\tif (confidence === 'absent') {\n\t\t\t\t\t\t\t\t\t\tfill = 'transparent';\n\t\t\t\t\t\t\t\t\t\tstrokeDasharray = '3 3';\n\t\t\t\t\t\t\t\t\t\tstroke = theme === 'dark' ? '#4b5563' : '#9ca3af';\n\t\t\t\t\t\t\t\t\t\trectStrokeWidth = 1;\n\t\t\t\t\t\t\t\t\t} else if (confidence === 'suppress') {\n\t\t\t\t\t\t\t\t\t\tfill = `url(#${suppressPatternId})`;\n\t\t\t\t\t\t\t\t\t\tstrokeDasharray = '3 3';\n\t\t\t\t\t\t\t\t\t\tstroke = theme === 'dark' ? '#4b5563' : '#9ca3af';\n\t\t\t\t\t\t\t\t\t\trectStrokeWidth = 1;\n\t\t\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\t\t\tconst t = vmax > vmin ? ((cell?.value ?? 0) - vmin) / (vmax - vmin) : 0;\n\t\t\t\t\t\t\t\t\t\tfill = interpolateStops(stops, t);\n\t\t\t\t\t\t\t\t\t\tif (confidence === 'grey') {\n\t\t\t\t\t\t\t\t\t\t\topacity = 0.55;\n\t\t\t\t\t\t\t\t\t\t\tstrokeDasharray = '4 2';\n\t\t\t\t\t\t\t\t\t\t\tstroke = theme === 'dark' ? '#6b7280' : '#9ca3af';\n\t\t\t\t\t\t\t\t\t\t\trectStrokeWidth = 1;\n\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\t\t\tconst haloFill = confidence === 'ok' || confidence === 'grey'\n\t\t\t\t\t\t\t\t\t\t? interpolateStops(stops, vmax > vmin ? ((cell?.value ?? 0) - vmin) / (vmax - vmin) : 0)\n\t\t\t\t\t\t\t\t\t\t: '#808080';\n\t\t\t\t\t\t\t\t\tconst outer = haloOuter(haloFill);\n\n\t\t\t\t\t\t\t\t\tconst setRef = (el: SVGGElement | null) => {\n\t\t\t\t\t\t\t\t\t\tif (el) { cellRefs.current.set(`${ri}|${ci}`, el as unknown as HTMLElement); }\n\t\t\t\t\t\t\t\t\t\telse { cellRefs.current.delete(`${ri}|${ci}`); }\n\t\t\t\t\t\t\t\t\t};\n\n\t\t\t\t\t\t\t\t\treturn (\n\t\t\t\t\t\t\t\t\t\t<g\n\t\t\t\t\t\t\t\t\t\t\tkey={col}\n\t\t\t\t\t\t\t\t\t\t\tref={setRef}\n\t\t\t\t\t\t\t\t\t\t\trole=\"gridcell\"\n\t\t\t\t\t\t\t\t\t\t\taria-label={aria}\n\t\t\t\t\t\t\t\t\t\t\tdata-confidence={confidence}\n\t\t\t\t\t\t\t\t\t\t\ttabIndex={isActive ? 0 : -1}\n\t\t\t\t\t\t\t\t\t\t\tonKeyDown={(e) => handleKeyDown(e, ri, ci)}\n\t\t\t\t\t\t\t\t\t\t\tonFocus={() => setActive([ri, ci])}\n\t\t\t\t\t\t\t\t\t\t\tstyle={{ outline: 'none', cursor: 'default' }}\n\t\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\t\t<rect\n\t\t\t\t\t\t\t\t\t\t\t\tx={cx}\n\t\t\t\t\t\t\t\t\t\t\t\ty={cy}\n\t\t\t\t\t\t\t\t\t\t\t\twidth={cellSize}\n\t\t\t\t\t\t\t\t\t\t\t\theight={cellSize}\n\t\t\t\t\t\t\t\t\t\t\t\trx={4}\n\t\t\t\t\t\t\t\t\t\t\t\try={4}\n\t\t\t\t\t\t\t\t\t\t\t\tstyle={{ fill }}\n\t\t\t\t\t\t\t\t\t\t\t\topacity={opacity}\n\t\t\t\t\t\t\t\t\t\t\t\tstroke={stroke}\n\t\t\t\t\t\t\t\t\t\t\t\tstrokeWidth={rectStrokeWidth}\n\t\t\t\t\t\t\t\t\t\t\t\tstrokeDasharray={strokeDasharray}\n\t\t\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\t\t\t<title>{aria}</title>\n\t\t\t\t\t\t\t\t\t\t\t</rect>\n\t\t\t\t\t\t\t\t\t\t\t{isActive\n\t\t\t\t\t\t\t\t\t\t\t\t? (\n\t\t\t\t\t\t\t\t\t\t\t\t\t<>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t<rect\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tx={cx - 1}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\ty={cy - 1}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\twidth={cellSize + 2}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\theight={cellSize + 2}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\trx={5}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\try={5}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tfill=\"none\"\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tstroke={outer}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tstrokeWidth={1}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tpointerEvents=\"none\"\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t<rect\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tx={cx}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\ty={cy}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\twidth={cellSize}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\theight={cellSize}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\trx={4}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\try={4}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tfill=\"none\"\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tstroke=\"var(--color-accent, #3b82f6)\"\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tstrokeWidth={1}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tpointerEvents=\"none\"\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t\t\t\t\t\t\t</>\n\t\t\t\t\t\t\t\t\t\t\t\t)\n\t\t\t\t\t\t\t\t\t\t\t\t: null}\n\t\t\t\t\t\t\t\t\t\t</g>\n\t\t\t\t\t\t\t\t\t);\n\t\t\t\t\t\t\t\t})}\n\t\t\t\t\t\t\t</g>\n\t\t\t\t\t\t);\n\t\t\t\t\t})}\n\t\t\t\t</svg>\n\t\t\t</div>\n\n\t\t\t{/* Color-scale legend below grid */}\n\t\t\t<div style={{ marginTop: 8 }}>\n\t\t\t\t<HeatmapColorLegend\n\t\t\t\t\tstops={stops}\n\t\t\t\t\tvmin={vmin}\n\t\t\t\t\tvmax={vmax}\n\t\t\t\t\twidth={gridWidth}\n\t\t\t\t\taxis={data.axis}\n\t\t\t\t\tapprox={approx}\n\t\t\t\t/>\n\t\t\t</div>\n\t\t</div>\n\t);\n}\n","export interface ParsedReplicationPath {\n\tsource: string;\n\tdatabase: string;\n\ttable: string;\n}\n\n/** Parse a replication-latency `path` field shaped `<source>.<db>.<table>`.\n *\n * Two-stage parse:\n * 1. **Anchored** — try the longest matching node from `knownNodes`. If\n * a hostname prefix matches exactly, this is precise.\n * 2. **Heuristic fallback** — if no known-node prefix matches, treat the\n * last two dot-segments as `<db>.<table>` and everything before as the\n * source FQDN. Recovers records whose source isn't a destination in\n * the current cluster snapshot (asymmetric replication, decommissioned\n * nodes, gateways that send but never receive). May mis-parse if a\n * database name itself contains dots — accept the trade since the\n * alternative is silently dropping the record.\n *\n * Returns null only when the path is empty, < 3 segments, or has empty\n * trailing segments. */\nexport function parseReplicationPath(\n\tpath: string,\n\tknownNodes: readonly string[],\n): ParsedReplicationPath | null {\n\tif (typeof path !== 'string' || path.length === 0) { return null; }\n\n\tconst sorted = [...knownNodes].sort((a, b) => b.length - a.length);\n\n\tfor (const node of sorted) {\n\t\tif (!path.startsWith(node + '.')) { continue; }\n\t\tconst rest = path.slice(node.length + 1);\n\t\tconst firstDot = rest.indexOf('.');\n\t\tif (firstDot === -1) { return null; }\n\t\tconst database = rest.slice(0, firstDot);\n\t\tconst table = rest.slice(firstDot + 1);\n\t\tif (database.length === 0 || table.length === 0) { return null; }\n\t\treturn { source: node, database, table };\n\t}\n\n\t// Heuristic: split on '.', last two segments are db.table.\n\tconst segments = path.split('.');\n\tif (segments.length < 3) { return null; }\n\tconst table = segments[segments.length - 1];\n\tconst database = segments[segments.length - 2];\n\tconst source = segments.slice(0, -2).join('.');\n\tif (!source || !database || !table) { return null; }\n\treturn { source, database, table };\n}\n","import { type JSX, useMemo, useState } from 'react';\nimport { HeatmapMatrix } from '../primitives/HeatmapMatrix.tsx';\nimport { LineChart } from '../primitives/LineChart.tsx';\nimport type {\n\tAnalyticsDataPoint,\n\tHeatmapCell,\n\tHeatmapData,\n\tMetricSpec,\n\tSeries,\n\tSeriesData,\n\tSeriesPoint,\n\tSpecRegistryRendererProps,\n} from '../types/analytics.ts';\nimport { type AggInput, aggregate } from './aggregators.ts';\nimport { parseReplicationPath } from './pathParser.ts';\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Spec\n// ─────────────────────────────────────────────────────────────────────────────\n\nexport const replicationLatencySpec: MetricSpec = {\n\ttitle: 'Replication latency',\n\tdescription: 'Source → destination p95 latency, count-weighted-mean across the window. Approximate.',\n\ttab: 'replication',\n\tprimaryDimension: 'path',\n\tsubDimension: 'node',\n\tseries: {\n\t\tkind: 'groupBy',\n\t\tdimension: 'path',\n\t\tfield: { field: 'p95', label: 'p95 latency' },\n\t},\n\ttimestamp: 'time',\n\tbucket: { source: 'period-field', fallbackMs: 60000 },\n\taggregator: { temporal: 'count-weighted-mean', crossNode: 'count-weighted-mean' },\n\t// TODO(spec): {greyBelow:40, suppressBelow:100} are calibrated for high-volume clusters.\n\t// Real Harper data with per-record count 2-14 may need re-tuning. See Step 2.5 follow-up.\n\tconfidence: { field: 'count', greyBelow: 40, suppressBelow: 100 },\n\tprimitive: 'heatmap',\n\tyAxis: { unit: '', formatter: 'ms' },\n};\n\nfunction pluralize(n: number, one: string, many: string): string {\n\treturn n === 1 ? one : many;\n}\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Pure pipeline: records -> HeatmapData\n// ─────────────────────────────────────────────────────────────────────────────\n\ninterface ParsedRecord {\n\tsource: string;\n\tdestination: string;\n\tvalue: number;\n\tcount: number;\n\ttime: number;\n}\n\nimport { QUANTILE_FIELDS as REPLICATION_QUANTILE_FIELDS, type QuantileField } from './quantileFields.ts';\nexport { REPLICATION_QUANTILE_FIELDS };\nexport type ReplicationQuantileField = QuantileField['field'];\n\nfunction parseRecords(\n\trecords: AnalyticsDataPoint[],\n\tnodes: readonly string[],\n\tquantileField: ReplicationQuantileField,\n): { parsed: ParsedRecord[]; skipped: number; unrecognizedSources: string[] } {\n\tlet skipped = 0;\n\tconst parsed: ParsedRecord[] = [];\n\tconst unrecognized = new Set<string>();\n\tconst knownSet = new Set(nodes);\n\tfor (const r of records) {\n\t\tconst path = typeof r.path === 'string' ? r.path : '';\n\t\tconst parsedPath = parseReplicationPath(path, nodes);\n\t\tif (!parsedPath) {\n\t\t\tskipped++;\n\t\t\tcontinue;\n\t\t}\n\t\t// pathParser falls back to a heuristic split when no known-node\n\t\t// matches. Track the heuristic-recovered sources so the renderer\n\t\t// can surface them — the operator may want to confirm those are\n\t\t// real peers.\n\t\tif (!knownSet.has(parsedPath.source)) {\n\t\t\tunrecognized.add(parsedPath.source);\n\t\t}\n\t\tconst v = (r as Record<string, unknown>)[quantileField];\n\t\tconst value = typeof v === 'number' ? v : NaN;\n\t\tconst count = typeof r.count === 'number' ? r.count : 0;\n\t\tif (!Number.isFinite(value)) {\n\t\t\tskipped++;\n\t\t\tcontinue;\n\t\t}\n\t\tparsed.push({\n\t\t\tsource: parsedPath.source,\n\t\t\tdestination: r.node,\n\t\t\tvalue,\n\t\t\tcount,\n\t\t\ttime: typeof r.time === 'number' ? r.time : 0,\n\t\t});\n\t}\n\treturn { parsed, skipped, unrecognizedSources: [...unrecognized].sort() };\n}\n\nexport function aggregateReplicationMatrix(\n\trecords: AnalyticsDataPoint[],\n\tnodes: readonly string[],\n\tquantileField: ReplicationQuantileField = 'p95',\n): HeatmapData {\n\tconst { parsed, skipped, unrecognizedSources } = parseRecords(records, nodes, quantileField);\n\n\tif (parsed.length === 0) {\n\t\treturn {\n\t\t\trows: [],\n\t\t\tcols: [],\n\t\t\tcells: [],\n\t\t\taxis: { unit: '', formatter: 'ms' },\n\t\t\tconfidence: { greyBelow: 40, suppressBelow: 100 },\n\t\t\trowAxisLabel: 'Source',\n\t\t\tcolAxisLabel: 'Destination',\n\t\t\tskippedRecordsCount: skipped,\n\t\t\tunrecognizedSources,\n\t\t\tapprox: true,\n\t\t};\n\t}\n\n\t// Group by (source, destination)\n\tconst groups = new Map<string, { items: AggInput[]; totalCount: number }>();\n\tconst sourceSet = new Set<string>();\n\tconst destSet = new Set<string>();\n\tfor (const r of parsed) {\n\t\tsourceSet.add(r.source);\n\t\tdestSet.add(r.destination);\n\t\tconst key = `${r.source}|${r.destination}`;\n\t\tlet g = groups.get(key);\n\t\tif (!g) {\n\t\t\tg = { items: [], totalCount: 0 };\n\t\t\tgroups.set(key, g);\n\t\t}\n\t\tg.items.push({ value: r.value, count: r.count });\n\t\tg.totalCount += r.count;\n\t}\n\n\tconst rows = [...sourceSet].sort();\n\tconst cols = [...destSet].sort();\n\n\tlet approx = false;\n\tconst cells: HeatmapCell[] = [];\n\tfor (const row of rows) {\n\t\tfor (const col of cols) {\n\t\t\tconst g = groups.get(`${row}|${col}`);\n\t\t\tif (g) {\n\t\t\t\tif (g.items.length > 1) { approx = true; }\n\t\t\t\tcells.push({\n\t\t\t\t\trow,\n\t\t\t\t\tcol,\n\t\t\t\t\tvalue: aggregate('count-weighted-mean', g.items),\n\t\t\t\t\tcount: g.totalCount,\n\t\t\t\t});\n\t\t\t} else {\n\t\t\t\tcells.push({ row, col, value: null, count: 0 });\n\t\t\t}\n\t\t}\n\t}\n\n\treturn {\n\t\trows,\n\t\tcols,\n\t\tcells,\n\t\taxis: { unit: '', formatter: 'ms' },\n\t\tconfidence: { greyBelow: 40, suppressBelow: 100 },\n\t\trowAxisLabel: 'Source',\n\t\tcolAxisLabel: 'Destination',\n\t\tskippedRecordsCount: skipped,\n\t\tunrecognizedSources,\n\t\tapprox,\n\t};\n}\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Pure helper: per (source, dest) line series with count-weighted-mean buckets\n// keyed by record.time. Returns approx=true when any time-bucket aggregates\n// more than one source record (e.g. multiple table paths).\n// ─────────────────────────────────────────────────────────────────────────────\n\nexport function bucketLineSeries(\n\trecords: AnalyticsDataPoint[],\n\tsource: string,\n\tdest: string,\n\tnodes: readonly string[],\n\tquantileField: ReplicationQuantileField = 'p95',\n): { points: SeriesPoint[]; approx: boolean } {\n\tconst matching: { time: number; value: number; count: number }[] = [];\n\tfor (const r of records) {\n\t\tif (r.node !== dest) { continue; }\n\t\tconst path = typeof r.path === 'string' ? r.path : '';\n\t\tconst parsed = parseReplicationPath(path, nodes);\n\t\tif (!parsed || parsed.source !== source) { continue; }\n\t\tif (typeof r.time !== 'number') { continue; }\n\t\tconst v = (r as Record<string, unknown>)[quantileField];\n\t\tconst value = typeof v === 'number' ? v : NaN;\n\t\tif (!Number.isFinite(value)) { continue; }\n\t\tconst count = typeof r.count === 'number' ? r.count : 0;\n\t\tmatching.push({ time: r.time, value, count });\n\t}\n\n\tconst bucketsByTime = new Map<number, AggInput[]>();\n\tconst totalCountByTime = new Map<number, number>();\n\tfor (const m of matching) {\n\t\tlet bucket = bucketsByTime.get(m.time);\n\t\tif (!bucket) {\n\t\t\tbucket = [];\n\t\t\tbucketsByTime.set(m.time, bucket);\n\t\t}\n\t\tbucket.push({ value: m.value, count: m.count });\n\t\ttotalCountByTime.set(m.time, (totalCountByTime.get(m.time) ?? 0) + m.count);\n\t}\n\n\tlet approx = false;\n\tconst sortedTimes = [...bucketsByTime.keys()].sort((a, b) => a - b);\n\tconst points: SeriesPoint[] = [];\n\tfor (const time of sortedTimes) {\n\t\tconst bucket = bucketsByTime.get(time)!;\n\t\tif (bucket.length > 1) { approx = true; }\n\t\tpoints.push({\n\t\t\tx: time,\n\t\t\ty: aggregate('count-weighted-mean', bucket),\n\t\t\tcount: totalCountByTime.get(time) ?? 0,\n\t\t});\n\t}\n\n\treturn { points, approx };\n}\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Renderer\n// ─────────────────────────────────────────────────────────────────────────────\n\nconst FALLBACK_MAX_CELLS = 12;\n\nfunction buildLineSeries(\n\trecords: AnalyticsDataPoint[],\n\tnodes: readonly string[],\n\tdata: HeatmapData,\n\tquantileField: ReplicationQuantileField,\n): { seriesData: SeriesData; omittedPairsCount: number } {\n\tconst greyBelow = data.confidence?.greyBelow ?? 0;\n\tconst suppressBelow = data.confidence?.suppressBelow ?? Infinity;\n\n\tconst series: Series[] = [];\n\tlet omittedPairsCount = 0;\n\n\tfor (const cell of data.cells) {\n\t\tconst count = cell.count ?? 0;\n\t\tif (count === 0) {\n\t\t\t// Truly absent pair — skip silently.\n\t\t\tcontinue;\n\t\t}\n\t\tif (count < greyBelow) {\n\t\t\tomittedPairsCount += 1;\n\t\t\tcontinue;\n\t\t}\n\t\tconst out = bucketLineSeries(records, cell.row, cell.col, nodes, quantileField);\n\t\tif (out.points.length === 0) { continue; }\n\t\tconst key = `${cell.row}→${cell.col}`;\n\t\tif (count < suppressBelow) {\n\t\t\t// Grey tier: dim to flag low confidence.\n\t\t\tseries.push({\n\t\t\t\tkey,\n\t\t\t\tlabel: key,\n\t\t\t\tpoints: out.points,\n\t\t\t\tapprox: out.approx,\n\t\t\t\topacity: 0.55,\n\t\t\t});\n\t\t} else {\n\t\t\tseries.push({\n\t\t\t\tkey,\n\t\t\t\tlabel: key,\n\t\t\t\tpoints: out.points,\n\t\t\t\tapprox: out.approx,\n\t\t\t});\n\t\t}\n\t}\n\n\treturn { seriesData: { series }, omittedPairsCount };\n}\n\nexport function ReplicationLatencyRenderer(props: SpecRegistryRendererProps): JSX.Element {\n\tconst { records, nodes, theme, timeRange, fillParent } = props;\n\n\tconst [quantile, setQuantile] = useState<ReplicationQuantileField>('p95');\n\n\tconst data = useMemo(\n\t\t() => aggregateReplicationMatrix(records, nodes, quantile),\n\t\t[records, nodes, quantile],\n\t);\n\n\t// Empty state — still surface skipped-records banner so users see the cause\n\t// when 100% of records had unparseable source nodes.\n\tif (data.rows.length === 0 || data.cols.length === 0) {\n\t\treturn (\n\t\t\t<div>\n\t\t\t\t<RecognitionBanner data={data} theme={theme} />\n\n\t\t\t\t<div>No data in window</div>\n\t\t\t</div>\n\t\t);\n\t}\n\n\tconst cellTotal = data.rows.length * data.cols.length;\n\tconst tooManyCells = cellTotal > FALLBACK_MAX_CELLS;\n\tconst tooFewDimensions = data.rows.length < 2 || data.cols.length < 2;\n\tconst useFallback = tooFewDimensions || tooManyCells;\n\n\tif (useFallback) {\n\t\tconst { seriesData, omittedPairsCount } = buildLineSeries(records, nodes, data, quantile);\n\t\tconst greyBelow = data.confidence?.greyBelow ?? 40;\n\t\tconst message = tooManyCells\n\t\t\t? 'Too many source-destination pairs for a heatmap — showing as lines.'\n\t\t\t: 'Only one source node emitted data in this window. This is typical for clusters with a single write origin — each line below shows latency from that source to one destination.';\n\n\t\tconst allDropped = seriesData.series.length === 0 && omittedPairsCount > 0;\n\t\tconst noData = seriesData.series.length === 0 && omittedPairsCount === 0;\n\n\t\tconst warningStyle = {\n\t\t\tmarginBottom: 8,\n\t\t\tpadding: '4px 8px',\n\t\t\tfontSize: 12,\n\t\t\tborderLeft: '3px solid var(--color-warning, #f59e0b)',\n\t\t\tbackground: theme === 'dark' ? '#1f2937' : '#fffbeb',\n\t\t\tcolor: 'currentColor',\n\t\t} as const;\n\n\t\t// Banners stack at the top in normal document flow; the chart sits\n\t\t// below with explicit vertical space. No flex height-juggling — the\n\t\t// fixed-height LineChart was clipping/overlapping when it competed\n\t\t// with the banner stack for a flex-shared box.\n\t\treturn (\n\t\t\t<div>\n\t\t\t\t<div\n\t\t\t\t\tstyle={{\n\t\t\t\t\t\tfontSize: 11,\n\t\t\t\t\t\topacity: 0.7,\n\t\t\t\t\t\tmarginBottom: 4,\n\t\t\t\t\t}}\n\t\t\t\t>\n\t\t\t\t\tShowing as lines\n\t\t\t\t</div>\n\t\t\t\t{data.skippedRecordsCount > 0\n\t\t\t\t\t? (\n\t\t\t\t\t\t<div\n\t\t\t\t\t\t\tkey={data.skippedRecordsCount}\n\t\t\t\t\t\t\trole=\"status\"\n\t\t\t\t\t\t\taria-atomic=\"true\"\n\t\t\t\t\t\t\tstyle={warningStyle}\n\t\t\t\t\t\t>\n\t\t\t\t\t\t\t{data.unrecognizedSources && data.unrecognizedSources.length > 0\n\t\t\t\t\t\t\t\t? `${data.skippedRecordsCount} record(s) omitted (no value for the selected percentile). Sources recovered via heuristic: ${\n\t\t\t\t\t\t\t\t\tdata.unrecognizedSources.join(', ')\n\t\t\t\t\t\t\t\t}.`\n\t\t\t\t\t\t\t\t: `${data.skippedRecordsCount} record(s) omitted (no value for the selected percentile).`}\n\t\t\t\t\t\t</div>\n\t\t\t\t\t)\n\t\t\t\t\t: null}\n\t\t\t\t{\n\t\t\t\t\t/* Suppress the omitted-pairs banner when all-dropped fires — the\n\t\t\t\t all-dropped banner already cites the count, so showing both is\n\t\t\t\t redundant. */\n\t\t\t\t}\n\t\t\t\t{omittedPairsCount > 0 && !allDropped\n\t\t\t\t\t? (\n\t\t\t\t\t\t<div\n\t\t\t\t\t\t\tkey={omittedPairsCount}\n\t\t\t\t\t\t\trole=\"status\"\n\t\t\t\t\t\t\taria-atomic=\"true\"\n\t\t\t\t\t\t\tstyle={warningStyle}\n\t\t\t\t\t\t>\n\t\t\t\t\t\t\t{`${omittedPairsCount} source-destination ${\n\t\t\t\t\t\t\t\tpluralize(omittedPairsCount, 'pair', 'pairs')\n\t\t\t\t\t\t\t} hidden — fewer than ${greyBelow} samples.`}\n\t\t\t\t\t\t</div>\n\t\t\t\t\t)\n\t\t\t\t\t: null}\n\t\t\t\t<div\n\t\t\t\t\tstyle={{\n\t\t\t\t\t\tmarginBottom: 8,\n\t\t\t\t\t\tpadding: '4px 8px',\n\t\t\t\t\t\tfontSize: 12,\n\t\t\t\t\t\tborderLeft: '3px solid var(--color-info, #3b82f6)',\n\t\t\t\t\t\tbackground: theme === 'dark' ? '#0f172a' : '#eff6ff',\n\t\t\t\t\t\tcolor: 'currentColor',\n\t\t\t\t\t}}\n\t\t\t\t>\n\t\t\t\t\t{message}\n\t\t\t\t</div>\n\t\t\t\t{allDropped\n\t\t\t\t\t? (\n\t\t\t\t\t\t<div\n\t\t\t\t\t\t\trole=\"status\"\n\t\t\t\t\t\t\taria-atomic=\"true\"\n\t\t\t\t\t\t\tstyle={warningStyle}\n\t\t\t\t\t\t>\n\t\t\t\t\t\t\t{`No source-destination pairs cleared the confidence threshold (${greyBelow}+ samples). All ${omittedPairsCount} ${\n\t\t\t\t\t\t\t\tpluralize(omittedPairsCount, 'pair', 'pairs')\n\t\t\t\t\t\t\t} had fewer than ${greyBelow} samples in this window.`}\n\t\t\t\t\t\t</div>\n\t\t\t\t\t)\n\t\t\t\t\t: noData\n\t\t\t\t\t? <div>No data in window</div>\n\t\t\t\t\t: (\n\t\t\t\t\t\t<div style={{ marginTop: 20 }}>\n\t\t\t\t\t\t\t<LineChart\n\t\t\t\t\t\t\t\tdata={seriesData}\n\t\t\t\t\t\t\t\ttheme={theme}\n\t\t\t\t\t\t\t\tyAxis={data.axis}\n\t\t\t\t\t\t\t\theight={320}\n\t\t\t\t\t\t\t\txDomain={[timeRange.startTime, timeRange.endTime]}\n\t\t\t\t\t\t\t\tfillParent={fillParent}\n\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t)}\n\t\t\t</div>\n\t\t);\n\t}\n\n\treturn (\n\t\t<div className=\"flex h-full flex-col\">\n\t\t\t<QuantileSelector value={quantile} onChange={setQuantile} />\n\t\t\t<RecognitionBanner data={data} theme={theme} />\n\t\t\t<div className=\"min-h-0 flex-1\">\n\t\t\t\t<HeatmapMatrix data={data} theme={theme} title=\"Replication latency\" />\n\t\t\t</div>\n\t\t</div>\n\t);\n}\n\ninterface RecognitionBannerProps {\n\tdata: HeatmapData;\n\ttheme: 'light' | 'dark';\n}\n\n/** Renders a single role='status' banner combining skipped-record count\n * and heuristic-recovered source count. Either or both may be present. */\nfunction RecognitionBanner({ data, theme }: RecognitionBannerProps) {\n\tconst skipped = data.skippedRecordsCount;\n\tconst unrecognized = data.unrecognizedSources ?? [];\n\tif (skipped === 0 && unrecognized.length === 0) { return null; }\n\n\tconst parts: string[] = [];\n\tif (skipped > 0) {\n\t\tparts.push(`${skipped} record${skipped === 1 ? '' : 's'} omitted (no value for the selected percentile).`);\n\t}\n\tif (unrecognized.length > 0) {\n\t\tparts.push(\n\t\t\t`Recovered ${unrecognized.length} source${unrecognized.length === 1 ? '' : 's'} not in the cluster snapshot: ${\n\t\t\t\tunrecognized.join(', ')\n\t\t\t}.`,\n\t\t);\n\t}\n\n\treturn (\n\t\t<div\n\t\t\tkey={`${skipped}-${unrecognized.length}`}\n\t\t\trole=\"status\"\n\t\t\taria-atomic=\"true\"\n\t\t\tstyle={{\n\t\t\t\tmarginBottom: 8,\n\t\t\t\tpadding: '4px 8px',\n\t\t\t\tfontSize: 12,\n\t\t\t\tborderLeft: '3px solid var(--color-warning, #f59e0b)',\n\t\t\t\tbackground: theme === 'dark' ? '#1f2937' : '#fffbeb',\n\t\t\t\tcolor: 'currentColor',\n\t\t\t}}\n\t\t>\n\t\t\t{parts.join(' ')}\n\t\t</div>\n\t);\n}\n\ninterface QuantileSelectorProps {\n\tvalue: ReplicationQuantileField;\n\tonChange: (q: ReplicationQuantileField) => void;\n}\n\nfunction QuantileSelector({ value, onChange }: QuantileSelectorProps) {\n\treturn (\n\t\t<div role=\"radiogroup\" aria-label=\"Quantile\" className=\"flex flex-wrap gap-1 pb-2\">\n\t\t\t{REPLICATION_QUANTILE_FIELDS.map((q) => {\n\t\t\t\tconst active = q.field === value;\n\t\t\t\treturn (\n\t\t\t\t\t<button\n\t\t\t\t\t\tkey={q.field}\n\t\t\t\t\t\ttype=\"button\"\n\t\t\t\t\t\trole=\"radio\"\n\t\t\t\t\t\taria-checked={active}\n\t\t\t\t\t\tdata-testid=\"quantile-button\"\n\t\t\t\t\t\tdata-value={q.field}\n\t\t\t\t\t\tonClick={() => onChange(q.field)}\n\t\t\t\t\t\tclassName={`rounded px-2 py-0.5 text-[11px] ${\n\t\t\t\t\t\t\tactive\n\t\t\t\t\t\t\t\t? 'bg-(--color-accent)/20 text-(--color-text-primary) font-semibold'\n\t\t\t\t\t\t\t\t: 'bg-(--color-bg-tertiary) text-(--color-text-secondary) hover:text-(--color-text-primary)'\n\t\t\t\t\t\t}`}\n\t\t\t\t\t>\n\t\t\t\t\t\t{q.label}\n\t\t\t\t\t</button>\n\t\t\t\t);\n\t\t\t})}\n\t\t</div>\n\t);\n}\n","import type { MetricSpec } from '../types/analytics.ts';\n\n// Per-field `crossNode` aggregator overrides (e.g. cpuUtilization's\n// `crossNode: 'max'`) are honored by pipeline.ts as of Step 4.5: the\n// temporal aggregator runs per (time, node), then the crossNode aggregator\n// folds across nodes within each time bucket.\nexport const resourceUsageSpec: MetricSpec = {\n\ttitle: 'Resource usage',\n\tdescription: 'CPU + I/O + page faults + context switches per node — small-multiples view.',\n\ttab: 'health',\n\tprimaryDimension: 'node',\n\tseries: {\n\t\tkind: 'field',\n\t\tfields: [\n\t\t\t{\n\t\t\t\tfield: 'cpuUtilization',\n\t\t\t\t// Cores-equivalent CPU consumption per process. 1.0 = one\n\t\t\t\t// core fully busy; saturating an N-core box means the value\n\t\t\t\t// approaches N. The previous `percent-of-core` transform (×100)\n\t\t\t\t// was cancelled by the cores formatter (÷100); both removed.\n\t\t\t\tlabel: 'Process CPU (cores used)',\n\t\t\t\taggregator: { temporal: 'max', crossNode: 'max' },\n\t\t\t\tyAxis: { unit: '', formatter: 'cores' },\n\t\t\t},\n\t\t\t{\n\t\t\t\tfield: 'fsWrite',\n\t\t\t\tlabel: 'Disk write (B/s)',\n\t\t\t\ttransform: { kind: 'rate' },\n\t\t\t\taggregator: { temporal: 'sum', crossNode: 'sum' },\n\t\t\t\tyAxis: { unit: '/s', formatter: 'bytes-si' },\n\t\t\t},\n\t\t\t{\n\t\t\t\tfield: 'majorPageFault',\n\t\t\t\tlabel: 'Major page faults /s',\n\t\t\t\ttransform: { kind: 'rate' },\n\t\t\t\taggregator: { temporal: 'sum', crossNode: 'max' },\n\t\t\t\tyAxis: { unit: '/s', formatter: 'count-si' },\n\t\t\t},\n\t\t\t{\n\t\t\t\tfield: {\n\t\t\t\t\tkind: 'op',\n\t\t\t\t\top: '+',\n\t\t\t\t\tleft: { kind: 'ref', field: 'voluntaryContextSwitches' },\n\t\t\t\t\tright: { kind: 'ref', field: 'involuntaryContextSwitches' },\n\t\t\t\t},\n\t\t\t\tlabel: 'Context switches /s',\n\t\t\t\ttransform: { kind: 'rate' },\n\t\t\t\taggregator: { temporal: 'sum', crossNode: 'max' },\n\t\t\t\tyAxis: { unit: '/s', formatter: 'count-si' },\n\t\t\t},\n\t\t],\n\t},\n\ttimestamp: 'time',\n\tbucket: { source: 'period-field', fallbackMs: 60000 },\n\taggregator: { temporal: 'sum', crossNode: 'sum' },\n\tprimitive: 'small-multiples',\n\tyAxis: { unit: '', formatter: 'count' },\n\tlayout: { colSpan: 2 },\n};\n","import { DimensionSelectorRenderer } from '../primitives/DimensionSelectorRenderer.tsx';\nimport type { AnalyticsDataPoint, MetricSpec, TimeRange } from '../types/analytics.ts';\n\nexport const response200Spec: MetricSpec = {\n\ttitle: 'HTTP 200 ratio',\n\tdescription: 'Per-path 2xx ratio (mean across nodes; count-weighted across time). Threshold 99.9%, min count 1000.',\n\ttab: 'requests',\n\tprimaryDimension: 'path',\n\tsubDimension: 'method',\n\tseries: {\n\t\tkind: 'groupBy',\n\t\tdimension: 'path',\n\t\t// Harper omits `ratio` on operation/fastify-route records but always\n\t\t// emits total + count. Compute via FieldExpr to avoid silently\n\t\t// dropping ~34% of records. See success.tsx for the matching rationale.\n\t\tfield: {\n\t\t\tfield: {\n\t\t\t\tkind: 'op',\n\t\t\t\top: '/',\n\t\t\t\tleft: { kind: 'ref', field: 'total' },\n\t\t\t\tright: { kind: 'ref', field: 'count' },\n\t\t\t},\n\t\t\tlabel: '200 ratio',\n\t\t},\n\t\ttopN: 10,\n\t\totherBucket: true,\n\t},\n\ttimestamp: 'time',\n\tbucket: { source: 'period-field', fallbackMs: 60000 },\n\taggregator: { temporal: 'mean', crossNode: 'count-weighted-mean' },\n\tconfidence: { field: 'count', greyBelow: 40, suppressBelow: 100 },\n\tprimitive: 'line',\n\tyAxis: { unit: '', formatter: 'percent' },\n\tthresholds: [\n\t\t{ value: 0.999, label: '99.9% SLO', direction: 'below-is-bad', minCount: 1000 },\n\t],\n};\n\ninterface RendererProps {\n\trecords: AnalyticsDataPoint[];\n\ttimeRange: TimeRange;\n\tnodes: string[];\n\ttheme: 'light' | 'dark';\n\tviewMode?: 'per-node' | 'aggregate';\n}\n\nexport function Response200Renderer(props: RendererProps) {\n\treturn <DimensionSelectorRenderer spec={response200Spec} {...props} ariaLabel=\"Path\" />;\n}\n","import type { MetricSpec } from '../types/analytics.ts';\n\nexport const storageVolumeSpec: MetricSpec = {\n\ttitle: 'Storage volume (available)',\n\tdescription: 'Per-node disk available bytes (latest snapshot in window; mean across nodes).',\n\ttab: 'storage',\n\tprimaryDimension: 'node',\n\tseries: {\n\t\tkind: 'groupBy',\n\t\tdimension: 'node',\n\t\tfield: { field: 'available', label: 'available (bytes)' },\n\t},\n\ttimestamp: 'id',\n\tbucket: { source: 'period-field', fallbackMs: 60000 },\n\taggregator: { temporal: 'last', crossNode: 'mean' },\n\tprimitive: 'line',\n\tyAxis: { unit: ' B', formatter: 'bytes-si' },\n};\n","import { DimensionSelectorRenderer } from '../primitives/DimensionSelectorRenderer.tsx';\nimport type { AnalyticsDataPoint, MetricSpec, TimeRange } from '../types/analytics.ts';\n\nexport const successSpec: MetricSpec = {\n\ttitle: 'Request success rate',\n\tdescription: 'Per-path success ratio (count-weighted-mean) — alert when ≥0.001 errors and Σcount ≥1000.',\n\ttab: 'requests',\n\tprimaryDimension: 'path',\n\tsubDimension: 'method',\n\tseries: {\n\t\tkind: 'groupBy',\n\t\tdimension: 'path',\n\t\t// Harper omits `ratio` on operation/fastify-route records (~34% of the\n\t\t// fixture) but always emits total + count. Compute total/count via\n\t\t// FieldExpr instead of reading the optional `ratio` field directly so\n\t\t// those records aren't silently dropped — without this, the displayed\n\t\t// success-rate is biased toward whichever request types Harper happens\n\t\t// to ratio-tag. fieldExpr.ts returns null for count === 0 (div-by-zero\n\t\t// guard), so 0-count buckets still gap correctly.\n\t\tfield: {\n\t\t\tfield: {\n\t\t\t\tkind: 'op',\n\t\t\t\top: '/',\n\t\t\t\tleft: { kind: 'ref', field: 'total' },\n\t\t\t\tright: { kind: 'ref', field: 'count' },\n\t\t\t},\n\t\t\tlabel: 'success ratio',\n\t\t},\n\t\ttopN: 10,\n\t\totherBucket: true,\n\t},\n\ttimestamp: 'time',\n\tbucket: { source: 'period-field', fallbackMs: 60000 },\n\taggregator: { temporal: 'count-weighted-mean', crossNode: 'count-weighted-mean' },\n\tconfidence: { field: 'count', greyBelow: 40, suppressBelow: 100 },\n\tprimitive: 'line',\n\tyAxis: { unit: '', formatter: 'percent' },\n\tthresholds: [\n\t\t{ value: 0.999, label: '99.9% SLO', direction: 'below-is-bad', minCount: 1000 },\n\t],\n};\n\ninterface RendererProps {\n\trecords: AnalyticsDataPoint[];\n\ttimeRange: TimeRange;\n\tnodes: string[];\n\ttheme: 'light' | 'dark';\n\tviewMode?: 'per-node' | 'aggregate';\n}\n\nexport function SuccessRenderer(props: RendererProps) {\n\treturn <DimensionSelectorRenderer spec={successSpec} {...props} ariaLabel=\"Path\" />;\n}\n","import type { MetricSpec } from '../types/analytics.ts';\n\nexport const tlsReusedSpec: MetricSpec = {\n\ttitle: 'TLS session reuse ratio',\n\tdescription: 'Fraction of TLS handshakes resumed via session ticket — higher is better.',\n\ttab: 'traffic',\n\tprimaryDimension: 'node',\n\tseries: {\n\t\tkind: 'groupBy',\n\t\tdimension: 'node',\n\t\tfield: { field: 'ratio', label: 'reuse ratio', transform: { kind: 'ratio' } },\n\t},\n\ttimestamp: 'time',\n\tbucket: { source: 'period-field', fallbackMs: 60000 },\n\taggregator: { temporal: 'count-weighted-mean', crossNode: 'count-weighted-mean' },\n\tconfidence: { field: 'count', greyBelow: 20, suppressBelow: 50 },\n\tprimitive: 'line',\n\tyAxis: { unit: '', formatter: 'percent' },\n\tthresholds: [\n\t\t{ value: 0.5, label: '50% reuse target', direction: 'below-is-bad', minCount: 50 },\n\t],\n};\n","import { DimensionSelectorRenderer } from '../primitives/DimensionSelectorRenderer.tsx';\nimport type { AnalyticsDataPoint, MetricSpec, TimeRange } from '../types/analytics.ts';\nimport { QUANTILE_DEFAULT, QUANTILE_FIELDS } from './quantileFields.ts';\n\nexport const transferSpec: MetricSpec = {\n\ttitle: 'Transfer duration (p95)',\n\tdescription: 'Per-path transfer p95 (count-weighted-mean) — top 10 paths + Other.',\n\ttab: 'requests',\n\tprimaryDimension: 'path',\n\tsubDimension: 'method',\n\tseries: {\n\t\tkind: 'groupBy',\n\t\tdimension: 'path',\n\t\tfield: { field: 'p95', label: 'p95 transfer (ms)' },\n\t\ttopN: 10,\n\t\totherBucket: true,\n\t},\n\ttimestamp: 'time',\n\tbucket: { source: 'period-field', fallbackMs: 60000 },\n\taggregator: { temporal: 'count-weighted-mean', crossNode: 'count-weighted-mean' },\n\tconfidence: { field: 'count', greyBelow: 40, suppressBelow: 100 },\n\tprimitive: 'line',\n\tyAxis: { unit: '', formatter: 'ms' },\n\tquantileSelector: { fields: QUANTILE_FIELDS, default: QUANTILE_DEFAULT },\n};\n\ninterface RendererProps {\n\trecords: AnalyticsDataPoint[];\n\ttimeRange: TimeRange;\n\tnodes: string[];\n\ttheme: 'light' | 'dark';\n\tviewMode?: 'per-node' | 'aggregate';\n}\n\nexport function TransferRenderer(props: RendererProps) {\n\treturn <DimensionSelectorRenderer spec={transferSpec} {...props} ariaLabel=\"Path\" />;\n}\n","import type { MetricSpec } from '../types/analytics.ts';\n\nexport const utilizationSpec: MetricSpec = {\n\ttitle: 'Cluster utilization',\n\tdescription: 'Active / (active + idle) per node — count-weighted-mean across time.',\n\ttab: 'health',\n\tprimaryDimension: 'node',\n\tseries: {\n\t\tkind: 'groupBy',\n\t\tdimension: 'node',\n\t\tfield: { field: 'utilization', label: 'utilization', transform: { kind: 'ratio' } },\n\t},\n\ttimestamp: 'time',\n\tbucket: { source: 'period-field', fallbackMs: 60000 },\n\taggregator: { temporal: 'mean', crossNode: 'max' },\n\tconfidence: { field: 'count', greyBelow: 40, suppressBelow: 100 },\n\tprimitive: 'line',\n\tyAxis: { unit: '', formatter: 'percent' },\n};\n","// Spec registry barrel. Step 1 populates this file with per-metric spec imports\n// and a `specRegistry` map. Step 0 ships the scaffold + known-names export so the\n// allowlist codegen can read something.\n\n// When a spec lands at src/lib/metricSpecs/<kebab>.ts it is added to KNOWN_METRICS\n// here. This list is the source of truth for the backend allowlist generator.\nexport const KNOWN_METRICS = [\n\t'replication-latency',\n\t'resource-usage',\n\t'memory',\n\t'mqtt-connections',\n\t'ws-connections',\n\t'main-thread-utilization',\n\t'bytes-sent',\n\t'bytes-received',\n\t'table-size',\n\t'duration',\n\t'success',\n\t'transfer',\n\t'tls-reused',\n\t'connection',\n\t'cpu-usage',\n\t'db-read',\n\t'db-write',\n\t'db-message',\n\t'response_200',\n\t'utilization',\n\t'database-size',\n\t'storage-volume',\n\t'cache-hit',\n\t'cache-resolution',\n] as const;\n\nexport type KnownMetric = (typeof KNOWN_METRICS)[number];\n\nimport type { SpecRegistryEntry } from '../types/analytics.ts';\nimport { BytesReceivedRenderer, bytesReceivedSpec } from './bytes-received.tsx';\nimport { BytesSentRenderer, bytesSentSpec } from './bytes-sent.tsx';\nimport { CacheHitRenderer, cacheHitSpec } from './cache-hit.tsx';\nimport { CacheResolutionRenderer, cacheResolutionSpec } from './cache-resolution.tsx';\nimport { ConnectionRenderer, connectionSpec } from './connection.tsx';\nimport { ConnectionsRenderer, connectionsSpec } from './connections.tsx';\nimport { CpuUsageRenderer, cpuUsageSpec } from './cpu-usage.tsx';\nimport { DatabaseSizeRenderer, databaseSizeSpec } from './database-size.tsx';\nimport { DbMessageRenderer, dbMessageSpec } from './db-message.tsx';\nimport { DbReadRenderer, dbReadSpec } from './db-read.tsx';\nimport { DbWriteRenderer, dbWriteSpec } from './db-write.tsx';\nimport { DurationRenderer, durationSpec } from './duration.tsx';\nimport { MainThreadRenderer, mainThreadUtilizationSpec } from './main-thread-utilization.tsx';\nimport { MemoryRenderer, memorySpec } from './memory.tsx';\nimport { ReplicationLatencyRenderer, replicationLatencySpec } from './replication-latency.tsx';\nimport { resourceUsageSpec } from './resource-usage.ts';\nimport { Response200Renderer, response200Spec } from './response-200.tsx';\nimport { storageVolumeSpec } from './storage-volume.ts';\nimport { SuccessRenderer, successSpec } from './success.tsx';\nimport { tlsReusedSpec } from './tls-reused.ts';\nimport { TransferRenderer, transferSpec } from './transfer.tsx';\nimport { utilizationSpec } from './utilization.ts';\n\nexport const specRegistry: Record<string, SpecRegistryEntry> = {\n\t'replication-latency': { spec: replicationLatencySpec, Renderer: ReplicationLatencyRenderer },\n\t'bytes-sent': { spec: bytesSentSpec, Renderer: BytesSentRenderer },\n\t'bytes-received': { spec: bytesReceivedSpec, Renderer: BytesReceivedRenderer },\n\t'resource-usage': { spec: resourceUsageSpec },\n\t'connections': { spec: connectionsSpec, Renderer: ConnectionsRenderer },\n\t'duration': { spec: durationSpec, Renderer: DurationRenderer },\n\t'success': { spec: successSpec, Renderer: SuccessRenderer },\n\t'transfer': { spec: transferSpec, Renderer: TransferRenderer },\n\t'tls-reused': { spec: tlsReusedSpec },\n\t'connection': { spec: connectionSpec, Renderer: ConnectionRenderer },\n\t'cpu-usage': { spec: cpuUsageSpec, Renderer: CpuUsageRenderer },\n\t'db-read': { spec: dbReadSpec, Renderer: DbReadRenderer },\n\t'db-write': { spec: dbWriteSpec, Renderer: DbWriteRenderer },\n\t'db-message': { spec: dbMessageSpec, Renderer: DbMessageRenderer },\n\t'response_200': { spec: response200Spec, Renderer: Response200Renderer },\n\t'utilization': { spec: utilizationSpec },\n\t'database-size': { spec: databaseSizeSpec, Renderer: DatabaseSizeRenderer },\n\t'storage-volume': { spec: storageVolumeSpec },\n\t'memory': { spec: memorySpec, Renderer: MemoryRenderer },\n\t'main-thread-utilization': { spec: mainThreadUtilizationSpec, Renderer: MainThreadRenderer },\n\t'cache-hit': { spec: cacheHitSpec, Renderer: CacheHitRenderer },\n\t'cache-resolution': { spec: cacheResolutionSpec, Renderer: CacheResolutionRenderer },\n};\n","import { derivedRegistry } from '../pipeline/derived/index.ts';\nimport { specRegistry } from '../pipeline/index.ts';\nimport type { FieldExpr, FieldSpec, MetricSpec, Transform } from '../types/analytics.ts';\n\n/** Per-derived-metric required-field overrides. Derived specs read raw\n * source-metric columns directly (their `recompute` doesn't go through\n * runPipeline), so a generic spec walker can't infer them. The keys here\n * are derived metric ids (matching `derivedRegistry` keys); values are\n * the source-record fields the recompute reads. */\nconst DERIVED_REQUIRED_FIELDS: Record<string, readonly string[]> = {\n\t'request-rate': ['count', 'period'],\n\t'error-rate': ['count', 'errors'],\n\t// mqtt-traffic-* delegate to runPipeline against bytes-{sent,received}\n\t// inner specs, so they are covered transitively through the source spec\n\t// and the renderer fetches the source metric directly.\n};\n\n/** Walks a MetricSpec and returns the union of Harper field names the\n * pipeline must read to render the *default* view. Used by panel\n * renderers to feed `useAnalyticsRecords({ requiredFields })` so a Harper\n * response missing a load-bearing column surfaces an explicit \"field\n * unavailable\" empty state instead of a blank chart.\n *\n * The bar for \"required\" is intentionally tight: only the fields that\n * must be present for the panel's *initial* render to produce data.\n * Optional drilldowns (quantile alternates the user can pick), labels\n * used purely for grouping, and primary/sub-dimension metadata are\n * excluded — surfacing them as \"missing\" produces false positives that\n * blank an otherwise-fine chart (we hit this with cpu-usage, which has a\n * quantile picker but ships zero quantile fields by default). */\nexport function getSpecRequiredFields(metric: string): readonly string[] {\n\tconst override = DERIVED_REQUIRED_FIELDS[metric];\n\tif (override) { return override; }\n\tconst derived = derivedRegistry[metric];\n\tif (derived) {\n\t\t// Unknown derived without an override: conservative default — require\n\t\t// nothing rather than mis-flag missingFields.\n\t\treturn [];\n\t}\n\tconst entry = specRegistry[metric];\n\tif (!entry?.spec) { return []; }\n\treturn collectFromSpec(entry.spec);\n}\n\nfunction collectFromSpec(spec: MetricSpec): string[] {\n\tconst fields = new Set<string>();\n\tconst series = spec.series;\n\tlet referencesRate = false;\n\n\tif (series.kind === 'field') {\n\t\tfor (const f of series.fields) {\n\t\t\tcollectFromFieldSpec(f, fields);\n\t\t\tif (transformReferencesRate(f.transform)) { referencesRate = true; }\n\t\t}\n\t} else {\n\t\t// groupBy: only the active series.field is required for the chart to\n\t\t// render. The dimension column is also required because the pipeline's\n\t\t// group step reads it; without it every record collapses into a\n\t\t// single bucket.\n\t\tcollectFromFieldSpec(series.field, fields);\n\t\tif (transformReferencesRate(series.field.transform)) { referencesRate = true; }\n\t\t// `dimension: 'node'` is structural — it's read off\n\t\t// AnalyticsDataPoint.node which is part of the contract, not a\n\t\t// schema-drift candidate. Other dimensions (path, type, table,\n\t\t// database, method) are real Harper columns that can drift.\n\t\tif (series.dimension && series.dimension !== 'node') {\n\t\t\tfields.add(series.dimension);\n\t\t}\n\t}\n\n\t// `period` is read for `transform: rate` and for period-snapping. Spec\n\t// authors don't list it explicitly; derive it from rate references.\n\tif (referencesRate) { fields.add('period'); }\n\n\t// NOT required and intentionally excluded:\n\t// - `spec.confidence.field`: confidence gating greys low-count cells but\n\t// a record without it just renders ungated, not blank.\n\t// - `spec.primaryDimension` / `spec.subDimension`: documentation /\n\t// subgroup labels, not always read by the rendering path. Including\n\t// them mis-flags specs whose visible field is something else (we hit\n\t// this with cpu-usage where primaryDimension='path' but the panel\n\t// reads user/harper percentages).\n\t// - `spec.quantileSelector.fields`: alternates the user CAN pick. Only\n\t// the active one matters at any time; flagging all 9–11 alternates\n\t// blanks a perfectly fine default-render whenever a quantile column\n\t// is sparsely populated.\n\n\treturn [...fields];\n}\n\nfunction transformReferencesRate(t: Transform | undefined): boolean {\n\tif (!t) { return false; }\n\tswitch (t.kind) {\n\t\tcase 'rate':\n\t\t\treturn true;\n\t\tcase 'compose':\n\t\t\treturn t.steps.some(transformReferencesRate);\n\t\tdefault:\n\t\t\treturn false;\n\t}\n}\n\nfunction collectFromFieldSpec(f: FieldSpec, out: Set<string>) {\n\tcollectFromExpr(f.field, out);\n}\n\nfunction collectFromExpr(expr: string | FieldExpr, out: Set<string>) {\n\tif (typeof expr === 'string') {\n\t\tout.add(expr);\n\t\treturn;\n\t}\n\tswitch (expr.kind) {\n\t\tcase 'ref':\n\t\t\tout.add(expr.field);\n\t\t\treturn;\n\t\tcase 'const':\n\t\t\treturn;\n\t\tcase 'op':\n\t\t\tcollectFromExpr(expr.left, out);\n\t\t\tcollectFromExpr(expr.right, out);\n\t\t\treturn;\n\t}\n}\n","import type { AnalyticsDataPoint, SeriesData, TimeRange } from '../types/analytics.ts';\nimport { SmallMultiples } from './SmallMultiples.tsx';\n\nconst isDev = import.meta.env?.DEV ?? false;\n\nconst RESERVED_FIELDS = new Set([\n\t'time',\n\t'node',\n\t'id',\n\t'period',\n\t'metric',\n\t// Dimensional / metadata fields that should not be rendered as numeric\n\t// series even when they happen to be numeric (e.g. tls-reused.path = 9926).\n\t'count',\n\t'threadId',\n\t'path',\n\t'method',\n\t'type',\n\t'database',\n\t'table',\n\t'source',\n]);\n\nconst MAX_FALLBACK_PANELS = 8;\n\ninterface Props {\n\tmetric: string;\n\trecords: AnalyticsDataPoint[];\n\twindow: TimeRange;\n\tnodes: string[];\n\ttheme: 'light' | 'dark';\n\t/** Optional inline banner shown above the dev hint — used by callers that\n\t * fell through to FallbackRenderer for a known reason (e.g. legacy chart\n\t * failed to load), so users see the cause. */\n\thint?: string;\n}\n\nfunction inferNumericFields(records: AnalyticsDataPoint[]): string[] {\n\tconst candidates = new Map<string, number>();\n\tfor (const r of records) {\n\t\tfor (const key of Object.keys(r)) {\n\t\t\tif (RESERVED_FIELDS.has(key)) { continue; }\n\t\t\tif (typeof r[key] === 'number' && Number.isFinite(r[key] as number)) {\n\t\t\t\tcandidates.set(key, (candidates.get(key) ?? 0) + 1);\n\t\t\t}\n\t\t}\n\t}\n\t// Keep fields that appeared numeric in at least half the records.\n\tconst half = Math.max(1, Math.floor(records.length / 2));\n\treturn [...candidates.entries()]\n\t\t.filter(([, count]) => count >= half)\n\t\t.map(([key]) => key);\n}\n\nexport function FallbackRenderer({ metric, records, theme, hint }: Props) {\n\tconst fields = inferNumericFields(records);\n\tconst visibleFields = fields.slice(0, MAX_FALLBACK_PANELS);\n\tconst overflow = fields.length - visibleFields.length;\n\n\tconst panels = visibleFields.map((field) => {\n\t\tconst data: SeriesData = {\n\t\t\tseries: [\n\t\t\t\t{\n\t\t\t\t\tkey: field,\n\t\t\t\t\tlabel: field,\n\t\t\t\t\tpoints: records\n\t\t\t\t\t\t.filter((r) => typeof r[field] === 'number')\n\t\t\t\t\t\t.map((r) => ({\n\t\t\t\t\t\t\tx: typeof r.time === 'number' ? r.time : 0,\n\t\t\t\t\t\t\ty: r[field] as number,\n\t\t\t\t\t\t})),\n\t\t\t\t},\n\t\t\t],\n\t\t};\n\t\treturn { title: field, data };\n\t});\n\n\tconst kebab = metric.replace(/_/g, '-');\n\tconst banner = isDev\n\t\t? `Unspecced metric \"${metric}\" — add a spec at src/lib/metricSpecs/${kebab}.ts for a tailored view.`\n\t\t: null;\n\n\treturn (\n\t\t<div>\n\t\t\t{hint && (\n\t\t\t\t<div\n\t\t\t\t\trole=\"status\"\n\t\t\t\t\tstyle={{\n\t\t\t\t\t\tfontSize: 12,\n\t\t\t\t\t\tpadding: '4px 8px',\n\t\t\t\t\t\tmarginBottom: 8,\n\t\t\t\t\t\tbackground: 'color-mix(in srgb, var(--color-text-secondary) 10%, transparent)',\n\t\t\t\t\t\tcolor: 'var(--color-text-secondary)',\n\t\t\t\t\t\tborder: '1px solid color-mix(in srgb, var(--color-text-secondary) 30%, transparent)',\n\t\t\t\t\t\tborderRadius: 4,\n\t\t\t\t\t}}\n\t\t\t\t>\n\t\t\t\t\t{hint}\n\t\t\t\t</div>\n\t\t\t)}\n\t\t\t{banner && (\n\t\t\t\t<div\n\t\t\t\t\trole=\"status\"\n\t\t\t\t\tstyle={{\n\t\t\t\t\t\tfontSize: 12,\n\t\t\t\t\t\tpadding: '4px 8px',\n\t\t\t\t\t\tmarginBottom: 8,\n\t\t\t\t\t\tbackground: 'color-mix(in srgb, var(--color-warning) 15%, transparent)',\n\t\t\t\t\t\tcolor: 'var(--color-warning)',\n\t\t\t\t\t\tborder: '1px solid color-mix(in srgb, var(--color-warning) 40%, transparent)',\n\t\t\t\t\t\tborderRadius: 4,\n\t\t\t\t\t}}\n\t\t\t\t>\n\t\t\t\t\t{banner}\n\t\t\t\t</div>\n\t\t\t)}\n\t\t\t<SmallMultiples panels={panels} theme={theme} />\n\t\t\t{overflow > 0 && (\n\t\t\t\t<div style={{ fontSize: 11, marginTop: 4, opacity: 0.7 }}>\n\t\t\t\t\t{`… and ${overflow} more fields not shown. Add a spec at src/lib/metricSpecs/${kebab}.ts to customize.`}\n\t\t\t\t</div>\n\t\t\t)}\n\t\t</div>\n\t);\n}\n","// Wraps LineChart with a shared NodeLegend below the chart, hiding the\n// in-chart Recharts <Legend>. Used by generic-dispatch line panels\n// (utilization, tls-reused, storage-volume, database-size,\n// main-thread-utilization) so they get the same per-node legend\n// behavior as the chip-selector panels and small-multiples grids.\n\nimport { useMemo } from 'react';\nimport { NodeLegend } from '../charts/NodeLegend.tsx';\nimport { useNodeSelection } from '../hooks/useNodeSelection.ts';\nimport { getNodeColor } from '../lib/nodeColors.ts';\nimport type { AxisSpec, SeriesData } from '../types/analytics.ts';\nimport { LineChart } from './LineChart.tsx';\n\ninterface Props {\n\tdata: SeriesData;\n\ttheme: 'light' | 'dark';\n\tyAxis?: AxisSpec | { left: AxisSpec; right?: AxisSpec };\n\txDomain?: [number, number];\n\tfillParent?: boolean;\n}\n\nfunction extractNode(seriesKey: string): string | null {\n\tconst sep = seriesKey.indexOf('|');\n\tif (sep !== -1) { return seriesKey.slice(sep + 1); }\n\t// Some specs (utilization, tls-reused, storage-volume) groupBy 'node'\n\t// directly — the series key IS the node id, no separator.\n\treturn seriesKey;\n}\n\nexport function LineChartWithNodeLegend({ data, theme, yAxis, xDomain, fillParent }: Props) {\n\t// All series are per-node when this component is used (either via\n\t// pipeline perNode mode or groupBy:'node'). Collect node ids from\n\t// series keys.\n\tconst nodeIds = useMemo(() => {\n\t\tconst set = new Set<string>();\n\t\tfor (const s of data.series) {\n\t\t\tconst node = extractNode(s.key);\n\t\t\tif (node) { set.add(node); }\n\t\t}\n\t\treturn [...set].sort();\n\t}, [data]);\n\n\tconst { isActive, handleLegendClick } = useNodeSelection(nodeIds);\n\n\tconst filteredData = useMemo<SeriesData>(() => ({\n\t\t...data,\n\t\tseries: data.series\n\t\t\t.filter((s) => {\n\t\t\t\tconst node = extractNode(s.key);\n\t\t\t\treturn node === null || isActive(node);\n\t\t\t})\n\t\t\t.map((s) => {\n\t\t\t\tconst node = extractNode(s.key);\n\t\t\t\tif (!node) { return s; }\n\t\t\t\treturn { ...s, color: s.color ?? getNodeColor(node, nodeIds) };\n\t\t\t}),\n\t}), [data, isActive, nodeIds]);\n\n\treturn (\n\t\t<div className=\"flex h-full flex-col\">\n\t\t\t<div className=\"min-h-0 flex-1\">\n\t\t\t\t<LineChart\n\t\t\t\t\tdata={filteredData}\n\t\t\t\t\ttheme={theme}\n\t\t\t\t\tyAxis={yAxis}\n\t\t\t\t\txDomain={xDomain}\n\t\t\t\t\tfillParent={fillParent}\n\t\t\t\t\thideLegend\n\t\t\t\t/>\n\t\t\t</div>\n\t\t\t{nodeIds.length > 0 && (\n\t\t\t\t<NodeLegend\n\t\t\t\t\tnodeIds={nodeIds}\n\t\t\t\t\tisActive={isActive}\n\t\t\t\t\tonClickNode={handleLegendClick}\n\t\t\t\t/>\n\t\t\t)}\n\t\t</div>\n\t);\n}\n","import { Component, type ReactNode } from 'react';\nimport { derivedRegistry } from '../pipeline/derived/index.ts';\nimport { specRegistry } from '../pipeline/index.ts';\nimport { runPipeline } from '../pipeline/pipeline.ts';\nimport type { AnalyticsDataPoint, AxisSpec, MetricSpec, SeriesData, TimeRange } from '../types/analytics.ts';\nimport { FallbackRenderer } from './FallbackRenderer.tsx';\nimport { LineChartWithNodeLegend } from './LineChartWithNodeLegend.tsx';\nimport { SmallMultiples } from './SmallMultiples.tsx';\nimport { StackedAreaChart } from './StackedAreaChart.tsx';\n\nfunction renderPrimitive(\n\tprimitive: MetricSpec['primitive'],\n\tdata: SeriesData,\n\ttheme: 'light' | 'dark',\n\tyAxis: MetricSpec['yAxis'],\n\txDomain?: [number, number],\n\tfillParent?: boolean,\n) {\n\tswitch (primitive) {\n\t\tcase 'line':\n\t\t\treturn (\n\t\t\t\t<LineChartWithNodeLegend data={data} theme={theme} yAxis={yAxis} xDomain={xDomain} fillParent={fillParent} />\n\t\t\t);\n\t\tcase 'stacked-area':\n\t\t\treturn (\n\t\t\t\t<StackedAreaChart\n\t\t\t\t\tdata={data}\n\t\t\t\t\ttheme={theme}\n\t\t\t\t\tyAxis={yAxis as AxisSpec}\n\t\t\t\t\txDomain={xDomain}\n\t\t\t\t\tfillParent={fillParent}\n\t\t\t\t/>\n\t\t\t);\n\t\tcase 'small-multiples':\n\t\t\tthrow new Error('small-multiples is dispatched at the spec branch, not via renderPrimitive');\n\t\tcase 'heatmap':\n\t\t\tthrow new Error('heatmap dispatch is via custom Renderer (replication-latency)');\n\t\tdefault:\n\t\t\tthrow new Error(`Unknown primitive: ${primitive as string}`);\n\t}\n}\n\ninterface MetricErrorBoundaryProps {\n\tchildren: ReactNode;\n\tfallback: ReactNode;\n}\n\nclass MetricErrorBoundary extends Component<MetricErrorBoundaryProps, { failed: boolean }> {\n\tstate = { failed: false };\n\tstatic getDerivedStateFromError() {\n\t\treturn { failed: true };\n\t}\n\tcomponentDidCatch(error: Error) {\n\t\tconsole.error('[MetricRenderer] render failed; falling back:', error);\n\t}\n\trender() {\n\t\treturn this.state.failed ? this.props.fallback : this.props.children;\n\t}\n}\n\ninterface Props {\n\tmetric: string;\n\trecords: AnalyticsDataPoint[];\n\twindow: TimeRange;\n\tnodes: string[];\n\ttheme: 'light' | 'dark';\n\tviewMode?: 'per-node' | 'aggregate';\n\t/** When true, the chart fills its parent's vertical space (used by the\n\t * expand-to-fullscreen dialog). The parent must be a definite-height\n\t * flex column for this to resolve. */\n\tfillParent?: boolean;\n}\n\nexport function MetricRenderer({\n\tmetric,\n\trecords,\n\twindow: timeRange,\n\tnodes,\n\ttheme,\n\tviewMode,\n\tfillParent,\n}: Props) {\n\tconst errorFallback = (\n\t\t<FallbackRenderer\n\t\t\tmetric={metric}\n\t\t\trecords={records}\n\t\t\twindow={timeRange}\n\t\t\tnodes={nodes}\n\t\t\ttheme={theme}\n\t\t\thint=\"Render failed — showing fallback.\"\n\t\t/>\n\t);\n\n\tconst xDomain: [number, number] = [timeRange.startTime, timeRange.endTime];\n\tlet body: ReactNode;\n\tconst derived = derivedRegistry[metric];\n\tif (derived) {\n\t\tif (derived.Renderer) {\n\t\t\tbody = (\n\t\t\t\t<derived.Renderer\n\t\t\t\t\trecords={records}\n\t\t\t\t\ttimeRange={timeRange}\n\t\t\t\t\tnodes={nodes}\n\t\t\t\t\ttheme={theme}\n\t\t\t\t\tviewMode={viewMode}\n\t\t\t\t\tfillParent={fillParent}\n\t\t\t\t/>\n\t\t\t);\n\t\t} else {\n\t\t\tconst seriesData = derived.recompute(records, timeRange, nodes, viewMode);\n\t\t\tbody = renderPrimitive(derived.primitive, seriesData, theme, derived.yAxis, xDomain, fillParent);\n\t\t}\n\t} else {\n\t\tconst entry = specRegistry[metric];\n\t\tif (entry?.Renderer) {\n\t\t\tbody = (\n\t\t\t\t<entry.Renderer\n\t\t\t\t\trecords={records}\n\t\t\t\t\ttimeRange={timeRange}\n\t\t\t\t\tnodes={nodes}\n\t\t\t\t\ttheme={theme}\n\t\t\t\t\tviewMode={viewMode}\n\t\t\t\t\tfillParent={fillParent}\n\t\t\t\t/>\n\t\t\t);\n\t\t} else if (entry?.spec) {\n\t\t\tconst isPerNodeMode = (viewMode ?? 'per-node') === 'per-node';\n\t\t\tif (entry.spec.primitive === 'small-multiples') {\n\t\t\t\tconst outerSpec = entry.spec;\n\t\t\t\tconst series = outerSpec.series;\n\t\t\t\tif (series.kind !== 'field') {\n\t\t\t\t\tthrow new Error(\"small-multiples requires kind='field' series source\");\n\t\t\t\t}\n\t\t\t\tconst panels = series.fields.map((field) => {\n\t\t\t\t\tconst innerSpec: MetricSpec = {\n\t\t\t\t\t\t...outerSpec,\n\t\t\t\t\t\tseries: { kind: 'field', fields: [field] },\n\t\t\t\t\t\taggregator: {\n\t\t\t\t\t\t\ttemporal: field.aggregator?.temporal ?? outerSpec.aggregator.temporal,\n\t\t\t\t\t\t\tcrossNode: field.aggregator?.crossNode ?? outerSpec.aggregator.crossNode,\n\t\t\t\t\t\t},\n\t\t\t\t\t};\n\t\t\t\t\treturn {\n\t\t\t\t\t\ttitle: field.label,\n\t\t\t\t\t\tdata: runPipeline(innerSpec, records, timeRange, nodes, { perNode: isPerNodeMode, snapToPeriod: true }),\n\t\t\t\t\t\tyAxis: field.yAxis ?? outerSpec.yAxis,\n\t\t\t\t\t};\n\t\t\t\t});\n\t\t\t\tbody = <SmallMultiples panels={panels} theme={theme} xDomain={xDomain} fillParent={fillParent} />;\n\t\t\t} else if (\n\t\t\t\tentry.spec.primitive === 'stacked-area'\n\t\t\t\t&& isPerNodeMode\n\t\t\t\t&& entry.spec.series.kind === 'groupBy'\n\t\t\t\t&& entry.spec.series.dimension !== 'node'\n\t\t\t) {\n\t\t\t\tconst remapped: MetricSpec = {\n\t\t\t\t\t...entry.spec,\n\t\t\t\t\tseries: { ...entry.spec.series, dimension: 'node' },\n\t\t\t\t};\n\t\t\t\tconst seriesData = runPipeline(remapped, records, timeRange, nodes, { snapToPeriod: true });\n\t\t\t\tbody = renderPrimitive('stacked-area', seriesData, theme, entry.spec.yAxis, xDomain, fillParent);\n\t\t\t} else if (\n\t\t\t\tentry.spec.primitive === 'line'\n\t\t\t\t&& !isPerNodeMode\n\t\t\t\t&& entry.spec.series.kind === 'groupBy'\n\t\t\t\t&& entry.spec.series.dimension === 'node'\n\t\t\t) {\n\t\t\t\tconst groupSrc = entry.spec.series;\n\t\t\t\tconst inner: MetricSpec = {\n\t\t\t\t\t...entry.spec,\n\t\t\t\t\tseries: { kind: 'field', fields: [{ ...groupSrc.field, label: 'cluster' }] },\n\t\t\t\t};\n\t\t\t\tconst seriesData = runPipeline(inner, records, timeRange, nodes, { snapToPeriod: true });\n\t\t\t\tbody = renderPrimitive(entry.spec.primitive, seriesData, theme, entry.spec.yAxis, xDomain, fillParent);\n\t\t\t} else if (\n\t\t\t\t// `primitive: 'line'` + `groupBy` on a non-node dimension (e.g.\n\t\t\t\t// database-size groupBy 'database'). The default per-node split\n\t\t\t\t// emits one series per (dim, node) and the LineChartWithNodeLegend\n\t\t\t\t// wrapper extracts only the node id from the key — so two\n\t\t\t\t// dimension values on a single-node Harper render as two lines\n\t\t\t\t// sharing the same color and a single legend entry. Run the\n\t\t\t\t// pipeline with perNode=false so series keys are just the\n\t\t\t\t// dimension values; the wrapper then renders one legend chip\n\t\t\t\t// per dimension value (colored via getNodeColor's deterministic\n\t\t\t\t// hash). The crossNode aggregator on the spec folds nodes into\n\t\t\t\t// the cluster-wide line per dimension.\n\t\t\t\tentry.spec.primitive === 'line'\n\t\t\t\t&& entry.spec.series.kind === 'groupBy'\n\t\t\t\t&& entry.spec.series.dimension !== 'node'\n\t\t\t) {\n\t\t\t\tconst seriesData = runPipeline(entry.spec, records, timeRange, nodes, {\n\t\t\t\t\tperNode: false,\n\t\t\t\t\tsnapToPeriod: true,\n\t\t\t\t});\n\t\t\t\tbody = renderPrimitive('line', seriesData, theme, entry.spec.yAxis, xDomain, fillParent);\n\t\t\t} else {\n\t\t\t\tconst seriesData = runPipeline(entry.spec, records, timeRange, nodes, {\n\t\t\t\t\tperNode: isPerNodeMode,\n\t\t\t\t\tsnapToPeriod: true,\n\t\t\t\t});\n\t\t\t\tbody = renderPrimitive(entry.spec.primitive, seriesData, theme, entry.spec.yAxis, xDomain, fillParent);\n\t\t\t}\n\t\t} else {\n\t\t\tbody = <FallbackRenderer metric={metric} records={records} window={timeRange} nodes={nodes} theme={theme} />;\n\t\t}\n\t}\n\n\treturn <MetricErrorBoundary key={metric} fallback={errorFallback}>{body}</MetricErrorBoundary>;\n}\n","import { Component, type ReactNode } from 'react';\n\ninterface Props {\n\tmetric: string;\n\t/** When this changes, the boundary clears its caught-error state so the\n\t * child gets a fresh render attempt. Wire to the time-range stamp so a\n\t * user changing the window retries the panel without a page reload. */\n\tresetKey?: string | number;\n\tchildren: ReactNode;\n}\n\ninterface State {\n\tfailed: boolean;\n\tmessage?: string;\n\tlastResetKey?: string | number;\n}\n\nexport class PanelErrorBoundary extends Component<Props, State> {\n\tstate: State = { failed: false };\n\n\tstatic getDerivedStateFromError(error: unknown): Partial<State> {\n\t\treturn { failed: true, message: error instanceof Error ? error.message : String(error) };\n\t}\n\n\tstatic getDerivedStateFromProps(nextProps: Props, prevState: State): Partial<State> | null {\n\t\tif (prevState.lastResetKey !== nextProps.resetKey) {\n\t\t\treturn { failed: false, message: undefined, lastResetKey: nextProps.resetKey };\n\t\t}\n\t\treturn null;\n\t}\n\n\tcomponentDidCatch(error: Error) {\n\t\tconsole.error(`[panel:${this.props.metric}] render failed`, error);\n\t}\n\n\trender() {\n\t\tif (this.state.failed) {\n\t\t\treturn (\n\t\t\t\t<div className=\"rounded-lg border border-destructive/40 bg-destructive/10 p-4 text-sm text-destructive\">\n\t\t\t\t\t<div className=\"font-medium mb-1\">{`Panel \"${this.props.metric}\" is unavailable`}</div>\n\t\t\t\t\t<div className=\"text-xs opacity-80\">{this.state.message}</div>\n\t\t\t\t</div>\n\t\t\t);\n\t\t}\n\t\treturn this.props.children;\n\t}\n}\n","import { Button } from '@/components/ui/button';\nimport { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';\nimport { type ReactNode, useRef } from 'react';\nimport { ChartCopyButton } from '../components/ChartCopyButton.tsx';\nimport { ChartExpandButton } from '../components/ChartExpandButton.tsx';\nimport { ChartExportButton } from '../components/ChartExportButton.tsx';\nimport { useAnalyticsContext } from '../context/AnalyticsContext.tsx';\nimport { useAnalyticsRecords } from '../hooks/useAnalyticsRecords.ts';\nimport { getSpecRequiredFields } from '../lib/specRequiredFields.ts';\nimport { derivedRegistry } from '../pipeline/derived/index.ts';\nimport { specRegistry } from '../pipeline/index.ts';\nimport { MetricRenderer } from '../primitives/MetricRenderer.tsx';\nimport { PanelErrorBoundary } from './PanelErrorBoundary.tsx';\n\ninterface Props {\n\tmetric: string;\n\ttitleOverride?: string;\n}\n\n/** Renders one Card with a metric chart inside, fetching its own data via\n * the adapter. Used by every analytics tab except Storage's table-size\n * panels (which compose their own custom charts). */\nexport function MetricPanel({ metric, titleOverride }: Props) {\n\tconst { timeRange } = useAnalyticsContext();\n\treturn (\n\t\t<PanelErrorBoundary metric={metric} resetKey={`${timeRange.startTime}-${timeRange.endTime}`}>\n\t\t\t<MetricPanelInner metric={metric} titleOverride={titleOverride} />\n\t\t</PanelErrorBoundary>\n\t);\n}\n\nfunction MetricPanelInner({ metric, titleOverride }: Props) {\n\tconst { timeRange, bucketMs, refreshIntervalMs, theme, instanceParams } = useAnalyticsContext();\n\tconst sourceMetric = derivedRegistry[metric]?.sourceMetric ?? metric;\n\tconst requiredFields = getSpecRequiredFields(metric);\n\tconst { data, isLoading, isError, error, isEmpty, missingFields, refetch } = useAnalyticsRecords({\n\t\tmetric: sourceMetric,\n\t\tstartTime: timeRange.startTime,\n\t\tendTime: timeRange.endTime,\n\t\tinstanceParams,\n\t\trefetchIntervalMs: refreshIntervalMs,\n\t\tbucketMs,\n\t\trequiredFields,\n\t});\n\n\tconst specEntry = specRegistry[metric];\n\tconst derivedEntry = derivedRegistry[metric];\n\tconst title = titleOverride ?? specEntry?.spec?.title ?? derivedEntry?.title ?? metric;\n\tconst description = specEntry?.spec?.description ?? derivedEntry?.subtitle;\n\tconst nodes = collectNodes(data);\n\t// Capture only the chart body, not the whole Card. Otherwise the exported\n\t// PNG includes the title bar's Expand/Copy/Download icons.\n\tconst chartRef = useRef<HTMLDivElement>(null);\n\tconst canExport = !isLoading && !isError && !isEmpty;\n\n\tconst renderChart = (opts: { fillParent: boolean } = { fillParent: false }) => (\n\t\t<MetricRenderer\n\t\t\tmetric={metric}\n\t\t\trecords={data}\n\t\t\twindow={timeRange}\n\t\t\tnodes={nodes}\n\t\t\ttheme={theme}\n\t\t\tfillParent={opts.fillParent}\n\t\t/>\n\t);\n\n\treturn (\n\t\t<Card>\n\t\t\t<CardHeader className=\"flex flex-row items-start justify-between gap-2\">\n\t\t\t\t<div className=\"flex-1 min-w-0\">\n\t\t\t\t\t<CardTitle>{title}</CardTitle>\n\t\t\t\t\t{description && <CardDescription>{description}</CardDescription>}\n\t\t\t\t</div>\n\t\t\t\t{canExport && (\n\t\t\t\t\t<div className=\"flex items-center gap-1 shrink-0\">\n\t\t\t\t\t\t<ChartExpandButton\n\t\t\t\t\t\t\texportSlug={metric}\n\t\t\t\t\t\t\ttitle={title}\n\t\t\t\t\t\t\tdescription={description}\n\t\t\t\t\t\t\trenderChart={renderChart}\n\t\t\t\t\t\t/>\n\t\t\t\t\t\t<ChartCopyButton captureRef={chartRef} exportSlug={metric} />\n\t\t\t\t\t\t<ChartExportButton captureRef={chartRef} exportSlug={metric} />\n\t\t\t\t\t</div>\n\t\t\t\t)}\n\t\t\t</CardHeader>\n\t\t\t<CardContent>\n\t\t\t\t<div ref={chartRef}>\n\t\t\t\t\t<PanelStateOrChart\n\t\t\t\t\t\tisLoading={isLoading}\n\t\t\t\t\t\tisError={isError}\n\t\t\t\t\t\terror={error}\n\t\t\t\t\t\tisEmpty={isEmpty}\n\t\t\t\t\t\tmissingFields={missingFields}\n\t\t\t\t\t\tonRetry={refetch}\n\t\t\t\t\t>\n\t\t\t\t\t\t{renderChart()}\n\t\t\t\t\t</PanelStateOrChart>\n\t\t\t\t</div>\n\t\t\t</CardContent>\n\t\t</Card>\n\t);\n}\n\nfunction PanelStateOrChart({\n\tisLoading,\n\tisError,\n\terror,\n\tisEmpty,\n\tmissingFields,\n\tonRetry,\n\tchildren,\n}: {\n\tisLoading: boolean;\n\tisError: boolean;\n\terror: Error | null;\n\tisEmpty: boolean;\n\tmissingFields: string[];\n\tonRetry: () => void;\n\tchildren: ReactNode;\n}) {\n\tif (isLoading) {\n\t\treturn (\n\t\t\t<div\n\t\t\t\trole=\"status\"\n\t\t\t\taria-live=\"polite\"\n\t\t\t\tclassName=\"h-64 rounded-md bg-muted/30 animate-pulse\"\n\t\t\t\taria-label=\"Loading\"\n\t\t\t/>\n\t\t);\n\t}\n\tif (isError) {\n\t\treturn (\n\t\t\t<div className=\"h-64 rounded-md border border-destructive/40 bg-destructive/10 p-4 text-sm text-destructive flex flex-col items-start justify-center gap-3\">\n\t\t\t\t<div>{`Failed to load: ${error?.message ?? 'unknown error'}`}</div>\n\t\t\t\t<Button variant=\"outline\" size=\"sm\" onClick={onRetry}>Retry</Button>\n\t\t\t</div>\n\t\t);\n\t}\n\tif (isEmpty) {\n\t\treturn (\n\t\t\t<div className=\"h-64 rounded-md border border-border bg-muted/20 p-4 text-sm text-muted-foreground flex items-center justify-center\">\n\t\t\t\t{missingFields.length > 0\n\t\t\t\t\t? `No data — server response is missing required field(s): ${missingFields.join(', ')}.`\n\t\t\t\t\t: 'No data in the selected time range.'}\n\t\t\t</div>\n\t\t);\n\t}\n\treturn <>{children}</>;\n}\n\nfunction collectNodes(rows: { node?: unknown }[]): string[] {\n\tconst set = new Set<string>();\n\tfor (const r of rows) {\n\t\tif (typeof r.node === 'string') { set.add(r.node); }\n\t}\n\treturn [...set].sort();\n}\n","import { MetricPanel } from './MetricPanel.tsx';\n\nconst METRICS = ['db-read', 'db-write', 'db-message'] as const;\n\nexport function DatabaseTab() {\n\treturn (\n\t\t<div className=\"grid grid-cols-1 xl:grid-cols-2 gap-4\">\n\t\t\t{METRICS.map((m) => <MetricPanel key={m} metric={m} />)}\n\t\t</div>\n\t);\n}\n","import { MetricPanel } from './MetricPanel.tsx';\n\nconst METRICS = [\n\t'resource-usage',\n\t'memory',\n\t'main-thread-utilization',\n\t'cpu-usage',\n\t'utilization',\n] as const;\n\nexport function HealthTab() {\n\treturn (\n\t\t<div className=\"grid grid-cols-1 xl:grid-cols-2 gap-4\">\n\t\t\t{METRICS.map((m) => <MetricPanel key={m} metric={m} />)}\n\t\t</div>\n\t);\n}\n","import { cn } from '@/lib/cn';\nimport * as AccordionPrimitive from '@radix-ui/react-accordion';\nimport { ChevronDownIcon } from 'lucide-react';\nimport * as React from 'react';\n\nfunction Accordion({\n\t...props\n}: React.ComponentProps<typeof AccordionPrimitive.Root>) {\n\treturn <AccordionPrimitive.Root data-slot=\"accordion\" {...props} />;\n}\n\nfunction AccordionItem({\n\tclassName,\n\t...props\n}: React.ComponentProps<typeof AccordionPrimitive.Item>) {\n\treturn (\n\t\t<AccordionPrimitive.Item\n\t\t\tdata-slot=\"accordion-item\"\n\t\t\tclassName={cn('border-b last:border-b-0', className)}\n\t\t\t{...props}\n\t\t/>\n\t);\n}\n\nfunction AccordionTrigger({\n\tclassName,\n\tchildren,\n\t...props\n}: React.ComponentProps<typeof AccordionPrimitive.Trigger>) {\n\treturn (\n\t\t<AccordionPrimitive.Header className=\"flex\">\n\t\t\t<AccordionPrimitive.Trigger\n\t\t\t\tdata-slot=\"accordion-trigger\"\n\t\t\t\tclassName={cn(\n\t\t\t\t\t'focus-visible:border-ring focus-visible:ring-ring/50 flex flex-1 items-start justify-between gap-4 rounded-md py-4 text-left text-sm font-medium transition-all outline-none hover:underline focus-visible:ring-[3px] disabled:pointer-events-none disabled:opacity-50 [&[data-state=open]>svg]:rotate-180',\n\t\t\t\t\tclassName,\n\t\t\t\t)}\n\t\t\t\t{...props}\n\t\t\t>\n\t\t\t\t{children}\n\t\t\t\t<ChevronDownIcon className=\"text-muted-foreground pointer-events-none size-4 shrink-0 translate-y-0.5 transition-transform duration-200\" />\n\t\t\t</AccordionPrimitive.Trigger>\n\t\t</AccordionPrimitive.Header>\n\t);\n}\n\nfunction AccordionContent({\n\tclassName,\n\tchildren,\n\t...props\n}: React.ComponentProps<typeof AccordionPrimitive.Content>) {\n\treturn (\n\t\t<AccordionPrimitive.Content\n\t\t\tdata-slot=\"accordion-content\"\n\t\t\tclassName=\"data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down overflow-hidden text-sm\"\n\t\t\t{...props}\n\t\t>\n\t\t\t<div className={cn('pt-0 pb-4', className)}>{children}</div>\n\t\t</AccordionPrimitive.Content>\n\t);\n}\n\nexport { Accordion, AccordionContent, AccordionItem, AccordionTrigger };\n","import { InstanceClientIdConfig } from '@/config/instanceClientConfig';\nimport { queryOptions } from '@tanstack/react-query';\n\ninterface SystemInformationResponse {\n\tsystem: {\n\t\tplatform: 'darwin' | 'linux' | string;\n\t\tdistro: 'macOS' | 'Debian GNU/Linux' | string;\n\t\trelease: string;\n\t\tcodename: string;\n\t\tkernel: string;\n\t\tarch: 'arm64' | 'x64' | string;\n\t\thostname: string;\n\t\tfqdn: string;\n\t\tnode_version: string;\n\t\tnpm_version: string;\n\t};\n\tcpu: {\n\t\tmanufacturer: 'Apple' | 'AMD' | string;\n\t\tbrand: 'M4' | string;\n\t\tvendor: 'Apple' | 'AMD' | string;\n\t\tspeed: number;\n\t\tspeedMin: number;\n\t\tspeedMax: number;\n\t\tcores: number;\n\t\tphysicalCores: number;\n\t\tperformanceCores: number;\n\t\tefficiencyCores: number;\n\t\tprocessors: number;\n\t\tflags: string;\n\t\tvirtualization: boolean;\n\t\tcpu_speed: {\n\t\t\tmin: number;\n\t\t\tmax: number;\n\t\t\tavg: number;\n\t\t\tcores: number[];\n\t\t};\n\t\tcurrent_load: {\n\t\t\tavgLoad: number;\n\t\t\tcurrentLoad: number;\n\t\t\tcurrentLoadUser: number;\n\t\t\tcurrentLoadSystem: number;\n\t\t\tcurrentLoadNice: number;\n\t\t\tcurrentLoadIdle: number;\n\t\t\tcurrentLoadIrq: number;\n\t\t\tcurrentLoadSteal: number;\n\t\t\tcurrentLoadGuest: number;\n\t\t\trawCurrentLoad: number;\n\t\t\trawCurrentLoadUser: number;\n\t\t\trawCurrentLoadSystem: number;\n\t\t\trawCurrentLoadNice: number;\n\t\t\trawCurrentLoadIdle: number;\n\t\t\trawCurrentLoadIrq: number;\n\t\t\trawCurrentLoadSteal: number;\n\t\t\trawCurrentLoadGuest: number;\n\t\t\tcpus: Array<{\n\t\t\t\tload: number;\n\t\t\t\tloadUser: number;\n\t\t\t\tloadSystem: number;\n\t\t\t\tloadNice: number;\n\t\t\t\tloadIdle: number;\n\t\t\t\tloadIrq: number;\n\t\t\t\tloadSteal: number;\n\t\t\t\tloadGuest: number;\n\t\t\t\trawLoadSteal: number;\n\t\t\t\trawLoadGuest: number;\n\t\t\t}>;\n\t\t};\n\t};\n\tmemory: {\n\t\ttotal: number;\n\t\tfree: number;\n\t\tused: number;\n\t\tactive: number;\n\t\tavailable: number;\n\t\treclaimable: number;\n\t\tswaptotal: number;\n\t\tswapused: number;\n\t\tswapfree: number;\n\t\twriteback: unknown;\n\t\tdirty: unknown;\n\t\trss: number;\n\t\theapTotal: number;\n\t\theapUsed: number;\n\t\texternal: number;\n\t\tarrayBuffers: number;\n\t};\n\tdisk: Record<string, unknown>;\n\tnetwork: {\n\t\tdefault_interface: unknown;\n\t\tlatency: Record<string, unknown>;\n\t\tinterfaces: Array<Record<string, unknown>>;\n\t\tstats: Array<Record<string, unknown>>;\n\t\tconnections: Array<Record<string, unknown>>;\n\t};\n\n\t[key: string]: unknown;\n}\n\nexport function getSystemInformationQueryOptions({ entityId, instanceClient }: InstanceClientIdConfig) {\n\treturn queryOptions({\n\t\tqueryKey: [entityId, 'system_information'] as const,\n\t\tqueryFn: async () => {\n\t\t\tconst { data } = await instanceClient.post<SystemInformationResponse>('/', {\n\t\t\t\toperation: 'system_information',\n\t\t\t\tattributes: ['network', 'disk', 'cpu', 'memory', 'system'],\n\t\t\t});\n\t\t\treturn data;\n\t\t},\n\t});\n}\n","import { excludeFalsy } from '@/lib/arrays/excludeFalsy';\nimport { humanFileSize } from '@/lib/humanFileSize';\nimport { translateSecondsToAgo } from '@/lib/translateSecondsToAgo';\n\nconst startOf2025 = new Date(2025, 0).getTime();\nconst oneDayInMs = 24 * 60 * 60 * 1000;\n\ninterface TitleItem {\n\ttitle: string;\n\tdepth: number;\n}\n\ninterface NameValuePairItem {\n\tname: string;\n\tvalue: string;\n\tdepth: number;\n}\n\ntype ItemForDisplay = TitleItem | NameValuePairItem;\n\nexport function crawlData(data: Record<string, unknown>): ItemForDisplay[] {\n\tconst sections: ItemForDisplay[] = [];\n\tfor (const key in data) {\n\t\tconst value = data[key];\n\t\tsections.push(...parseValue(key, value, 0));\n\t}\n\treturn sections;\n}\n\nexport function hasTitle(item: ItemForDisplay): item is TitleItem {\n\treturn !!(item as TitleItem).title;\n}\n\nfunction parseValue(name: string, value: unknown, depth: number, parentName?: string): ItemForDisplay[] {\n\tif (value && Array.isArray(value)) {\n\t\tconst array = value;\n\t\treturn [\n\t\t\tarray.length > 1 && { title: name, depth },\n\t\t\t...value.map((item, index) =>\n\t\t\t\tparseValue(\n\t\t\t\t\tarray.length > 1 ? String(index + 1) : name,\n\t\t\t\t\titem,\n\t\t\t\t\tdepth + 1,\n\t\t\t\t\tname,\n\t\t\t\t)\n\t\t\t).flat(1),\n\t\t].filter(excludeFalsy);\n\t}\n\tif (isObject(value)) {\n\t\tconst obj = value;\n\t\treturn [\n\t\t\t{ title: name, depth },\n\t\t\t...Object.keys(value).map(subKey =>\n\t\t\t\tparseValue(\n\t\t\t\t\tString(subKey),\n\t\t\t\t\tobj[subKey],\n\t\t\t\t\tdepth + 1,\n\t\t\t\t\tname,\n\t\t\t\t)\n\t\t\t).flat(1),\n\t\t];\n\t}\n\tif (name === '__updatedtime__' || name === '__createdtime__') {\n\t\tname = name.replace(/_/g, '').replace('time', '');\n\t}\n\tif (typeof value === 'number') {\n\t\tif (value > startOf2025 && value < Date.now() + oneDayInMs) {\n\t\t\tconst elapsed = Date.now() - value;\n\t\t\tvalue = translateSecondsToAgo(elapsed, value);\n\t\t} else if (parentName === 'memory') {\n\t\t\tvalue = humanFileSize(value);\n\t\t} else if (!name.startsWith('raw') && name.toLowerCase().includes('load')) {\n\t\t\tvalue = Math.round(value * 10) / 10 + '%';\n\t\t}\n\t} else if (typeof value === 'boolean') {\n\t\tvalue = value ? 'Yes' : 'No';\n\t}\n\treturn [\n\t\t{ name, value: String(value), depth },\n\t];\n}\n\nfunction isObject(value: unknown): value is Record<string, unknown> {\n\treturn !!value && typeof value === 'object';\n}\n","import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from '@/components/ui/accordion';\nimport { Card, CardContent } from '@/components/ui/card';\nimport type { InstanceClientIdConfig, InstanceTypeConfig } from '@/config/instanceClientConfig.ts';\nimport { getStatusQueryOptions } from '@/integrations/api/instance/status/getStatus';\nimport { getSystemInformationQueryOptions } from '@/integrations/api/instance/status/getSystemInformation';\nimport { useSuspenseQuery } from '@tanstack/react-query';\nimport { Suspense, useMemo } from 'react';\nimport { crawlData, hasTitle } from '../lib/crawlData.ts';\n\ninterface Props {\n\tinstanceParams: InstanceClientIdConfig & InstanceTypeConfig;\n\tisLocalStudio: boolean;\n}\n\nexport function OverviewTab({ instanceParams, isLocalStudio }: Props) {\n\treturn (\n\t\t<Suspense fallback={<OverviewSkeleton />}>\n\t\t\t{isLocalStudio\n\t\t\t\t? <LocalOverview instanceParams={instanceParams} />\n\t\t\t\t: <CloudOverview instanceParams={instanceParams} />}\n\t\t</Suspense>\n\t);\n}\n\nfunction OverviewSkeleton() {\n\treturn (\n\t\t<div className=\"grid grid-cols-1 md:grid-cols-2 gap-4\">\n\t\t\t{[0, 1, 2, 3].map((i) => <div key={i} className=\"h-40 rounded-lg bg-muted/30 animate-pulse\" />)}\n\t\t</div>\n\t);\n}\n\nfunction LocalOverview({ instanceParams }: { instanceParams: InstanceClientIdConfig & InstanceTypeConfig }) {\n\tconst { data } = useSuspenseQuery(getSystemInformationQueryOptions(instanceParams));\n\treturn <OverviewBody data={data as Record<string, unknown>} />;\n}\n\nfunction CloudOverview({ instanceParams }: { instanceParams: InstanceClientIdConfig & InstanceTypeConfig }) {\n\tconst { data } = useSuspenseQuery(getStatusQueryOptions(instanceParams));\n\treturn <OverviewBody data={data as Record<string, unknown>} />;\n}\n\nexport interface SectionGroup {\n\ttitle: string;\n\trows: { name: string; value: string }[];\n\tsubSections: SectionGroup[];\n}\n\nfunction OverviewBody({ data }: { data: Record<string, unknown> }) {\n\tconst sections = useMemo(() => groupSections(data), [data]);\n\n\treturn (\n\t\t<div className=\"space-y-4\">\n\t\t\t{\n\t\t\t\t/* Default-open the first section only — opening every accordion\n\t\t\t item produces a wall of text on first paint and defeats the\n\t\t\t purpose of the collapsible. */\n\t\t\t}\n\t\t\t<Accordion type=\"multiple\" defaultValue={sections.slice(0, 1).map((s) => s.title)}>\n\t\t\t\t{sections.map((section) => (\n\t\t\t\t\t<AccordionItem key={section.title} value={section.title}>\n\t\t\t\t\t\t<AccordionTrigger className=\"text-base font-semibold\">{section.title}</AccordionTrigger>\n\t\t\t\t\t\t<AccordionContent>\n\t\t\t\t\t\t\t<SectionContent section={section} />\n\t\t\t\t\t\t</AccordionContent>\n\t\t\t\t\t</AccordionItem>\n\t\t\t\t))}\n\t\t\t</Accordion>\n\t\t\t<details className=\"rounded-lg border border-border bg-card\">\n\t\t\t\t<summary className=\"cursor-pointer px-4 py-2 text-sm text-muted-foreground hover:text-foreground\">\n\t\t\t\t\tView raw JSON\n\t\t\t\t</summary>\n\t\t\t\t<pre className=\"px-4 pb-4 text-xs overflow-auto max-h-96\">\n\t\t\t\t\t{JSON.stringify(data, null, 2)}\n\t\t\t\t</pre>\n\t\t\t</details>\n\t\t</div>\n\t);\n}\n\nfunction SectionContent({ section }: { section: SectionGroup }) {\n\treturn (\n\t\t<div className=\"space-y-3\">\n\t\t\t{section.rows.length > 0 && (\n\t\t\t\t<Card>\n\t\t\t\t\t<CardContent className=\"pt-4\">\n\t\t\t\t\t\t<dl className=\"grid grid-cols-1 sm:grid-cols-2 gap-x-6 gap-y-2 text-sm\">\n\t\t\t\t\t\t\t{section.rows.map((row) => (\n\t\t\t\t\t\t\t\t<div key={row.name} className=\"flex justify-between gap-4\">\n\t\t\t\t\t\t\t\t\t<dt className=\"text-muted-foreground\">{row.name}</dt>\n\t\t\t\t\t\t\t\t\t<dd className=\"font-mono text-right truncate\" title={row.value}>{row.value}</dd>\n\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t))}\n\t\t\t\t\t\t</dl>\n\t\t\t\t\t</CardContent>\n\t\t\t\t</Card>\n\t\t\t)}\n\t\t\t{section.subSections.map((sub) => (\n\t\t\t\t<div key={sub.title} className=\"pl-3 border-l border-border\">\n\t\t\t\t\t<div className=\"text-sm font-semibold mb-2 text-muted-foreground\">{sub.title}</div>\n\t\t\t\t\t<SectionContent section={sub} />\n\t\t\t\t</div>\n\t\t\t))}\n\t\t</div>\n\t);\n}\n\nexport function groupSections(data: Record<string, unknown>): SectionGroup[] {\n\tconst items = crawlData(data);\n\ttype GroupWithDepth = SectionGroup & { _depth: number };\n\tconst top: SectionGroup[] = [];\n\tconst stack: GroupWithDepth[] = [];\n\tlet general: SectionGroup | null = null;\n\n\tfor (const item of items) {\n\t\tif (hasTitle(item)) {\n\t\t\twhile (stack.length > 0 && stack[stack.length - 1]._depth >= item.depth) {\n\t\t\t\tstack.pop();\n\t\t\t}\n\t\t\tconst group: GroupWithDepth = {\n\t\t\t\ttitle: item.title,\n\t\t\t\trows: [],\n\t\t\t\tsubSections: [],\n\t\t\t\t_depth: item.depth,\n\t\t\t};\n\t\t\tif (stack.length === 0) {\n\t\t\t\ttop.push(group);\n\t\t\t} else {\n\t\t\t\tstack[stack.length - 1].subSections.push(group);\n\t\t\t}\n\t\t\tstack.push(group);\n\t\t} else {\n\t\t\tconst target = stack[stack.length - 1];\n\t\t\tif (target) {\n\t\t\t\ttarget.rows.push({ name: item.name, value: item.value });\n\t\t\t} else {\n\t\t\t\tif (!general) {\n\t\t\t\t\tgeneral = { title: 'General', rows: [], subSections: [] };\n\t\t\t\t\ttop.unshift(general);\n\t\t\t\t}\n\t\t\t\tgeneral.rows.push({ name: item.name, value: item.value });\n\t\t\t}\n\t\t}\n\t}\n\treturn top;\n}\n","import { MetricPanel } from './MetricPanel.tsx';\n\nexport function ReplicationTab() {\n\treturn (\n\t\t<div className=\"grid grid-cols-1 gap-4\">\n\t\t\t<MetricPanel metric=\"replication-latency\" />\n\t\t</div>\n\t);\n}\n","import { MetricPanel } from './MetricPanel.tsx';\n\nconst METRICS = [\n\t'request-rate',\n\t'error-rate',\n\t'duration',\n\t'success',\n\t'transfer',\n\t'response_200',\n\t'cache-hit',\n\t'cache-resolution',\n] as const;\n\nexport function RequestsTab() {\n\treturn (\n\t\t<div className=\"grid grid-cols-1 xl:grid-cols-2 gap-4\">\n\t\t\t{METRICS.map((m) => <MetricPanel key={m} metric={m} />)}\n\t\t</div>\n\t);\n}\n","/**\n * Categorical palette for tables in the table-size dashboard.\n *\n * Deliberately distinct from NODE_PALETTE in `nodeColors.ts` so the two\n * categorical encodings (nodes and tables) don't visually collide.\n * Chosen to meet WCAG AA contrast on both dark and light backgrounds.\n */\nexport const TABLE_PALETTE = [\n\t'#e45756', // red\n\t'#f58518', // orange\n\t'#eeca3b', // yellow\n\t'#54a24b', // green\n\t'#4c78a8', // blue\n\t'#b279a2', // mauve\n\t'#9d755d', // brown\n\t'#17becf', // cyan\n\t'#72b7b2', // teal\n\t'#bab0ac', // grey\n] as const;\n\n/** Colour for the rolled-up \"Other\" stack — neutral grey so it reads as an aggregate. */\nexport const OTHER_COLOR = '#6b7280';\n\nexport function getTableColor(index: number): string {\n\treturn TABLE_PALETTE[index % TABLE_PALETTE.length];\n}\n","import type { TableSizeRecord, TimeRange } from '../types/analytics.ts';\n\nexport type RankBy = 'bytes' | 'percent';\nexport type EmptyCause = 'upstream-empty' | 'all-other' | null;\n\nexport interface NormalizedRecord {\n\tdatabase: string;\n\ttable: string;\n\t/** Stable \"table key\" used everywhere as \"database.table\". */\n\ttableKey: string;\n\tnode: string;\n\t/** Timestamp in ms (normalized from Harper's `id` field). */\n\ttime: number;\n\tsize: number;\n}\n\nexport interface SnapshotByNodeEntry {\n\tnode: string;\n\t/** Map of tableKey -> bytes, restricted to tables in `tableSet` (plus \"Other\" when applicable). */\n\tstacks: Record<string, number>;\n\t/** Sum of all bytes for this node across ALL tables (not just the top-N). */\n\ttotal: number;\n}\n\nexport interface Snapshot {\n\tbyNode: SnapshotByNodeEntry[];\n\t/** Top-N table keys, in stable display order (rank desc, tie-break alphabetical). */\n\ttableSet: string[];\n\thasOther: boolean;\n\t/** Tables rolled into Other (for tooltips/diagnostics). */\n\totherMembers: string[];\n}\n\nexport interface TrendPoint {\n\ttime: number; // bucket start (ms)\n\tvalues: Record<string, /* node */ number /* bytes */>;\n}\n\nexport interface TableSizeDerived {\n\tsnapshot: Snapshot;\n\ttrend: (selectedTable: string) => TrendPoint[];\n\tdefaultSelection: (rankBy: RankBy) => string | null;\n\temptyCause: EmptyCause;\n\t/** Content signature for memo keys. */\n\tsignature: string;\n}\n\n/** Targeted max number of top-N tables on Panel 1. */\nexport const TOP_N = 8;\n\nexport const OTHER_KEY = '__other__';\n\n/** Threshold below which a table is considered empty/static and excluded from top-N. */\nconst MEANINGFUL_SIZE_THRESHOLD = 4096;\n\n/** Compute bucket width (ms) for trend rendering. */\nexport function computeBucketMs(windowMs: number): number {\n\treturn Math.max(60_000, Math.ceil(windowMs / 90));\n}\n\n/** Build a stable \"database.table\" key. */\nexport function toTableKey(r: { database: string; table: string }): string {\n\treturn `${r.database}.${r.table}`;\n}\n\n/** Normalize raw records: map id→time, sort by time, build tableKey. Does NOT dedup. */\nexport function normalizeRecords(raw: TableSizeRecord[]): NormalizedRecord[] {\n\tconst out: NormalizedRecord[] = raw.map((r) => ({\n\t\tdatabase: r.database,\n\t\ttable: r.table,\n\t\ttableKey: toTableKey(r),\n\t\tnode: r.node,\n\t\ttime: r.id,\n\t\tsize: r.size,\n\t}));\n\t// Stable sort by time ascending; Array.prototype.sort is stable in Node 22+.\n\tout.sort((a, b) => a.time - b.time);\n\treturn out;\n}\n\n/** Drop consecutive unchanged-size repeats per (node, tableKey). */\nexport function dedupRecords(normalized: NormalizedRecord[]): NormalizedRecord[] {\n\tconst lastSize = new Map<string, number>(); // key = `${node}\\0${tableKey}`\n\tconst kept: NormalizedRecord[] = [];\n\tfor (const r of normalized) {\n\t\tconst key = `${r.node}\\0${r.tableKey}`;\n\t\tif (lastSize.get(key) === r.size) { continue; // unchanged repeat\n\t\t }\n\t\tkept.push(r);\n\t\tlastSize.set(key, r.size);\n\t}\n\treturn kept;\n}\n\n/** Rank tables by max-per-node size; return top-N keys and rollup membership. */\nexport function computeTableSet(\n\tnormalized: NormalizedRecord[],\n): { tableSet: string[]; hasOther: boolean; otherMembers: string[] } {\n\t// For each tableKey, compute max size observed on ANY single node in the window.\n\t// Note: use `has`/`undefined` as the \"not yet seen\" sentinel, not `0`, so a\n\t// table whose size is 0 still appears in the key set and flows through to\n\t// otherMembers (rather than being silently dropped).\n\tconst maxPerNode = new Map<string, number>(); // tableKey -> max over nodes of (max over time)\n\tconst perNodeMax = new Map<string, number>(); // `${tableKey}\\0${node}` -> max size\n\tfor (const r of normalized) {\n\t\tconst k = `${r.tableKey}\\0${r.node}`;\n\t\tconst prev = perNodeMax.get(k);\n\t\tif (prev === undefined || r.size > prev) { perNodeMax.set(k, r.size); }\n\t}\n\tfor (const [k, v] of perNodeMax) {\n\t\tconst [tableKey] = k.split('\\0');\n\t\tconst prev = maxPerNode.get(tableKey);\n\t\tif (prev === undefined || v > prev) { maxPerNode.set(tableKey, v); }\n\t}\n\n\t// Keep only meaningful tables.\n\tconst meaningful = [...maxPerNode.entries()].filter(\n\t\t([, v]) => v > MEANINGFUL_SIZE_THRESHOLD,\n\t);\n\n\t// Rank desc, tie-break alphabetical.\n\tmeaningful.sort((a, b) => {\n\t\tif (b[1] !== a[1]) { return b[1] - a[1]; }\n\t\treturn a[0].localeCompare(b[0]);\n\t});\n\n\tconst allMeaningfulKeys = meaningful.map(([k]) => k);\n\n\t// Top-N rule:\n\t// <= TOP_N+1 meaningful tables -> keep all inline; no rollup.\n\t// > TOP_N+1 -> keep top-N; roll up the rest into Other.\n\t// Below-threshold tables always land in Other so they stay discoverable in\n\t// the tooltip / aggregate stack. Without this, on clusters like ours — where\n\t// ~17 static 4 KB tables sit alongside 3 growing ones — the static cohort\n\t// vanishes from the UI entirely.\n\tconst belowThreshold = [...maxPerNode.keys()].filter(\n\t\t(k) => !allMeaningfulKeys.includes(k),\n\t);\n\tbelowThreshold.sort((a, b) => a.localeCompare(b));\n\n\tif (allMeaningfulKeys.length <= TOP_N + 1) {\n\t\treturn {\n\t\t\ttableSet: allMeaningfulKeys,\n\t\t\thasOther: belowThreshold.length > 0,\n\t\t\totherMembers: belowThreshold,\n\t\t};\n\t}\n\n\tconst tableSet = allMeaningfulKeys.slice(0, TOP_N);\n\tconst rolledUpMeaningful = allMeaningfulKeys.slice(TOP_N);\n\tconst otherMembers = [...rolledUpMeaningful, ...belowThreshold];\n\treturn { tableSet, hasOther: otherMembers.length > 0, otherMembers };\n}\n\n/** Build the per-node snapshot rows using the supplied tableSet. */\nexport function computeSnapshot(\n\tnormalized: NormalizedRecord[],\n\ttableSet: string[],\n\thasOther: boolean,\n): SnapshotByNodeEntry[] {\n\t// For each (node, tableKey), find the latest size.\n\tconst latest = new Map<string, { size: number; time: number; tableKey: string; node: string }>();\n\tfor (const r of normalized) {\n\t\tconst k = `${r.node}\\0${r.tableKey}`;\n\t\tconst prev = latest.get(k);\n\t\tif (!prev || r.time >= prev.time) {\n\t\t\tlatest.set(k, { size: r.size, time: r.time, tableKey: r.tableKey, node: r.node });\n\t\t}\n\t}\n\n\tconst top = new Set(tableSet);\n\tconst byNode = new Map<string, SnapshotByNodeEntry>();\n\tfor (const { size, tableKey, node } of latest.values()) {\n\t\tif (!byNode.has(node)) { byNode.set(node, { node, stacks: {}, total: 0 }); }\n\t\tconst entry = byNode.get(node)!;\n\t\tentry.total += size;\n\t\tif (top.has(tableKey)) {\n\t\t\tentry.stacks[tableKey] = size;\n\t\t} else if (hasOther) {\n\t\t\tentry.stacks[OTHER_KEY] = (entry.stacks[OTHER_KEY] ?? 0) + size;\n\t\t}\n\t\t// else: below-threshold table in a no-rollup case — contributes to total only.\n\t}\n\n\t// Sort nodes for stable display order.\n\treturn [...byNode.values()].sort((a, b) => a.node.localeCompare(b.node));\n}\n\n/** Build a factory that returns trend points for a given table. */\nexport function computeTrendFactory(\n\tnormalized: NormalizedRecord[],\n\trange: TimeRange,\n): (selectedTable: string) => TrendPoint[] {\n\tconst bucketMs = computeBucketMs(range.endTime - range.startTime);\n\n\treturn function trend(selectedTable: string): TrendPoint[] {\n\t\t// Collect the latest sample per (bucket, node) for the selected table.\n\t\tconst byBucket = new Map<number, Map<string, { size: number; time: number }>>();\n\t\t// Track each node's last-sample time across the window to truncate trailing buckets.\n\t\tconst lastSampleTime = new Map<string, number>();\n\n\t\tfor (const r of normalized) {\n\t\t\tif (r.tableKey !== selectedTable) { continue; }\n\t\t\tif (r.time < range.startTime || r.time > range.endTime) { continue; }\n\t\t\tconst bucketStart = range.startTime + Math.floor((r.time - range.startTime) / bucketMs) * bucketMs;\n\t\t\tif (!byBucket.has(bucketStart)) { byBucket.set(bucketStart, new Map()); }\n\t\t\tconst nodeMap = byBucket.get(bucketStart)!;\n\t\t\tconst prev = nodeMap.get(r.node);\n\t\t\tif (!prev || r.time >= prev.time) {\n\t\t\t\tnodeMap.set(r.node, { size: r.size, time: r.time });\n\t\t\t}\n\t\t\tconst lastTime = lastSampleTime.get(r.node) ?? 0;\n\t\t\tif (r.time > lastTime) { lastSampleTime.set(r.node, r.time); }\n\t\t}\n\n\t\tconst points: TrendPoint[] = [];\n\t\tconst sortedBuckets = [...byBucket.keys()].sort((a, b) => a - b);\n\t\tfor (const bucketStart of sortedBuckets) {\n\t\t\tconst nodeMap = byBucket.get(bucketStart)!;\n\t\t\tconst values: Record<string, number> = {};\n\t\t\tfor (const [node, { size }] of nodeMap) {\n\t\t\t\t// Drop buckets that start after the node's last sample (truncate trailing).\n\t\t\t\tconst lastTime = lastSampleTime.get(node) ?? 0;\n\t\t\t\tif (bucketStart > lastTime) { continue; }\n\t\t\t\tvalues[node] = size;\n\t\t\t}\n\t\t\tif (Object.keys(values).length > 0) { points.push({ time: bucketStart, values }); }\n\t\t}\n\t\treturn points;\n\t};\n}\n\n/** Compute the default selection (largest delta) for a given ranking. */\nexport function computeDefaultSelection(\n\tnormalized: NormalizedRecord[],\n\trankBy: RankBy,\n): string | null {\n\tif (normalized.length === 0) { return null; }\n\n\t// Group samples by (tableKey, node) to compute per-node min/max.\n\tconst perPair = new Map<string, { min: number; max: number; distinctTimes: Set<number> }>();\n\tfor (const r of normalized) {\n\t\tconst key = `${r.tableKey}\\0${r.node}`;\n\t\tlet agg = perPair.get(key);\n\t\tif (!agg) {\n\t\t\tagg = { min: r.size, max: r.size, distinctTimes: new Set() };\n\t\t\tperPair.set(key, agg);\n\t\t}\n\t\tif (r.size < agg.min) { agg.min = r.size; }\n\t\tif (r.size > agg.max) { agg.max = r.size; }\n\t\tagg.distinctTimes.add(r.time);\n\t}\n\n\t// Per table, find the best (max) per-node delta.\n\ttype Score = { tableKey: string; delta: number; maxSize: number; hasDelta: boolean };\n\tconst perTable = new Map<string, Score>();\n\tfor (const [key, agg] of perPair) {\n\t\tconst [tableKey] = key.split('\\0');\n\t\tconst hasDelta = agg.distinctTimes.size >= 2 && agg.max > agg.min;\n\t\tconst deltaBytes = agg.max - agg.min;\n\t\tconst deltaPct = agg.max > 0 ? deltaBytes / agg.max : 0;\n\t\tconst score = rankBy === 'bytes' ? deltaBytes : deltaPct;\n\n\t\tconst prev = perTable.get(tableKey);\n\t\tif (!prev) {\n\t\t\tperTable.set(tableKey, { tableKey, delta: score, maxSize: agg.max, hasDelta });\n\t\t} else {\n\t\t\tif (score > prev.delta) { prev.delta = score; }\n\t\t\tif (agg.max > prev.maxSize) { prev.maxSize = agg.max; }\n\t\t\tif (hasDelta) { prev.hasDelta = true; }\n\t\t}\n\t}\n\n\tconst all = [...perTable.values()];\n\n\t// Any table with a computable delta?\n\tconst withDelta = all.filter((s) => s.hasDelta);\n\tif (withDelta.length > 0) {\n\t\twithDelta.sort((a, b) => {\n\t\t\tif (b.delta !== a.delta) { return b.delta - a.delta; }\n\t\t\treturn a.tableKey.localeCompare(b.tableKey);\n\t\t});\n\t\treturn withDelta[0].tableKey;\n\t}\n\n\t// Flat window fallback: largest max-size.\n\tall.sort((a, b) => {\n\t\tif (b.maxSize !== a.maxSize) { return b.maxSize - a.maxSize; }\n\t\treturn a.tableKey.localeCompare(b.tableKey);\n\t});\n\treturn all[0]?.tableKey ?? null;\n}\n\n/** Determine the empty-state discriminator. */\nexport function computeEmptyCause(\n\trawCount: number,\n\ttableSet: string[],\n\thasOther: boolean,\n): EmptyCause {\n\tif (rawCount === 0) { return 'upstream-empty'; }\n\tif (tableSet.length === 0 && hasOther) { return 'all-other'; }\n\treturn null;\n}\n\n/** Assemble a `TableSizeDerived` from raw records + current time range. */\nexport function buildDerived(raw: TableSizeRecord[], range: TimeRange): TableSizeDerived {\n\tconst normalized = dedupRecords(normalizeRecords(raw));\n\tconst { tableSet, hasOther, otherMembers } = computeTableSet(normalized);\n\tconst byNode = computeSnapshot(normalized, tableSet, hasOther);\n\tconst trend = computeTrendFactory(normalized, range);\n\tconst emptyCause = computeEmptyCause(raw.length, tableSet, hasOther);\n\n\t// Content signature: window + a cheap digest of the raw input.\n\tconst maxId = raw.reduce((m, r) => (r.id > m ? r.id : m), 0);\n\tconst signature = `${range.startTime}:${range.endTime}:${raw.length}:${maxId}`;\n\n\treturn {\n\t\tsnapshot: { byNode, tableSet, hasOther, otherMembers },\n\t\ttrend,\n\t\tdefaultSelection: (rankBy: RankBy) => computeDefaultSelection(normalized, rankBy),\n\t\temptyCause,\n\t\tsignature,\n\t};\n}\n\nexport interface SelectionResolution {\n\tnextTable: string | null;\n\tnextManual: boolean;\n}\n\n/**\n * Decide what `selectedTable` / `manualSelection` should become given the\n * previous values and the latest derived data. Pure function, so the logic\n * can be unit-tested without a React harness.\n *\n * Rules:\n * - If the user's manual pick is still in `tableSet`, keep it.\n * - Otherwise fall back to `defaultSelection(rankBy)` and clear manual.\n */\nexport function resolveSelection(input: {\n\tprev: string | null;\n\tsnapshot: Snapshot;\n\trankBy: RankBy;\n\tisManual: boolean;\n\tdefaultSelection: (rankBy: RankBy) => string | null;\n}): SelectionResolution {\n\tconst { prev, snapshot, rankBy, isManual, defaultSelection } = input;\n\tconst stillPresent = prev !== null && snapshot.tableSet.includes(prev);\n\tif (isManual && stillPresent) {\n\t\treturn { nextTable: prev, nextManual: true };\n\t}\n\treturn { nextTable: defaultSelection(rankBy), nextManual: false };\n}\n\nexport interface EmptyStateFlags {\n\t/** Render the ChartPanel's generic empty state (no data from upstream). */\n\tisEmpty: boolean;\n\t/** Render the snapshot's \"all tables are small\" inline hint in place of the chart. */\n\tallOtherHint: boolean;\n}\n\n/** Map `emptyCause` to the UI flags both panels consume. Pure for testability. */\nexport function emptyCauseToFlags(cause: EmptyCause): EmptyStateFlags {\n\treturn {\n\t\tisEmpty: cause === 'upstream-empty',\n\t\tallOtherHint: cause === 'all-other',\n\t};\n}\n\n/**\n * Compute the legend growth annotation for a single node across a trend's\n * points. `windowMs` is the panel's requested range (not the samples' span)\n * so the `/hr` rate reflects the user-selected window.\n *\n * Returns an empty string when the annotation wouldn't be meaningful:\n * - fewer than 2 samples\n * - no observed change (delta ≤ 0)\n * - windowMs ≤ 0 (inverted or zero-width range)\n */\nexport function computeGrowthAnnotation(input: {\n\tpoints: Array<{ time: number; values: Record<string, number> }>;\n\tnode: string;\n\twindowMs: number;\n\trankBy: RankBy;\n\tformatBytes: (bytes: number) => string;\n}): string {\n\tconst { points, node, windowMs, rankBy, formatBytes } = input;\n\tconst samples = points\n\t\t.map((p) => p.values[node])\n\t\t.filter((v): v is number => typeof v === 'number');\n\tif (samples.length < 2) { return ''; }\n\tlet min = samples[0];\n\tlet max = samples[0];\n\tfor (const v of samples) {\n\t\tif (v < min) { min = v; }\n\t\tif (v > max) { max = v; }\n\t}\n\tconst delta = max - min;\n\tif (delta <= 0) { return ''; }\n\tif (rankBy === 'percent') {\n\t\tconst pct = max > 0 ? (delta / max) * 100 : 0;\n\t\treturn `+${pct.toFixed(1)}%/window`;\n\t}\n\tif (windowMs <= 0) { return ''; }\n\tconst hours = windowMs / (1000 * 60 * 60);\n\tconst perHr = delta / hours;\n\treturn `+${formatBytes(delta)} (${formatBytes(perHr)}/hr)`;\n}\n","export type Theme = 'light' | 'dark';\n\nconst STORAGE_KEY = 'analytics-viz-theme';\n\nexport function getStoredTheme(): Theme {\n\t// Safari private mode + sandboxed iframes throw on localStorage access.\n\t// Fall back to the OS preference rather than blowing up the whole tab.\n\ttry {\n\t\tconst stored = localStorage.getItem(STORAGE_KEY);\n\t\tif (stored === 'light' || stored === 'dark') { return stored; }\n\t} catch {\n\t\t// fall through to media-query fallback\n\t}\n\treturn window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';\n}\n\nexport function setStoredTheme(theme: Theme): void {\n\ttry {\n\t\tlocalStorage.setItem(STORAGE_KEY, theme);\n\t} catch {\n\t\t// best-effort; silently drop in restricted-storage environments\n\t}\n}\n\nexport function applyTheme(theme: Theme): void {\n\tdocument.documentElement.classList.toggle('dark', theme === 'dark');\n}\n\n/** Reads the studio chart-surface CSS tokens defined in src/index.css.\n * All charts render inside a `Card`, so axis/grid/tooltip colors resolve\n * against `--card`, not the brand-purple `--background`. The hex defaults\n * here are fallbacks for non-DOM environments (tests). */\nexport function getChartColors(_theme: Theme) {\n\treturn {\n\t\taxisColor: 'var(--chart-axis, #6b7280)',\n\t\tgridColor: 'var(--chart-grid, #e5e7eb)',\n\t\ttooltipBg: 'var(--chart-tooltip-bg, #ffffff)',\n\t\ttooltipBorder: 'var(--chart-grid, #d1d5db)',\n\t\ttextColor: 'var(--chart-tooltip-fg, #1f2937)',\n\t};\n}\n","import { type KeyboardEvent, useEffect, useRef } from 'react';\nimport { getTableColor, OTHER_COLOR } from '../lib/tableColors.ts';\nimport { OTHER_KEY } from '../lib/tableSize.ts';\n\ninterface TableSizeChipRowProps {\n\t/** Selectable table keys, in display order. */\n\ttableSet: string[];\n\t/** Whether to render a non-interactive \"Other\" chip. */\n\thasOther: boolean;\n\t/** Currently-selected table key (or null). */\n\tselectedTable: string | null;\n\t/**\n\t * Called when the user picks a chip via click, Enter, Space, or arrow-key\n\t * traversal. Per the ARIA radiogroup pattern, arrow keys move focus AND\n\t * selection — both routes pin `manualSelection=true` in Dashboard so the\n\t * pick survives subsequent data refreshes until the selected table\n\t * disappears from `tableSet`.\n\t */\n\tonSelectTable: (tableKey: string) => void;\n}\n\nexport function TableSizeChipRow({\n\ttableSet,\n\thasOther,\n\tselectedTable,\n\tonSelectTable,\n}: TableSizeChipRowProps) {\n\tconst chipRefs = useRef<Array<HTMLButtonElement | null>>([]);\n\n\tuseEffect(() => {\n\t\tchipRefs.current = chipRefs.current.slice(0, tableSet.length);\n\t}, [tableSet.length]);\n\n\t// Figure out which chip gets tabIndex=0. Default to the selected chip; if\n\t// selectedTable is not in tableSet (e.g. transient refetch gap, or selection\n\t// is `Other`), fall back to the first chip so the radiogroup stays reachable.\n\tconst activeIdx = selectedTable === null ? -1 : tableSet.indexOf(selectedTable);\n\tconst tabbableIdx = activeIdx >= 0 ? activeIdx : 0;\n\n\tfunction handleKeyDown(e: KeyboardEvent<HTMLButtonElement>, idx: number) {\n\t\tif (e.key === 'Enter' || e.key === ' ') {\n\t\t\te.preventDefault();\n\t\t\tonSelectTable(tableSet[idx]);\n\t\t\treturn;\n\t\t}\n\n\t\tif (\n\t\t\te.key !== 'ArrowLeft'\n\t\t\t&& e.key !== 'ArrowRight'\n\t\t\t&& e.key !== 'ArrowDown'\n\t\t\t&& e.key !== 'ArrowUp'\n\t\t) {\n\t\t\treturn;\n\t\t}\n\n\t\te.preventDefault();\n\t\tconst n = tableSet.length;\n\t\tif (n === 0) { return; }\n\t\tlet next = idx;\n\t\tif (e.key === 'ArrowRight' || e.key === 'ArrowDown') { next = (idx + 1) % n; }\n\t\tif (e.key === 'ArrowLeft' || e.key === 'ArrowUp') { next = (idx - 1 + n) % n; }\n\t\tchipRefs.current[next]?.focus();\n\t\tonSelectTable(tableSet[next]);\n\t}\n\n\tif (tableSet.length === 0 && !hasOther) { return null; }\n\n\treturn (\n\t\t<div\n\t\t\trole=\"radiogroup\"\n\t\t\taria-label=\"Table selector\"\n\t\t\tdata-testid=\"table-size-chip-row\"\n\t\t\tclassName=\"flex flex-wrap gap-2 pt-3\"\n\t\t>\n\t\t\t{tableSet.map((tableKey, idx) => {\n\t\t\t\tconst selected = tableKey === selectedTable;\n\t\t\t\tconst color = getTableColor(idx);\n\t\t\t\treturn (\n\t\t\t\t\t<button\n\t\t\t\t\t\tkey={tableKey}\n\t\t\t\t\t\tref={(el) => {\n\t\t\t\t\t\t\tchipRefs.current[idx] = el;\n\t\t\t\t\t\t}}\n\t\t\t\t\t\trole=\"radio\"\n\t\t\t\t\t\taria-checked={selected}\n\t\t\t\t\t\ttabIndex={idx === tabbableIdx ? 0 : -1}\n\t\t\t\t\t\tdata-testid=\"table-size-chip\"\n\t\t\t\t\t\tdata-table={tableKey}\n\t\t\t\t\t\tonKeyDown={(e) => handleKeyDown(e, idx)}\n\t\t\t\t\t\tonClick={() => onSelectTable(tableKey)}\n\t\t\t\t\t\tclassName={`inline-flex min-h-8 items-center gap-1.5 rounded-full border px-2.5 py-1 text-xs ${\n\t\t\t\t\t\t\tselected\n\t\t\t\t\t\t\t\t? 'font-semibold text-(--color-text-primary)'\n\t\t\t\t\t\t\t\t: 'border-(--color-border) text-(--color-text-secondary) hover:text-(--color-text-primary)'\n\t\t\t\t\t\t}`}\n\t\t\t\t\t\tstyle={{ borderColor: selected ? color : undefined }}\n\t\t\t\t\t>\n\t\t\t\t\t\t<span className=\"inline-block h-2 w-2 rounded-full\" style={{ backgroundColor: color }} />\n\t\t\t\t\t\t{tableKey}\n\t\t\t\t\t</button>\n\t\t\t\t);\n\t\t\t})}\n\t\t\t{hasOther && (\n\t\t\t\t<button\n\t\t\t\t\ttype=\"button\"\n\t\t\t\t\taria-disabled=\"true\"\n\t\t\t\t\ttabIndex={-1}\n\t\t\t\t\tdata-testid=\"table-size-chip\"\n\t\t\t\t\tdata-table={OTHER_KEY}\n\t\t\t\t\ttitle=\"Aggregate of smaller tables; not selectable.\"\n\t\t\t\t\tclassName=\"inline-flex min-h-8 items-center gap-1.5 rounded-full border border-dashed border-(--color-border) px-2.5 py-1 text-xs text-(--color-text-secondary)/60 cursor-not-allowed\"\n\t\t\t\t>\n\t\t\t\t\t<span className=\"inline-block h-2 w-2 rounded-full\" style={{ backgroundColor: OTHER_COLOR }} />\n\t\t\t\t\tOther\n\t\t\t\t</button>\n\t\t\t)}\n\t\t</div>\n\t);\n}\n","import { Bar, BarChart, CartesianGrid, Cell, ResponsiveContainer, Tooltip, XAxis, YAxis } from 'recharts';\nimport { useNodeSelection } from '../hooks/useNodeSelection.ts';\nimport { getTableColor, OTHER_COLOR } from '../lib/tableColors.ts';\nimport { OTHER_KEY, type Snapshot } from '../lib/tableSize.ts';\nimport { getChartColors, type Theme } from '../lib/theme.ts';\nimport { formatBytes } from '../lib/time.ts';\nimport type { ViewMode } from '../types/analytics.ts';\nimport { NodeLegend } from './NodeLegend.tsx';\nimport { TableSizeChipRow } from './TableSizeChipRow.tsx';\n\ninterface Props {\n\tsnapshot: Snapshot;\n\t/** Display mode: 'per-node' → Absolute (raw bytes), 'aggregate' → Normalized (percent of cluster max). */\n\tviewMode: ViewMode;\n\ttheme: Theme;\n\t/** Snapshot's own highlight — drives chip `aria-checked` + bar-segment outline. */\n\tselectedTable: string | null;\n\t/** Chip click / Enter / Space / arrow-nav — local to this panel; should NOT\n\t * drive Trend. */\n\tonChipSelect: (tableKey: string) => void;\n\t/** Bar-segment click — drilldown signal: should drive both this panel's\n\t * highlight AND the Trend panel's selection. */\n\tonBarClick: (tableKey: string) => void;\n\t/** Rendered inline when `emptyCause === 'all-other'`. */\n\tallOtherHint?: boolean;\n}\n\ninterface Row {\n\tnode: string;\n\t__total__: number;\n\t/** Aliased values keyed by `t_<idx>` (matching `stackKeys` index) so Recharts'\n\t * string `dataKey` lookup works — table names like `data.events` would\n\t * otherwise be split as object paths. */\n\t[aliasKey: string]: number | string;\n}\n\n/**\n * Both modes produce the same row shape: stacks carry absolute bytes, with\n * `__total__` tracking the node's cross-all-tables total. The mode only\n * changes how the y-axis renders those bytes. This is what gives Normalized\n * its \"visible gap\" behavior: a node missing a top-N table has a shorter\n * bar (not a re-stretched 100%), and a tall node anchors the 100% tick.\n */\nfunction toRows(\n\tsnapshot: Snapshot,\n\tactiveNodes: (n: string) => boolean,\n\tstackKeys: string[],\n): Row[] {\n\treturn snapshot.byNode\n\t\t.filter((n) => activeNodes(n.node))\n\t\t.map((n) => {\n\t\t\tconst aliased: Record<string, number> = {};\n\t\t\tstackKeys.forEach((tableKey, idx) => {\n\t\t\t\taliased[`t_${idx}`] = n.stacks[tableKey] ?? 0;\n\t\t\t});\n\t\t\treturn { node: n.node, ...aliased, __total__: n.total };\n\t\t});\n}\n\nexport function TableSizeSnapshot({\n\tsnapshot,\n\tviewMode,\n\ttheme,\n\tselectedTable,\n\tonChipSelect,\n\tonBarClick,\n\tallOtherHint,\n}: Props) {\n\tconst colors = getChartColors(theme);\n\t// The full cluster node list, used both for the legend and for color\n\t// assignment so the same node gets the same color on both panels.\n\tconst clusterNodeIds = snapshot.byNode.map((n) => n.node);\n\tconst { isActive, handleLegendClick } = useNodeSelection(clusterNodeIds);\n\tconst normalized = viewMode === 'aggregate';\n\n\tif (allOtherHint) {\n\t\treturn (\n\t\t\t<div className=\"flex h-full items-center justify-center text-sm text-(--color-text-secondary)\">\n\t\t\t\tAll tables are small within this window — widen the range to see growth.\n\t\t\t</div>\n\t\t);\n\t}\n\n\tconst stackKeys = [...snapshot.tableSet, ...(snapshot.hasOther ? [OTHER_KEY] : [])];\n\tconst rows = toRows(snapshot, isActive, stackKeys);\n\n\t// In Normalized mode the y-axis is scaled so the tallest (node-total)\n\t// bar hits 100%. Other bars sit shorter and missing segments show up\n\t// as visible gaps rather than renormalized stacks.\n\tconst clusterMaxTotal = rows.reduce((m, r) => Math.max(m, r.__total__), 0) || 1;\n\n\treturn (\n\t\t<div className=\"h-full flex flex-col\">\n\t\t\t<div style={{ width: '100%', height: 300 }}>\n\t\t\t\t<ResponsiveContainer width=\"100%\" height=\"100%\" minWidth={0}>\n\t\t\t\t\t<BarChart data={rows} barCategoryGap=\"20%\">\n\t\t\t\t\t\t<CartesianGrid stroke={colors.gridColor} strokeDasharray=\"3 3\" />\n\t\t\t\t\t\t<XAxis dataKey=\"node\" stroke={colors.axisColor} tick={{ fontSize: 11 }} />\n\t\t\t\t\t\t<YAxis\n\t\t\t\t\t\t\tstroke={colors.axisColor}\n\t\t\t\t\t\t\ttick={{ fontSize: 11 }}\n\t\t\t\t\t\t\ttickFormatter={(v) => {\n\t\t\t\t\t\t\t\tconst n = Number(v);\n\t\t\t\t\t\t\t\tif (normalized) {\n\t\t\t\t\t\t\t\t\treturn `${Math.round((n / clusterMaxTotal) * 100)}%`;\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\treturn formatBytes(n);\n\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t\tdomain={normalized ? [0, clusterMaxTotal] : ['auto', 'auto']}\n\t\t\t\t\t\t/>\n\t\t\t\t\t\t<Tooltip\n\t\t\t\t\t\t\tcontentStyle={{\n\t\t\t\t\t\t\t\tbackgroundColor: colors.tooltipBg,\n\t\t\t\t\t\t\t\tborder: `1px solid ${colors.tooltipBorder}`,\n\t\t\t\t\t\t\t\tborderRadius: 8,\n\t\t\t\t\t\t\t\tfontSize: 12,\n\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t\tformatter={(value, name, ctx) => {\n\t\t\t\t\t\t\t\tconst nameStr = String(name);\n\t\t\t\t\t\t\t\tconst label = nameStr === OTHER_KEY ? 'Other' : nameStr;\n\t\t\t\t\t\t\t\tconst numValue = Number(value);\n\t\t\t\t\t\t\t\tconst total = ((ctx as { payload?: Row })?.payload?.__total__ as number) ?? 0;\n\t\t\t\t\t\t\t\tconst pct = total > 0 ? ((numValue / total) * 100).toFixed(1) : '0';\n\t\t\t\t\t\t\t\treturn [\n\t\t\t\t\t\t\t\t\t`${formatBytes(numValue)} (${pct}% of node total ${formatBytes(total)})`,\n\t\t\t\t\t\t\t\t\tlabel,\n\t\t\t\t\t\t\t\t];\n\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t/>\n\t\t\t\t\t\t{stackKeys.map((tableKey, idx) => {\n\t\t\t\t\t\t\tconst baseColor = tableKey === OTHER_KEY ? OTHER_COLOR : getTableColor(idx);\n\t\t\t\t\t\t\tconst isSelected = tableKey === selectedTable;\n\t\t\t\t\t\t\treturn (\n\t\t\t\t\t\t\t\t<Bar\n\t\t\t\t\t\t\t\t\tkey={tableKey}\n\t\t\t\t\t\t\t\t\t// Aliased dataKey ('t_<idx>') so Recharts' string-path lookup\n\t\t\t\t\t\t\t\t\t// works — table names like 'data.events' contain dots and would\n\t\t\t\t\t\t\t\t\t// otherwise be parsed as object paths. `name` keeps the real\n\t\t\t\t\t\t\t\t\t// table key for tooltips.\n\t\t\t\t\t\t\t\t\tdataKey={`t_${idx}`}\n\t\t\t\t\t\t\t\t\tname={tableKey}\n\t\t\t\t\t\t\t\t\tstackId=\"size\"\n\t\t\t\t\t\t\t\t\tfill={baseColor}\n\t\t\t\t\t\t\t\t\tstroke={isSelected ? baseColor : 'transparent'}\n\t\t\t\t\t\t\t\t\tstrokeWidth={isSelected ? 2 : 0}\n\t\t\t\t\t\t\t\t\tonClick={() => {\n\t\t\t\t\t\t\t\t\t\tif (tableKey !== OTHER_KEY) { onBarClick(tableKey); }\n\t\t\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t\t\t\tstyle={{ cursor: tableKey === OTHER_KEY ? 'not-allowed' : 'pointer' }}\n\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t{rows.map((row) => (\n\t\t\t\t\t\t\t\t\t\t<Cell\n\t\t\t\t\t\t\t\t\t\t\tkey={row.node}\n\t\t\t\t\t\t\t\t\t\t\tdata-testid=\"table-size-segment\"\n\t\t\t\t\t\t\t\t\t\t\tdata-table={tableKey}\n\t\t\t\t\t\t\t\t\t\t\tdata-node={row.node}\n\t\t\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t\t\t))}\n\t\t\t\t\t\t\t\t</Bar>\n\t\t\t\t\t\t\t);\n\t\t\t\t\t\t})}\n\t\t\t\t\t</BarChart>\n\t\t\t\t</ResponsiveContainer>\n\t\t\t</div>\n\t\t\t<NodeLegend nodeIds={clusterNodeIds} isActive={isActive} onClickNode={handleLegendClick} />\n\t\t\t<TableSizeChipRow\n\t\t\t\ttableSet={snapshot.tableSet}\n\t\t\t\thasOther={snapshot.hasOther}\n\t\t\t\tselectedTable={selectedTable}\n\t\t\t\tonSelectTable={onChipSelect}\n\t\t\t/>\n\t\t</div>\n\t);\n}\n","import { useMemo } from 'react';\nimport { CartesianGrid, Line, LineChart, ResponsiveContainer, Tooltip, XAxis, YAxis } from 'recharts';\nimport { useNodeSelection } from '../hooks/useNodeSelection.ts';\nimport { getNodeColor } from '../lib/nodeColors.ts';\nimport { computeGrowthAnnotation, type RankBy, type TableSizeDerived } from '../lib/tableSize.ts';\nimport { getChartColors, type Theme } from '../lib/theme.ts';\nimport { formatAxisTick, formatBytes, formatTooltipTime } from '../lib/time.ts';\nimport type { TimeRange, ViewMode } from '../types/analytics.ts';\nimport { NodeLegend } from './NodeLegend.tsx';\nimport { TableSizeChipRow } from './TableSizeChipRow.tsx';\n\ninterface Props {\n\tderived: TableSizeDerived;\n\tviewMode: ViewMode;\n\ttheme: Theme;\n\tselectedTable: string | null;\n\t/** Trend chip click — local to this panel; should NOT touch the Snapshot. */\n\tonChipSelect: (tableKey: string) => void;\n\t/** Whether the current selectedTable was user-chosen (true) vs auto (false). */\n\tmanualSelection: boolean;\n\t/** Panel's active time range. Used for `/hr` growth annotation so the rate\n\t * is grounded in the user-requested window, not the span of actual samples. */\n\trange: TimeRange;\n\t/** Cluster-wide node list (from snapshot). Ensures node colors match across panels. */\n\tclusterNodeIds: string[];\n\t/** Current ranking preference — controlled by Dashboard (single source of truth). */\n\trankBy: RankBy;\n\t/** User toggled the rank. Dashboard persists to localStorage. */\n\tonRankChange: (r: RankBy) => void;\n}\n\nexport function TableSizeTrend({\n\tderived,\n\tviewMode,\n\ttheme,\n\tselectedTable,\n\tonChipSelect,\n\tmanualSelection,\n\trange,\n\tclusterNodeIds,\n\trankBy,\n\tonRankChange,\n}: Props) {\n\tconst colors = getChartColors(theme);\n\n\tconst points = useMemo(\n\t\t() => (selectedTable ? derived.trend(selectedTable) : []),\n\t\t[derived, selectedTable],\n\t);\n\n\tconst nodesWithData = useMemo(() => {\n\t\tconst s = new Set<string>();\n\t\tfor (const p of points) { for (const n of Object.keys(p.values)) { s.add(n); } }\n\t\treturn [...s].sort();\n\t}, [points]);\n\n\tconst { isActive, handleLegendClick } = useNodeSelection(nodesWithData);\n\n\tconst windowMs = Math.max(0, range.endTime - range.startTime);\n\n\t// Alias node FQDN keys to `n_<idx>` so Recharts' string `dataKey` lookup\n\t// works — node names contain dots and would otherwise be parsed as paths.\n\t// MUST stay above the early `return` below to satisfy Rules of Hooks —\n\t// selectedTable can flip null↔string between renders.\n\tconst nodeAlias = useMemo(() => {\n\t\tconst m = new Map<string, string>();\n\t\tnodesWithData.forEach((node, idx) => m.set(node, `n_${idx}`));\n\t\treturn m;\n\t}, [nodesWithData]);\n\n\tconst chartData = useMemo(() =>\n\t\tpoints.map((p) => {\n\t\t\tconst aliased: Record<string, number | null> = {};\n\t\t\tfor (const [node, alias] of nodeAlias) {\n\t\t\t\taliased[alias] = p.values[node] ?? null;\n\t\t\t}\n\t\t\treturn { time: p.time, ...aliased };\n\t\t}), [points, nodeAlias]);\n\n\tif (!selectedTable) {\n\t\treturn (\n\t\t\t<div className=\"flex h-full items-center justify-center text-sm text-(--color-text-secondary)\">\n\t\t\t\tSelect a table to view trend.\n\t\t\t</div>\n\t\t);\n\t}\n\n\treturn (\n\t\t<div className=\"h-full flex flex-col\">\n\t\t\t{\n\t\t\t\t/* SR-only live region: announces selection changes without adding visible\n\t\t\t duplication of the ChartPanel title (which already interpolates the name). */\n\t\t\t}\n\t\t\t<div\n\t\t\t\taria-live=\"polite\"\n\t\t\t\tdata-testid=\"table-size-trend-title\"\n\t\t\t\tclassName=\"sr-only\"\n\t\t\t>\n\t\t\t\tTrend selection: {selectedTable}\n\t\t\t\t{!manualSelection\n\t\t\t\t\t? ` (auto-selected — ${rankBy === 'bytes' ? 'largest bytes change' : 'largest percent change'})`\n\t\t\t\t\t: ''}\n\t\t\t</div>\n\n\t\t\t{!manualSelection && (\n\t\t\t\t<div className=\"mb-1 text-[10px] text-(--color-text-secondary)\">\n\t\t\t\t\tAuto-selected by {rankBy === 'bytes' ? 'largest bytes change' : 'largest % change'}\n\t\t\t\t</div>\n\t\t\t)}\n\n\t\t\t{/* Rank toggle */}\n\t\t\t<div className=\"mb-2 flex items-center gap-2\">\n\t\t\t\t<span className=\"text-[11px] text-(--color-text-secondary)\">Rank by:</span>\n\t\t\t\t<div\n\t\t\t\t\tclassName=\"inline-flex rounded border border-(--color-border) bg-(--color-bg-tertiary) p-0.5\"\n\t\t\t\t\tdata-testid=\"table-size-rank-toggle\"\n\t\t\t\t>\n\t\t\t\t\t<button\n\t\t\t\t\t\ttype=\"button\"\n\t\t\t\t\t\taria-pressed={rankBy === 'bytes'}\n\t\t\t\t\t\tonClick={() => onRankChange('bytes')}\n\t\t\t\t\t\tclassName={`rounded px-2 py-0.5 text-[11px] ${\n\t\t\t\t\t\t\trankBy === 'bytes'\n\t\t\t\t\t\t\t\t? 'bg-(--color-bg-secondary) text-(--color-text-primary)'\n\t\t\t\t\t\t\t\t: 'text-(--color-text-secondary)'\n\t\t\t\t\t\t}`}\n\t\t\t\t\t>\n\t\t\t\t\t\tBytes changed\n\t\t\t\t\t</button>\n\t\t\t\t\t<button\n\t\t\t\t\t\ttype=\"button\"\n\t\t\t\t\t\taria-pressed={rankBy === 'percent'}\n\t\t\t\t\t\tonClick={() => onRankChange('percent')}\n\t\t\t\t\t\tclassName={`rounded px-2 py-0.5 text-[11px] ${\n\t\t\t\t\t\t\trankBy === 'percent'\n\t\t\t\t\t\t\t\t? 'bg-(--color-bg-secondary) text-(--color-text-primary)'\n\t\t\t\t\t\t\t\t: 'text-(--color-text-secondary)'\n\t\t\t\t\t\t}`}\n\t\t\t\t\t>\n\t\t\t\t\t\t% change\n\t\t\t\t\t</button>\n\t\t\t\t</div>\n\t\t\t</div>\n\n\t\t\t<div style={{ width: '100%', height: 300 }}>\n\t\t\t\t<ResponsiveContainer width=\"100%\" height=\"100%\" minWidth={0}>\n\t\t\t\t\t<LineChart data={chartData}>\n\t\t\t\t\t\t<CartesianGrid stroke={colors.gridColor} strokeDasharray=\"3 3\" />\n\t\t\t\t\t\t<XAxis\n\t\t\t\t\t\t\tdataKey=\"time\"\n\t\t\t\t\t\t\ttype=\"number\"\n\t\t\t\t\t\t\t// Pin to the requested window so a sparse table-size response\n\t\t\t\t\t\t\t// (typically one sample every few minutes) doesn't collapse\n\t\t\t\t\t\t\t// the axis to the data span.\n\t\t\t\t\t\t\tdomain={[range.startTime, range.endTime]}\n\t\t\t\t\t\t\tallowDataOverflow\n\t\t\t\t\t\t\ttickFormatter={formatAxisTick}\n\t\t\t\t\t\t\tstroke={colors.axisColor}\n\t\t\t\t\t\t\ttick={{ fontSize: 11 }}\n\t\t\t\t\t\t\tallowDuplicatedCategory={false}\n\t\t\t\t\t\t/>\n\t\t\t\t\t\t<YAxis\n\t\t\t\t\t\t\tstroke={colors.axisColor}\n\t\t\t\t\t\t\ttick={{ fontSize: 11 }}\n\t\t\t\t\t\t\ttickFormatter={formatBytes}\n\t\t\t\t\t\t\tscale={viewMode === 'per-node' ? 'log' : 'auto'}\n\t\t\t\t\t\t\tdomain={viewMode === 'per-node' ? [1, 'auto'] : ['auto', 'auto']}\n\t\t\t\t\t\t\tallowDataOverflow\n\t\t\t\t\t\t/>\n\t\t\t\t\t\t<Tooltip\n\t\t\t\t\t\t\tcontentStyle={{\n\t\t\t\t\t\t\t\tbackgroundColor: colors.tooltipBg,\n\t\t\t\t\t\t\t\tborder: `1px solid ${colors.tooltipBorder}`,\n\t\t\t\t\t\t\t\tborderRadius: 8,\n\t\t\t\t\t\t\t\tfontSize: 12,\n\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t\tlabelFormatter={(label) => formatTooltipTime(Number(label))}\n\t\t\t\t\t\t\tformatter={(value) => formatBytes(Number(value))}\n\t\t\t\t\t\t/>\n\t\t\t\t\t\t{nodesWithData\n\t\t\t\t\t\t\t.filter((n) => isActive(n))\n\t\t\t\t\t\t\t.map((node) => (\n\t\t\t\t\t\t\t\t<Line\n\t\t\t\t\t\t\t\t\tkey={node}\n\t\t\t\t\t\t\t\t\t// Aliased dataKey ('n_<idx>') so Recharts' string-path lookup\n\t\t\t\t\t\t\t\t\t// works — node FQDNs contain dots and would be parsed as paths.\n\t\t\t\t\t\t\t\t\tdataKey={nodeAlias.get(node)!}\n\t\t\t\t\t\t\t\t\tname={`${node} ${computeGrowthAnnotation({ points, node, windowMs, rankBy, formatBytes })}`.trim()}\n\t\t\t\t\t\t\t\t\t// Use clusterNodeIds (not nodesWithData) so the same node gets\n\t\t\t\t\t\t\t\t\t// the same color on both snapshot and trend panels.\n\t\t\t\t\t\t\t\t\tstroke={getNodeColor(node, clusterNodeIds)}\n\t\t\t\t\t\t\t\t\tstrokeWidth={2}\n\t\t\t\t\t\t\t\t\tdot={false}\n\t\t\t\t\t\t\t\t\ttype=\"monotone\"\n\t\t\t\t\t\t\t\t\tconnectNulls={false}\n\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t))}\n\t\t\t\t\t</LineChart>\n\t\t\t\t</ResponsiveContainer>\n\t\t\t</div>\n\t\t\t<NodeLegend nodeIds={nodesWithData} isActive={isActive} onClickNode={handleLegendClick} />\n\t\t\t<TableSizeChipRow\n\t\t\t\ttableSet={derived.snapshot.tableSet}\n\t\t\t\thasOther={derived.snapshot.hasOther}\n\t\t\t\tselectedTable={selectedTable}\n\t\t\t\tonSelectTable={onChipSelect}\n\t\t\t/>\n\t\t</div>\n\t);\n}\n","import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';\nimport { useMemo, useRef, useState } from 'react';\nimport { TableSizeSnapshot } from '../charts/TableSizeSnapshot.tsx';\nimport { TableSizeTrend } from '../charts/TableSizeTrend.tsx';\nimport { ChartCopyButton } from '../components/ChartCopyButton.tsx';\nimport { ChartExpandButton } from '../components/ChartExpandButton.tsx';\nimport { ChartExportButton } from '../components/ChartExportButton.tsx';\nimport { useAnalyticsContext } from '../context/AnalyticsContext.tsx';\nimport { useAnalyticsRecords } from '../hooks/useAnalyticsRecords.ts';\nimport { buildDerived, type RankBy, type TableSizeDerived } from '../lib/tableSize.ts';\nimport type { TableSizeRecord } from '../types/analytics.ts';\n\nfunction derivedClusterNodes(derived: TableSizeDerived): string[] {\n\treturn derived.snapshot.byNode.map((b) => b.node);\n}\nimport { MetricPanel } from './MetricPanel.tsx';\nimport { PanelErrorBoundary } from './PanelErrorBoundary.tsx';\n\nexport function StorageTab() {\n\treturn (\n\t\t<div className=\"grid grid-cols-1 gap-4\">\n\t\t\t<PanelErrorBoundary metric=\"table-size\">\n\t\t\t\t<TableSizePanels />\n\t\t\t</PanelErrorBoundary>\n\t\t\t<div className=\"grid grid-cols-1 xl:grid-cols-2 gap-4\">\n\t\t\t\t<MetricPanel metric=\"database-size\" />\n\t\t\t\t<MetricPanel metric=\"transaction-log-growth\" />\n\t\t\t\t<MetricPanel metric=\"storage-volume\" />\n\t\t\t</div>\n\t\t</div>\n\t);\n}\n\nfunction TableSizePanels() {\n\tconst { timeRange, bucketMs, refreshIntervalMs, theme, instanceParams } = useAnalyticsContext();\n\tconst { data, isLoading, isError } = useAnalyticsRecords({\n\t\tmetric: 'table-size',\n\t\tstartTime: timeRange.startTime,\n\t\tendTime: timeRange.endTime,\n\t\tinstanceParams,\n\t\trefetchIntervalMs: refreshIntervalMs,\n\t\tbucketMs,\n\t});\n\n\t// Some Harper builds emit the bucket timestamp as `id`, others as `time`.\n\t// Accept either, but synthesize `id` from `time` so downstream\n\t// (buildDerived → tableSize.normalizeRecords) gets the contract it\n\t// expects. Drop rows that have neither — they'd feed NaN timestamps and\n\t// silently produce wrong snapshots.\n\tconst raw = useMemo(() => {\n\t\tconst rows = (data ?? []) as unknown as Array<TableSizeRecord & { time?: number }>;\n\t\tconst cleaned: TableSizeRecord[] = [];\n\t\tlet withoutTimestamp = 0;\n\t\tfor (const r of rows) {\n\t\t\tif (typeof r.id === 'number') {\n\t\t\t\tcleaned.push(r);\n\t\t\t} else if (typeof r.time === 'number') {\n\t\t\t\tcleaned.push({ ...r, id: r.time });\n\t\t\t} else {\n\t\t\t\twithoutTimestamp++;\n\t\t\t}\n\t\t}\n\t\tif (withoutTimestamp > 0) {\n\t\t\tconsole.warn('[table-size] dropped rows missing both id and time', {\n\t\t\t\tdropped: withoutTimestamp,\n\t\t\t\ttotal: rows.length,\n\t\t\t});\n\t\t}\n\t\treturn cleaned;\n\t}, [data]);\n\tconst derived = useMemo(() => buildDerived(raw, timeRange), [raw, timeRange.startTime, timeRange.endTime]);\n\n\tconst rankBy: RankBy = 'bytes';\n\tconst [selected, setSelected] = useState<string | null>(null);\n\tconst snapshotCardRef = useRef<HTMLDivElement>(null);\n\tconst trendCardRef = useRef<HTMLDivElement>(null);\n\tconst effectiveSelection = useMemo(() => {\n\t\tif (selected && derived.snapshot.tableSet.includes(selected)) { return selected; }\n\t\treturn derived.defaultSelection(rankBy);\n\t}, [selected, derived]);\n\n\tif (isLoading) {\n\t\treturn (\n\t\t\t<Card>\n\t\t\t\t<CardHeader>\n\t\t\t\t\t<CardTitle>Table sizes</CardTitle>\n\t\t\t\t</CardHeader>\n\t\t\t\t<CardContent>\n\t\t\t\t\t<div className=\"h-64 rounded-md bg-muted/30 animate-pulse\" aria-label=\"Loading\" />\n\t\t\t\t</CardContent>\n\t\t\t</Card>\n\t\t);\n\t}\n\n\tif (isError) {\n\t\treturn (\n\t\t\t<Card>\n\t\t\t\t<CardHeader>\n\t\t\t\t\t<CardTitle>Table sizes</CardTitle>\n\t\t\t\t\t<CardDescription>Per-table storage breakdown.</CardDescription>\n\t\t\t\t</CardHeader>\n\t\t\t\t<CardContent>\n\t\t\t\t\t<div className=\"h-32 rounded-md border border-destructive/40 bg-destructive/10 p-4 text-sm text-destructive flex items-center justify-center\">\n\t\t\t\t\t\tFailed to load table-size data. Try a different time window or refresh.\n\t\t\t\t\t</div>\n\t\t\t\t</CardContent>\n\t\t\t</Card>\n\t\t);\n\t}\n\n\tif (derived.emptyCause === 'upstream-empty') {\n\t\treturn (\n\t\t\t<Card>\n\t\t\t\t<CardHeader>\n\t\t\t\t\t<CardTitle>Table sizes</CardTitle>\n\t\t\t\t\t<CardDescription>Per-table storage breakdown.</CardDescription>\n\t\t\t\t</CardHeader>\n\t\t\t\t<CardContent>\n\t\t\t\t\t<div className=\"h-32 rounded-md border border-border bg-muted/20 p-4 text-sm text-muted-foreground flex items-center justify-center\">\n\t\t\t\t\t\tNo table-size data in the selected window.\n\t\t\t\t\t</div>\n\t\t\t\t</CardContent>\n\t\t\t</Card>\n\t\t);\n\t}\n\n\t// renderSnapshot/renderTrend accept the opts shape ChartExpandButton passes,\n\t// but TableSizeSnapshot/Trend don't yet support fillParent — they render at\n\t// their native size in both inline and dialog views. Wiring fillParent into\n\t// those charts is a follow-up.\n\tconst renderSnapshot = (_opts?: { fillParent: boolean }) => (\n\t\t<TableSizeSnapshot\n\t\t\tsnapshot={derived.snapshot}\n\t\t\tviewMode=\"per-node\"\n\t\t\ttheme={theme}\n\t\t\tselectedTable={effectiveSelection}\n\t\t\tonChipSelect={setSelected}\n\t\t\tonBarClick={setSelected}\n\t\t\tallOtherHint={derived.emptyCause === 'all-other'}\n\t\t/>\n\t);\n\tconst renderTrend = (_opts?: { fillParent: boolean }) => (\n\t\teffectiveSelection\n\t\t\t? (\n\t\t\t\t<TableSizeTrend\n\t\t\t\t\tderived={derived}\n\t\t\t\t\tviewMode=\"per-node\"\n\t\t\t\t\ttheme={theme}\n\t\t\t\t\tselectedTable={effectiveSelection}\n\t\t\t\t\tonChipSelect={setSelected}\n\t\t\t\t\tmanualSelection={selected !== null}\n\t\t\t\t\trange={timeRange}\n\t\t\t\t\tclusterNodeIds={derivedClusterNodes(derived)}\n\t\t\t\t\trankBy={rankBy}\n\t\t\t\t\tonRankChange={() => {}}\n\t\t\t\t/>\n\t\t\t)\n\t\t\t: (\n\t\t\t\t<div className=\"h-64 rounded-md border border-border bg-muted/20 p-4 text-sm text-muted-foreground flex items-center justify-center\">\n\t\t\t\t\tNo table selected.\n\t\t\t\t</div>\n\t\t\t)\n\t);\n\n\treturn (\n\t\t<div className=\"grid grid-cols-1 xl:grid-cols-2 gap-4\">\n\t\t\t<Card ref={snapshotCardRef}>\n\t\t\t\t<CardHeader className=\"flex flex-row items-start justify-between gap-2\">\n\t\t\t\t\t<div className=\"flex-1 min-w-0\">\n\t\t\t\t\t\t<CardTitle>Table size — snapshot</CardTitle>\n\t\t\t\t\t\t<CardDescription>\n\t\t\t\t\t\t\tBytes per table, per node. Click a segment to pin the trend below.\n\t\t\t\t\t\t</CardDescription>\n\t\t\t\t\t</div>\n\t\t\t\t\t<div className=\"flex items-center gap-1 shrink-0\">\n\t\t\t\t\t\t<ChartExpandButton\n\t\t\t\t\t\t\texportSlug=\"table-size-snapshot\"\n\t\t\t\t\t\t\ttitle=\"Table size — snapshot\"\n\t\t\t\t\t\t\tdescription=\"Bytes per table, per node.\"\n\t\t\t\t\t\t\trenderChart={renderSnapshot}\n\t\t\t\t\t\t/>\n\t\t\t\t\t\t<ChartCopyButton captureRef={snapshotCardRef} exportSlug=\"table-size-snapshot\" />\n\t\t\t\t\t\t<ChartExportButton captureRef={snapshotCardRef} exportSlug=\"table-size-snapshot\" />\n\t\t\t\t\t</div>\n\t\t\t\t</CardHeader>\n\t\t\t\t<CardContent>\n\t\t\t\t\t{renderSnapshot()}\n\t\t\t\t</CardContent>\n\t\t\t</Card>\n\t\t\t<Card ref={trendCardRef}>\n\t\t\t\t<CardHeader className=\"flex flex-row items-start justify-between gap-2\">\n\t\t\t\t\t<div className=\"flex-1 min-w-0\">\n\t\t\t\t\t\t<CardTitle>Table size — trend</CardTitle>\n\t\t\t\t\t\t<CardDescription>\n\t\t\t\t\t\t\t{effectiveSelection\n\t\t\t\t\t\t\t\t? `Growth of ${effectiveSelection} over the selected window.`\n\t\t\t\t\t\t\t\t: 'Pick a table to see its trend.'}\n\t\t\t\t\t\t</CardDescription>\n\t\t\t\t\t</div>\n\t\t\t\t\t{effectiveSelection && (\n\t\t\t\t\t\t<div className=\"flex items-center gap-1 shrink-0\">\n\t\t\t\t\t\t\t<ChartExpandButton\n\t\t\t\t\t\t\t\texportSlug=\"table-size-trend\"\n\t\t\t\t\t\t\t\ttitle={`Table size — trend: ${effectiveSelection}`}\n\t\t\t\t\t\t\t\tdescription={`Growth of ${effectiveSelection} over the selected window.`}\n\t\t\t\t\t\t\t\trenderChart={renderTrend}\n\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t<ChartCopyButton captureRef={trendCardRef} exportSlug=\"table-size-trend\" />\n\t\t\t\t\t\t\t<ChartExportButton captureRef={trendCardRef} exportSlug=\"table-size-trend\" />\n\t\t\t\t\t\t</div>\n\t\t\t\t\t)}\n\t\t\t\t</CardHeader>\n\t\t\t\t<CardContent>\n\t\t\t\t\t{renderTrend()}\n\t\t\t\t</CardContent>\n\t\t\t</Card>\n\t\t</div>\n\t);\n}\n","import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';\nimport { useMemo, useRef } from 'react';\nimport { ChartCopyButton } from '../components/ChartCopyButton.tsx';\nimport { ChartExpandButton } from '../components/ChartExpandButton.tsx';\nimport { ChartExportButton } from '../components/ChartExportButton.tsx';\nimport { useAnalyticsContext } from '../context/AnalyticsContext.tsx';\nimport { useAnalyticsRecords } from '../hooks/useAnalyticsRecords.ts';\nimport { ConnectionsRenderer } from '../pipeline/connections.tsx';\nimport type { AnalyticsDataPoint } from '../types/analytics.ts';\nimport { PanelErrorBoundary } from './PanelErrorBoundary.tsx';\n\n/** Some Harper builds split active-session telemetry across two metrics:\n * `mqtt-connections` (active MQTT sessions) and `ws-connections` (active\n * WebSocket sessions). Each row carries a `connections` field but no\n * protocol discriminator — the metric NAME is the discriminator. We\n * fetch both, tag each row with a synthesized `type` so the renderer's\n * groupBy('type') has something to key on, and pass the merged stream\n * into the standard ConnectionsRenderer.\n *\n * This mirrors analytics-viz's PanelConnections approach (DashboardPanel\n * multi-source merge). It's why this panel doesn't go through the\n * generic MetricPanel — that path is single-metric-per-card. */\nexport function ConnectionsPanel() {\n\tconst { timeRange } = useAnalyticsContext();\n\treturn (\n\t\t<PanelErrorBoundary metric=\"connections\" resetKey={`${timeRange.startTime}-${timeRange.endTime}`}>\n\t\t\t<ConnectionsPanelInner />\n\t\t</PanelErrorBoundary>\n\t);\n}\n\nfunction ConnectionsPanelInner() {\n\tconst { timeRange, bucketMs, refreshIntervalMs, theme, instanceParams } = useAnalyticsContext();\n\t// Chart-only ref; capturing the whole Card would include the action buttons\n\t// in the exported PNG.\n\tconst chartRef = useRef<HTMLDivElement>(null);\n\n\tconst mqtt = useAnalyticsRecords({\n\t\tmetric: 'mqtt-connections',\n\t\tstartTime: timeRange.startTime,\n\t\tendTime: timeRange.endTime,\n\t\tinstanceParams,\n\t\trefetchIntervalMs: refreshIntervalMs,\n\t\tbucketMs,\n\t});\n\tconst ws = useAnalyticsRecords({\n\t\tmetric: 'ws-connections',\n\t\tstartTime: timeRange.startTime,\n\t\tendTime: timeRange.endTime,\n\t\tinstanceParams,\n\t\trefetchIntervalMs: refreshIntervalMs,\n\t\tbucketMs,\n\t});\n\n\tconst isLoading = mqtt.isLoading || ws.isLoading;\n\tconst isError = mqtt.isError || ws.isError;\n\tconst error = mqtt.error || ws.error;\n\n\tconst merged = useMemo<AnalyticsDataPoint[]>(() => {\n\t\tconst out: AnalyticsDataPoint[] = [];\n\t\tfor (const r of mqtt.data) { out.push({ ...r, type: 'mqtt' }); }\n\t\tfor (const r of ws.data) { out.push({ ...r, type: 'ws' }); }\n\t\treturn out;\n\t}, [mqtt.data, ws.data]);\n\n\tconst isEmpty = merged.length === 0;\n\tconst nodes = useMemo(() => {\n\t\tconst set = new Set<string>();\n\t\tfor (const r of merged) {\n\t\t\tif (typeof r.node === 'string') { set.add(r.node); }\n\t\t}\n\t\treturn [...set].sort();\n\t}, [merged]);\n\n\tconst canExport = !isLoading && !isError && !isEmpty;\n\tconst renderChart = (opts: { fillParent: boolean } = { fillParent: false }) => (\n\t\t<ConnectionsRenderer\n\t\t\trecords={merged}\n\t\t\ttimeRange={timeRange}\n\t\t\tnodes={nodes}\n\t\t\ttheme={theme}\n\t\t\tfillParent={opts.fillParent}\n\t\t/>\n\t);\n\n\treturn (\n\t\t<Card>\n\t\t\t<CardHeader className=\"flex flex-row items-start justify-between gap-2\">\n\t\t\t\t<div className=\"flex-1 min-w-0\">\n\t\t\t\t\t<CardTitle>Connections</CardTitle>\n\t\t\t\t\t<CardDescription>Active MQTT + WebSocket sessions — chips solo / Ctrl-toggle.</CardDescription>\n\t\t\t\t</div>\n\t\t\t\t{canExport && (\n\t\t\t\t\t<div className=\"flex items-center gap-1 shrink-0\">\n\t\t\t\t\t\t<ChartExpandButton\n\t\t\t\t\t\t\texportSlug=\"connections\"\n\t\t\t\t\t\t\ttitle=\"Connections\"\n\t\t\t\t\t\t\tdescription=\"Active MQTT + WebSocket sessions.\"\n\t\t\t\t\t\t\trenderChart={renderChart}\n\t\t\t\t\t\t/>\n\t\t\t\t\t\t<ChartCopyButton captureRef={chartRef} exportSlug=\"connections\" />\n\t\t\t\t\t\t<ChartExportButton captureRef={chartRef} exportSlug=\"connections\" />\n\t\t\t\t\t</div>\n\t\t\t\t)}\n\t\t\t</CardHeader>\n\t\t\t<CardContent>\n\t\t\t\t<div ref={chartRef}>\n\t\t\t\t\t{isLoading\n\t\t\t\t\t\t? <div className=\"h-64 rounded-md bg-muted/30 animate-pulse\" aria-label=\"Loading\" />\n\t\t\t\t\t\t: isError\n\t\t\t\t\t\t? (\n\t\t\t\t\t\t\t<div className=\"h-64 rounded-md border border-destructive/40 bg-destructive/10 p-4 text-sm text-destructive\">\n\t\t\t\t\t\t\t\t{`Failed to load: ${error?.message ?? 'unknown error'}`}\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t)\n\t\t\t\t\t\t: isEmpty\n\t\t\t\t\t\t? (\n\t\t\t\t\t\t\t<div className=\"h-64 rounded-md border border-border bg-muted/20 p-4 text-sm text-muted-foreground flex items-center justify-center\">\n\t\t\t\t\t\t\t\tNo active sessions in the selected time range.\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t)\n\t\t\t\t\t\t: renderChart()}\n\t\t\t\t</div>\n\t\t\t</CardContent>\n\t\t</Card>\n\t);\n}\n","import { ConnectionsPanel } from './ConnectionsPanel.tsx';\nimport { MetricPanel } from './MetricPanel.tsx';\n\n// Connections is rendered by a custom panel because it merges two source\n// metrics (mqtt-connections + ws-connections) — the generic MetricPanel\n// is single-metric. The remaining traffic panels go through MetricPanel\n// in the order below.\nconst METRICS = [\n\t'mqtt-traffic-sent',\n\t'mqtt-traffic-received',\n\t'bytes-sent',\n\t'bytes-received',\n\t'tls-reused',\n\t'connection',\n] as const;\n\nexport function TrafficTab() {\n\treturn (\n\t\t<div className=\"grid grid-cols-1 xl:grid-cols-2 gap-4\">\n\t\t\t<ConnectionsPanel />\n\t\t\t{METRICS.map((m) => <MetricPanel key={m} metric={m} />)}\n\t\t</div>\n\t);\n}\n","import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';\nimport { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';\nimport type { InstanceClientIdConfig, InstanceTypeConfig } from '@/config/instanceClientConfig.ts';\nimport { useNavigate, useSearch } from '@tanstack/react-router';\nimport { useTheme } from 'next-themes';\nimport { useCallback, useEffect, useMemo, useState } from 'react';\nimport { AnalyticsOnboardingHint } from './components/AnalyticsOnboardingHint.tsx';\nimport { TimeRangePicker } from './components/TimeRangePicker.tsx';\nimport { type AnalyticsContextValue, AnalyticsProvider } from './context/AnalyticsContext.tsx';\nimport { DEFAULT_PRESET_ID, DEFAULT_REFRESH_MS, getPreset, type TimePresetId } from './context/timePresets.ts';\nimport { useAnalyticsCapability } from './hooks/useAnalyticsCapability.ts';\nimport { DatabaseTab } from './tabs/DatabaseTab.tsx';\nimport { HealthTab } from './tabs/HealthTab.tsx';\nimport { OverviewTab } from './tabs/OverviewTab.tsx';\nimport { ReplicationTab } from './tabs/ReplicationTab.tsx';\nimport { RequestsTab } from './tabs/RequestsTab.tsx';\nimport { StorageTab } from './tabs/StorageTab.tsx';\nimport { TrafficTab } from './tabs/TrafficTab.tsx';\n\ninterface Props {\n\tinstanceParams: InstanceClientIdConfig & InstanceTypeConfig;\n\tisLocalStudio: boolean;\n}\n\nconst TAB_DEFS = [\n\t{ id: 'health', label: 'Health' },\n\t{ id: 'traffic', label: 'Traffic' },\n\t{ id: 'requests', label: 'Requests' },\n\t{ id: 'database', label: 'Database' },\n\t{ id: 'replication', label: 'Replication' },\n\t{ id: 'storage', label: 'Storage' },\n\t{ id: 'overview', label: 'Overview' },\n] as const;\n\ntype TabId = (typeof TAB_DEFS)[number]['id'];\n\nexport function StatusTabs({ instanceParams, isLocalStudio }: Props) {\n\tconst capability = useAnalyticsCapability(instanceParams);\n\n\tif (capability.isLoading) {\n\t\treturn (\n\t\t\t<div role=\"status\" aria-live=\"polite\" className=\"px-4 py-8 text-sm text-muted-foreground\">\n\t\t\t\tChecking analytics availability…\n\t\t\t</div>\n\t\t);\n\t}\n\n\tif (capability.error) {\n\t\treturn (\n\t\t\t<div role=\"alert\" className=\"px-4 py-8 text-sm text-muted-foreground\">\n\t\t\t\t<p className=\"mb-1 font-medium text-foreground\">Analytics unavailable on this instance.</p>\n\t\t\t\t<p>\n\t\t\t\t\tThe Harper instance returned an error from{' '}\n\t\t\t\t\t<code>get_analytics</code>. Check that the instance is reachable and that analytics is enabled, then reload.\n\t\t\t\t</p>\n\t\t\t</div>\n\t\t);\n\t}\n\n\treturn (\n\t\t<StatusTabsInner\n\t\t\tinstanceParams={instanceParams}\n\t\t\tisLocalStudio={isLocalStudio}\n\t\t/>\n\t);\n}\n\nfunction StatusTabsInner({ instanceParams, isLocalStudio }: Props) {\n\tconst navigate = useNavigate();\n\tconst raw: { tab?: string; range?: string; refresh?: string | number } = useSearch({ strict: false });\n\tconst tab: TabId = TAB_DEFS.some((t) => t.id === raw.tab) ? (raw.tab as TabId) : 'health';\n\tconst presetId: TimePresetId = raw.range && VALID_PRESETS.includes(raw.range)\n\t\t? (raw.range as TimePresetId)\n\t\t: DEFAULT_PRESET_ID;\n\tconst refreshMs: number = raw.refresh !== undefined && VALID_REFRESH.includes(Number(raw.refresh))\n\t\t? Number(raw.refresh)\n\t\t: DEFAULT_REFRESH_MS;\n\n\t// Manual refresh ticks bump this to force a fresh window when the user\n\t// clicks the refresh button.\n\tconst [tick, setTick] = useState(0);\n\n\tconst { resolvedTheme } = useTheme();\n\tconst theme = resolvedTheme === 'dark' ? 'dark' : 'light';\n\n\tconst updatePreset = useCallback((id: TimePresetId) => {\n\t\tvoid navigate({ to: '.', search: { tab, range: id, refresh: refreshMs } });\n\t}, [navigate, tab, refreshMs]);\n\n\tconst updateTab = useCallback((id: TabId) => {\n\t\tvoid navigate({ to: '.', search: { tab: id, range: presetId, refresh: refreshMs } });\n\t}, [navigate, presetId, refreshMs]);\n\n\tconst updateRefreshMs = useCallback((ms: number) => {\n\t\tvoid navigate({ to: '.', search: { tab, range: presetId, refresh: ms } });\n\t}, [navigate, tab, presetId]);\n\n\t// Strip our query params on unmount so they don't bleed into sibling\n\t// routes (e.g. navigating from /status to /databases shouldn't carry\n\t// tab/range/refresh forward).\n\tuseEffect(() => {\n\t\treturn () => {\n\t\t\tvoid navigate({ search: undefined, replace: true });\n\t\t};\n\t}, [navigate]);\n\n\tconst ctxValue = useMemo<AnalyticsContextValue>(() => {\n\t\tconst preset = getPreset(presetId);\n\t\tconst endTime = Date.now();\n\t\tconst startTime = endTime - preset.durationMs;\n\t\t// `tick` participates in memo deps so a manual refresh produces a fresh\n\t\t// window even if the user did not change presets.\n\t\tvoid tick;\n\t\treturn {\n\t\t\ttimeRange: { startTime, endTime },\n\t\t\tbucketMs: preset.bucketMs,\n\t\t\trefreshIntervalMs: refreshMs,\n\t\t\ttheme,\n\t\t\tinstanceParams,\n\t\t};\n\t}, [presetId, refreshMs, theme, instanceParams, tick]);\n\n\tconst showTimePicker = tab !== 'overview';\n\tconst picker = showTimePicker\n\t\t? (\n\t\t\t<TimeRangePicker\n\t\t\t\tpresetId={presetId}\n\t\t\t\tonPresetChange={updatePreset}\n\t\t\t\trefreshMs={refreshMs}\n\t\t\t\tonRefreshChange={updateRefreshMs}\n\t\t\t\tonManualRefresh={() => setTick((t) => t + 1)}\n\t\t\t/>\n\t\t)\n\t\t: null;\n\n\treturn (\n\t\t<AnalyticsProvider value={ctxValue}>\n\t\t\t<Tabs value={tab} onValueChange={(v) => updateTab(v as TabId)} className=\"px-4 py-2\">\n\t\t\t\t{\n\t\t\t\t\t/* Hint applies to chart interactions; hide it on Overview which\n\t\t\t\t has none of those affordances. */\n\t\t\t\t}\n\t\t\t\t{tab !== 'overview' && <AnalyticsOnboardingHint />}\n\t\t\t\t{\n\t\t\t\t\t/*\n\t\t\t\t\t * Tab strip layout:\n\t\t\t\t\t * md+ → horizontal Radix tab strip (no wrap, scrolls horizontally\n\t\t\t\t\t * if cramped) on the left; sub-toolbar inside each tab\n\t\t\t\t\t * renders the time-range picker so it stays sticky with\n\t\t\t\t\t * the chart it controls.\n\t\t\t\t\t * <md → Radix Tabs cannot collapse, so we render a Select for\n\t\t\t\t\t * tab navigation; the Tabs.List remains hidden but kept\n\t\t\t\t\t * mounted so TabsContent stays bound to value.\n\t\t\t\t\t */\n\t\t\t\t}\n\t\t\t\t<div className=\"md:hidden mb-3\">\n\t\t\t\t\t<Select value={tab} onValueChange={(v) => updateTab(v as TabId)}>\n\t\t\t\t\t\t<SelectTrigger className=\"w-full\" aria-label=\"Select status tab\">\n\t\t\t\t\t\t\t<SelectValue />\n\t\t\t\t\t\t</SelectTrigger>\n\t\t\t\t\t\t<SelectContent>\n\t\t\t\t\t\t\t{TAB_DEFS.map((t) => <SelectItem key={t.id} value={t.id}>{t.label}</SelectItem>)}\n\t\t\t\t\t\t</SelectContent>\n\t\t\t\t\t</Select>\n\t\t\t\t</div>\n\t\t\t\t<TabsList className=\"hidden md:inline-flex max-w-full overflow-x-auto mb-4\">\n\t\t\t\t\t{TAB_DEFS.map((t) => <TabsTrigger key={t.id} value={t.id}>{t.label}</TabsTrigger>)}\n\t\t\t\t</TabsList>\n\n\t\t\t\t<TabsContent value=\"health\">\n\t\t\t\t\t<TabBody picker={picker}>\n\t\t\t\t\t\t<HealthTab />\n\t\t\t\t\t</TabBody>\n\t\t\t\t</TabsContent>\n\t\t\t\t<TabsContent value=\"traffic\">\n\t\t\t\t\t<TabBody picker={picker}>\n\t\t\t\t\t\t<TrafficTab />\n\t\t\t\t\t</TabBody>\n\t\t\t\t</TabsContent>\n\t\t\t\t<TabsContent value=\"requests\">\n\t\t\t\t\t<TabBody picker={picker}>\n\t\t\t\t\t\t<RequestsTab />\n\t\t\t\t\t</TabBody>\n\t\t\t\t</TabsContent>\n\t\t\t\t<TabsContent value=\"database\">\n\t\t\t\t\t<TabBody picker={picker}>\n\t\t\t\t\t\t<DatabaseTab />\n\t\t\t\t\t</TabBody>\n\t\t\t\t</TabsContent>\n\t\t\t\t<TabsContent value=\"replication\">\n\t\t\t\t\t<TabBody picker={picker}>\n\t\t\t\t\t\t<ReplicationTab />\n\t\t\t\t\t</TabBody>\n\t\t\t\t</TabsContent>\n\t\t\t\t<TabsContent value=\"storage\">\n\t\t\t\t\t<TabBody picker={picker}>\n\t\t\t\t\t\t<StorageTab />\n\t\t\t\t\t</TabBody>\n\t\t\t\t</TabsContent>\n\t\t\t\t<TabsContent value=\"overview\">\n\t\t\t\t\t<OverviewTab instanceParams={instanceParams} isLocalStudio={isLocalStudio} />\n\t\t\t\t</TabsContent>\n\t\t\t</Tabs>\n\t\t</AnalyticsProvider>\n\t);\n}\n\n/** Wrap each chart-bearing tab with a sticky sub-toolbar so the time-range\n * picker stays in view as the user scrolls past long panel grids. The\n * picker is colocated with the data it controls instead of the tab strip\n * so its scope is unambiguous. */\nfunction TabBody({ picker, children }: { picker: React.ReactNode; children: React.ReactNode }) {\n\treturn (\n\t\t<>\n\t\t\t{picker && (\n\t\t\t\t<div className=\"sticky top-0 z-10 -mx-4 px-4 py-2 mb-3 bg-background border-b border-border shadow-sm flex items-center justify-end gap-2\">\n\t\t\t\t\t{picker}\n\t\t\t\t</div>\n\t\t\t)}\n\t\t\t{children}\n\t\t</>\n\t);\n}\n\nconst VALID_PRESETS: readonly string[] = ['1h', '6h', '24h', '7d', '30d'];\nconst VALID_REFRESH: readonly number[] = [0, 30_000, 60_000, 300_000];\n","import { isLocalStudio } from '@/config/constants';\nimport { useInstanceClientIdParams } from '@/config/useInstanceClient.tsx';\nimport { StatusTabs } from '@/features/instance/status/analytics/StatusTabs.tsx';\n\nexport function StatusIndex() {\n\tconst instanceParams = useInstanceClientIdParams();\n\treturn <StatusTabs instanceParams={instanceParams} isLocalStudio={isLocalStudio} />;\n}\n"],"mappings":"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"}