navigate('/metrics')}
- role="button"
- tabIndex={0}
- onKeyDown={(e) => { if (e.key === 'Enter' || e.key === ' ') navigate('/metrics') }}
- >
-
▤
-
+ {/* Routes tree (collapsible, label navigates to /routes) */}
+
+
+
+ navigate('/routes')}
+ role="button"
+ tabIndex={0}
+ onKeyDown={(e) => { if (e.key === 'Enter' || e.key === ' ') navigate('/routes') }}
+ >
+ Routes
+
+ {!routesCollapsed && (
+
+ )}
{/* No results message */}
diff --git a/src/mocks/metrics.ts b/src/mocks/metrics.ts
index 5f6abe5..6d2c58e 100644
--- a/src/mocks/metrics.ts
+++ b/src/mocks/metrics.ts
@@ -147,6 +147,7 @@ export const errorCountSeries: MetricSeries[] = [
export interface RouteMetricRow {
routeId: string
routeName: string
+ appId: string
exchangeCount: number
successRate: number
avgDurationMs: number
@@ -159,6 +160,7 @@ export const routeMetrics: RouteMetricRow[] = [
{
routeId: 'order-intake',
routeName: 'order-intake',
+ appId: 'order-service',
exchangeCount: 892,
successRate: 99.2,
avgDurationMs: 88,
@@ -169,6 +171,7 @@ export const routeMetrics: RouteMetricRow[] = [
{
routeId: 'order-enrichment',
routeName: 'order-enrichment',
+ appId: 'order-service',
exchangeCount: 541,
successRate: 97.6,
avgDurationMs: 156,
@@ -179,6 +182,7 @@ export const routeMetrics: RouteMetricRow[] = [
{
routeId: 'payment-process',
routeName: 'payment-process',
+ appId: 'payment-svc',
exchangeCount: 414,
successRate: 96.1,
avgDurationMs: 234,
@@ -186,9 +190,21 @@ export const routeMetrics: RouteMetricRow[] = [
errorCount: 16,
sparkline: [210, 225, 232, 218, 241, 235, 228, 242, 238, 231, 244, 237, 233, 234],
},
+ {
+ routeId: 'payment-validate',
+ routeName: 'payment-validate',
+ appId: 'payment-svc',
+ exchangeCount: 498,
+ successRate: 99.8,
+ avgDurationMs: 142,
+ p99DurationMs: 198,
+ errorCount: 1,
+ sparkline: [138, 141, 140, 143, 145, 142, 144, 141, 139, 143, 142, 140, 141, 142],
+ },
{
routeId: 'shipment-dispatch',
routeName: 'shipment-dispatch',
+ appId: 'shipment-tracker',
exchangeCount: 387,
successRate: 98.4,
avgDurationMs: 118,
@@ -196,4 +212,26 @@ export const routeMetrics: RouteMetricRow[] = [
errorCount: 6,
sparkline: [112, 115, 118, 114, 120, 116, 119, 117, 118, 121, 116, 118, 119, 118],
},
+ {
+ routeId: 'shipment-track',
+ routeName: 'shipment-track',
+ appId: 'shipment-tracker',
+ exchangeCount: 923,
+ successRate: 99.5,
+ avgDurationMs: 94,
+ p99DurationMs: 167,
+ errorCount: 5,
+ sparkline: [88, 91, 93, 95, 92, 94, 96, 93, 91, 95, 94, 92, 93, 94],
+ },
+ {
+ routeId: 'notification-dispatch',
+ routeName: 'notification-dispatch',
+ appId: 'notification-hub',
+ exchangeCount: 471,
+ successRate: 98.9,
+ avgDurationMs: 62,
+ p99DurationMs: 124,
+ errorCount: 5,
+ sparkline: [58, 60, 63, 61, 64, 62, 60, 63, 65, 62, 61, 63, 62, 62],
+ },
]
diff --git a/src/pages/Metrics/Metrics.module.css b/src/pages/Metrics/Metrics.module.css
deleted file mode 100644
index cb5b3ae..0000000
--- a/src/pages/Metrics/Metrics.module.css
+++ /dev/null
@@ -1,134 +0,0 @@
-/* Scrollable content area */
-.content {
- flex: 1;
- overflow-y: auto;
- padding: 20px 24px 40px;
- min-width: 0;
- background: var(--bg-body);
-}
-
-.refreshIndicator {
- display: flex;
- align-items: center;
- gap: 6px;
- margin-bottom: 12px;
- justify-content: flex-end;
-}
-
-.refreshDot {
- width: 7px;
- height: 7px;
- border-radius: 50%;
- background: var(--success);
- box-shadow: 0 0 4px rgba(61, 124, 71, 0.5);
- animation: pulse 2s ease-in-out infinite;
-}
-
-@keyframes pulse {
- 0%, 100% { opacity: 1; }
- 50% { opacity: 0.5; }
-}
-
-.refreshText {
- font-size: 11px;
- color: var(--text-muted);
- font-family: var(--font-mono);
-}
-
-/* KPI strip */
-.kpiStrip {
- display: grid;
- grid-template-columns: repeat(5, 1fr);
- gap: 10px;
- margin-bottom: 16px;
-}
-
-/* Route performance table */
-.tableSection {
- background: var(--bg-surface);
- border: 1px solid var(--border-subtle);
- border-radius: var(--radius-lg);
- box-shadow: var(--shadow-card);
- overflow: hidden;
- margin-bottom: 20px;
-}
-
-.tableHeader {
- display: flex;
- align-items: center;
- justify-content: space-between;
- padding: 12px 16px;
- border-bottom: 1px solid var(--border-subtle);
-}
-
-.tableTitle {
- font-size: 13px;
- font-weight: 600;
- color: var(--text-primary);
-}
-
-.tableRight {
- display: flex;
- align-items: center;
- gap: 10px;
-}
-
-.tableMeta {
- font-size: 11px;
- color: var(--text-muted);
- font-family: var(--font-mono);
-}
-
-/* Route name in table */
-.routeNameCell {
- font-size: 12px;
- font-weight: 500;
- color: var(--text-primary);
- font-family: var(--font-mono);
-}
-
-/* Rate color classes */
-.rateGood {
- color: var(--success);
-}
-
-.rateWarn {
- color: var(--warning);
-}
-
-.rateBad {
- color: var(--error);
-}
-
-.rateNeutral {
- color: var(--text-secondary);
-}
-
-/* 2x2 chart grid */
-.chartGrid {
- display: grid;
- grid-template-columns: 1fr 1fr;
- gap: 16px;
-}
-
-.chartCard {
- background: var(--bg-surface);
- border: 1px solid var(--border-subtle);
- border-radius: var(--radius-lg);
- box-shadow: var(--shadow-card);
- padding: 16px;
- overflow: hidden;
-}
-
-.chartTitle {
- font-size: 12px;
- font-weight: 600;
- color: var(--text-secondary);
- text-transform: uppercase;
- letter-spacing: 0.5px;
- margin-bottom: 12px;
-}
-
-.chart {
- width: 100%;
-}
diff --git a/src/pages/Metrics/Metrics.tsx b/src/pages/Metrics/Metrics.tsx
deleted file mode 100644
index f371b8c..0000000
--- a/src/pages/Metrics/Metrics.tsx
+++ /dev/null
@@ -1,309 +0,0 @@
-import { useNavigate } from 'react-router-dom'
-import styles from './Metrics.module.css'
-
-// Layout
-import { AppShell } from '../../design-system/layout/AppShell/AppShell'
-import { Sidebar } from '../../design-system/layout/Sidebar/Sidebar'
-import { TopBar } from '../../design-system/layout/TopBar/TopBar'
-
-// Composites
-import { AreaChart } from '../../design-system/composites/AreaChart/AreaChart'
-import { LineChart } from '../../design-system/composites/LineChart/LineChart'
-import { BarChart } from '../../design-system/composites/BarChart/BarChart'
-import { DataTable } from '../../design-system/composites/DataTable/DataTable'
-import type { Column } from '../../design-system/composites/DataTable/types'
-
-// Primitives
-import { StatCard } from '../../design-system/primitives/StatCard/StatCard'
-import { Sparkline } from '../../design-system/primitives/Sparkline/Sparkline'
-import { MonoText } from '../../design-system/primitives/MonoText/MonoText'
-import { Badge } from '../../design-system/primitives/Badge/Badge'
-
-// Mock data
-import {
- throughputSeries,
- latencySeries,
- errorCountSeries,
- routeMetrics,
- type RouteMetricRow,
-} from '../../mocks/metrics'
-import { SIDEBAR_APPS, buildRouteToAppMap } from '../../mocks/sidebar'
-
-const ROUTE_TO_APP = buildRouteToAppMap()
-
-// ─── Metrics KPI cards (5 cards per spec) ─────────────────────────────────────
-const METRIC_KPIS = [
- {
- label: 'Throughput',
- value: '47.2',
- unit: 'msg/s',
- trend: 'neutral' as const,
- trendValue: '→',
- detail: 'Capacity: 120 msg/s · 39%',
- accent: 'running' as const,
- sparkline: [44, 46, 45, 47, 48, 46, 47, 48, 46, 47, 48, 47, 46, 47],
- },
- {
- label: 'Latency p99',
- value: '287ms',
- trend: 'up' as const,
- trendValue: '+23ms',
- detail: 'SLA: <300ms · CLOSE',
- accent: 'warning' as const,
- sparkline: [198, 212, 205, 218, 224, 231, 238, 245, 252, 261, 268, 275, 281, 287],
- },
- {
- label: 'Error Rate',
- value: '2.9%',
- trend: 'up' as const,
- trendValue: '+0.4%',
- detail: '38 errors this shift',
- accent: 'error' as const,
- sparkline: [1.2, 1.8, 1.5, 2.1, 2.4, 2.2, 2.5, 2.6, 2.7, 2.8, 2.7, 2.9, 2.8, 2.9],
- },
- {
- label: 'Success Rate',
- value: '97.1%',
- trend: 'down' as const,
- trendValue: '-0.4%',
- detail: '3,147 ok · 56 warn · 38 err',
- accent: 'success' as const,
- sparkline: [98.2, 97.9, 98.1, 97.8, 97.5, 97.6, 97.4, 97.2, 97.3, 97.1, 97.0, 97.1, 97.2, 97.1],
- },
- {
- label: 'Active Routes',
- value: 7,
- trend: 'neutral' as const,
- trendValue: '→',
- detail: '4 healthy · 2 degraded · 1 stale',
- accent: 'amber' as const,
- sparkline: [7, 7, 7, 7, 7, 7, 7, 6, 7, 7, 7, 7, 7, 7],
- },
-]
-
-// ─── Route metric row with id field (required by DataTable) ──────────────────
-type RouteMetricRowWithId = RouteMetricRow & { id: string }
-
-const routeMetricsWithId: RouteMetricRowWithId[] = routeMetrics.map((r) => ({
- ...r,
- id: r.routeId,
-}))
-
-// ─── Route performance table columns ──────────────────────────────────────────
-const ROUTE_COLUMNS: Column
[] = [
- {
- key: 'routeName',
- header: 'Route',
- sortable: true,
- render: (_, row) => (
- {row.routeName}
- ),
- },
- {
- key: 'exchangeCount',
- header: 'Exchanges',
- sortable: true,
- render: (_, row) => (
- {row.exchangeCount.toLocaleString()}
- ),
- },
- {
- key: 'successRate',
- header: 'Success %',
- sortable: true,
- render: (_, row) => {
- const cls = row.successRate >= 99 ? styles.rateGood : row.successRate >= 97 ? styles.rateWarn : styles.rateBad
- return {row.successRate.toFixed(1)}%
- },
- },
- {
- key: 'avgDurationMs',
- header: 'Avg Duration',
- sortable: true,
- render: (_, row) => (
- {row.avgDurationMs}ms
- ),
- },
- {
- key: 'p99DurationMs',
- header: 'p99 Duration',
- sortable: true,
- render: (_, row) => {
- const cls = row.p99DurationMs > 300 ? styles.rateBad : row.p99DurationMs > 200 ? styles.rateWarn : styles.rateGood
- return {row.p99DurationMs}ms
- },
- },
- {
- key: 'errorCount',
- header: 'Errors',
- sortable: true,
- render: (_, row) => (
- 10 ? styles.rateBad : styles.rateNeutral}>
- {row.errorCount}
-
- ),
- },
- {
- key: 'sparkline',
- header: 'Trend',
- render: (_, row) => (
-
- ),
- },
-]
-
-// ─── Build bar chart data from error series ────────────────────────────────────
-function buildErrorBarSeries() {
- // Take every 5th point and format x as time label
- const sampleInterval = 5
- return errorCountSeries.map((s) => ({
- label: s.label,
- data: s.data
- .filter((_, i) => i % sampleInterval === 0)
- .map((pt) => ({
- x: pt.timestamp.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }),
- y: Math.round(pt.value),
- })),
- }))
-}
-
-// ─── Build volume area chart (derived from throughput) ─────────────────────────
-function buildVolumeSeries() {
- return throughputSeries.map((s) => ({
- label: s.label,
- data: s.data.map((pt) => ({
- x: pt.timestamp,
- y: Math.round(pt.value * 60), // approx msg/min
- })),
- }))
-}
-
-const ERROR_BAR_SERIES = buildErrorBarSeries()
-const VOLUME_SERIES = buildVolumeSeries()
-
-// Convert MetricSeries (from mocks) to ChartSeries format
-function convertSeries(series: typeof throughputSeries) {
- return series.map((s) => ({
- label: s.label,
- data: s.data.map((pt) => ({ x: pt.timestamp, y: pt.value })),
- }))
-}
-
-// ─── Metrics page ─────────────────────────────────────────────────────────────
-export function Metrics() {
- const navigate = useNavigate()
-
- return (
-
- }
- >
- {/* Top bar */}
-
-
- {/* Scrollable content */}
-
-
- {/* Auto-refresh indicator */}
-
-
- Auto-refresh: 30s
-
-
- {/* KPI stat cards (5) */}
-
- {METRIC_KPIS.map((kpi, i) => (
-
- ))}
-
-
- {/* Per-route performance table */}
-
-
-
Per-Route Performance
-
- {routeMetrics.length} routes
-
-
-
-
navigate(`/apps/${ROUTE_TO_APP.get(row.routeId) ?? row.routeId}/${row.routeId}`)}
- />
-
-
- {/* 2x2 chart grid */}
-
- {/* Throughput area chart */}
-
-
- {/* Latency line chart with SLA threshold */}
-
-
- {/* Error bar chart */}
-
-
- {/* Volume area chart */}
-
-
Message Volume (msg/min)
-
-
-
-
-
-
- )
-}
diff --git a/src/pages/Routes/Routes.module.css b/src/pages/Routes/Routes.module.css
new file mode 100644
index 0000000..80d4abf
--- /dev/null
+++ b/src/pages/Routes/Routes.module.css
@@ -0,0 +1,359 @@
+/* Scrollable content area */
+.content {
+ flex: 1;
+ overflow-y: auto;
+ padding: 20px 24px 40px;
+ min-width: 0;
+ background: var(--bg-body);
+}
+
+.refreshIndicator {
+ display: flex;
+ align-items: center;
+ gap: 6px;
+ margin-bottom: 12px;
+ justify-content: flex-end;
+}
+
+.refreshDot {
+ width: 7px;
+ height: 7px;
+ border-radius: 50%;
+ background: var(--success);
+ box-shadow: 0 0 4px rgba(61, 124, 71, 0.5);
+ animation: pulse 2s ease-in-out infinite;
+}
+
+@keyframes pulse {
+ 0%, 100% { opacity: 1; }
+ 50% { opacity: 0.5; }
+}
+
+.refreshText {
+ font-size: 11px;
+ color: var(--text-muted);
+ font-family: var(--font-mono);
+}
+
+/* KPI strip */
+.kpiStrip {
+ display: grid;
+ grid-template-columns: repeat(5, 1fr);
+ gap: 12px;
+ margin-bottom: 20px;
+}
+
+/* KPI card */
+.kpiCard {
+ background: var(--bg-surface);
+ border: 1px solid var(--border-subtle);
+ border-radius: var(--radius-lg);
+ padding: 16px 18px 12px;
+ box-shadow: var(--shadow-card);
+ position: relative;
+ overflow: hidden;
+ transition: box-shadow 0.15s;
+}
+
+.kpiCard:hover {
+ box-shadow: var(--shadow-md);
+}
+
+.kpiCard::before {
+ content: '';
+ position: absolute;
+ top: 0;
+ left: 0;
+ right: 0;
+ height: 3px;
+}
+
+.kpiCardAmber::before { background: linear-gradient(90deg, var(--amber), transparent); }
+.kpiCardGreen::before { background: linear-gradient(90deg, var(--success), transparent); }
+.kpiCardError::before { background: linear-gradient(90deg, var(--error), transparent); }
+.kpiCardTeal::before { background: linear-gradient(90deg, var(--running), transparent); }
+.kpiCardWarn::before { background: linear-gradient(90deg, var(--warning), transparent); }
+
+.kpiLabel {
+ font-size: 10px;
+ font-weight: 600;
+ text-transform: uppercase;
+ letter-spacing: 0.6px;
+ color: var(--text-muted);
+ margin-bottom: 6px;
+}
+
+.kpiValueRow {
+ display: flex;
+ align-items: baseline;
+ gap: 6px;
+ margin-bottom: 4px;
+}
+
+.kpiValue {
+ font-family: var(--font-mono);
+ font-size: 26px;
+ font-weight: 600;
+ line-height: 1.2;
+}
+
+.kpiValueAmber { color: var(--amber); }
+.kpiValueGreen { color: var(--success); }
+.kpiValueError { color: var(--error); }
+.kpiValueTeal { color: var(--running); }
+.kpiValueWarn { color: var(--warning); }
+
+.kpiUnit {
+ font-size: 12px;
+ color: var(--text-muted);
+}
+
+.kpiTrend {
+ font-family: var(--font-mono);
+ font-size: 11px;
+ display: inline-flex;
+ align-items: center;
+ gap: 2px;
+ margin-left: auto;
+}
+
+.trendUpGood { color: var(--success); }
+.trendUpBad { color: var(--error); }
+.trendDownGood { color: var(--success); }
+.trendDownBad { color: var(--error); }
+.trendFlat { color: var(--text-muted); }
+
+.kpiDetail {
+ font-size: 11px;
+ color: var(--text-muted);
+ margin-top: 2px;
+}
+
+.kpiDetailStrong {
+ color: var(--text-secondary);
+ font-weight: 600;
+}
+
+.kpiSparkline {
+ margin-top: 8px;
+ height: 32px;
+}
+
+/* Latency percentiles card */
+.latencyValues {
+ display: flex;
+ gap: 12px;
+ margin-bottom: 4px;
+}
+
+.latencyItem {
+ flex: 1;
+ display: flex;
+ flex-direction: column;
+ gap: 2px;
+}
+
+.latencyLabel {
+ font-size: 9px;
+ font-weight: 600;
+ text-transform: uppercase;
+ letter-spacing: 0.5px;
+ color: var(--text-muted);
+}
+
+.latencyVal {
+ font-family: var(--font-mono);
+ font-size: 18px;
+ font-weight: 600;
+ line-height: 1.2;
+}
+
+.latValGreen { color: var(--success); }
+.latValAmber { color: var(--amber); }
+.latValRed { color: var(--error); }
+
+.latencyTrend {
+ font-family: var(--font-mono);
+ font-size: 9px;
+}
+
+/* Active routes donut */
+.donutWrap {
+ display: flex;
+ align-items: center;
+ gap: 10px;
+ margin-top: 4px;
+}
+
+.donutLabel {
+ font-family: var(--font-mono);
+ font-size: 10px;
+ font-weight: 600;
+ color: var(--text-secondary);
+}
+
+.donutLegend {
+ display: flex;
+ flex-direction: column;
+ gap: 2px;
+ font-size: 10px;
+ color: var(--text-muted);
+}
+
+.donutLegendActive {
+ color: var(--running);
+ font-weight: 600;
+}
+
+/* Route performance table */
+.tableSection {
+ background: var(--bg-surface);
+ border: 1px solid var(--border-subtle);
+ border-radius: var(--radius-lg);
+ box-shadow: var(--shadow-card);
+ overflow: hidden;
+ margin-bottom: 20px;
+}
+
+.tableHeader {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ padding: 12px 16px;
+ border-bottom: 1px solid var(--border-subtle);
+}
+
+.tableTitle {
+ font-size: 13px;
+ font-weight: 600;
+ color: var(--text-primary);
+}
+
+.tableRight {
+ display: flex;
+ align-items: center;
+ gap: 10px;
+}
+
+.tableMeta {
+ font-size: 11px;
+ color: var(--text-muted);
+ font-family: var(--font-mono);
+}
+
+/* Route name in table */
+.routeNameCell {
+ font-size: 12px;
+ font-weight: 500;
+ color: var(--text-primary);
+ font-family: var(--font-mono);
+}
+
+/* Rate color classes */
+.rateGood {
+ color: var(--success);
+}
+
+.rateWarn {
+ color: var(--warning);
+}
+
+.rateBad {
+ color: var(--error);
+}
+
+.rateNeutral {
+ color: var(--text-secondary);
+}
+
+/* 2x2 chart grid */
+.chartGrid {
+ display: grid;
+ grid-template-columns: 1fr 1fr;
+ gap: 16px;
+}
+
+.chartCard {
+ background: var(--bg-surface);
+ border: 1px solid var(--border-subtle);
+ border-radius: var(--radius-lg);
+ box-shadow: var(--shadow-card);
+ padding: 16px;
+ overflow: hidden;
+}
+
+.chartTitle {
+ font-size: 12px;
+ font-weight: 600;
+ color: var(--text-secondary);
+ text-transform: uppercase;
+ letter-spacing: 0.5px;
+ margin-bottom: 12px;
+}
+
+.chart {
+ width: 100%;
+}
+
+/* Processor type badges */
+.processorType {
+ display: inline-flex;
+ align-items: center;
+ padding: 2px 6px;
+ border-radius: var(--radius-sm);
+ font-size: 9px;
+ font-weight: 600;
+ text-transform: uppercase;
+ letter-spacing: 0.5px;
+}
+
+.typeConsumer {
+ background: var(--running-bg);
+ color: var(--running);
+}
+
+.typeProducer {
+ background: var(--success-bg);
+ color: var(--success);
+}
+
+.typeEnricher {
+ background: var(--amber-bg);
+ color: var(--amber);
+}
+
+.typeValidator {
+ background: var(--running-bg);
+ color: var(--running);
+}
+
+.typeTransformer {
+ background: var(--bg-hover);
+ color: var(--text-muted);
+}
+
+.typeRouter {
+ background: #F3EEFA;
+ color: #7C3AED;
+}
+
+.typeProcessor {
+ background: var(--bg-hover);
+ color: var(--text-secondary);
+}
+
+/* Route flow section */
+.routeFlowSection {
+ background: var(--bg-surface);
+ border: 1px solid var(--border-subtle);
+ border-radius: var(--radius-lg);
+ box-shadow: var(--shadow-card);
+ padding: 16px;
+ margin-top: 16px;
+}
+
+/* Application column in table */
+.appCell {
+ font-size: 12px;
+ color: var(--text-secondary);
+}
diff --git a/src/pages/Routes/Routes.tsx b/src/pages/Routes/Routes.tsx
new file mode 100644
index 0000000..106034d
--- /dev/null
+++ b/src/pages/Routes/Routes.tsx
@@ -0,0 +1,595 @@
+import { useMemo } from 'react'
+import { useNavigate, useParams } from 'react-router-dom'
+import styles from './Routes.module.css'
+
+// Layout
+import { AppShell } from '../../design-system/layout/AppShell/AppShell'
+import { Sidebar } from '../../design-system/layout/Sidebar/Sidebar'
+import { TopBar } from '../../design-system/layout/TopBar/TopBar'
+
+// Composites
+import { AreaChart } from '../../design-system/composites/AreaChart/AreaChart'
+import { LineChart } from '../../design-system/composites/LineChart/LineChart'
+import { BarChart } from '../../design-system/composites/BarChart/BarChart'
+import { DataTable } from '../../design-system/composites/DataTable/DataTable'
+import type { Column } from '../../design-system/composites/DataTable/types'
+import { RouteFlow } from '../../design-system/composites/RouteFlow/RouteFlow'
+import type { RouteNode } from '../../design-system/composites/RouteFlow/RouteFlow'
+
+// Primitives
+import { Sparkline } from '../../design-system/primitives/Sparkline/Sparkline'
+import { MonoText } from '../../design-system/primitives/MonoText/MonoText'
+import { Badge } from '../../design-system/primitives/Badge/Badge'
+
+// Mock data
+import {
+ throughputSeries,
+ latencySeries,
+ errorCountSeries,
+ routeMetrics,
+ type RouteMetricRow,
+} from '../../mocks/metrics'
+import { routes } from '../../mocks/routes'
+import { SIDEBAR_APPS, buildRouteToAppMap } from '../../mocks/sidebar'
+
+const ROUTE_TO_APP = buildRouteToAppMap()
+
+// ─── KPI Header Strip (matches mock-v3-metrics-dashboard) ────────────────────
+function KpiHeader({ scopedMetrics }: { scopedMetrics: RouteMetricRow[] }) {
+ const totalExchanges = scopedMetrics.reduce((sum, r) => sum + r.exchangeCount, 0)
+ const totalErrors = scopedMetrics.reduce((sum, r) => sum + r.errorCount, 0)
+ const errorRate = totalExchanges > 0 ? ((totalErrors / totalExchanges) * 100) : 0
+ const avgLatency = scopedMetrics.length > 0
+ ? Math.round(scopedMetrics.reduce((sum, r) => sum + r.avgDurationMs, 0) / scopedMetrics.length)
+ : 0
+ const p99Latency = scopedMetrics.length > 0
+ ? Math.max(...scopedMetrics.map((r) => r.p99DurationMs))
+ : 0
+ const avgSuccessRate = scopedMetrics.length > 0
+ ? Number((scopedMetrics.reduce((sum, r) => sum + r.successRate, 0) / scopedMetrics.length).toFixed(1))
+ : 0
+ const throughputPerSec = totalExchanges > 0 ? (totalExchanges / 360).toFixed(1) : '0'
+ const activeRoutes = scopedMetrics.length
+ const totalRoutes = routeMetrics.length
+
+ return (
+
+ {/* Card 1: Total Throughput */}
+
+
Total Throughput
+
+ {totalExchanges.toLocaleString()}
+ msg/shift
+ ▲ +8%
+
+
+ {throughputPerSec} msg/s · Capacity 39%
+
+
+
+
+
+
+ {/* Card 2: System Error Rate */}
+
+
System Error Rate
+
+ {errorRate.toFixed(2)}%
+
+ {errorRate < 1 ? '\u25BC -0.1%' : '\u25B2 +0.4%'}
+
+
+
+ {totalErrors} errors / {totalExchanges.toLocaleString()} total (6h)
+
+
+
+
+
+
+ {/* Card 3: Latency Percentiles */}
+
300 ? styles.kpiCardWarn : styles.kpiCardGreen}`}>
+
Latency Percentiles
+
+
+ P50
+ {Math.round(avgLatency * 0.5)}ms
+ ▼3
+
+
+ P95
+ 150 ? styles.latValAmber : styles.latValGreen}`}>{Math.round(avgLatency * 1.4)}ms
+ ▲12
+
+
+ P99
+ 300 ? styles.latValRed : styles.latValAmber}`}>{p99Latency}ms
+ ▲28
+
+
+
+ SLA: <300ms P99 · {p99Latency > 300
+ ? BREACH
+ : OK}
+
+
+
+ {/* Card 4: Active Routes */}
+
+
Active Routes
+
+ {activeRoutes}
+ of {totalRoutes}
+ ↔ stable
+
+
+
+
+ {activeRoutes} active
+ {totalRoutes - activeRoutes} stopped
+
+
+
+
+ {/* Card 5: In-Flight Exchanges */}
+
+
In-Flight Exchanges
+
+ 23
+ ↔
+
+
+ High-water: 67 (2h ago)
+
+
+
+
+
+
+ )
+}
+
+// ─── Route metric row with id field (required by DataTable) ──────────────────
+type RouteMetricRowWithId = RouteMetricRow & { id: string }
+
+// ─── Processor metrics types and generator ───────────────────────────────────
+
+interface ProcessorMetric {
+ name: string
+ type: string
+ invocations: number
+ avgDurationMs: number
+ p99DurationMs: number
+ errorCount: number
+ errorRate: number
+ sparkline: number[]
+}
+
+type ProcessorMetricWithId = ProcessorMetric & { id: string }
+
+function generateProcessorMetrics(processors: string[], routeExchangeCount: number): ProcessorMetric[] {
+ return processors.map((proc, i) => {
+ const name = proc
+ const type = proc.startsWith('from(') ? 'consumer'
+ : proc.startsWith('to(') ? 'producer'
+ : proc.startsWith('enrich(') ? 'enricher'
+ : proc.startsWith('validate(') || proc.startsWith('check(') ? 'validator'
+ : proc.startsWith('unmarshal(') || proc.startsWith('marshal(') ? 'transformer'
+ : proc.startsWith('route(') || proc.startsWith('choice(') ? 'router'
+ : 'processor'
+ const invocations = routeExchangeCount
+ const avgBase = 10 + (i * 15) + (proc.includes('enrich') ? 40 : 0) + (proc.includes('http') ? 80 : 0)
+ const avgDurationMs = avgBase + Math.round(Math.sin(i * 2.1) * 10)
+ const p99DurationMs = Math.round(avgDurationMs * 2.5)
+ const errorCount = proc.includes('enrich') || proc.includes('http') ? Math.round(invocations * 0.01) : Math.round(invocations * 0.001)
+ const errorRate = Number(((errorCount / invocations) * 100).toFixed(2))
+ const sparkline = Array.from({ length: 14 }, (_, j) => avgDurationMs + Math.round(Math.sin(j * 0.8 + i) * avgDurationMs * 0.15))
+ return { name, type, invocations, avgDurationMs, p99DurationMs, errorCount, errorRate, sparkline }
+ })
+}
+
+// ─── Map processor type to RouteNode type ────────────────────────────────────
+function toRouteNodeType(procType: string): RouteNode['type'] {
+ switch (procType) {
+ case 'consumer': return 'from'
+ case 'producer': return 'to'
+ case 'enricher': return 'process'
+ case 'validator': return 'process'
+ case 'transformer': return 'process'
+ case 'router': return 'choice'
+ default: return 'process'
+ }
+}
+
+// ─── Processor type badge classes ────────────────────────────────────────────
+const TYPE_STYLE_MAP: Record = {
+ consumer: styles.typeConsumer,
+ producer: styles.typeProducer,
+ enricher: styles.typeEnricher,
+ validator: styles.typeValidator,
+ transformer: styles.typeTransformer,
+ router: styles.typeRouter,
+ processor: styles.typeProcessor,
+}
+
+// ─── Route performance table columns ──────────────────────────────────────────
+const ROUTE_COLUMNS: Column[] = [
+ {
+ key: 'routeName',
+ header: 'Route',
+ sortable: true,
+ render: (_, row) => (
+ {row.routeName}
+ ),
+ },
+ {
+ key: 'appId',
+ header: 'Application',
+ sortable: true,
+ render: (_, row) => (
+ {row.appId}
+ ),
+ },
+ {
+ key: 'exchangeCount',
+ header: 'Exchanges',
+ sortable: true,
+ render: (_, row) => (
+ {row.exchangeCount.toLocaleString()}
+ ),
+ },
+ {
+ key: 'successRate',
+ header: 'Success %',
+ sortable: true,
+ render: (_, row) => {
+ const cls = row.successRate >= 99 ? styles.rateGood : row.successRate >= 97 ? styles.rateWarn : styles.rateBad
+ return {row.successRate.toFixed(1)}%
+ },
+ },
+ {
+ key: 'avgDurationMs',
+ header: 'Avg Duration',
+ sortable: true,
+ render: (_, row) => (
+ {row.avgDurationMs}ms
+ ),
+ },
+ {
+ key: 'p99DurationMs',
+ header: 'p99 Duration',
+ sortable: true,
+ render: (_, row) => {
+ const cls = row.p99DurationMs > 300 ? styles.rateBad : row.p99DurationMs > 200 ? styles.rateWarn : styles.rateGood
+ return {row.p99DurationMs}ms
+ },
+ },
+ {
+ key: 'errorCount',
+ header: 'Errors',
+ sortable: true,
+ render: (_, row) => (
+ 10 ? styles.rateBad : styles.rateNeutral}>
+ {row.errorCount}
+
+ ),
+ },
+ {
+ key: 'sparkline',
+ header: 'Trend',
+ render: (_, row) => (
+
+ ),
+ },
+]
+
+// ─── Processor performance table columns ─────────────────────────────────────
+const PROCESSOR_COLUMNS: Column[] = [
+ {
+ key: 'name',
+ header: 'Processor',
+ sortable: true,
+ render: (_, row) => (
+ {row.name}
+ ),
+ },
+ {
+ key: 'type',
+ header: 'Type',
+ sortable: true,
+ render: (_, row) => (
+
+ {row.type}
+
+ ),
+ },
+ {
+ key: 'invocations',
+ header: 'Invocations',
+ sortable: true,
+ render: (_, row) => (
+ {row.invocations.toLocaleString()}
+ ),
+ },
+ {
+ key: 'avgDurationMs',
+ header: 'Avg Duration',
+ sortable: true,
+ render: (_, row) => {
+ const cls = row.avgDurationMs > 200 ? styles.rateBad : row.avgDurationMs > 100 ? styles.rateWarn : styles.rateGood
+ return {row.avgDurationMs}ms
+ },
+ },
+ {
+ key: 'p99DurationMs',
+ header: 'p99 Duration',
+ sortable: true,
+ render: (_, row) => {
+ const cls = row.p99DurationMs > 300 ? styles.rateBad : row.p99DurationMs > 200 ? styles.rateWarn : styles.rateGood
+ return {row.p99DurationMs}ms
+ },
+ },
+ {
+ key: 'errorCount',
+ header: 'Errors',
+ sortable: true,
+ render: (_, row) => (
+ 10 ? styles.rateBad : styles.rateNeutral}>
+ {row.errorCount}
+
+ ),
+ },
+ {
+ key: 'errorRate',
+ header: 'Error Rate',
+ sortable: true,
+ render: (_, row) => {
+ const cls = row.errorRate > 1 ? styles.rateBad : row.errorRate > 0.5 ? styles.rateWarn : styles.rateGood
+ return {row.errorRate}%
+ },
+ },
+ {
+ key: 'sparkline',
+ header: 'Trend',
+ render: (_, row) => (
+
+ ),
+ },
+]
+
+// ─── Build bar chart data from error series ────────────────────────────────────
+function buildErrorBarSeries() {
+ const sampleInterval = 5
+ return errorCountSeries.map((s) => ({
+ label: s.label,
+ data: s.data
+ .filter((_, i) => i % sampleInterval === 0)
+ .map((pt) => ({
+ x: pt.timestamp.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }),
+ y: Math.round(pt.value),
+ })),
+ }))
+}
+
+// ─── Build volume area chart (derived from throughput) ─────────────────────────
+function buildVolumeSeries() {
+ return throughputSeries.map((s) => ({
+ label: s.label,
+ data: s.data.map((pt) => ({
+ x: pt.timestamp,
+ y: Math.round(pt.value * 60),
+ })),
+ }))
+}
+
+const ERROR_BAR_SERIES = buildErrorBarSeries()
+const VOLUME_SERIES = buildVolumeSeries()
+
+// Convert MetricSeries (from mocks) to ChartSeries format
+function convertSeries(series: typeof throughputSeries) {
+ return series.map((s) => ({
+ label: s.label,
+ data: s.data.map((pt) => ({ x: pt.timestamp, y: pt.value })),
+ }))
+}
+
+// ─── Routes page ──────────────────────────────────────────────────────────────
+export function Routes() {
+ const navigate = useNavigate()
+ const { appId, routeId } = useParams<{ appId: string; routeId: string }>()
+
+ // ── Breadcrumbs ─────────────────────────────────────────────────────────────
+ const breadcrumb = useMemo(() => {
+ if (routeId && appId) {
+ return [
+ { label: 'Routes', href: '/routes' },
+ { label: appId, href: `/routes/${appId}` },
+ { label: routeId },
+ ]
+ }
+ if (appId) {
+ return [
+ { label: 'Routes', href: '/routes' },
+ { label: appId },
+ ]
+ }
+ return [{ label: 'Routes' }]
+ }, [appId, routeId])
+
+ // ── Data filtering ──────────────────────────────────────────────────────────
+ const filteredMetrics = useMemo(() => {
+ const data = appId
+ ? routeMetrics.filter((r) => r.appId === appId)
+ : routeMetrics
+ return data.map((r) => ({ ...r, id: r.routeId }))
+ }, [appId])
+
+ // ── Route detail data ───────────────────────────────────────────────────────
+ const routeDef = useMemo(() => {
+ if (!routeId) return null
+ return routes.find((r) => r.id === routeId) ?? null
+ }, [routeId])
+
+ const processorMetrics = useMemo(() => {
+ if (!routeDef) return []
+ return generateProcessorMetrics(routeDef.processors, routeDef.exchangeCount).map((pm, i) => ({
+ ...pm,
+ id: `proc-${i}`,
+ }))
+ }, [routeDef])
+
+ const routeFlowNodes = useMemo(() => {
+ if (!processorMetrics.length) return []
+ return processorMetrics.map((pm) => ({
+ name: pm.name,
+ type: toRouteNodeType(pm.type),
+ durationMs: pm.avgDurationMs,
+ status: pm.errorRate > 1 ? 'fail' as const : pm.avgDurationMs > 150 ? 'slow' as const : 'ok' as const,
+ }))
+ }, [processorMetrics])
+
+ // Scoped metrics for KPI header
+ const scopedMetricsForKpi = useMemo(() => {
+ if (routeId) return routeMetrics.filter((r) => r.routeId === routeId)
+ if (appId) return routeMetrics.filter((r) => r.appId === appId)
+ return routeMetrics
+ }, [appId, routeId])
+
+ // ── Route detail view ───────────────────────────────────────────────────────
+ if (routeId && appId && routeDef) {
+ return (
+ }>
+
+
+
+
+ Auto-refresh: 30s
+
+
+
+
+ {/* Processor Performance table */}
+
+
+
Processor Performance
+
+ {processorMetrics.length} processors
+
+
+
+
+
+
+ {/* Route Flow diagram */}
+
+
+
+ )
+ }
+
+ // ── Top level / Application level view ──────────────────────────────────────
+ return (
+ }>
+
+
+
+
+ Auto-refresh: 30s
+
+
+ {/* KPI header cards */}
+
+
+ {/* Per-route performance table */}
+
+
+
Per-Route Performance
+
+ {filteredMetrics.length} routes
+
+
+
+
{
+ const rowAppId = appId ?? ROUTE_TO_APP.get(row.routeId) ?? row.routeId
+ navigate(`/routes/${rowAppId}/${row.routeId}`)
+ }}
+ />
+
+
+ {/* 2x2 chart grid */}
+
+
+
+
+
+
+
+
+
Message Volume (msg/min)
+
+
+
+
+
+ )
+}