From e7f0f0ade1e729a4e0e9fe934feb9dcaeb70ac3f Mon Sep 17 00:00:00 2001 From: TaprootFreak <142087526+TaprootFreak@users.noreply.github.com> Date: Sat, 23 May 2026 12:25:33 +0200 Subject: [PATCH 1/3] feat(dashboard): add RealUnit API tracing dashboard Adds a live dashboard for the RealUnit internal test phase. The screen calls the existing /gs/debug/logs endpoint with the traces-by-message template (filter: RealUnitTrace) and renders summary cards, top endpoints (count / median / p95), top IPs and a recent-activity feed. Auto-refresh every 5s, time ranges 15min/1h/6h/24h. Reachable via /dashboard/realunit-tracing, with a card link added to the main dashboard hub. Admin-guarded; backend gating remains on the existing DEBUG-role endpoint (admins are included via RoleGuard additionalRoles). --- src/App.tsx | 5 + src/hooks/realunit-tracing.hook.ts | 74 ++++ .../dashboard-realunit-tracing.screen.tsx | 375 ++++++++++++++++++ src/screens/dashboard.screen.tsx | 9 + 4 files changed, 463 insertions(+) create mode 100644 src/hooks/realunit-tracing.hook.ts create mode 100644 src/screens/dashboard-realunit-tracing.screen.tsx diff --git a/src/App.tsx b/src/App.tsx index ad813ffc..d95ce5ee 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -95,6 +95,7 @@ const DashboardFinancialHistoryScreen = lazy(() => import('./screens/dashboard-f const DashboardFinancialLiveScreen = lazy(() => import('./screens/dashboard-financial-live.screen')); const DashboardFinancialExpensesScreen = lazy(() => import('./screens/dashboard-financial-expenses.screen')); const DashboardFinancialLiquidityScreen = lazy(() => import('./screens/dashboard-financial-liquidity.screen')); +const DashboardRealunitTracingScreen = lazy(() => import('./screens/dashboard-realunit-tracing.screen')); const SitemapScreen = lazy(() => import('./screens/sitemap.screen')); setupLanguages(); @@ -548,6 +549,10 @@ export const Routes = [ }, ], }, + { + path: 'realunit-tracing', + element: withSuspense(), + }, ], }, ], diff --git a/src/hooks/realunit-tracing.hook.ts b/src/hooks/realunit-tracing.hook.ts new file mode 100644 index 00000000..2ea41f79 --- /dev/null +++ b/src/hooks/realunit-tracing.hook.ts @@ -0,0 +1,74 @@ +import { useApi } from '@dfx.swiss/react'; +import { useMemo } from 'react'; + +export interface LogQueryColumn { + name: string; + type: string; +} + +export interface LogQueryResult { + columns: LogQueryColumn[]; + rows: unknown[][]; +} + +export interface ParsedTrace { + timestamp: string; + method: string; + url: string; + pathPattern: string; + status: number; + durationMs: number; + client: string; + ip: string; + raw: string; +} + +const TRACE_HEADLINE_RE = + /^\[RealUnitTrace\]\s+([A-Z]+)\s+(\S+)\s+→\s+(\d{3})\s+\((\d+)ms\)\s+client=(\S+)\s+ip=(\S+)/; + +function normalizePath(url: string): string { + const path = url.split('?')[0]; + return path + .split('/') + .map((seg) => { + if (/^0x[a-f0-9]{40}$/i.test(seg)) return ':address'; + if (/^[a-f0-9]{8}-[a-f0-9]{4}-/i.test(seg)) return ':uuid'; + if (/^\d+$/.test(seg)) return ':id'; + return seg; + }) + .join('/'); +} + +export function parseTrace(timestamp: string, message: string): ParsedTrace | null { + const m = message.match(TRACE_HEADLINE_RE); + if (!m) return null; + return { + timestamp, + method: m[1], + url: m[2], + pathPattern: normalizePath(m[2]), + status: parseInt(m[3], 10), + durationMs: parseInt(m[4], 10), + client: m[5], + ip: m[6], + raw: message, + }; +} + +export function useRealunitTracing() { + const { call } = useApi(); + + async function getRealunitTraces(hours: number): Promise { + return call({ + url: 'gs/debug/logs', + method: 'POST', + data: { + template: 'traces-by-message', + messageFilter: 'RealUnitTrace', + hours, + }, + }); + } + + return useMemo(() => ({ getRealunitTraces }), [call]); +} diff --git a/src/screens/dashboard-realunit-tracing.screen.tsx b/src/screens/dashboard-realunit-tracing.screen.tsx new file mode 100644 index 00000000..72e73ade --- /dev/null +++ b/src/screens/dashboard-realunit-tracing.screen.tsx @@ -0,0 +1,375 @@ +import { useSessionContext } from '@dfx.swiss/react'; +import { SpinnerSize, StyledLoadingSpinner } from '@dfx.swiss/react-components'; +import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; +import { useAdminGuard } from 'src/hooks/guard.hook'; +import { useLayoutOptions } from 'src/hooks/layout-config.hook'; +import { LogQueryResult, ParsedTrace, parseTrace, useRealunitTracing } from 'src/hooks/realunit-tracing.hook'; + +const TIME_RANGES: { label: string; hours: number }[] = [ + { label: '15 min', hours: 1 }, // API minimum is 1h; we filter client-side for 15 min + { label: '1 h', hours: 1 }, + { label: '6 h', hours: 6 }, + { label: '24 h', hours: 24 }, +]; +const REFRESH_MS = 5000; +const SLOW_MS = 2000; +const VERY_SLOW_MS = 5000; + +function median(values: number[]): number { + if (values.length === 0) return 0; + const sorted = [...values].sort((a, b) => a - b); + const mid = Math.floor(sorted.length / 2); + return sorted.length % 2 === 0 ? Math.round((sorted[mid - 1] + sorted[mid]) / 2) : sorted[mid]; +} + +function percentile(values: number[], p: number): number { + if (values.length === 0) return 0; + const sorted = [...values].sort((a, b) => a - b); + const idx = Math.min(sorted.length - 1, Math.floor((sorted.length * p) / 100)); + return sorted[idx]; +} + +function statusColor(status: number): string { + if (status >= 500) return '#ef4444'; + if (status >= 400) return '#f59e0b'; + if (status >= 300) return '#3b82f6'; + return '#22c55e'; +} + +function durationColor(ms: number): string { + if (ms >= VERY_SLOW_MS) return '#ef4444'; + if (ms >= SLOW_MS) return '#f59e0b'; + return '#111827'; +} + +function formatMs(ms: number): string { + return ms >= 1000 ? `${(ms / 1000).toFixed(2)} s` : `${ms} ms`; +} + +function formatTime(iso: string): string { + return new Date(iso).toLocaleTimeString('de-CH', { hour: '2-digit', minute: '2-digit', second: '2-digit' }); +} + +function colIdx(result: LogQueryResult, name: string): number { + return result.columns.findIndex((c) => c.name === name); +} + +function rowsToTraces(result: LogQueryResult): ParsedTrace[] { + const tsIdx = colIdx(result, 'timestamp'); + const msgIdx = colIdx(result, 'message'); + if (tsIdx === -1 || msgIdx === -1) return []; + return result.rows + .map((row) => parseTrace(String(row[tsIdx]), String(row[msgIdx]))) + .filter((t): t is ParsedTrace => t !== null); +} + +interface SummaryCardProps { + label: string; + value: string; + color?: string; +} + +function SummaryCard({ label, value, color }: SummaryCardProps) { + return ( +
+
+ {label} +
+
+ {value} +
+
+ ); +} + +interface EndpointStat { + key: string; + method: string; + pathPattern: string; + count: number; + median: number; + p95: number; + errors: number; +} + +function aggregateEndpoints(traces: ParsedTrace[]): EndpointStat[] { + const buckets = new Map(); + for (const t of traces) { + const key = `${t.method} ${t.pathPattern}`; + const arr = buckets.get(key) ?? []; + arr.push(t); + buckets.set(key, arr); + } + return Array.from(buckets.entries()) + .map(([key, arr]) => { + const durations = arr.map((t) => t.durationMs); + return { + key, + method: arr[0].method, + pathPattern: arr[0].pathPattern, + count: arr.length, + median: median(durations), + p95: percentile(durations, 95), + errors: arr.filter((t) => t.status >= 400).length, + }; + }) + .sort((a, b) => b.count - a.count); +} + +interface IpStat { + ip: string; + count: number; + lastSeen: string; +} + +function aggregateIps(traces: ParsedTrace[]): IpStat[] { + const buckets = new Map(); + for (const t of traces) { + const arr = buckets.get(t.ip) ?? []; + arr.push(t); + buckets.set(t.ip, arr); + } + return Array.from(buckets.entries()) + .map(([ip, arr]) => ({ + ip, + count: arr.length, + lastSeen: arr.reduce((max, t) => (t.timestamp > max ? t.timestamp : max), arr[0].timestamp), + })) + .sort((a, b) => b.count - a.count); +} + +export default function DashboardRealunitTracingScreen(): JSX.Element { + useAdminGuard(); + useLayoutOptions({ title: 'RealUnit Tracing', noMaxWidth: true }); + + const { isLoggedIn } = useSessionContext(); + const { getRealunitTraces } = useRealunitTracing(); + + const [rangeIdx, setRangeIdx] = useState(1); // default: 1h + const [traces, setTraces] = useState([]); + const [lastFetched, setLastFetched] = useState(null); + const [fetchError, setFetchError] = useState(null); + const [isInitialLoading, setIsInitialLoading] = useState(true); + const inflight = useRef(false); + + const fetchTraces = useCallback(async () => { + if (inflight.current) return; + inflight.current = true; + try { + const range = TIME_RANGES[rangeIdx]; + const result = await getRealunitTraces(range.hours); + const parsed = rowsToTraces(result); + const cutoff = rangeIdx === 0 ? Date.now() - 15 * 60 * 1000 : 0; + setTraces(cutoff ? parsed.filter((t) => new Date(t.timestamp).getTime() >= cutoff) : parsed); + setLastFetched(new Date()); + setFetchError(null); + } catch (e) { + setFetchError(e instanceof Error ? e.message : 'unknown error'); + } finally { + setIsInitialLoading(false); + inflight.current = false; + } + }, [getRealunitTraces, rangeIdx]); + + useEffect(() => { + if (!isLoggedIn) return; + setIsInitialLoading(true); + fetchTraces(); + const interval = setInterval(fetchTraces, REFRESH_MS); + return () => clearInterval(interval); + }, [isLoggedIn, fetchTraces]); + + const stats = useMemo(() => { + const total = traces.length; + const errors5xx = traces.filter((t) => t.status >= 500).length; + const errors4xx = traces.filter((t) => t.status >= 400 && t.status < 500).length; + const slow = traces.filter((t) => t.durationMs >= SLOW_MS).length; + return { total, errors5xx, errors4xx, slow }; + }, [traces]); + + const endpoints = useMemo(() => aggregateEndpoints(traces).slice(0, 12), [traces]); + const ips = useMemo(() => aggregateIps(traces).slice(0, 10), [traces]); + const recent = useMemo( + () => [...traces].sort((a, b) => (a.timestamp < b.timestamp ? 1 : -1)).slice(0, 30), + [traces], + ); + + if (isInitialLoading) { + return ( +
+ +
+ ); + } + + return ( +
+ {/* Toolbar */} +
+
+ {TIME_RANGES.map((r, i) => ( + + ))} +
+
+ {fetchError ? ( + Fetch failed: {fetchError} + ) : ( + + + live · refresh {REFRESH_MS / 1000}s + + )} + last update {lastFetched ? lastFetched.toLocaleTimeString('de-CH') : '-'} +
+
+ + {/* Summary */} +
+ + 0 ? '#ef4444' : undefined} /> + 0 ? '#f59e0b' : undefined} /> + 0 ? '#f59e0b' : undefined} /> +
+ +
+ {/* Top Endpoints */} +
+
Top Endpoints
+
+ + + + + + + + + + + + {endpoints.map((e) => ( + + + + + + + + ))} + {endpoints.length === 0 && ( + + + + )} + +
EndpointCountErrorsMedianp95
+ {e.method} + {e.pathPattern} + {e.count} 0 ? '#f59e0b' : '#6b7280' }}> + {e.errors} + + {formatMs(e.median)} + + {formatMs(e.p95)} +
+ No traces in window +
+
+
+ + {/* Top IPs */} +
+
Top IPs
+ + + + + + + + + + {ips.map((ip) => ( + + + + + + ))} + {ips.length === 0 && ( + + + + )} + +
IPCallsLast seen
{ip.ip}{ip.count} + {formatTime(ip.lastSeen)} +
+ - +
+
+
+ + {/* Recent Activity */} +
+
Recent Activity (last {recent.length})
+
+ + + + + + + + + + + + + + {recent.map((t, i) => ( + + + + + + + + + + ))} + {recent.length === 0 && ( + + + + )} + +
TimeMethodURLStatusDurationClientIP
{formatTime(t.timestamp)}{t.method}{t.url} + {t.status} + + {formatMs(t.durationMs)} + {t.client} + {t.ip} +
+ No traces yet +
+
+
+
+ ); +} diff --git a/src/screens/dashboard.screen.tsx b/src/screens/dashboard.screen.tsx index c1eeb4a9..7cb359be 100644 --- a/src/screens/dashboard.screen.tsx +++ b/src/screens/dashboard.screen.tsx @@ -19,6 +19,15 @@ export default function DashboardScreen(): JSX.Element { Balance overview, history, liquidity & expenses +
navigate('/dashboard/realunit-tracing')} + > +
RealUnit Tracing
+
+ Live API-call tracing for the RealUnit wallet (test phase) +
+
); } From 3f32fbe47dc0ca81a3f2f0d618564d302607dc40 Mon Sep 17 00:00:00 2001 From: TaprootFreak <142087526+TaprootFreak@users.noreply.github.com> Date: Sat, 23 May 2026 12:47:30 +0200 Subject: [PATCH 2/3] refactor(dashboard): address review feedback on RealUnit tracing - cancellation-safe refresh on range change (H1) - correct API granularity comment (H2) - tighter trace-headline regex (M1) - keep dashboard visible during range refresh (L1) - stable React keys, robust timestamp sort (L2, L5) - default range = 1h, full UUID + interpolated p95 (N1-N3) - drop raw message from client state (S1) --- src/hooks/realunit-tracing.hook.ts | 6 +- .../dashboard-realunit-tracing.screen.tsx | 86 ++++++++++++------- 2 files changed, 55 insertions(+), 37 deletions(-) diff --git a/src/hooks/realunit-tracing.hook.ts b/src/hooks/realunit-tracing.hook.ts index 2ea41f79..6bc288ab 100644 --- a/src/hooks/realunit-tracing.hook.ts +++ b/src/hooks/realunit-tracing.hook.ts @@ -20,11 +20,10 @@ export interface ParsedTrace { durationMs: number; client: string; ip: string; - raw: string; } const TRACE_HEADLINE_RE = - /^\[RealUnitTrace\]\s+([A-Z]+)\s+(\S+)\s+→\s+(\d{3})\s+\((\d+)ms\)\s+client=(\S+)\s+ip=(\S+)/; + /^\[RealUnitTrace\]\s+([A-Z]+)\s+(\S+)\s+→\s+(\d{3})\s+\((\d+)ms\)\s+client=(\S+?)\s+ip=(\S+)/; function normalizePath(url: string): string { const path = url.split('?')[0]; @@ -32,7 +31,7 @@ function normalizePath(url: string): string { .split('/') .map((seg) => { if (/^0x[a-f0-9]{40}$/i.test(seg)) return ':address'; - if (/^[a-f0-9]{8}-[a-f0-9]{4}-/i.test(seg)) return ':uuid'; + if (/^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/i.test(seg)) return ':uuid'; if (/^\d+$/.test(seg)) return ':id'; return seg; }) @@ -51,7 +50,6 @@ export function parseTrace(timestamp: string, message: string): ParsedTrace | nu durationMs: parseInt(m[4], 10), client: m[5], ip: m[6], - raw: message, }; } diff --git a/src/screens/dashboard-realunit-tracing.screen.tsx b/src/screens/dashboard-realunit-tracing.screen.tsx index 72e73ade..c5359600 100644 --- a/src/screens/dashboard-realunit-tracing.screen.tsx +++ b/src/screens/dashboard-realunit-tracing.screen.tsx @@ -1,13 +1,14 @@ import { useSessionContext } from '@dfx.swiss/react'; import { SpinnerSize, StyledLoadingSpinner } from '@dfx.swiss/react-components'; -import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; +import { useEffect, useMemo, useState } from 'react'; import { useAdminGuard } from 'src/hooks/guard.hook'; import { useLayoutOptions } from 'src/hooks/layout-config.hook'; import { LogQueryResult, ParsedTrace, parseTrace, useRealunitTracing } from 'src/hooks/realunit-tracing.hook'; +// KQL granularity is hours; we tighten client-side for the 15 min window. const TIME_RANGES: { label: string; hours: number }[] = [ - { label: '15 min', hours: 1 }, // API minimum is 1h; we filter client-side for 15 min { label: '1 h', hours: 1 }, + { label: '15 min', hours: 1 }, { label: '6 h', hours: 6 }, { label: '24 h', hours: 24 }, ]; @@ -25,8 +26,11 @@ function median(values: number[]): number { function percentile(values: number[], p: number): number { if (values.length === 0) return 0; const sorted = [...values].sort((a, b) => a - b); - const idx = Math.min(sorted.length - 1, Math.floor((sorted.length * p) / 100)); - return sorted[idx]; + const rank = ((p / 100) * (sorted.length - 1)); + const lo = Math.floor(rank); + const hi = Math.ceil(rank); + if (lo === hi) return sorted[lo]; + return Math.round(sorted[lo] + (sorted[hi] - sorted[lo]) * (rank - lo)); } function statusColor(status: number): string { @@ -145,39 +149,47 @@ export default function DashboardRealunitTracingScreen(): JSX.Element { const { isLoggedIn } = useSessionContext(); const { getRealunitTraces } = useRealunitTracing(); - const [rangeIdx, setRangeIdx] = useState(1); // default: 1h + const [rangeIdx, setRangeIdx] = useState(0); // default: 1h (first option) const [traces, setTraces] = useState([]); const [lastFetched, setLastFetched] = useState(null); const [fetchError, setFetchError] = useState(null); const [isInitialLoading, setIsInitialLoading] = useState(true); - const inflight = useRef(false); - - const fetchTraces = useCallback(async () => { - if (inflight.current) return; - inflight.current = true; - try { - const range = TIME_RANGES[rangeIdx]; - const result = await getRealunitTraces(range.hours); - const parsed = rowsToTraces(result); - const cutoff = rangeIdx === 0 ? Date.now() - 15 * 60 * 1000 : 0; - setTraces(cutoff ? parsed.filter((t) => new Date(t.timestamp).getTime() >= cutoff) : parsed); - setLastFetched(new Date()); - setFetchError(null); - } catch (e) { - setFetchError(e instanceof Error ? e.message : 'unknown error'); - } finally { - setIsInitialLoading(false); - inflight.current = false; - } - }, [getRealunitTraces, rangeIdx]); + const [isRefreshing, setIsRefreshing] = useState(false); useEffect(() => { if (!isLoggedIn) return; - setIsInitialLoading(true); - fetchTraces(); - const interval = setInterval(fetchTraces, REFRESH_MS); - return () => clearInterval(interval); - }, [isLoggedIn, fetchTraces]); + let cancelled = false; + + const run = async () => { + setIsRefreshing(true); + try { + const range = TIME_RANGES[rangeIdx]; + const result = await getRealunitTraces(range.hours); + if (cancelled) return; + const parsed = rowsToTraces(result); + // '15 min' shares the 1h API window with '1 h'; tighten client-side. + const cutoff = range.label === '15 min' ? Date.now() - 15 * 60 * 1000 : 0; + setTraces(cutoff ? parsed.filter((t) => new Date(t.timestamp).getTime() >= cutoff) : parsed); + setLastFetched(new Date()); + setFetchError(null); + } catch (e) { + if (cancelled) return; + setFetchError(e instanceof Error ? e.message : 'unknown error'); + } finally { + if (!cancelled) { + setIsInitialLoading(false); + setIsRefreshing(false); + } + } + }; + + run(); + const interval = setInterval(run, REFRESH_MS); + return () => { + cancelled = true; + clearInterval(interval); + }; + }, [isLoggedIn, rangeIdx, getRealunitTraces]); const stats = useMemo(() => { const total = traces.length; @@ -190,7 +202,10 @@ export default function DashboardRealunitTracingScreen(): JSX.Element { const endpoints = useMemo(() => aggregateEndpoints(traces).slice(0, 12), [traces]); const ips = useMemo(() => aggregateIps(traces).slice(0, 10), [traces]); const recent = useMemo( - () => [...traces].sort((a, b) => (a.timestamp < b.timestamp ? 1 : -1)).slice(0, 30), + () => + [...traces] + .sort((a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime()) + .slice(0, 30), [traces], ); @@ -229,6 +244,7 @@ export default function DashboardRealunitTracingScreen(): JSX.Element { )} last update {lastFetched ? lastFetched.toLocaleTimeString('de-CH') : '-'} + {isRefreshing && loading…} @@ -336,8 +352,12 @@ export default function DashboardRealunitTracingScreen(): JSX.Element { - {recent.map((t, i) => ( - + {recent.map((t) => ( + {formatTime(t.timestamp)} {t.method} {t.url} From abef3efefc3b6381baf8cc9af261c6b3fd4121f3 Mon Sep 17 00:00:00 2001 From: TaprootFreak <142087526+TaprootFreak@users.noreply.github.com> Date: Sat, 23 May 2026 12:57:48 +0200 Subject: [PATCH 3/3] refactor(dashboard): polish RealUnit tracing dashboard - explicit tightenToMs discriminator on TIME_RANGES (N5) - consistent getTime-based comparison in aggregateIps (N7) - remove redundant parens in percentile (N4) --- .../dashboard-realunit-tracing.screen.tsx | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/src/screens/dashboard-realunit-tracing.screen.tsx b/src/screens/dashboard-realunit-tracing.screen.tsx index c5359600..b80835cb 100644 --- a/src/screens/dashboard-realunit-tracing.screen.tsx +++ b/src/screens/dashboard-realunit-tracing.screen.tsx @@ -5,10 +5,10 @@ import { useAdminGuard } from 'src/hooks/guard.hook'; import { useLayoutOptions } from 'src/hooks/layout-config.hook'; import { LogQueryResult, ParsedTrace, parseTrace, useRealunitTracing } from 'src/hooks/realunit-tracing.hook'; -// KQL granularity is hours; we tighten client-side for the 15 min window. -const TIME_RANGES: { label: string; hours: number }[] = [ +// KQL granularity is hours; entries with `tightenToMs` are filtered client-side to a tighter window. +const TIME_RANGES: { label: string; hours: number; tightenToMs?: number }[] = [ { label: '1 h', hours: 1 }, - { label: '15 min', hours: 1 }, + { label: '15 min', hours: 1, tightenToMs: 15 * 60 * 1000 }, { label: '6 h', hours: 6 }, { label: '24 h', hours: 24 }, ]; @@ -26,7 +26,7 @@ function median(values: number[]): number { function percentile(values: number[], p: number): number { if (values.length === 0) return 0; const sorted = [...values].sort((a, b) => a - b); - const rank = ((p / 100) * (sorted.length - 1)); + const rank = (p / 100) * (sorted.length - 1); const lo = Math.floor(rank); const hi = Math.ceil(rank); if (lo === hi) return sorted[lo]; @@ -137,7 +137,10 @@ function aggregateIps(traces: ParsedTrace[]): IpStat[] { .map(([ip, arr]) => ({ ip, count: arr.length, - lastSeen: arr.reduce((max, t) => (t.timestamp > max ? t.timestamp : max), arr[0].timestamp), + lastSeen: arr.reduce( + (max, t) => (new Date(t.timestamp).getTime() > new Date(max).getTime() ? t.timestamp : max), + arr[0].timestamp, + ), })) .sort((a, b) => b.count - a.count); } @@ -167,8 +170,8 @@ export default function DashboardRealunitTracingScreen(): JSX.Element { const result = await getRealunitTraces(range.hours); if (cancelled) return; const parsed = rowsToTraces(result); - // '15 min' shares the 1h API window with '1 h'; tighten client-side. - const cutoff = range.label === '15 min' ? Date.now() - 15 * 60 * 1000 : 0; + // Ranges with `tightenToMs` reuse a coarser KQL window and are filtered client-side. + const cutoff = range.tightenToMs ? Date.now() - range.tightenToMs : 0; setTraces(cutoff ? parsed.filter((t) => new Date(t.timestamp).getTime() >= cutoff) : parsed); setLastFetched(new Date()); setFetchError(null);