@kb-labs/studio-app 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (168) hide show
  1. package/.env.example +10 -0
  2. package/README.md +74 -0
  3. package/UI-KIT-COMPONENTS.md +783 -0
  4. package/UI-KIT-PLAN.md +326 -0
  5. package/eslint-rules/no-direct-antd-import.js +151 -0
  6. package/eslint.config.ts +27 -0
  7. package/index.html +17 -0
  8. package/package.json +72 -0
  9. package/postcss.config.js +7 -0
  10. package/public/favicon.ico +0 -0
  11. package/public/favicon.svg +7 -0
  12. package/rspack.config.mjs +118 -0
  13. package/src/App.tsx +46 -0
  14. package/src/components/devtools/devtools-log-feed.module.css +165 -0
  15. package/src/components/devtools/devtools-log-feed.tsx +300 -0
  16. package/src/components/devtools/devtools-panel.module.css +121 -0
  17. package/src/components/devtools/devtools-panel.tsx +115 -0
  18. package/src/components/empty-state.tsx +28 -0
  19. package/src/components/error-boundary.tsx +210 -0
  20. package/src/components/health-banner.module.css +103 -0
  21. package/src/components/health-banner.tsx +134 -0
  22. package/src/components/health-indicator.tsx +12 -0
  23. package/src/components/page-header.tsx +49 -0
  24. package/src/components/page-transition.tsx +24 -0
  25. package/src/components/role-switcher.tsx +148 -0
  26. package/src/components/run-badge.tsx +13 -0
  27. package/src/components/ui/icons.tsx +509 -0
  28. package/src/components/ui/index.ts +25 -0
  29. package/src/components/ui/kb-config-provider.tsx +130 -0
  30. package/src/components/ui/kb-header.module.css +51 -0
  31. package/src/components/ui/kb-header.tsx +112 -0
  32. package/src/components/ui/kb-notification-bell.module.css +103 -0
  33. package/src/components/ui/kb-notification-bell.tsx +199 -0
  34. package/src/components/ui/kb-page-layout.tsx +66 -0
  35. package/src/components/ui/kb-quick-search.module.css +86 -0
  36. package/src/components/ui/kb-quick-search.tsx +196 -0
  37. package/src/components/ui/kb-sidebar.module.css +47 -0
  38. package/src/components/ui/kb-sidebar.tsx +196 -0
  39. package/src/components/ui/kb-stat-card.module.css +21 -0
  40. package/src/components/ui/kb-stat-card.tsx +22 -0
  41. package/src/components/ui/kb-stat-grid.tsx +22 -0
  42. package/src/components/ui/kb-status-bar.module.css +49 -0
  43. package/src/components/ui/kb-status-bar.tsx +145 -0
  44. package/src/components/ui/kb-system-health-indicator.tsx +229 -0
  45. package/src/components/ui/kb-theme-toggle.tsx +42 -0
  46. package/src/components/ui/theme-adapter.ts +812 -0
  47. package/src/components/ui/theme-transition-overlay.tsx +70 -0
  48. package/src/components/ui/utils.ts +6 -0
  49. package/src/components/widget-error-boundary.tsx +172 -0
  50. package/src/config/env.ts +5 -0
  51. package/src/config/feature-flags.ts +482 -0
  52. package/src/config/studio.config.schema.ts +88 -0
  53. package/src/config/studio.config.ts +84 -0
  54. package/src/env.d.ts +23 -0
  55. package/src/hooks/use-command-palette.ts +6 -0
  56. package/src/hooks/use-devtools-store.ts +25 -0
  57. package/src/hooks/use-feature-flags.ts +101 -0
  58. package/src/index.css +10 -0
  59. package/src/layouts/dashboard-grid.tsx +122 -0
  60. package/src/layouts/index.ts +12 -0
  61. package/src/layouts/two-pane.tsx +134 -0
  62. package/src/main.tsx +15 -0
  63. package/src/modules/analytics/components/analytics-daily-stats-chart.tsx +257 -0
  64. package/src/modules/analytics/components/analytics-date-range-picker.tsx +121 -0
  65. package/src/modules/analytics/pages/analytics-cache-page.tsx +285 -0
  66. package/src/modules/analytics/pages/analytics-embeddings-page.tsx +274 -0
  67. package/src/modules/analytics/pages/analytics-events-page.tsx +425 -0
  68. package/src/modules/analytics/pages/analytics-llm-page.tsx +434 -0
  69. package/src/modules/analytics/pages/analytics-overview-page.tsx +336 -0
  70. package/src/modules/analytics/pages/analytics-page.tsx +20 -0
  71. package/src/modules/analytics/pages/analytics-storage-page.tsx +344 -0
  72. package/src/modules/analytics/pages/analytics-vectorstore-page.tsx +279 -0
  73. package/src/modules/analytics/routes/index.tsx +133 -0
  74. package/src/modules/analytics/utils/formatters.test.ts +148 -0
  75. package/src/modules/analytics/utils/formatters.ts +100 -0
  76. package/src/modules/auth/pages/login-page.module.css +113 -0
  77. package/src/modules/auth/pages/login-page.tsx +80 -0
  78. package/src/modules/auth/routes/index.tsx +24 -0
  79. package/src/modules/dashboard/components/ActivityTimelineWidget.tsx +199 -0
  80. package/src/modules/dashboard/components/AiInsightsWidget.tsx +358 -0
  81. package/src/modules/dashboard/components/AnimatedMetricCard.css +88 -0
  82. package/src/modules/dashboard/components/AnimatedMetricCard.tsx +125 -0
  83. package/src/modules/dashboard/components/HeroMetricCard.css +114 -0
  84. package/src/modules/dashboard/components/HeroMetricCard.tsx +152 -0
  85. package/src/modules/dashboard/components/HeroMetricsWidget.tsx +146 -0
  86. package/src/modules/dashboard/components/SystemResourcesWidget.tsx +197 -0
  87. package/src/modules/dashboard/components/dashboard-empty.tsx +22 -0
  88. package/src/modules/dashboard/components/dashboard-kpis-widget.tsx +59 -0
  89. package/src/modules/dashboard/components/dashboard-kpis.tsx +13 -0
  90. package/src/modules/dashboard/components/dashboard-latency-percentiles-widget.tsx +85 -0
  91. package/src/modules/dashboard/components/dashboard-metrics-widget.tsx +78 -0
  92. package/src/modules/dashboard/components/dashboard-plugins-widget.tsx +67 -0
  93. package/src/modules/dashboard/components/dashboard-requests-widget.tsx +62 -0
  94. package/src/modules/dashboard/layouts/dashboard-layout.tsx +16 -0
  95. package/src/modules/dashboard/pages/ai-insights-page.tsx +563 -0
  96. package/src/modules/dashboard/pages/dashboard-animations.css +317 -0
  97. package/src/modules/dashboard/pages/dashboard-page.tsx +13 -0
  98. package/src/modules/dashboard/routes/index.tsx +70 -0
  99. package/src/modules/observability/pages/devkit-page.tsx +263 -0
  100. package/src/modules/observability/pages/incident-detail-page.tsx +571 -0
  101. package/src/modules/observability/pages/incidents-page.tsx +370 -0
  102. package/src/modules/observability/pages/log-detail-page.tsx +508 -0
  103. package/src/modules/observability/pages/logs-page.tsx +1129 -0
  104. package/src/modules/observability/pages/prometheus-metrics-page.tsx +486 -0
  105. package/src/modules/observability/pages/state-broker-page.tsx +229 -0
  106. package/src/modules/observability/pages/system-events-page.tsx +255 -0
  107. package/src/modules/observability/routes/index.tsx +133 -0
  108. package/src/modules/plugins/components/marketplace-card.tsx +153 -0
  109. package/src/modules/plugins/components/plugin-ai-assistant-modal.tsx +374 -0
  110. package/src/modules/plugins/components/plugin-card.tsx +136 -0
  111. package/src/modules/plugins/components/plugin-detail/capabilities-summary.tsx +101 -0
  112. package/src/modules/plugins/components/plugin-detail/cli-commands-table.tsx +101 -0
  113. package/src/modules/plugins/components/plugin-detail/index.ts +7 -0
  114. package/src/modules/plugins/components/plugin-detail/jobs-table.tsx +64 -0
  115. package/src/modules/plugins/components/plugin-detail/permissions/env-permissions.tsx +30 -0
  116. package/src/modules/plugins/components/plugin-detail/permissions/filesystem-permissions.tsx +68 -0
  117. package/src/modules/plugins/components/plugin-detail/permissions/index.ts +6 -0
  118. package/src/modules/plugins/components/plugin-detail/permissions/network-permissions.tsx +43 -0
  119. package/src/modules/plugins/components/plugin-detail/permissions/platform-services.tsx +242 -0
  120. package/src/modules/plugins/components/plugin-detail/permissions/resource-quotas.tsx +96 -0
  121. package/src/modules/plugins/components/plugin-detail/permissions/shell-permissions.tsx +55 -0
  122. package/src/modules/plugins/components/plugin-detail/permissions-view.tsx +47 -0
  123. package/src/modules/plugins/components/plugin-detail/plugin-header.tsx +169 -0
  124. package/src/modules/plugins/components/plugin-detail/rest-routes-table.tsx +159 -0
  125. package/src/modules/plugins/components/plugin-detail/workflows-table.tsx +40 -0
  126. package/src/modules/plugins/components/plugin-grid.tsx +20 -0
  127. package/src/modules/plugins/components/plugin-table.tsx +178 -0
  128. package/src/modules/plugins/pages/marketplace-page.tsx +200 -0
  129. package/src/modules/plugins/pages/plugin-detail-page.tsx +1145 -0
  130. package/src/modules/plugins/routes/index.tsx +48 -0
  131. package/src/modules/plugins/utils/index.ts +1 -0
  132. package/src/modules/plugins/utils/schema-helpers.ts +21 -0
  133. package/src/modules/settings/components/api-routes-viewer.tsx +415 -0
  134. package/src/modules/settings/components/appearance-settings.tsx +200 -0
  135. package/src/modules/settings/components/configuration-settings.tsx +321 -0
  136. package/src/modules/settings/components/data-privacy-settings.tsx +111 -0
  137. package/src/modules/settings/components/experimental-settings.tsx +243 -0
  138. package/src/modules/settings/components/navigation-settings.tsx +198 -0
  139. package/src/modules/settings/components/notifications-settings.tsx +40 -0
  140. package/src/modules/settings/pages/settings-page.tsx +298 -0
  141. package/src/modules/settings/routes/index.tsx +47 -0
  142. package/src/pages/README-assistant.md +91 -0
  143. package/src/pages/login-page.tsx +177 -0
  144. package/src/pages/not-found-page.tsx +85 -0
  145. package/src/providers/auth-provider.tsx +40 -0
  146. package/src/providers/command-palette-provider.tsx +193 -0
  147. package/src/providers/data-sources-provider.tsx +27 -0
  148. package/src/providers/devtools-provider.tsx +72 -0
  149. package/src/providers/navigation-items-provider.tsx +38 -0
  150. package/src/providers/registry-v2-provider.tsx +93 -0
  151. package/src/providers/settings-provider.tsx +158 -0
  152. package/src/router.tsx +302 -0
  153. package/src/routes/helpers.tsx +25 -0
  154. package/src/routes/plugin-page-v2.tsx +52 -0
  155. package/src/routes/types.ts +18 -0
  156. package/src/styles/antd-overrides.css +211 -0
  157. package/src/styles/base.css +190 -0
  158. package/src/styles/settings.css +75 -0
  159. package/src/styles/variables.css +94 -0
  160. package/src/utils/analytics.ts +183 -0
  161. package/src/utils/apply-navigation-categories.ts +83 -0
  162. package/src/utils/commands.ts +252 -0
  163. package/src/utils/export.ts +174 -0
  164. package/src/utils/format-date.ts +166 -0
  165. package/src/utils/logger.ts +110 -0
  166. package/tailwind.config.ts +13 -0
  167. package/tsconfig.json +13 -0
  168. package/vitest-setup.ts +1 -0
