diff --git a/ui/src/pages/AgentInstance/AgentInstance.tsx b/ui/src/pages/AgentInstance/AgentInstance.tsx
index be84453c..98de8471 100644
--- a/ui/src/pages/AgentInstance/AgentInstance.tsx
+++ b/ui/src/pages/AgentInstance/AgentInstance.tsx
@@ -2,7 +2,7 @@ import { useMemo, useState } from 'react';
import { useParams, Link } from 'react-router';
import { RefreshCw, ChevronRight } from 'lucide-react';
import {
- StatCard, StatusDot, Badge, LineChart, AreaChart, BarChart,
+ StatCard, StatusDot, Badge, ThemedChart, Line, Area, ReferenceLine, CHART_COLORS,
EventFeed, Spinner, EmptyState, SectionHeader, MonoText,
LogViewer, ButtonGroup, useGlobalFilters,
} from '@cameleer/design-system';
@@ -100,46 +100,42 @@ export default function AgentInstance() {
return eventSortAsc ? mapped.toReversed() : mapped;
}, [events, instanceId, eventSortAsc]);
- // JVM chart series helpers
- const cpuSeries = useMemo(() => {
+ const formatTime = (t: string) =>
+ new Date(t).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
+
+ const cpuData = useMemo(() => {
const pts = jvmMetrics?.metrics?.['process.cpu.usage.value'];
- if (!pts?.length) return null;
- return [{ label: 'CPU %', data: pts.map((p: any) => ({ x: new Date(p.time), y: p.value * 100 })) }];
+ if (!pts?.length) return [];
+ return pts.map((p: any) => ({ time: p.time, cpu: p.value * 100 }));
}, [jvmMetrics]);
- const heapSeries = useMemo(() => {
+ const heapData = useMemo(() => {
const pts = jvmMetrics?.metrics?.['jvm.memory.used.value'];
- if (!pts?.length) return null;
- return [{ label: 'Heap MB', data: pts.map((p: any) => ({ x: new Date(p.time), y: p.value / (1024 * 1024) })) }];
+ if (!pts?.length) return [];
+ return pts.map((p: any) => ({ time: p.time, heap: p.value / (1024 * 1024) }));
}, [jvmMetrics]);
- const threadSeries = useMemo(() => {
+ const threadData = useMemo(() => {
const pts = jvmMetrics?.metrics?.['jvm.threads.live.value'];
- if (!pts?.length) return null;
- return [{ label: 'Threads', data: pts.map((p: any) => ({ x: new Date(p.time), y: p.value })) }];
+ if (!pts?.length) return [];
+ return pts.map((p: any) => ({ time: p.time, threads: p.value }));
}, [jvmMetrics]);
- const gcSeries = useMemo(() => {
+ const gcData = useMemo(() => {
const pts = jvmMetrics?.metrics?.['jvm.gc.pause.total_time'];
- if (!pts?.length) return null;
- return [{ label: 'GC ms', data: pts.map((p: any) => ({ x: new Date(p.time), y: p.value })) }];
+ if (!pts?.length) return [];
+ return pts.map((p: any) => ({ time: p.time, gc: p.value }));
}, [jvmMetrics]);
- const throughputSeries = useMemo(
- () =>
- chartData.length
- ? [{ label: 'msg/s', data: chartData.map((d: any) => ({ x: d.date, y: d.throughput })) }]
- : null,
- [chartData],
- );
+ const throughputData = useMemo(() => {
+ if (!chartData.length) return [];
+ return chartData.map((d: any) => ({ time: d.date.toISOString(), throughput: d.throughput }));
+ }, [chartData]);
- const errorSeries = useMemo(
- () =>
- chartData.length
- ? [{ label: 'Error %', data: chartData.map((d: any) => ({ x: d.date, y: d.errorPct })) }]
- : null,
- [chartData],
- );
+ const errorData = useMemo(() => {
+ if (!chartData.length) return [];
+ return chartData.map((d: any) => ({ time: d.date.toISOString(), errorPct: d.errorPct }));
+ }, [chartData]);
// Application logs
const { data: rawLogs } = useApplicationLogs(appId, instanceId, { toOverride: logRefreshTo, source: logSource || undefined });
@@ -315,13 +311,14 @@ export default function AgentInstance() {
{cpuDisplay != null ? `${cpuDisplay}% current` : ''}
- {cpuSeries ? (
-
+ {cpuData.length ? (
+
+
+
+
) : (
)}
@@ -336,13 +333,16 @@ export default function AgentInstance() {
: ''}
- {heapSeries ? (
-
+ {heapData.length ? (
+
+
+ {heapMax != null && (
+
+ )}
+
) : (
)}
@@ -355,8 +355,12 @@ export default function AgentInstance() {
{agent?.tps != null ? `${agent.tps.toFixed(1)} msg/s` : ''}
- {throughputSeries ? (
-
+ {throughputData.length ? (
+
+
+
) : (
)}
@@ -369,8 +373,12 @@ export default function AgentInstance() {
{agent?.errorRate != null ? `${(agent.errorRate * 100).toFixed(1)}%` : ''}
- {errorSeries ? (
-
+ {errorData.length ? (
+
+
+
) : (
)}
@@ -380,13 +388,17 @@ export default function AgentInstance() {
Thread Count
- {threadSeries
- ? `${threadSeries[0].data[threadSeries[0].data.length - 1]?.y.toFixed(0)} active`
+ {threadData.length
+ ? `${threadData[threadData.length - 1].threads.toFixed(0)} active`
: ''}
- {threadSeries ? (
-
+ {threadData.length ? (
+
+
+
) : (
)}
@@ -397,8 +409,12 @@ export default function AgentInstance() {
GC Pauses
- {gcSeries ? (
-
+ {gcData.length ? (
+
+
+
) : (
)}
diff --git a/ui/src/pages/DashboardTab/DashboardL1.tsx b/ui/src/pages/DashboardTab/DashboardL1.tsx
index d0468f18..4b3c353e 100644
--- a/ui/src/pages/DashboardTab/DashboardL1.tsx
+++ b/ui/src/pages/DashboardTab/DashboardL1.tsx
@@ -3,8 +3,10 @@ import { useNavigate } from 'react-router';
import {
KpiStrip,
DataTable,
- AreaChart,
- LineChart,
+ ThemedChart,
+ Area,
+ Line,
+ CHART_COLORS,
Card,
Sparkline,
MonoText,
@@ -372,28 +374,39 @@ export default function DashboardL1() {
throughputSparkline, successSparkline, latencySparkline, slaSparkline, errorSparkline],
);
- // ── Per-app chart series (throughput stacked area) ──────────────────────
- const throughputByAppSeries = useMemo(() => {
- if (!timeseriesByApp) return [];
- return Object.entries(timeseriesByApp).map(([appId, { buckets }]) => ({
- label: appId,
- data: buckets.map((b, i) => ({
- x: i as number,
- y: b.totalCount,
- })),
- }));
+ // ── Per-app flat chart data (throughput stacked area) ───────────────────
+ const throughputByAppData = useMemo(() => {
+ if (!timeseriesByApp) return { data: [], keys: [] as string[] };
+ const entries = Object.entries(timeseriesByApp);
+ if (!entries.length) return { data: [], keys: [] as string[] };
+ const len = entries[0][1].buckets.length;
+ const keys = entries.map(([appId]) => appId);
+ const data = Array.from({ length: len }, (_, i) => {
+ const point: Record = { idx: i };
+ for (const [appId, { buckets }] of entries) {
+ point[appId] = buckets[i]?.totalCount ?? 0;
+ }
+ return point;
+ });
+ return { data, keys };
}, [timeseriesByApp]);
- // ── Per-app chart series (error rate line) ─────────────────────────────
- const errorRateByAppSeries = useMemo(() => {
- if (!timeseriesByApp) return [];
- return Object.entries(timeseriesByApp).map(([appId, { buckets }]) => ({
- label: appId,
- data: buckets.map((b, i) => ({
- x: i as number,
- y: b.totalCount > 0 ? (b.failedCount / b.totalCount) * 100 : 0,
- })),
- }));
+ // ── Per-app flat chart data (error rate line) ────────────────────────────
+ const errorRateByAppData = useMemo(() => {
+ if (!timeseriesByApp) return { data: [], keys: [] as string[] };
+ const entries = Object.entries(timeseriesByApp);
+ if (!entries.length) return { data: [], keys: [] as string[] };
+ const len = entries[0][1].buckets.length;
+ const keys = entries.map(([appId]) => appId);
+ const data = Array.from({ length: len }, (_, i) => {
+ const point: Record = { idx: i };
+ for (const [appId, { buckets }] of entries) {
+ const b = buckets[i];
+ point[appId] = b && b.totalCount > 0 ? (b.failedCount / b.totalCount) * 100 : 0;
+ }
+ return point;
+ });
+ return { data, keys };
}, [timeseriesByApp]);
// Treemap items: one per app, sized by exchange count, colored by SLA
@@ -430,24 +443,24 @@ export default function DashboardL1() {
{/* Side-by-side charts */}
- {throughputByAppSeries.length > 0 && (
+ {throughputByAppData.data.length > 0 && (
-
+
+ {throughputByAppData.keys.map((key, i) => (
+
+ ))}
+
-
+
+ {errorRateByAppData.keys.map((key, i) => (
+
+ ))}
+
)}
diff --git a/ui/src/pages/DashboardTab/DashboardL2.tsx b/ui/src/pages/DashboardTab/DashboardL2.tsx
index aef35faa..e0f1fa45 100644
--- a/ui/src/pages/DashboardTab/DashboardL2.tsx
+++ b/ui/src/pages/DashboardTab/DashboardL2.tsx
@@ -3,8 +3,11 @@ import { useParams, useNavigate } from 'react-router';
import {
KpiStrip,
DataTable,
- AreaChart,
- LineChart,
+ ThemedChart,
+ Area,
+ Line,
+ ReferenceLine,
+ CHART_COLORS,
Card,
Sparkline,
MonoText,
@@ -328,37 +331,31 @@ export default function DashboardL2() {
[stats, slaThresholdMs, throughputSparkline, latencySparkline, errors, windowSeconds],
);
- // Throughput by Route — stacked area chart series
- const throughputByRouteSeries = useMemo(() => {
- if (!timeseriesByRoute) return [];
- return Object.entries(timeseriesByRoute).map(([routeId, data]) => ({
- label: routeId,
- data: (data.buckets || []).map((b, i) => ({
- x: i as number,
- y: b.totalCount,
- })),
- }));
+ // Throughput by Route — flat data for ThemedChart
+ const throughputByRouteData = useMemo(() => {
+ if (!timeseriesByRoute) return { data: [], keys: [] as string[] };
+ const entries = Object.entries(timeseriesByRoute);
+ if (!entries.length) return { data: [], keys: [] as string[] };
+ const len = entries[0][1].buckets.length;
+ const keys = entries.map(([routeId]) => routeId);
+ const data = Array.from({ length: len }, (_, i) => {
+ const point: Record = { idx: i };
+ for (const [routeId, { buckets }] of entries) {
+ point[routeId] = buckets[i]?.totalCount ?? 0;
+ }
+ return point;
+ });
+ return { data, keys };
}, [timeseriesByRoute]);
- // Latency percentiles chart — P99 line from app-level timeseries
- const latencyChartSeries = useMemo(() => {
+ // Latency percentiles chart — flat data for ThemedChart
+ const latencyChartData = useMemo(() => {
const buckets = timeseries?.buckets || [];
- return [
- {
- label: 'P99',
- data: buckets.map((b, i) => ({
- x: i as number,
- y: b.p99DurationMs,
- })),
- },
- {
- label: 'Avg',
- data: buckets.map((b, i) => ({
- x: i as number,
- y: b.avgDurationMs,
- })),
- },
- ];
+ return buckets.map((b, i) => ({
+ idx: i,
+ p99: b.p99DurationMs,
+ avg: b.avgDurationMs,
+ }));
}, [timeseries]);
// Error rows with stable identity
@@ -398,22 +395,21 @@ export default function DashboardL2() {
{(timeseries?.buckets?.length ?? 0) > 0 && (
-
+
+ {throughputByRouteData.keys.map((key, i) => (
+
+ ))}
+
-
+
+
+
+
+
)}
diff --git a/ui/src/pages/DashboardTab/DashboardL3.tsx b/ui/src/pages/DashboardTab/DashboardL3.tsx
index 47b7f181..139d2909 100644
--- a/ui/src/pages/DashboardTab/DashboardL3.tsx
+++ b/ui/src/pages/DashboardTab/DashboardL3.tsx
@@ -3,8 +3,11 @@ import { useParams } from 'react-router';
import {
KpiStrip,
DataTable,
- AreaChart,
- LineChart,
+ ThemedChart,
+ Area,
+ Line,
+ ReferenceLine,
+ CHART_COLORS,
Card,
MonoText,
Badge,
@@ -285,31 +288,16 @@ export default function DashboardL3() {
[stats, slaThresholdMs, bottleneck, throughputSparkline, windowSeconds],
);
- // ── Chart series ────────────────────────────────────────────────────────
- const throughputChartSeries = useMemo(() => [{
- label: 'Throughput',
- data: (timeseries?.buckets || []).map((b: any, i: number) => ({
- x: i,
- y: b.totalCount,
+ // ── Chart data ───────────────────────────────────────────────────────────
+ const chartData = useMemo(() =>
+ (timeseries?.buckets || []).map((b: any, i: number) => ({
+ idx: i,
+ throughput: b.totalCount,
+ p99: b.p99DurationMs,
+ errorRate: b.totalCount > 0 ? (b.failedCount / b.totalCount) * 100 : 0,
})),
- }], [timeseries]);
-
- const latencyChartSeries = useMemo(() => [{
- label: 'P99',
- data: (timeseries?.buckets || []).map((b: any, i: number) => ({
- x: i,
- y: b.p99DurationMs,
- })),
- }], [timeseries]);
-
- const errorRateChartSeries = useMemo(() => [{
- label: 'Error Rate',
- data: (timeseries?.buckets || []).map((b: any, i: number) => ({
- x: i,
- y: b.totalCount > 0 ? (b.failedCount / b.totalCount) * 100 : 0,
- })),
- color: 'var(--error)',
- }], [timeseries]);
+ [timeseries],
+ );
// ── Processor table rows ────────────────────────────────────────────────
const processorRows: ProcessorRow[] = useMemo(() => {
@@ -364,28 +352,25 @@ export default function DashboardL3() {
{(timeseries?.buckets?.length ?? 0) > 0 && (
-
+
+
+
-
+
+
+
+
-
+
+
+
)}
diff --git a/ui/src/pages/Routes/RouteDetail.tsx b/ui/src/pages/Routes/RouteDetail.tsx
index 1f9637ee..ee9a64ca 100644
--- a/ui/src/pages/Routes/RouteDetail.tsx
+++ b/ui/src/pages/Routes/RouteDetail.tsx
@@ -8,9 +8,12 @@ import {
DataTable,
EmptyState,
Tabs,
- AreaChart,
- LineChart,
- BarChart,
+ ThemedChart,
+ Area,
+ Line,
+ Bar,
+ ReferenceLine,
+ CHART_COLORS,
RouteFlow,
Spinner,
MonoText,
@@ -752,44 +755,31 @@ export default function RouteDetail() {
Throughput
-
({ x: i, y: d.throughput })),
- }]}
- height={200}
- />
+
+
+
Latency
-
({ x: i, y: d.latency })),
- }]}
- height={200}
- threshold={{ value: 300, label: 'SLA 300ms' }}
- />
+
+
+
+
Errors
-
({ x: d.time, y: d.errors })),
- }]}
- height={200}
- />
+
+
+
Success Rate
-
({ x: i, y: d.successRate })),
- }]}
- height={200}
- />
+
+
+
)}
diff --git a/ui/src/pages/Routes/RoutesMetrics.tsx b/ui/src/pages/Routes/RoutesMetrics.tsx
index 4019351e..96d70edb 100644
--- a/ui/src/pages/Routes/RoutesMetrics.tsx
+++ b/ui/src/pages/Routes/RoutesMetrics.tsx
@@ -3,9 +3,12 @@ import { useParams, useNavigate } from 'react-router';
import {
KpiStrip,
DataTable,
- AreaChart,
- LineChart,
- BarChart,
+ ThemedChart,
+ Area,
+ Line,
+ Bar,
+ ReferenceLine,
+ CHART_COLORS,
Card,
Sparkline,
MonoText,
@@ -243,41 +246,17 @@ export default function RoutesMetrics() {
[timeseries],
);
- // Chart series from timeseries buckets
- const throughputChartSeries = useMemo(() => [{
- label: 'Throughput',
- data: (timeseries?.buckets || []).map((b, i) => ({
- x: i as number,
- y: b.totalCount,
+ // Flat chart data from timeseries buckets
+ const chartData = useMemo(() =>
+ (timeseries?.buckets || []).map((b, i) => ({
+ idx: i,
+ throughput: b.totalCount,
+ latency: b.avgDurationMs,
+ errors: b.failedCount,
+ volume: b.totalCount,
})),
- }], [timeseries]);
-
- const latencyChartSeries = useMemo(() => [{
- label: 'Latency',
- data: (timeseries?.buckets || []).map((b, i) => ({
- x: i as number,
- y: b.avgDurationMs,
- })),
- }], [timeseries]);
-
- const errorBarSeries = useMemo(() => [{
- label: 'Errors',
- data: (timeseries?.buckets || []).map((b) => {
- const ts = new Date(b.time);
- const label = !isNaN(ts.getTime())
- ? ts.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })
- : '—';
- return { x: label, y: b.failedCount };
- }),
- }], [timeseries]);
-
- const volumeChartSeries = useMemo(() => [{
- label: 'Volume',
- data: (timeseries?.buckets || []).map((b, i) => ({
- x: i as number,
- y: b.totalCount,
- })),
- }], [timeseries]);
+ [timeseries],
+ );
const kpiItems = useMemo(() =>
buildKpiItems(stats, rows.length, throughputSparkline, errorSparkline),
@@ -315,42 +294,34 @@ export default function RoutesMetrics() {
{/* 2x2 chart grid */}
- {(timeseries?.buckets?.length ?? 0) > 0 && (
+ {chartData.length > 0 && (
-
+
+
+
-
+
+
+
+
-
-
+
+
+
+
-
+
+
+
)}