'use client'; import { useEffect, useMemo, useState } from 'react'; import { fmt } from '@/lib/format'; import { COMPONENT_LABEL, COMPONENT_ORDER } from '@/lib/pressure'; import { useTime } from '@/lib/time'; import type { ComponentId, History, HistoryRange } from '@/lib/types'; import { AXIS_STYLE, TOOLTIP_STYLE, levelMarkArea, type EChartsOption, type LineSeriesOption } from './echarts'; import { useEChart } from './useEChart'; const COMP_COLORS: Record = { routing: '#5B8DEF', latency: '#E9C46A', dns: '#4CC9F0', availability: '#E76F51', http_tls: '#F4A261', path: '#B497F0', corroboration: '#8B98A5', }; const RANGES: HistoryRange[] = ['1h', '6h', '24h', '7d', '30d']; /** * Pressure history: area of the scope pressure with level bands, optional component lines, range switcher. * `initial` is the SSR 24h response; other ranges are fetched client-side (server-side aggregation, spec §51). */ export function HistoryChart({ initial, scopeType = 'global', scopeId = null, height = 280, showComponents = true, title }: { initial: History | null; scopeType?: string; scopeId?: string | null; height?: number; showComponents?: boolean; title?: string }) { const [range, setRange] = useState(initial?.range ?? '24h'); const [fetched, setFetched] = useState(null); const [comps, setComps] = useState>(new Set()); const { mode, format } = useTime(); const useInitial = Boolean(initial && range === initial.range); const data = useInitial ? initial : fetched && fetched.range === range ? fetched : null; const loading = !useInitial && data === null; useEffect(() => { if (useInitial) return; const ctrl = new AbortController(); const qs = new URLSearchParams({ scope_type: scopeType, range }); if (scopeId) qs.set('scope_id', scopeId); fetch(`/api/v1/pressure/history?${qs}`, { signal: ctrl.signal }) .then((r) => (r.ok ? r.json() : Promise.reject(new Error(String(r.status))))) .then((d: History) => setFetched(d)) .catch(() => {}); return () => ctrl.abort(); }, [range, scopeType, scopeId, useInitial]); const option = useMemo(() => { if (!data) return null; const ts = data.points.map((p) => new Date(p.ts).getTime()); const series: LineSeriesOption[] = [ { name: 'Pressure', type: 'line', data: ts.map((t, i) => [t, data.points[i]!.pressure]), showSymbol: false, smooth: false, lineStyle: { width: 1.5, color: '#E6EDF3' }, areaStyle: { color: 'rgba(230,237,243,0.06)' }, markArea: levelMarkArea(0.06), z: 3, }, ]; for (const c of comps) { series.push({ name: COMPONENT_LABEL[c], type: 'line', data: ts.map((t, i) => [t, data.points[i]!.components?.[c] ?? null]), showSymbol: false, lineStyle: { width: 1, color: COMP_COLORS[c] }, connectNulls: false, z: 2 }); } return { animation: false, grid: { left: 36, right: 12, top: 12, bottom: 28 }, tooltip: { trigger: 'axis', ...TOOLTIP_STYLE, axisPointer: { lineStyle: { color: '#243040' } }, formatter: (params: unknown) => { const arr = params as { seriesName: string; value: [number, number | null]; color: string }[]; if (!arr.length) return ''; const t = arr[0]!.value[0]; return `
${format(t, 'short')}
` + arr.map((p) => `
${p.seriesName} ${p.value[1] == null ? '—' : fmt(p.value[1])}
`).join(''); }, }, xAxis: { type: 'time', ...AXIS_STYLE, splitLine: { show: false }, axisLabel: { ...AXIS_STYLE.axisLabel, formatter: (v: number) => format(v, range === '1h' || range === '6h' ? 'time' : range === '24h' ? 'short' : 'date').replace(' UTC', '') } }, yAxis: { type: 'value', min: 0, max: 100, interval: 25, ...AXIS_STYLE }, series, }; // `mode` is a dependency because the axis formatter closes over the UTC/local preference. // eslint-disable-next-line react-hooks/exhaustive-deps }, [data, comps, range, mode]); const { ref } = useEChart(option); return (
{title && {title}}
{RANGES.map((r) => ( ))}
{loading && loading…}
{showComponents && (
{COMPONENT_ORDER.filter((c) => c !== 'corroboration').map((c) => { const on = comps.has(c); return ( ); })}
)}
{data && (

min {fmt(data.summary.min)} avg {fmt(data.summary.avg)} max {fmt(data.summary.max)} at {format(data.summary.max_ts, 'short')} step {data.step_seconds >= 3600 ? `${data.step_seconds / 3600} h` : data.step_seconds >= 60 ? `${data.step_seconds / 60} min` : `${data.step_seconds} s`}

)}
); }