@@ -0,0 +1,486 @@
1
+ import {
2
+ UIRow,
3
+ UICol,
4
+ UITable,
5
+ UICard,
6
+ UIStatistic,
7
+ UIAlert,
8
+ UITag,
9
+ UIProgress,
10
+ UITimeline,
11
+ UIBadge,
12
+ UIIcon,
13
+ } from '@kb-labs/studio-ui-kit';
14
+ import { usePrometheusMetrics } from '@kb-labs/studio-data-client';
15
+ import { useDataSources } from '../../../providers/data-sources-provider';
16
+ import type { PluginMetrics, TenantMetrics } from '@kb-labs/studio-data-client';
17
+ import { UIPage, UIPageHeader } from '@kb-labs/studio-ui-kit';
18
+
19
+ /**
20
+ * Format uptime duration to human-readable string
21
+ */
22
+ function formatUptime(seconds: number): string {
23
+ const hours = Math.floor(seconds / 3600);
24
+ const minutes = Math.floor((seconds % 3600) / 60);
25
+
26
+ if (hours > 0) {
27
+ return `${hours}h ${minutes}m`;
28
+ }
29
+ return `${minutes}m`;
30
+ }
31
+
32
+ /**
33
+ * Format number with optional decimal places
34
+ */
35
+ function formatNumber(value: number, decimals = 2): string {
36
+ return value.toFixed(decimals);
37
+ }
38
+
39
+ /**
40
+ * Format timestamp to relative time
41
+ */
42
+ function formatRelativeTime(timestamp: number): string {
43
+ const now = Date.now();
44
+ const diff = now - timestamp;
45
+ const seconds = Math.floor(diff / 1000);
46
+ const minutes = Math.floor(seconds / 60);
47
+ const hours = Math.floor(minutes / 60);
48
+
49
+ if (hours > 0) {
50
+ return `${hours}h ${minutes % 60}m ago`;
51
+ }
52
+ if (minutes > 0) {
53
+ return `${minutes}m ago`;
54
+ }
55
+ return `${seconds}s ago`;
56
+ }
57
+
58
+ /**
59
+ * Get status code color
60
+ */
61
+ function getStatusCodeColor(code: number): string {
62
+ if (code >= 500) {return '#ff4d4f';} // red
63
+ if (code >= 400) {return '#faad14';} // orange
64
+ if (code >= 300) {return '#1890ff';} // blue
65
+ return '#52c41a'; // green
66
+ }
67
+
68
+ /**
69
+ * Prometheus Metrics Observability Page
70
+ *
71
+ * Shows real-time metrics parsed from the canonical REST API /metrics endpoint:
72
+ * - Request counts and error rates
73
+ * - Latency percentiles (p50, p95, p99)
74
+ * - Per-plugin and per-tenant breakdown
75
+ * - Redis health status
76
+ * - Plugin mount statistics
77
+ */
78
+ export function PrometheusMetricsPage() {
79
+ const sources = useDataSources();
80
+ const { data, isLoading, error, isError } = usePrometheusMetrics(sources.observability);
81
+
82
+ if (isError) {
83
+ return (
84
+ <UIPage>
85
+ <UIPageHeader
86
+ title="Prometheus Metrics"
87
+ description="REST API performance and health metrics"
88
+ />
89
+ <UIAlert
90
+ message="Failed to load Prometheus metrics"
91
+ description={error instanceof Error ? error.message : 'Unknown error. Make sure REST API is running on localhost:5050'}
92
+ variant="error"
93
+ showIcon
94
+ style={{ marginBottom: 24 }}
95
+ />
96
+ </UIPage>
97
+ );
98
+ }
99
+
100
+ if (isLoading || !data) {
101
+ return (
102
+ <UIPage>
103
+ <UIPageHeader
104
+ title="Prometheus Metrics"
105
+ description="REST API performance and health metrics"
106
+ />
107
+ <div style={{ padding: '24px 0', textAlign: 'center' }}>
108
+ Loading...
109
+ </div>
110
+ </UIPage>
111
+ );
112
+ }
113
+
114
+ const errorRate = data.requests?.total > 0
115
+ ? (((data.requests.clientErrors ?? 0) + (data.requests.serverErrors ?? 0)) / data.requests.total) * 100
116
+ : 0;
117
+
118
+ const successRate = data.requests?.total > 0
119
+ ? ((data.requests.success ?? 0) / data.requests.total) * 100
120
+ : 0;
121
+
122
+ return (
123
+ <UIPage width="full">
124
+ <UIPageHeader
125
+ title="Prometheus Metrics"
126
+ description="REST API performance metrics - Auto-refresh every 10s"
127
+ />
128
+
129
+ {/* Overview Cards */}
130
+ <UIRow gutter={[16, 16]} style={{ marginBottom: 24 }}>
131
+ <UICol xs={24} sm={12} lg={6}>
132
+ <UICard>
133
+ <UIStatistic
134
+ title="Total Requests"
135
+ value={data.requests?.total ?? 0}
136
+ prefix={<UIIcon name="ApiOutlined" />}
137
+ />
138
+ </UICard>
139
+ </UICol>
140
+ <UICol xs={24} sm={12} lg={6}>
141
+ <UICard>
142
+ <UIStatistic
143
+ title="Avg Latency (p50)"
144
+ value={formatNumber(data.latency?.p50 ?? 0, 1)}
145
+ suffix="ms"
146
+ prefix={<UIIcon name="ThunderboltOutlined" />}
147
+ />
148
+ </UICard>
149
+ </UICol>
150
+ <UICol xs={24} sm={12} lg={6}>
151
+ <UICard>
152
+ <UIStatistic
153
+ title="Error Rate"
154
+ value={formatNumber(errorRate, 1)}
155
+ suffix="%"
156
+ prefix={<UIIcon name="WarningOutlined" />}
157
+ valueStyle={{ color: errorRate > 5 ? '#ff4d4f' : '#52c41a' }}
158
+ />
159
+ </UICard>
160
+ </UICol>
161
+ <UICol xs={24} sm={12} lg={6}>
162
+ <UICard>
163
+ <UIStatistic
164
+ title="Uptime"
165
+ value={formatUptime(data.uptime?.seconds ?? 0)}
166
+ prefix={<UIIcon name="ClockCircleOutlined" />}
167
+ />
168
+ </UICard>
169
+ </UICol>
170
+ </UIRow>
171
+
172
+ {/* Latency Percentiles */}
173
+ <UIRow gutter={[16, 16]} style={{ marginBottom: 24 }}>
174
+ <UICol xs={24} lg={12}>
175
+ <UICard title="Latency Percentiles" extra={<UIIcon name="ThunderboltOutlined" style={{ color: '#1890ff' }} />}>
176
+ <UIRow gutter={16}>
177
+ <UICol span={8}>
178
+ <UIStatistic
179
+ title="p50 (Median)"
180
+ value={formatNumber(data.latency?.p50 ?? 0, 1)}
181
+ suffix="ms"
182
+ valueStyle={{ fontSize: 20 }}
183
+ />
184
+ </UICol>
185
+ <UICol span={8}>
186
+ <UIStatistic
187
+ title="p95"
188
+ value={formatNumber(data.latency?.p95 ?? 0, 1)}
189
+ suffix="ms"
190
+ valueStyle={{ fontSize: 20, color: (data.latency?.p95 ?? 0) > 200 ? '#faad14' : '#52c41a' }}
191
+ />
192
+ </UICol>
193
+ <UICol span={8}>
194
+ <UIStatistic
195
+ title="p99"
196
+ value={formatNumber(data.latency?.p99 ?? 0, 1)}
197
+ suffix="ms"
198
+ valueStyle={{ fontSize: 20, color: (data.latency?.p99 ?? 0) > 500 ? '#ff4d4f' : '#faad14' }}
199
+ />
200
+ </UICol>
201
+ </UIRow>
202
+ <div style={{ marginTop: 16, fontSize: 12, color: '#8c8c8c' }}>
203
+ Min: {formatNumber(data.latency?.min ?? 0, 1)}ms | Max: {formatNumber(data.latency?.max ?? 0, 1)}ms | Avg: {formatNumber(data.latency?.average ?? 0, 1)}ms
204
+ </div>
205
+ </UICard>
206
+ </UICol>
207
+
208
+ <UICol xs={24} lg={12}>
209
+ <UICard title="Success Rate" extra={<UIIcon name="CheckCircleOutlined" style={{ color: successRate >= 95 ? '#52c41a' : '#faad14' }} />}>
210
+ <UIProgress
211
+ percent={Math.round(successRate)}
212
+ status={successRate >= 95 ? 'success' : successRate >= 90 ? 'normal' : 'exception'}
213
+ strokeColor={successRate >= 95 ? '#52c41a' : successRate >= 90 ? '#1890ff' : '#ff4d4f'}
214
+ />
215
+ <div style={{ marginTop: 16 }}>
216
+ <UIRow gutter={16}>
217
+ <UICol span={8}>
218
+ <UIStatistic
219
+ title="Success"
220
+ value={data.requests?.success ?? 0}
221
+ valueStyle={{ fontSize: 18, color: '#52c41a' }}
222
+ />
223
+ </UICol>
224
+ <UICol span={8}>
225
+ <UIStatistic
226
+ title="4xx Errors"
227
+ value={data.requests?.clientErrors ?? 0}
228
+ valueStyle={{ fontSize: 18, color: '#faad14' }}
229
+ />
230
+ </UICol>
231
+ <UICol span={8}>
232
+ <UIStatistic
233
+ title="5xx Errors"
234
+ value={data.requests?.serverErrors ?? 0}
235
+ valueStyle={{ fontSize: 18, color: '#ff4d4f' }}
236
+ />
237
+ </UICol>
238
+ </UIRow>
239
+ </div>
240
+ </UICard>
241
+ </UICol>
242
+ </UIRow>
243
+
244
+ {/* Per-Plugin Metrics */}
245
+ <UICard title="Per-Plugin Metrics" style={{ marginBottom: 24 }}>
246
+ <UITable<{ key: string; plugin: string } & PluginMetrics>
247
+ dataSource={(data.perPlugin ?? []).map((item) => ({
248
+ key: item.pluginId,
249
+ plugin: item.pluginId,
250
+ ...item,
251
+ }))}
252
+ columns={[
253
+ {
254
+ title: 'Plugin',
255
+ dataIndex: 'plugin',
256
+ key: 'plugin',
257
+ render: (plugin: string) => <UITag color="blue">{plugin}</UITag>,
258
+ },
259
+ {
260
+ title: 'Requests',
261
+ dataIndex: 'requests',
262
+ key: 'requests',
263
+ align: 'right' as const,
264
+ sorter: (a, b) => a.requests - b.requests,
265
+ },
266
+ {
267
+ title: 'Errors',
268
+ dataIndex: 'errors',
269
+ key: 'errors',
270
+ align: 'right' as const,
271
+ render: (errors: number) => (
272
+ <span style={{ color: errors > 10 ? '#ff4d4f' : errors > 0 ? '#faad14' : '#52c41a' }}>
273
+ {errors}
274
+ </span>
275
+ ),
276
+ },
277
+ {
278
+ title: 'Avg Latency',
279
+ key: 'avgLatency',
280
+ align: 'right' as const,
281
+ render: (_: unknown, record: PluginMetrics) =>
282
+ record.latency ? `${formatNumber(record.latency.average, 1)} ms` : 'N/A',
283
+ sorter: (a, b) => (a.latency?.average ?? 0) - (b.latency?.average ?? 0),
284
+ },
285
+ {
286
+ title: 'Min / Max',
287
+ key: 'minMax',
288
+ align: 'right' as const,
289
+ render: (_: unknown, record: PluginMetrics) =>
290
+ record.latency ? (
291
+ <span style={{ fontSize: 12, color: '#8c8c8c' }}>
292
+ {formatNumber(record.latency.min, 1)} / {formatNumber(record.latency.max, 1)} ms
293
+ </span>
294
+ ) : <span>N/A</span>,
295
+ },
296
+ ]}
297
+ pagination={false}
298
+ size="small"
299
+ />
300
+ </UICard>
301
+
302
+ {/* Per-Tenant Metrics */}
303
+ <UICard title="Per-Tenant Metrics" style={{ marginBottom: 24 }}>
304
+ <UITable<{ key: string; tenant: string } & TenantMetrics>
305
+ dataSource={(data.perTenant ?? []).map((item) => ({
306
+ key: item.tenantId,
307
+ tenant: item.tenantId,
308
+ ...item,
309
+ }))}
310
+ columns={[
311
+ {
312
+ title: 'Tenant',
313
+ dataIndex: 'tenant',
314
+ key: 'tenant',
315
+ render: (tenant: string) => <UITag color="green">{tenant}</UITag>,
316
+ },
317
+ {
318
+ title: 'Requests',
319
+ dataIndex: 'requests',
320
+ key: 'requests',
321
+ align: 'right' as const,
322
+ },
323
+ {
324
+ title: 'Errors',
325
+ dataIndex: 'errors',
326
+ key: 'errors',
327
+ align: 'right' as const,
328
+ render: (errors: number) => (
329
+ <span style={{ color: errors > 50 ? '#ff4d4f' : errors > 10 ? '#faad14' : '#52c41a' }}>
330
+ {errors}
331
+ </span>
332
+ ),
333
+ },
334
+ {
335
+ title: 'Avg Latency',
336
+ key: 'avgLatency',
337
+ align: 'right' as const,
338
+ render: (_: unknown, record: TenantMetrics) =>
339
+ record.latency ? `${formatNumber(record.latency.average, 1)} ms` : 'N/A',
340
+ },
341
+ {
342
+ title: 'Error Rate',
343
+ key: 'errorRate',
344
+ align: 'right' as const,
345
+ render: (_: unknown, record: TenantMetrics) => {
346
+ const rate = record.requests > 0 ? (record.errors / record.requests) * 100 : 0;
347
+ return (
348
+ <span style={{ color: rate > 5 ? '#ff4d4f' : rate > 2 ? '#faad14' : '#52c41a' }}>
349
+ {formatNumber(rate, 1)}%
350
+ </span>
351
+ );
352
+ },
353
+ },
354
+ ]}
355
+ pagination={false}
356
+ size="small"
357
+ />
358
+ </UICard>
359
+
360
+ {/* Error Analytics */}
361
+ <UIRow gutter={[16, 16]} style={{ marginBottom: 24 }}>
362
+ <UICol xs={24} lg={12}>
363
+ <UICard title="Error Breakdown" extra={<UIIcon name="ExclamationCircleOutlined" style={{ color: '#faad14' }} />}>
364
+ <UIRow gutter={16}>
365
+ {Object.entries(data.errors?.byStatusCode ?? {})
366
+ .sort(([a], [b]) => Number(b) - Number(a))
367
+ .map(([code, count]) => (
368
+ <UICol span={8} key={code}>
369
+ <UIStatistic
370
+ title={`${code} Errors`}
371
+ value={count}
372
+ valueStyle={{ fontSize: 18, color: getStatusCodeColor(Number(code)) }}
373
+ />
374
+ </UICol>
375
+ ))}
376
+ </UIRow>
377
+ {Object.keys(data.errors?.byStatusCode ?? {}).length === 0 && (
378
+ <UIAlert message="No errors recorded" variant="success" showIcon />
379
+ )}
380
+ </UICard>
381
+ </UICol>
382
+
383
+ <UICol xs={24} lg={12}>
384
+ <UICard title="Recent Errors" extra={<UIBadge count={data.errors?.recent?.length ?? 0} />}>
385
+ {data.errors?.recent && data.errors.recent.length > 0 ? (
386
+ <UITimeline
387
+ items={data.errors.recent.map((error) => ({
388
+ color: getStatusCodeColor(error.statusCode),
389
+ children: (
390
+ <div>
391
+ <div style={{ marginBottom: 4 }}>
392
+ <UITag color={getStatusCodeColor(error.statusCode)}>{error.statusCode}</UITag>
393
+ {error.errorCode && <UITag>{error.errorCode}</UITag>}
394
+ <span style={{ fontSize: 12, color: '#8c8c8c', marginLeft: 8 }}>
395
+ {formatRelativeTime(error.timestamp)}
396
+ </span>
397
+ </div>
398
+ <div style={{ fontSize: 13 }}>{error.message}</div>
399
+ </div>
400
+ ),
401
+ }))}
402
+ />
403
+ ) : (
404
+ <UIAlert message="No recent errors" variant="success" showIcon />
405
+ )}
406
+ </UICard>
407
+ </UICol>
408
+ </UIRow>
409
+
410
+ {/* Redis Status & Plugin Mounts */}
411
+ <UIRow gutter={[16, 16]}>
412
+ <UICol xs={24} lg={12}>
413
+ <UICard title="Redis Status" extra={<UIIcon name="DatabaseOutlined" style={{ color: data.redis.lastStatus?.healthy ? '#52c41a' : '#ff4d4f' }} />}>
414
+ {data.redis.lastStatus ? (
415
+ <>
416
+ <UIRow gutter={16}>
417
+ <UICol span={12}>
418
+ <UIStatistic
419
+ title="Health"
420
+ value={data.redis.lastStatus.healthy ? 'Healthy' : 'Unhealthy'}
421
+ valueStyle={{ color: data.redis.lastStatus.healthy ? '#52c41a' : '#ff4d4f' }}
422
+ />
423
+ </UICol>
424
+ <UICol span={12}>
425
+ <UIStatistic
426
+ title="Role"
427
+ value={data.redis.lastStatus.role}
428
+ valueStyle={{ fontSize: 20 }}
429
+ />
430
+ </UICol>
431
+ </UIRow>
432
+ <div style={{ marginTop: 16 }}>
433
+ <p style={{ margin: 0, fontSize: 12, color: '#8c8c8c' }}>
434
+ State: <strong>{data.redis.lastStatus.state}</strong>
435
+ </p>
436
+ <p style={{ margin: 0, fontSize: 12, color: '#8c8c8c' }}>
437
+ Healthy transitions: {data.redis.healthyTransitions} | Unhealthy: {data.redis.unhealthyTransitions}
438
+ </p>
439
+ </div>
440
+ </>
441
+ ) : (
442
+ <UIAlert message="No Redis status data available" variant="info" showIcon />
443
+ )}
444
+ </UICard>
445
+ </UICol>
446
+
447
+ <UICol xs={24} lg={12}>
448
+ <UICard title="Plugin Mounts">
449
+ {data.pluginMounts ? (
450
+ <>
451
+ <UIRow gutter={16}>
452
+ <UICol span={8}>
453
+ <UIStatistic
454
+ title="Total"
455
+ value={data.pluginMounts.total}
456
+ valueStyle={{ fontSize: 20 }}
457
+ />
458
+ </UICol>
459
+ <UICol span={8}>
460
+ <UIStatistic
461
+ title="Succeeded"
462
+ value={data.pluginMounts.succeeded}
463
+ valueStyle={{ fontSize: 20, color: '#52c41a' }}
464
+ />
465
+ </UICol>
466
+ <UICol span={8}>
467
+ <UIStatistic
468
+ title="Failed"
469
+ value={data.pluginMounts.failed}
470
+ valueStyle={{ fontSize: 20, color: data.pluginMounts.failed > 0 ? '#ff4d4f' : '#52c41a' }}
471
+ />
472
+ </UICol>
473
+ </UIRow>
474
+ <div style={{ marginTop: 16, fontSize: 12, color: '#8c8c8c' }}>
475
+ Elapsed time: {formatNumber(data.pluginMounts.elapsedMs, 0)}ms
476
+ </div>
477
+ </>
478
+ ) : (
479
+ <UIAlert message="No plugin mount data available" variant="info" showIcon />
480
+ )}
481
+ </UICard>
482
+ </UICol>
483
+ </UIRow>
484
+ </UIPage>
485
+ );
486
+ }