From 6750a72b3e9c098bbc9c658e26bf47b05e4a818f Mon Sep 17 00:00:00 2001 From: TaprootFreak <142087526+TaprootFreak@users.noreply.github.com> Date: Sat, 23 May 2026 14:20:39 +0200 Subject: [PATCH 1/9] feat(dashboard): add calls-over-time area chart to RealUnit tracing Adds a stacked area chart binning trace counts into time buckets (2xx/3xx green, 4xx/5xx red). Bin width scales with the selected range: 30 s for 15 min, 1 min for 1 h, 5 min for 6 h, 15 min for 24 h. Uses ApexCharts via the existing react-apexcharts integration, same pattern as dashboard-financial-* charts. Animations are disabled because the chart re-renders every 5 s with the dashboard refresh. --- .../dashboard/realunit-trace-time-chart.tsx | 89 +++++++++++++++++++ .../dashboard-realunit-tracing.screen.tsx | 20 +++-- 2 files changed, 104 insertions(+), 5 deletions(-) create mode 100644 src/components/dashboard/realunit-trace-time-chart.tsx diff --git a/src/components/dashboard/realunit-trace-time-chart.tsx b/src/components/dashboard/realunit-trace-time-chart.tsx new file mode 100644 index 00000000..cfef49b2 --- /dev/null +++ b/src/components/dashboard/realunit-trace-time-chart.tsx @@ -0,0 +1,89 @@ +import { ApexOptions } from 'apexcharts'; +import { useMemo } from 'react'; +import Chart from 'react-apexcharts'; +import { ParsedTrace } from 'src/hooks/realunit-tracing.hook'; + +interface Props { + traces: ParsedTrace[]; + windowMs: number; + binMs: number; + endTime: number; +} + +interface Bucket { + normal: number; + errors: number; +} + +function bucketize(traces: ParsedTrace[], startTime: number, endTime: number, binMs: number): Bucket[] { + const numBins = Math.max(1, Math.ceil((endTime - startTime) / binMs)); + const buckets: Bucket[] = Array.from({ length: numBins }, () => ({ normal: 0, errors: 0 })); + for (const t of traces) { + const ts = new Date(t.timestamp).getTime(); + if (ts < startTime || ts > endTime) continue; + const idx = Math.min(numBins - 1, Math.floor((ts - startTime) / binMs)); + if (t.status >= 400) { + buckets[idx].errors += 1; + } else { + buckets[idx].normal += 1; + } + } + return buckets; +} + +export function RealUnitTraceTimeChart({ traces, windowMs, binMs, endTime }: Props): JSX.Element { + const startTime = endTime - windowMs; + + const series = useMemo(() => { + const buckets = bucketize(traces, startTime, endTime, binMs); + const normalData = buckets.map((b, i) => [startTime + i * binMs, b.normal] as [number, number]); + const errorData = buckets.map((b, i) => [startTime + i * binMs, b.errors] as [number, number]); + return [ + { name: '2xx / 3xx', data: normalData }, + { name: '4xx / 5xx', data: errorData }, + ]; + }, [traces, startTime, endTime, binMs]); + + const options: ApexOptions = useMemo( + () => ({ + chart: { + type: 'area', + stacked: true, + toolbar: { show: false }, + // Disable animations: this chart re-renders every 5s; animations make + // the refresh feel jittery. + animations: { enabled: false }, + background: '0', + }, + stroke: { width: 1.5, curve: 'smooth' }, + colors: ['#22c55e', '#ef4444'], + dataLabels: { enabled: false }, + fill: { type: 'gradient', gradient: { opacityFrom: 0.45, opacityTo: 0.05 } }, + grid: { borderColor: '#e5e7eb' }, + xaxis: { + type: 'datetime', + labels: { datetimeUTC: false, format: 'HH:mm' }, + min: startTime, + max: endTime, + }, + yaxis: { + title: { text: 'Calls / bin' }, + labels: { formatter: (val: number) => String(Math.round(val)) }, + forceNiceScale: true, + }, + tooltip: { + x: { format: 'dd MMM HH:mm' }, + y: { formatter: (val: number) => String(Math.round(val)) }, + }, + legend: { position: 'bottom' }, + }), + [startTime, endTime], + ); + + return ( +
+
Calls over time
+ +
+ ); +} diff --git a/src/screens/dashboard-realunit-tracing.screen.tsx b/src/screens/dashboard-realunit-tracing.screen.tsx index b80835cb..f3b56fcc 100644 --- a/src/screens/dashboard-realunit-tracing.screen.tsx +++ b/src/screens/dashboard-realunit-tracing.screen.tsx @@ -1,16 +1,18 @@ import { useSessionContext } from '@dfx.swiss/react'; import { SpinnerSize, StyledLoadingSpinner } from '@dfx.swiss/react-components'; import { useEffect, useMemo, useState } from 'react'; +import { RealUnitTraceTimeChart } from 'src/components/dashboard/realunit-trace-time-chart'; 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; 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, tightenToMs: 15 * 60 * 1000 }, - { label: '6 h', hours: 6 }, - { label: '24 h', hours: 24 }, +// `binMs` is the time-chart bucket width, chosen for ~30-100 buckets per range. +const TIME_RANGES: { label: string; hours: number; tightenToMs?: number; binMs: number }[] = [ + { label: '1 h', hours: 1, binMs: 60 * 1000 }, + { label: '15 min', hours: 1, tightenToMs: 15 * 60 * 1000, binMs: 30 * 1000 }, + { label: '6 h', hours: 6, binMs: 5 * 60 * 1000 }, + { label: '24 h', hours: 24, binMs: 15 * 60 * 1000 }, ]; const REFRESH_MS = 5000; const SLOW_MS = 2000; @@ -259,6 +261,14 @@ export default function DashboardRealunitTracingScreen(): JSX.Element { 0 ? '#f59e0b' : undefined} /> + {/* Calls over time */} + +
{/* Top Endpoints */}
From 4cf4aa279d9523220190390212095b4449b74e8c Mon Sep 17 00:00:00 2001 From: TaprootFreak <142087526+TaprootFreak@users.noreply.github.com> Date: Sat, 23 May 2026 14:25:30 +0200 Subject: [PATCH 2/9] refactor(dashboard): polish chart memo deps and fallback-free endTime MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - useMemo deps reference props directly (windowMs/endTime/binMs), with startTime derived inside the memo bodies — cleaner intent than listing a derived value in the dep array. - Drop the Date.now() fallback in the screen; render the chart only once lastFetched is set. --- .../dashboard/realunit-trace-time-chart.tsx | 9 ++++----- .../dashboard-realunit-tracing.screen.tsx | 16 +++++++++------- 2 files changed, 13 insertions(+), 12 deletions(-) diff --git a/src/components/dashboard/realunit-trace-time-chart.tsx b/src/components/dashboard/realunit-trace-time-chart.tsx index cfef49b2..4ffb9c49 100644 --- a/src/components/dashboard/realunit-trace-time-chart.tsx +++ b/src/components/dashboard/realunit-trace-time-chart.tsx @@ -32,9 +32,8 @@ function bucketize(traces: ParsedTrace[], startTime: number, endTime: number, bi } export function RealUnitTraceTimeChart({ traces, windowMs, binMs, endTime }: Props): JSX.Element { - const startTime = endTime - windowMs; - const series = useMemo(() => { + const startTime = endTime - windowMs; const buckets = bucketize(traces, startTime, endTime, binMs); const normalData = buckets.map((b, i) => [startTime + i * binMs, b.normal] as [number, number]); const errorData = buckets.map((b, i) => [startTime + i * binMs, b.errors] as [number, number]); @@ -42,7 +41,7 @@ export function RealUnitTraceTimeChart({ traces, windowMs, binMs, endTime }: Pro { name: '2xx / 3xx', data: normalData }, { name: '4xx / 5xx', data: errorData }, ]; - }, [traces, startTime, endTime, binMs]); + }, [traces, endTime, windowMs, binMs]); const options: ApexOptions = useMemo( () => ({ @@ -63,7 +62,7 @@ export function RealUnitTraceTimeChart({ traces, windowMs, binMs, endTime }: Pro xaxis: { type: 'datetime', labels: { datetimeUTC: false, format: 'HH:mm' }, - min: startTime, + min: endTime - windowMs, max: endTime, }, yaxis: { @@ -77,7 +76,7 @@ export function RealUnitTraceTimeChart({ traces, windowMs, binMs, endTime }: Pro }, legend: { position: 'bottom' }, }), - [startTime, endTime], + [endTime, windowMs], ); return ( diff --git a/src/screens/dashboard-realunit-tracing.screen.tsx b/src/screens/dashboard-realunit-tracing.screen.tsx index f3b56fcc..555464c4 100644 --- a/src/screens/dashboard-realunit-tracing.screen.tsx +++ b/src/screens/dashboard-realunit-tracing.screen.tsx @@ -261,13 +261,15 @@ export default function DashboardRealunitTracingScreen(): JSX.Element { 0 ? '#f59e0b' : undefined} />
- {/* Calls over time */} - + {/* Calls over time — gated on lastFetched so we don't paint with a Date.now() fallback */} + {lastFetched && ( + + )}
{/* Top Endpoints */} From 6a688c62422620f1036496cf64e2cb554b47f9e3 Mon Sep 17 00:00:00 2001 From: TaprootFreak <142087526+TaprootFreak@users.noreply.github.com> Date: Sat, 23 May 2026 15:40:11 +0200 Subject: [PATCH 3/9] refactor(dashboard): adopt dark-theme design from financial-overview Switches the RealUnit tracing dashboard to the dark dfxBlue palette to match dashboard/financial/overview: - container: bg-dfxBlue-800, white text - cards: bg-dfxBlue-700 (reusing the shared SummaryCard with dark prop) - range buttons: blue (#3b82f6) for selected, dark navy (#082948) otherwise - table borders and labels: #0A355C / #9AA5B8 - area chart: theme.mode='dark' + matching grid color - the chart's own wrapper is dropped; the screen now wraps it so the border-radius/padding stays consistent with the other dark cards. --- .../dashboard/realunit-trace-time-chart.tsx | 12 +- .../dashboard-realunit-tracing.screen.tsx | 112 +++++++++--------- 2 files changed, 62 insertions(+), 62 deletions(-) diff --git a/src/components/dashboard/realunit-trace-time-chart.tsx b/src/components/dashboard/realunit-trace-time-chart.tsx index 4ffb9c49..f823a25b 100644 --- a/src/components/dashboard/realunit-trace-time-chart.tsx +++ b/src/components/dashboard/realunit-trace-time-chart.tsx @@ -8,6 +8,7 @@ interface Props { windowMs: number; binMs: number; endTime: number; + dark?: boolean; } interface Bucket { @@ -31,7 +32,7 @@ function bucketize(traces: ParsedTrace[], startTime: number, endTime: number, bi return buckets; } -export function RealUnitTraceTimeChart({ traces, windowMs, binMs, endTime }: Props): JSX.Element { +export function RealUnitTraceTimeChart({ traces, windowMs, binMs, endTime, dark }: Props): JSX.Element { const series = useMemo(() => { const startTime = endTime - windowMs; const buckets = bucketize(traces, startTime, endTime, binMs); @@ -54,11 +55,12 @@ export function RealUnitTraceTimeChart({ traces, windowMs, binMs, endTime }: Pro animations: { enabled: false }, background: '0', }, + theme: { mode: dark ? 'dark' : 'light' }, stroke: { width: 1.5, curve: 'smooth' }, colors: ['#22c55e', '#ef4444'], dataLabels: { enabled: false }, fill: { type: 'gradient', gradient: { opacityFrom: 0.45, opacityTo: 0.05 } }, - grid: { borderColor: '#e5e7eb' }, + grid: { borderColor: dark ? '#0A355C' : '#e5e7eb' }, xaxis: { type: 'datetime', labels: { datetimeUTC: false, format: 'HH:mm' }, @@ -76,12 +78,12 @@ export function RealUnitTraceTimeChart({ traces, windowMs, binMs, endTime }: Pro }, legend: { position: 'bottom' }, }), - [endTime, windowMs], + [endTime, windowMs, dark], ); return ( -
-
Calls over time
+
+
Calls over time
); diff --git a/src/screens/dashboard-realunit-tracing.screen.tsx b/src/screens/dashboard-realunit-tracing.screen.tsx index 555464c4..391430eb 100644 --- a/src/screens/dashboard-realunit-tracing.screen.tsx +++ b/src/screens/dashboard-realunit-tracing.screen.tsx @@ -2,10 +2,19 @@ import { useSessionContext } from '@dfx.swiss/react'; import { SpinnerSize, StyledLoadingSpinner } from '@dfx.swiss/react-components'; import { useEffect, useMemo, useState } from 'react'; import { RealUnitTraceTimeChart } from 'src/components/dashboard/realunit-trace-time-chart'; +import { SummaryCard } from 'src/components/dashboard/summary-card'; 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'; +// Dark-theme palette — matches dashboard-financial-overview.screen.tsx +const LABEL_COLOR = '#9AA5B8'; +const TEXT_COLOR = '#ffffff'; +const BORDER_COLOR = '#0A355C'; +const BUTTON_BG = '#082948'; +const BUTTON_BG_ACTIVE = '#3b82f6'; +const SUBTLE_TEXT = '#D6DBE2'; + // KQL granularity is hours; entries with `tightenToMs` are filtered client-side to a tighter window. // `binMs` is the time-chart bucket width, chosen for ~30-100 buckets per range. const TIME_RANGES: { label: string; hours: number; tightenToMs?: number; binMs: number }[] = [ @@ -45,7 +54,7 @@ function statusColor(status: number): string { function durationColor(ms: number): string { if (ms >= VERY_SLOW_MS) return '#ef4444'; if (ms >= SLOW_MS) return '#f59e0b'; - return '#111827'; + return SUBTLE_TEXT; } function formatMs(ms: number): string { @@ -69,25 +78,6 @@ function rowsToTraces(result: LogQueryResult): ParsedTrace[] { .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; @@ -216,30 +206,43 @@ export default function DashboardRealunitTracingScreen(): JSX.Element { if (isInitialLoading) { return ( -
+
); } return ( -
+
+ {/* Summary */} +
+ + 0 ? '#ef4444' : undefined} dark /> + 0 ? '#f59e0b' : undefined} dark /> + 0 ? '#f59e0b' : undefined} dark /> +
+ {/* Toolbar */} -
-
+
+
{TIME_RANGES.map((r, i) => ( ))}
-
+
{fetchError ? ( Fetch failed: {fetchError} ) : ( @@ -253,32 +256,27 @@ export default function DashboardRealunitTracingScreen(): JSX.Element {
- {/* Summary */} -
- - 0 ? '#ef4444' : undefined} /> - 0 ? '#f59e0b' : undefined} /> - 0 ? '#f59e0b' : undefined} /> -
- {/* Calls over time — gated on lastFetched so we don't paint with a Date.now() fallback */} {lastFetched && ( - +
+ +
)}
{/* Top Endpoints */} -
+
Top Endpoints
- + @@ -288,13 +286,13 @@ export default function DashboardRealunitTracingScreen(): JSX.Element { {endpoints.map((e) => ( - + - - @@ -318,11 +316,11 @@ export default function DashboardRealunitTracingScreen(): JSX.Element { {/* Top IPs */} -
+
Top IPs
Endpoint Count Errors
{e.method} {e.pathPattern} {e.count} 0 ? '#f59e0b' : '#6b7280' }}> + 0 ? '#f59e0b' : LABEL_COLOR }}> {e.errors} @@ -307,7 +305,7 @@ export default function DashboardRealunitTracingScreen(): JSX.Element { ))} {endpoints.length === 0 && (
+ No traces in window
- + @@ -330,17 +328,17 @@ export default function DashboardRealunitTracingScreen(): JSX.Element { {ips.map((ip) => ( - + - ))} {ips.length === 0 && ( - @@ -351,12 +349,12 @@ export default function DashboardRealunitTracingScreen(): JSX.Element { {/* Recent Activity */} -
+
Recent Activity (last {recent.length})
IP Calls Last seen
{ip.ip} {ip.count} + {formatTime(ip.lastSeen)}
+ -
- + @@ -371,7 +369,7 @@ export default function DashboardRealunitTracingScreen(): JSX.Element { @@ -389,14 +387,14 @@ export default function DashboardRealunitTracingScreen(): JSX.Element { {formatMs(t.durationMs)} - ))} {recent.length === 0 && ( - From ce7afd28e3ee62317121b0a0fa241e8000de20a7 Mon Sep 17 00:00:00 2001 From: TaprootFreak <142087526+TaprootFreak@users.noreply.github.com> Date: Sat, 23 May 2026 17:31:29 +0200 Subject: [PATCH 4/9] perf(dashboard): poll RealUnit tracing every 60s instead of 5s MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit At 5s the dashboard generates ~720 /gs/debug/logs audit-log entries per hour, each of which matches `message contains "RealUnitTrace"` (the messageFilter is embedded verbatim in the audit) and crowds real traces out of the 200-row TRACES_BY_MESSAGE response. Polling at 60s reduces that to ~60/h, well below the real-trace volume — no API-side filter needed. --- src/screens/dashboard-realunit-tracing.screen.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/screens/dashboard-realunit-tracing.screen.tsx b/src/screens/dashboard-realunit-tracing.screen.tsx index 391430eb..db605601 100644 --- a/src/screens/dashboard-realunit-tracing.screen.tsx +++ b/src/screens/dashboard-realunit-tracing.screen.tsx @@ -23,7 +23,10 @@ const TIME_RANGES: { label: string; hours: number; tightenToMs?: number; binMs: { label: '6 h', hours: 6, binMs: 5 * 60 * 1000 }, { label: '24 h', hours: 24, binMs: 15 * 60 * 1000 }, ]; -const REFRESH_MS = 5000; +// 60s rather than tighter: at this rate, the /gs/debug/logs audit log (one entry +// per poll, ~60/h) is small enough vs real RealUnitTrace volume that audit +// entries don't crowd real traces out of the 200-row TRACES_BY_MESSAGE response. +const REFRESH_MS = 60_000; const SLOW_MS = 2000; const VERY_SLOW_MS = 5000; From b4bc02000fe5c3f12f13a7b5c6d6f0e4ed2eeefa Mon Sep 17 00:00:00 2001 From: TaprootFreak <142087526+TaprootFreak@users.noreply.github.com> Date: Sat, 23 May 2026 18:09:00 +0200 Subject: [PATCH 5/9] refactor(dashboard): rename realunit-tracing files to log-tracing prefix MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Restructure file naming ahead of adding generic log views: - src/screens/dashboard-realunit-tracing.screen.tsx -> src/screens/dashboard-log-tracing-realunit.screen.tsx - src/hooks/realunit-tracing.hook.ts -> src/hooks/log-tracing.hook.ts - src/components/dashboard/realunit-trace-time-chart.tsx -> src/components/dashboard/log-trace-time-chart.tsx Identifier renames: - useRealunitTracing -> useLogTracing - RealUnitTraceTimeChart -> LogTraceTimeChart - DashboardRealunitTracingScreen -> DashboardLogTracingRealunitScreen getRealunitTraces stays — it remains the RealUnit-specific fetcher. --- ...t-trace-time-chart.tsx => log-trace-time-chart.tsx} | 4 ++-- .../{realunit-tracing.hook.ts => log-tracing.hook.ts} | 2 +- ...n.tsx => dashboard-log-tracing-realunit.screen.tsx} | 10 +++++----- 3 files changed, 8 insertions(+), 8 deletions(-) rename src/components/dashboard/{realunit-trace-time-chart.tsx => log-trace-time-chart.tsx} (94%) rename src/hooks/{realunit-tracing.hook.ts => log-tracing.hook.ts} (97%) rename src/screens/{dashboard-realunit-tracing.screen.tsx => dashboard-log-tracing-realunit.screen.tsx} (97%) diff --git a/src/components/dashboard/realunit-trace-time-chart.tsx b/src/components/dashboard/log-trace-time-chart.tsx similarity index 94% rename from src/components/dashboard/realunit-trace-time-chart.tsx rename to src/components/dashboard/log-trace-time-chart.tsx index f823a25b..5c70cd91 100644 --- a/src/components/dashboard/realunit-trace-time-chart.tsx +++ b/src/components/dashboard/log-trace-time-chart.tsx @@ -1,7 +1,7 @@ import { ApexOptions } from 'apexcharts'; import { useMemo } from 'react'; import Chart from 'react-apexcharts'; -import { ParsedTrace } from 'src/hooks/realunit-tracing.hook'; +import { ParsedTrace } from 'src/hooks/log-tracing.hook'; interface Props { traces: ParsedTrace[]; @@ -32,7 +32,7 @@ function bucketize(traces: ParsedTrace[], startTime: number, endTime: number, bi return buckets; } -export function RealUnitTraceTimeChart({ traces, windowMs, binMs, endTime, dark }: Props): JSX.Element { +export function LogTraceTimeChart({ traces, windowMs, binMs, endTime, dark }: Props): JSX.Element { const series = useMemo(() => { const startTime = endTime - windowMs; const buckets = bucketize(traces, startTime, endTime, binMs); diff --git a/src/hooks/realunit-tracing.hook.ts b/src/hooks/log-tracing.hook.ts similarity index 97% rename from src/hooks/realunit-tracing.hook.ts rename to src/hooks/log-tracing.hook.ts index 6bc288ab..483dedaf 100644 --- a/src/hooks/realunit-tracing.hook.ts +++ b/src/hooks/log-tracing.hook.ts @@ -53,7 +53,7 @@ export function parseTrace(timestamp: string, message: string): ParsedTrace | nu }; } -export function useRealunitTracing() { +export function useLogTracing() { const { call } = useApi(); async function getRealunitTraces(hours: number): Promise { diff --git a/src/screens/dashboard-realunit-tracing.screen.tsx b/src/screens/dashboard-log-tracing-realunit.screen.tsx similarity index 97% rename from src/screens/dashboard-realunit-tracing.screen.tsx rename to src/screens/dashboard-log-tracing-realunit.screen.tsx index db605601..121bb38f 100644 --- a/src/screens/dashboard-realunit-tracing.screen.tsx +++ b/src/screens/dashboard-log-tracing-realunit.screen.tsx @@ -1,11 +1,11 @@ import { useSessionContext } from '@dfx.swiss/react'; import { SpinnerSize, StyledLoadingSpinner } from '@dfx.swiss/react-components'; import { useEffect, useMemo, useState } from 'react'; -import { RealUnitTraceTimeChart } from 'src/components/dashboard/realunit-trace-time-chart'; +import { LogTraceTimeChart } from 'src/components/dashboard/log-trace-time-chart'; import { SummaryCard } from 'src/components/dashboard/summary-card'; 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'; +import { LogQueryResult, ParsedTrace, parseTrace, useLogTracing } from 'src/hooks/log-tracing.hook'; // Dark-theme palette — matches dashboard-financial-overview.screen.tsx const LABEL_COLOR = '#9AA5B8'; @@ -140,12 +140,12 @@ function aggregateIps(traces: ParsedTrace[]): IpStat[] { .sort((a, b) => b.count - a.count); } -export default function DashboardRealunitTracingScreen(): JSX.Element { +export default function DashboardLogTracingRealunitScreen(): JSX.Element { useAdminGuard(); useLayoutOptions({ title: 'RealUnit Tracing', noMaxWidth: true }); const { isLoggedIn } = useSessionContext(); - const { getRealunitTraces } = useRealunitTracing(); + const { getRealunitTraces } = useLogTracing(); const [rangeIdx, setRangeIdx] = useState(0); // default: 1h (first option) const [traces, setTraces] = useState([]); @@ -262,7 +262,7 @@ export default function DashboardRealunitTracingScreen(): JSX.Element { {/* Calls over time — gated on lastFetched so we don't paint with a Date.now() fallback */} {lastFetched && (
- Date: Sat, 23 May 2026 18:14:53 +0200 Subject: [PATCH 6/9] feat(dashboard): add log-tracing hub and all-logs view MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Restructure the tracing dashboard around a hub URL with two sub-views: - /dashboard/log-tracing — hub screen with two cards (RealUnit, All Logs) - /dashboard/log-tracing/realunit — existing RealUnit tracing screen - /dashboard/log-tracing/all — new view over all API trace entries Hook expansion in log-tracing.hook.ts: - getAllTraces(hours) — fetches the 'all-traces' template - GenericTrace interface + parseGenericTrace() — extracts [Context] prefix and severity from generic trace rows The All-Logs screen mirrors the RealUnit screen's container, toolbar, and refresh patterns (60s refresh, cancellable, dark theme); time ranges limited to 1h/6h/24h, and the view shows 4 summary cards plus a 50-row recent-entries table grouped by severity and context. The /dashboard hub card 'RealUnit Tracing' is renamed to 'Log Tracing' and now navigates to the new hub URL. --- src/App.tsx | 21 +- src/hooks/log-tracing.hook.ts | 36 ++- .../dashboard-log-tracing-all.screen.tsx | 244 ++++++++++++++++++ src/screens/dashboard-log-tracing.screen.tsx | 33 +++ src/screens/dashboard.screen.tsx | 6 +- 5 files changed, 333 insertions(+), 7 deletions(-) create mode 100644 src/screens/dashboard-log-tracing-all.screen.tsx create mode 100644 src/screens/dashboard-log-tracing.screen.tsx diff --git a/src/App.tsx b/src/App.tsx index d95ce5ee..34c2e983 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -95,7 +95,9 @@ 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 DashboardLogTracingScreen = lazy(() => import('./screens/dashboard-log-tracing.screen')); +const DashboardLogTracingRealunitScreen = lazy(() => import('./screens/dashboard-log-tracing-realunit.screen')); +const DashboardLogTracingAllScreen = lazy(() => import('./screens/dashboard-log-tracing-all.screen')); const SitemapScreen = lazy(() => import('./screens/sitemap.screen')); setupLanguages(); @@ -550,8 +552,21 @@ export const Routes = [ ], }, { - path: 'realunit-tracing', - element: withSuspense(), + path: 'log-tracing', + children: [ + { + index: true, + element: withSuspense(), + }, + { + path: 'realunit', + element: withSuspense(), + }, + { + path: 'all', + element: withSuspense(), + }, + ], }, ], }, diff --git a/src/hooks/log-tracing.hook.ts b/src/hooks/log-tracing.hook.ts index 483dedaf..7934faa3 100644 --- a/src/hooks/log-tracing.hook.ts +++ b/src/hooks/log-tracing.hook.ts @@ -53,6 +53,32 @@ export function parseTrace(timestamp: string, message: string): ParsedTrace | nu }; } +export interface GenericTrace { + timestamp: string; + severityLevel: number; // 0 verbose, 1 info, 2 warn, 3 error, 4 critical + context: string; // extracted from "[Context]" prefix, '' if none + message: string; // remainder after the prefix + operationId: string; +} + +const CONTEXT_PREFIX_RE = /^\[([^\]]+)\]\s+([\s\S]*)$/; + +export function parseGenericTrace( + timestamp: string, + severityLevel: number, + message: string, + operationId: string, +): GenericTrace { + const m = message.match(CONTEXT_PREFIX_RE); + return { + timestamp, + severityLevel, + context: m?.[1] ?? '', + message: m?.[2] ?? message, + operationId, + }; +} + export function useLogTracing() { const { call } = useApi(); @@ -68,5 +94,13 @@ export function useLogTracing() { }); } - return useMemo(() => ({ getRealunitTraces }), [call]); + async function getAllTraces(hours: number): Promise { + return call({ + url: 'gs/debug/logs', + method: 'POST', + data: { template: 'all-traces', hours }, + }); + } + + return useMemo(() => ({ getRealunitTraces, getAllTraces }), [call]); } diff --git a/src/screens/dashboard-log-tracing-all.screen.tsx b/src/screens/dashboard-log-tracing-all.screen.tsx new file mode 100644 index 00000000..b2d0271e --- /dev/null +++ b/src/screens/dashboard-log-tracing-all.screen.tsx @@ -0,0 +1,244 @@ +import { useSessionContext } from '@dfx.swiss/react'; +import { SpinnerSize, StyledLoadingSpinner } from '@dfx.swiss/react-components'; +import { useEffect, useMemo, useState } from 'react'; +import { SummaryCard } from 'src/components/dashboard/summary-card'; +import { useAdminGuard } from 'src/hooks/guard.hook'; +import { useLayoutOptions } from 'src/hooks/layout-config.hook'; +import { GenericTrace, LogQueryResult, parseGenericTrace, useLogTracing } from 'src/hooks/log-tracing.hook'; + +// Dark-theme palette — matches dashboard-financial-overview.screen.tsx +const LABEL_COLOR = '#9AA5B8'; +const TEXT_COLOR = '#ffffff'; +const BORDER_COLOR = '#0A355C'; +const BUTTON_BG = '#082948'; +const BUTTON_BG_ACTIVE = '#3b82f6'; +const SUBTLE_TEXT = '#D6DBE2'; + +const TIME_RANGES: { label: string; hours: number }[] = [ + { label: '1 h', hours: 1 }, + { label: '6 h', hours: 6 }, + { label: '24 h', hours: 24 }, +]; + +// 60s rather than tighter: matches the RealUnit screen so /gs/debug/logs +// audit volume from this dashboard stays comparable. +const REFRESH_MS = 60_000; +const MAX_MESSAGE_LEN = 240; + +interface SeverityStyle { + label: string; + color: string; +} + +const SEVERITY_STYLES: Record = { + 0: { label: 'verbose', color: '#6b7280' }, + 1: { label: 'info', color: '#9AA5B8' }, + 2: { label: 'warn', color: '#f59e0b' }, + 3: { label: 'error', color: '#ef4444' }, + 4: { label: 'critical', color: '#ef4444' }, +}; + +function severityStyle(level: number): SeverityStyle { + return SEVERITY_STYLES[level] ?? { label: String(level), color: LABEL_COLOR }; +} + +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 rowsToGenericTraces(result: LogQueryResult): GenericTrace[] { + const tsIdx = colIdx(result, 'timestamp'); + const sevIdx = colIdx(result, 'severityLevel'); + const msgIdx = colIdx(result, 'message'); + const opIdx = colIdx(result, 'operationId'); + if (tsIdx === -1 || sevIdx === -1 || msgIdx === -1) return []; + return result.rows.map((row) => { + const sevRaw = row[sevIdx]; + const severity = typeof sevRaw === 'number' ? sevRaw : parseInt(String(sevRaw), 10); + const operationId = opIdx === -1 ? '' : String(row[opIdx] ?? ''); + return parseGenericTrace(String(row[tsIdx]), severity, String(row[msgIdx]), operationId); + }); +} + +function truncate(text: string, max: number): string { + if (text.length <= max) return text; + return text.slice(0, max) + '…'; +} + +export default function DashboardLogTracingAllScreen(): JSX.Element { + // Backend /gs/debug/logs is RoleGuard(DEBUG); additionalRoles allows + // ADMIN+SUPER_ADMIN, but not REALUNIT — so admin-only is the right gate. + useAdminGuard(); + useLayoutOptions({ title: 'All Logs', noMaxWidth: true }); + + const { isLoggedIn } = useSessionContext(); + const { getAllTraces } = useLogTracing(); + + const [rangeIdx, setRangeIdx] = useState(0); // default: 1h + const [traces, setTraces] = useState([]); + const [lastFetched, setLastFetched] = useState(null); + const [fetchError, setFetchError] = useState(null); + const [isInitialLoading, setIsInitialLoading] = useState(true); + const [isRefreshing, setIsRefreshing] = useState(false); + + useEffect(() => { + if (!isLoggedIn) return; + let cancelled = false; + + const run = async () => { + setIsRefreshing(true); + try { + const range = TIME_RANGES[rangeIdx]; + const result = await getAllTraces(range.hours); + if (cancelled) return; + setTraces(rowsToGenericTraces(result)); + 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, getAllTraces]); + + const stats = useMemo(() => { + const total = traces.length; + const errors = traces.filter((t) => t.severityLevel >= 3).length; + const warnings = traces.filter((t) => t.severityLevel === 2).length; + const info = traces.filter((t) => t.severityLevel === 1).length; + return { total, errors, warnings, info }; + }, [traces]); + + const recent = useMemo( + () => + [...traces] + .sort((a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime()) + .slice(0, 50), + [traces], + ); + + if (isInitialLoading) { + return ( +
+ +
+ ); + } + + return ( +
+ {/* Summary */} +
+ + 0 ? '#ef4444' : undefined} dark /> + 0 ? '#f59e0b' : undefined} + dark + /> + +
+ + {/* Toolbar */} +
+
+ {TIME_RANGES.map((r, i) => ( + + ))} +
+
+ {fetchError ? ( + Fetch failed: {fetchError} + ) : ( + + + live · refresh {REFRESH_MS / 1000}s + + )} + last update {lastFetched ? lastFetched.toLocaleTimeString('de-CH') : '-'} + {isRefreshing && loading…} +
+
+ + {/* Recent Entries */} +
+
Recent Entries (last {recent.length})
+
+
Time Method URL
{formatTime(t.timestamp)} {t.method} {t.client} + {t.ip}
+ No traces yet
+ + + + + + + + + + {recent.map((t, i) => { + const style = severityStyle(t.severityLevel); + return ( + + + + + + + ); + })} + {recent.length === 0 && ( + + + + )} + +
TimeSeverityContextMessage
{formatTime(t.timestamp)} + + {style.label} + + {t.context} + {truncate(t.message, MAX_MESSAGE_LEN)} +
+ No traces in window +
+
+
+
+ ); +} diff --git a/src/screens/dashboard-log-tracing.screen.tsx b/src/screens/dashboard-log-tracing.screen.tsx new file mode 100644 index 00000000..829f926a --- /dev/null +++ b/src/screens/dashboard-log-tracing.screen.tsx @@ -0,0 +1,33 @@ +import { useAdminGuard } from 'src/hooks/guard.hook'; +import { useLayoutOptions } from 'src/hooks/layout-config.hook'; +import { useNavigation } from 'src/hooks/navigation.hook'; + +export default function DashboardLogTracingScreen(): JSX.Element { + // Backend /gs/debug/logs is RoleGuard(DEBUG); additionalRoles allows + // ADMIN+SUPER_ADMIN, but not REALUNIT — so admin-only is the right gate. + useAdminGuard(); + useLayoutOptions({ title: 'Log Tracing', noMaxWidth: true }); + const { navigate } = useNavigation(); + return ( +
+
navigate('/dashboard/log-tracing/realunit')} + > +
RealUnit
+
+ Live API-call tracing for the RealUnit wallet (test phase) +
+
+
navigate('/dashboard/log-tracing/all')} + > +
All Logs
+
+ All API trace entries, grouped by severity and context +
+
+
+ ); +} diff --git a/src/screens/dashboard.screen.tsx b/src/screens/dashboard.screen.tsx index 7cb359be..6836dcc5 100644 --- a/src/screens/dashboard.screen.tsx +++ b/src/screens/dashboard.screen.tsx @@ -21,11 +21,11 @@ export default function DashboardScreen(): JSX.Element {
navigate('/dashboard/realunit-tracing')} + onClick={() => navigate('/dashboard/log-tracing')} > -
RealUnit Tracing
+
Log Tracing
- Live API-call tracing for the RealUnit wallet (test phase) + Live tracing of API calls — RealUnit dashboard and all logs
From 1d151a60f75ef25e8839bfb79313aae45ef6c573 Mon Sep 17 00:00:00 2001 From: TaprootFreak <142087526+TaprootFreak@users.noreply.github.com> Date: Sat, 23 May 2026 18:19:22 +0200 Subject: [PATCH 7/9] refactor(dashboard): re-enable chart animations and document admin-only guard MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The 5s-refresh cadence the disable-animations note referred to no longer exists — the chart now refreshes every 60s, so smooth transitions feel correct and aren't perceived as jitter. Document the choice of useAdminGuard() in the RealUnit screen: backend /gs/debug/logs is RoleGuard(DEBUG), and additionalRoles only grants ADMIN+SUPER_ADMIN — not REALUNIT — so admin-only matches the API gate. --- src/components/dashboard/log-trace-time-chart.tsx | 6 +++--- src/screens/dashboard-log-tracing-realunit.screen.tsx | 2 ++ 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/components/dashboard/log-trace-time-chart.tsx b/src/components/dashboard/log-trace-time-chart.tsx index 5c70cd91..0d3b49ad 100644 --- a/src/components/dashboard/log-trace-time-chart.tsx +++ b/src/components/dashboard/log-trace-time-chart.tsx @@ -50,9 +50,9 @@ export function LogTraceTimeChart({ traces, windowMs, binMs, endTime, dark }: Pr type: 'area', stacked: true, toolbar: { show: false }, - // Disable animations: this chart re-renders every 5s; animations make - // the refresh feel jittery. - animations: { enabled: false }, + // Re-enabled: pulses are rare enough at the every-60s refresh cadence + // that a smooth transition is preferable to an instant snap. + animations: { enabled: true }, background: '0', }, theme: { mode: dark ? 'dark' : 'light' }, diff --git a/src/screens/dashboard-log-tracing-realunit.screen.tsx b/src/screens/dashboard-log-tracing-realunit.screen.tsx index 121bb38f..0b674aa6 100644 --- a/src/screens/dashboard-log-tracing-realunit.screen.tsx +++ b/src/screens/dashboard-log-tracing-realunit.screen.tsx @@ -141,6 +141,8 @@ function aggregateIps(traces: ParsedTrace[]): IpStat[] { } export default function DashboardLogTracingRealunitScreen(): JSX.Element { + // Backend /gs/debug/logs is RoleGuard(DEBUG); additionalRoles allows + // ADMIN+SUPER_ADMIN, but not REALUNIT — so admin-only is the right gate. useAdminGuard(); useLayoutOptions({ title: 'RealUnit Tracing', noMaxWidth: true }); From 5be179a595ad2910292a642d68a48efeb38df087 Mon Sep 17 00:00:00 2001 From: TaprootFreak <142087526+TaprootFreak@users.noreply.github.com> Date: Sat, 23 May 2026 18:29:48 +0200 Subject: [PATCH 8/9] fix(dashboard): correct operation_Id column lookup in all-logs view The /gs/debug/logs all-traces template projects the column as operation_Id (snake_case), but the screen was looking it up as operationId. Made every GenericTrace silently empty for that field. --- src/screens/dashboard-log-tracing-all.screen.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/screens/dashboard-log-tracing-all.screen.tsx b/src/screens/dashboard-log-tracing-all.screen.tsx index b2d0271e..b0ed8054 100644 --- a/src/screens/dashboard-log-tracing-all.screen.tsx +++ b/src/screens/dashboard-log-tracing-all.screen.tsx @@ -54,7 +54,7 @@ function rowsToGenericTraces(result: LogQueryResult): GenericTrace[] { const tsIdx = colIdx(result, 'timestamp'); const sevIdx = colIdx(result, 'severityLevel'); const msgIdx = colIdx(result, 'message'); - const opIdx = colIdx(result, 'operationId'); + const opIdx = colIdx(result, 'operation_Id'); if (tsIdx === -1 || sevIdx === -1 || msgIdx === -1) return []; return result.rows.map((row) => { const sevRaw = row[sevIdx]; From 4078b1db0ced5540ef278ad730483b677fcaf708 Mon Sep 17 00:00:00 2001 From: TaprootFreak <142087526+TaprootFreak@users.noreply.github.com> Date: Sat, 23 May 2026 19:16:34 +0200 Subject: [PATCH 9/9] docs(sitemap): cover dashboard log-tracing routes and fix dashboard naming - Adds /dashboard/log-tracing, /log-tracing/realunit, /log-tracing/all that this PR introduces. - Adds /dashboard/financial/overview that was missing pre-existing. - Renames the "Financial Dashboard" section to "Dashboard" since it now covers non-financial sub-pages. - Cross-checked against App.tsx; flags any other gaps in the output. --- src/screens/sitemap.screen.tsx | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/screens/sitemap.screen.tsx b/src/screens/sitemap.screen.tsx index dc8ee045..0015f26f 100644 --- a/src/screens/sitemap.screen.tsx +++ b/src/screens/sitemap.screen.tsx @@ -52,6 +52,7 @@ const sections: PageSection[] = [ title: 'Payment Links & Invoices', pages: [ { path: '/pl', label: 'Payment Link' }, + { path: '/pl/assign', label: 'Payment Link Assign' }, { path: '/pl/pos', label: 'Payment Link POS' }, { path: '/pl/result', label: 'Payment Link Result' }, { path: '/payment-link', label: 'Payment Link (Legacy)' }, @@ -95,6 +96,8 @@ const sections: PageSection[] = [ { path: '/compliance/custody-orders', label: 'Custody Orders' }, { path: '/compliance/recalls', label: 'Recalls' }, { path: '/compliance/mros', label: 'MROS Reports' }, + { path: '/compliance/mros/create', label: 'Create MROS Report' }, + { path: '/compliance/call-queues', label: 'Call Queues' }, ], }, { @@ -102,6 +105,8 @@ const sections: PageSection[] = [ pages: [ { path: '/support/dashboard', label: 'Support Dashboard' }, { path: '/support/dashboard/create', label: 'Create Issue' }, + { path: '/notes', label: 'Notes' }, + { path: '/templates', label: 'Support Templates' }, ], }, { @@ -114,14 +119,18 @@ const sections: PageSection[] = [ ], }, { - title: 'Financial Dashboard', + title: 'Dashboard', pages: [ { path: '/dashboard', label: 'Dashboard' }, { path: '/dashboard/financial', label: 'Financial' }, + { path: '/dashboard/financial/overview', label: 'Financial Overview' }, { path: '/dashboard/financial/live', label: 'Financial Live' }, { path: '/dashboard/financial/history', label: 'Financial History' }, { path: '/dashboard/financial/history/expenses', label: 'Expenses' }, { path: '/dashboard/financial/liquidity', label: 'Liquidity' }, + { path: '/dashboard/log-tracing', label: 'Log Tracing' }, + { path: '/dashboard/log-tracing/realunit', label: 'RealUnit Tracing' }, + { path: '/dashboard/log-tracing/all', label: 'All Logs' }, ], }, {