@oh-my-pi/omp-stats 16.0.3 → 16.0.5

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 (107) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/build.ts +11 -0
  3. package/dist/client/index.css +1 -1
  4. package/dist/client/index.html +11 -0
  5. package/dist/client/index.js +108 -108
  6. package/dist/client/styles.css +1070 -631
  7. package/dist/types/client/api.d.ts +19 -10
  8. package/dist/types/client/app/AppLayout.d.ts +16 -0
  9. package/dist/types/client/app/NavRail.d.ts +7 -0
  10. package/dist/types/client/app/RangeControl.d.ts +7 -0
  11. package/dist/types/client/app/SyncButton.d.ts +14 -0
  12. package/dist/types/client/app/ThemeToggle.d.ts +1 -0
  13. package/dist/types/client/app/TopBar.d.ts +15 -0
  14. package/dist/types/client/app/routes.d.ts +12 -0
  15. package/dist/types/client/components/chart-shared.d.ts +26 -40
  16. package/dist/types/client/components/models-table-shared.d.ts +20 -40
  17. package/dist/types/client/data/charts.d.ts +1 -0
  18. package/dist/types/client/data/formatters.d.ts +7 -0
  19. package/dist/types/client/data/useHashRoute.d.ts +8 -0
  20. package/dist/types/client/data/useResource.d.ts +13 -0
  21. package/dist/types/client/data/view-models.d.ts +37 -0
  22. package/dist/types/client/index.d.ts +1 -0
  23. package/dist/types/client/routes/BehaviorRoute.d.ts +7 -0
  24. package/dist/types/client/routes/CostsRoute.d.ts +7 -0
  25. package/dist/types/client/routes/ErrorsRoute.d.ts +8 -0
  26. package/dist/types/client/routes/ModelsRoute.d.ts +7 -0
  27. package/dist/types/client/routes/OverviewRoute.d.ts +8 -0
  28. package/dist/types/client/routes/ProjectsRoute.d.ts +7 -0
  29. package/dist/types/client/routes/RequestsRoute.d.ts +8 -0
  30. package/dist/types/client/routes/index.d.ts +7 -0
  31. package/dist/types/client/ui/AsyncBoundary.d.ts +12 -0
  32. package/dist/types/client/ui/DataTable.d.ts +17 -0
  33. package/dist/types/client/ui/EmptyState.d.ts +7 -0
  34. package/dist/types/client/ui/ErrorState.d.ts +6 -0
  35. package/dist/types/client/ui/JsonBlock.d.ts +7 -0
  36. package/dist/types/client/ui/MetricCluster.d.ts +5 -0
  37. package/dist/types/client/ui/Panel.d.ts +7 -0
  38. package/dist/types/client/ui/RequestDrawer.d.ts +5 -0
  39. package/dist/types/client/ui/SegmentedControl.d.ts +12 -0
  40. package/dist/types/client/ui/Skeleton.d.ts +8 -0
  41. package/dist/types/client/ui/StatusPill.d.ts +7 -0
  42. package/dist/types/client/ui/index.d.ts +11 -0
  43. package/dist/types/client/useSystemTheme.d.ts +9 -0
  44. package/package.json +4 -4
  45. package/src/aggregator.ts +4 -3
  46. package/src/client/App.tsx +89 -207
  47. package/src/client/api.ts +55 -37
  48. package/src/client/app/AppLayout.tsx +93 -0
  49. package/src/client/app/NavRail.tsx +44 -0
  50. package/src/client/app/RangeControl.tsx +39 -0
  51. package/src/client/app/SyncButton.tsx +75 -0
  52. package/src/client/app/ThemeToggle.tsx +37 -0
  53. package/src/client/app/TopBar.tsx +73 -0
  54. package/src/client/app/routes.ts +50 -0
  55. package/src/client/components/chart-shared.tsx +28 -91
  56. package/src/client/components/models-table-shared.tsx +9 -29
  57. package/src/client/components/range-meta.ts +3 -2
  58. package/src/client/data/charts.ts +14 -0
  59. package/src/client/data/formatters.ts +38 -0
  60. package/src/client/data/useHashRoute.ts +85 -0
  61. package/src/client/data/useResource.ts +154 -0
  62. package/src/client/data/view-models.ts +178 -0
  63. package/src/client/index.tsx +4 -0
  64. package/src/client/routes/BehaviorRoute.tsx +623 -0
  65. package/src/client/routes/CostsRoute.tsx +234 -0
  66. package/src/client/routes/ErrorsRoute.tsx +118 -0
  67. package/src/client/routes/ModelsRoute.tsx +430 -0
  68. package/src/client/routes/OverviewRoute.tsx +332 -0
  69. package/src/client/routes/ProjectsRoute.tsx +163 -0
  70. package/src/client/routes/RequestsRoute.tsx +123 -0
  71. package/src/client/routes/index.ts +7 -0
  72. package/src/client/styles.css +1242 -225
  73. package/src/client/ui/AsyncBoundary.tsx +54 -0
  74. package/src/client/ui/DataTable.tsx +122 -0
  75. package/src/client/ui/EmptyState.tsx +16 -0
  76. package/src/client/ui/ErrorState.tsx +25 -0
  77. package/src/client/ui/JsonBlock.tsx +75 -0
  78. package/src/client/ui/MetricCluster.tsx +67 -0
  79. package/src/client/ui/Panel.tsx +24 -0
  80. package/src/client/ui/RequestDrawer.tsx +208 -0
  81. package/src/client/ui/SegmentedControl.tsx +36 -0
  82. package/src/client/ui/Skeleton.tsx +17 -0
  83. package/src/client/ui/StatusPill.tsx +15 -0
  84. package/src/client/ui/index.ts +11 -0
  85. package/src/client/useSystemTheme.ts +73 -17
  86. package/dist/types/client/components/BehaviorChart.d.ts +0 -6
  87. package/dist/types/client/components/BehaviorModelsTable.d.ts +0 -7
  88. package/dist/types/client/components/BehaviorSummary.d.ts +0 -7
  89. package/dist/types/client/components/ChartsContainer.d.ts +0 -7
  90. package/dist/types/client/components/CostChart.d.ts +0 -6
  91. package/dist/types/client/components/CostSummary.d.ts +0 -6
  92. package/dist/types/client/components/Header.d.ts +0 -12
  93. package/dist/types/client/components/ModelsTable.d.ts +0 -8
  94. package/dist/types/client/components/RequestDetail.d.ts +0 -6
  95. package/dist/types/client/components/RequestList.d.ts +0 -8
  96. package/dist/types/client/components/StatsGrid.d.ts +0 -6
  97. package/src/client/components/BehaviorChart.tsx +0 -189
  98. package/src/client/components/BehaviorModelsTable.tsx +0 -342
  99. package/src/client/components/BehaviorSummary.tsx +0 -95
  100. package/src/client/components/ChartsContainer.tsx +0 -221
  101. package/src/client/components/CostChart.tsx +0 -171
  102. package/src/client/components/CostSummary.tsx +0 -53
  103. package/src/client/components/Header.tsx +0 -72
  104. package/src/client/components/ModelsTable.tsx +0 -265
  105. package/src/client/components/RequestDetail.tsx +0 -172
  106. package/src/client/components/RequestList.tsx +0 -73
  107. package/src/client/components/StatsGrid.tsx +0 -135
@@ -0,0 +1,332 @@
1
+ import { format } from "date-fns";
2
+ import { useMemo } from "react";
3
+ import { Line } from "react-chartjs-2";
4
+ import { getOverviewStats, getRecentRequests } from "../api";
5
+ import { CHART_THEMES } from "../components/chart-shared";
6
+ import { formatCost, formatDurationMs, formatInteger, formatRelativeTime } from "../data/formatters";
7
+ import { useResource } from "../data/useResource";
8
+ import type { MessageStats, TimeRange } from "../types";
9
+ import { AsyncBoundary, DataTable, MetricCluster, Panel, Skeleton, StatusPill } from "../ui";
10
+ import { useSystemTheme } from "../useSystemTheme";
11
+
12
+ export interface OverviewRouteProps {
13
+ active: boolean;
14
+ range: TimeRange;
15
+ refreshTrigger: number;
16
+ onRequestClick: (id: number) => void;
17
+ }
18
+
19
+ export function OverviewRoute({ active, range, refreshTrigger, onRequestClick }: OverviewRouteProps) {
20
+ const {
21
+ data: overview,
22
+ error: overviewError,
23
+ loading: overviewLoading,
24
+ } = useResource(["overview", range, refreshTrigger], signal => getOverviewStats(range, signal), {
25
+ pollMs: 30000,
26
+ enabled: active,
27
+ });
28
+
29
+ const {
30
+ data: recentRequests,
31
+ error: requestsError,
32
+ loading: requestsLoading,
33
+ } = useResource(["recent-requests", refreshTrigger], signal => getRecentRequests(50, signal), {
34
+ pollMs: 30000,
35
+ enabled: active,
36
+ });
37
+
38
+ const theme = useSystemTheme();
39
+ const chartTheme = CHART_THEMES[theme];
40
+
41
+ const chartData = useMemo(() => {
42
+ if (!overview?.timeSeries) return { labels: [], datasets: [] };
43
+ const labels = overview.timeSeries.map(pt =>
44
+ format(new Date(pt.timestamp), range === "1h" || range === "24h" ? "HH:mm" : "MMM d"),
45
+ );
46
+ // Show point markers when the series is sparse (e.g. a quiet 1h window)
47
+ // so a 1-2 point line is still visible instead of an empty plot.
48
+ const pointRadius = overview.timeSeries.length <= 2 ? 3 : 0;
49
+ return {
50
+ labels,
51
+ datasets: [
52
+ {
53
+ label: "Requests",
54
+ data: overview.timeSeries.map(pt => pt.requests),
55
+ borderColor: "#5ad8e6",
56
+ backgroundColor: "rgba(90, 216, 230, 0.12)",
57
+ tension: 0.2,
58
+ borderWidth: 2,
59
+ pointRadius,
60
+ pointHoverRadius: 4,
61
+ fill: true,
62
+ },
63
+ {
64
+ label: "Errors",
65
+ data: overview.timeSeries.map(pt => pt.errors),
66
+ borderColor: "#ff6b7d",
67
+ backgroundColor: "rgba(255, 107, 125, 0.12)",
68
+ tension: 0.2,
69
+ borderWidth: 2,
70
+ pointRadius,
71
+ pointHoverRadius: 4,
72
+ fill: true,
73
+ },
74
+ ],
75
+ };
76
+ }, [overview?.timeSeries, range]);
77
+
78
+ const chartOptions = useMemo(() => {
79
+ return {
80
+ responsive: true,
81
+ maintainAspectRatio: false,
82
+ interaction: {
83
+ mode: "index" as const,
84
+ intersect: false,
85
+ },
86
+ plugins: {
87
+ legend: {
88
+ display: true,
89
+ position: "top" as const,
90
+ align: "end" as const,
91
+ labels: {
92
+ color: chartTheme.legendLabel,
93
+ boxWidth: 8,
94
+ usePointStyle: true,
95
+ font: { size: 11 },
96
+ },
97
+ },
98
+ tooltip: {
99
+ backgroundColor: chartTheme.tooltipBackground,
100
+ titleColor: chartTheme.tooltipTitle,
101
+ bodyColor: chartTheme.tooltipBody,
102
+ borderColor: chartTheme.tooltipBorder,
103
+ borderWidth: 1,
104
+ cornerRadius: 8,
105
+ padding: 10,
106
+ },
107
+ },
108
+ scales: {
109
+ x: {
110
+ grid: {
111
+ color: chartTheme.grid,
112
+ drawBorder: false,
113
+ },
114
+ ticks: {
115
+ color: chartTheme.tick,
116
+ font: { size: 10 },
117
+ },
118
+ },
119
+ y: {
120
+ grid: {
121
+ color: chartTheme.grid,
122
+ drawBorder: false,
123
+ },
124
+ ticks: {
125
+ color: chartTheme.tick,
126
+ font: { size: 10 },
127
+ },
128
+ min: 0,
129
+ },
130
+ },
131
+ };
132
+ }, [chartTheme]);
133
+
134
+ const columns = useMemo(
135
+ () => [
136
+ {
137
+ key: "model",
138
+ header: "Model",
139
+ render: (item: MessageStats) => (
140
+ <div>
141
+ <div className="stats-font-medium stats-text-primary">{item.model}</div>
142
+ <div className="stats-text-xs stats-text-muted">{item.provider}</div>
143
+ </div>
144
+ ),
145
+ },
146
+ {
147
+ key: "timestamp",
148
+ header: "Time",
149
+ render: (item: MessageStats) => formatRelativeTime(item.timestamp),
150
+ },
151
+ {
152
+ key: "tokens",
153
+ header: "Tokens",
154
+ numeric: true,
155
+ render: (item: MessageStats) => formatInteger(item.usage.totalTokens),
156
+ },
157
+ {
158
+ key: "cost",
159
+ header: "Cost",
160
+ numeric: true,
161
+ render: (item: MessageStats) => formatCost(item.usage.cost.total, 4),
162
+ },
163
+ {
164
+ key: "duration",
165
+ header: "Duration",
166
+ numeric: true,
167
+ render: (item: MessageStats) => formatDurationMs(item.duration),
168
+ },
169
+ {
170
+ key: "status",
171
+ header: "Status",
172
+ className: "stats-text-center",
173
+ render: (item: MessageStats) => (
174
+ <StatusPill variant={item.errorMessage ? "danger" : "success"}>
175
+ {item.errorMessage ? "Failed" : "Success"}
176
+ </StatusPill>
177
+ ),
178
+ },
179
+ ],
180
+ [],
181
+ );
182
+
183
+ const renderMobileCard = (item: MessageStats, onClick?: () => void) => (
184
+ <div className="stats-mobile-card" onClick={onClick}>
185
+ <div className="stats-mobile-card-header">
186
+ <div>
187
+ <div className="stats-font-semibold stats-text-primary">{item.model}</div>
188
+ <div className="stats-text-xs stats-text-muted">{item.provider}</div>
189
+ </div>
190
+ <StatusPill variant={item.errorMessage ? "danger" : "success"}>
191
+ {item.errorMessage ? "Failed" : "Success"}
192
+ </StatusPill>
193
+ </div>
194
+ <div className="stats-mobile-card-grid">
195
+ <div>
196
+ <div className="stats-mobile-card-label">Time</div>
197
+ <div className="stats-mobile-card-value">{formatRelativeTime(item.timestamp)}</div>
198
+ </div>
199
+ <div>
200
+ <div className="stats-mobile-card-label">Cost</div>
201
+ <div className="stats-mobile-card-value">{formatCost(item.usage.cost.total, 4)}</div>
202
+ </div>
203
+ <div>
204
+ <div className="stats-mobile-card-label">Tokens</div>
205
+ <div className="stats-mobile-card-value">{formatInteger(item.usage.totalTokens)}</div>
206
+ </div>
207
+ <div>
208
+ <div className="stats-mobile-card-label">Duration</div>
209
+ <div className="stats-mobile-card-value">{formatDurationMs(item.duration)}</div>
210
+ </div>
211
+ </div>
212
+ {item.errorMessage && <div className="stats-mobile-card-error truncate mt-2">{item.errorMessage}</div>}
213
+ </div>
214
+ );
215
+
216
+ const previewRequests = useMemo(() => {
217
+ if (!recentRequests) return [];
218
+ return recentRequests.slice(0, 10);
219
+ }, [recentRequests]);
220
+
221
+ return (
222
+ <div className="stats-route-container space-y-6">
223
+ <AsyncBoundary loading={overviewLoading} error={overviewError} data={overview}>
224
+ {overview && <MetricCluster stats={overview.overall} />}
225
+ </AsyncBoundary>
226
+
227
+ <div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
228
+ <div className="lg:col-span-2">
229
+ <Panel title="System Throughput" subtitle="Request volume and errors over time">
230
+ <AsyncBoundary loading={overviewLoading} error={overviewError} data={overview}>
231
+ <div className="h-[280px]">
232
+ {overview?.timeSeries && overview.timeSeries.length > 0 ? (
233
+ <Line data={chartData} options={chartOptions} />
234
+ ) : (
235
+ <div className="h-full flex items-center justify-center text-stats-muted text-sm">
236
+ No time-series data available
237
+ </div>
238
+ )}
239
+ </div>
240
+ </AsyncBoundary>
241
+ </Panel>
242
+ </div>
243
+
244
+ <div>
245
+ <Panel title="Operational Feed" subtitle="Real-time request log">
246
+ <AsyncBoundary
247
+ loading={requestsLoading}
248
+ error={requestsError}
249
+ data={recentRequests}
250
+ fallback={
251
+ <div className="space-y-4">
252
+ {Array.from({ length: 5 }).map((_, i) => (
253
+ <div key={i} className="flex items-center gap-3">
254
+ <Skeleton variant="circle" width={10} height={10} />
255
+ <div className="flex-1">
256
+ <Skeleton variant="text" width="60%" height={16} />
257
+ <Skeleton variant="text" width="40%" height={12} />
258
+ </div>
259
+ </div>
260
+ ))}
261
+ </div>
262
+ }
263
+ >
264
+ <div className="stats-feed-ledger overflow-y-auto max-h-[280px] pr-2">
265
+ {previewRequests.map(req => {
266
+ const isError = !!req.errorMessage;
267
+ return (
268
+ <div
269
+ key={req.id || `${req.sessionFile}-${req.entryId}`}
270
+ className="stats-feed-item flex items-start gap-3 p-2 rounded hover:bg-stats-surface-2 cursor-pointer transition-colors"
271
+ onClick={() => req.id && onRequestClick(req.id)}
272
+ >
273
+ <div
274
+ className={`w-2 h-2 mt-1.5 rounded-full flex-shrink-0 ${
275
+ isError ? "bg-stats-danger" : "bg-stats-success"
276
+ }`}
277
+ />
278
+ <div className="flex-1 min-w-0">
279
+ <div className="flex justify-between items-baseline gap-2">
280
+ <div className="stats-font-medium stats-text-primary text-sm truncate">
281
+ {req.model}
282
+ </div>
283
+ <div className="stats-text-xs stats-text-muted whitespace-nowrap">
284
+ {formatRelativeTime(req.timestamp)}
285
+ </div>
286
+ </div>
287
+ <div className="flex justify-between items-center text-xs stats-text-muted mt-0.5">
288
+ <div>{req.provider}</div>
289
+ <div>
290
+ {req.duration ? formatDurationMs(req.duration) : ""}{" "}
291
+ {req.usage?.cost?.total ? `· ${formatCost(req.usage.cost.total, 4)}` : ""}
292
+ </div>
293
+ </div>
294
+ {isError && (
295
+ <div className="text-xs text-stats-danger truncate mt-1">{req.errorMessage}</div>
296
+ )}
297
+ </div>
298
+ </div>
299
+ );
300
+ })}
301
+ {previewRequests.length === 0 && (
302
+ <div className="py-8 text-center stats-text-muted text-sm">No recent requests found</div>
303
+ )}
304
+ </div>
305
+ </AsyncBoundary>
306
+ </Panel>
307
+ </div>
308
+ </div>
309
+
310
+ <Panel
311
+ title="Recent Requests Preview"
312
+ subtitle="Latest transactions processed by the proxy"
313
+ actions={
314
+ <a href={`#/requests?range=${range}`} className="stats-button stats-button-secondary text-xs">
315
+ View All Requests
316
+ </a>
317
+ }
318
+ >
319
+ <AsyncBoundary loading={requestsLoading} error={requestsError} data={recentRequests}>
320
+ <DataTable
321
+ columns={columns}
322
+ data={previewRequests}
323
+ keyExtractor={item => item.id || `${item.sessionFile}-${item.entryId}`}
324
+ onRowClick={item => item.id && onRequestClick(item.id)}
325
+ renderMobileCard={renderMobileCard}
326
+ emptyText="No recent requests found"
327
+ />
328
+ </AsyncBoundary>
329
+ </Panel>
330
+ </div>
331
+ );
332
+ }
@@ -0,0 +1,163 @@
1
+ import { useMemo } from "react";
2
+ import { getFolderStats } from "../api";
3
+ import { formatCost, formatDurationMs, formatInteger, formatPercent } from "../data/formatters";
4
+ import { useResource } from "../data/useResource";
5
+ import { buildFolderRows, type FolderRowView } from "../data/view-models";
6
+ import type { TimeRange } from "../types";
7
+ import { AsyncBoundary, DataTable, Panel, StatusPill } from "../ui";
8
+
9
+ export interface ProjectsRouteProps {
10
+ active: boolean;
11
+ range: TimeRange;
12
+ refreshTrigger: number;
13
+ }
14
+
15
+ export function ProjectsRoute({ active, range, refreshTrigger }: ProjectsRouteProps) {
16
+ const {
17
+ data: foldersData,
18
+ error,
19
+ loading,
20
+ } = useResource(["projects", range, refreshTrigger], signal => getFolderStats(range, signal), {
21
+ pollMs: 30000,
22
+ enabled: active,
23
+ });
24
+
25
+ const folderRows = useMemo(() => {
26
+ if (!foldersData) return [];
27
+ return buildFolderRows(foldersData);
28
+ }, [foldersData]);
29
+
30
+ const columns = useMemo(
31
+ () => [
32
+ {
33
+ key: "folder",
34
+ header: "Project/Folder",
35
+ render: (item: FolderRowView) => (
36
+ <div
37
+ className="stats-font-medium stats-text-primary truncate max-w-[440px]"
38
+ title={item.folder || "(root)"}
39
+ >
40
+ {item.folder || "(root)"}
41
+ </div>
42
+ ),
43
+ },
44
+ {
45
+ key: "totalRequests",
46
+ header: "Requests",
47
+ numeric: true,
48
+ render: (item: FolderRowView) => (
49
+ <div className="stats-text-right">
50
+ <div className="font-mono">{formatInteger(item.totalRequests)}</div>
51
+ <div className="stats-progress-bar-track mt-1 ml-auto w-24 h-1">
52
+ <div
53
+ className="stats-progress-bar-fill"
54
+ data-variant="link"
55
+ style={{ width: `${item.requestsPercentage}%` }}
56
+ />
57
+ </div>
58
+ </div>
59
+ ),
60
+ },
61
+ {
62
+ key: "totalCost",
63
+ header: "Cost",
64
+ numeric: true,
65
+ render: (item: FolderRowView) => (
66
+ <div className="stats-text-right">
67
+ <div className="font-mono">{formatCost(item.totalCost)}</div>
68
+ <div className="stats-progress-bar-track mt-1 ml-auto w-24 h-1">
69
+ <div
70
+ className="stats-progress-bar-fill"
71
+ data-variant="success"
72
+ style={{ width: `${item.costPercentage}%` }}
73
+ />
74
+ </div>
75
+ </div>
76
+ ),
77
+ },
78
+ {
79
+ key: "totalTokens",
80
+ header: "Tokens",
81
+ numeric: true,
82
+ render: (item: FolderRowView) => (
83
+ <div className="font-mono">{formatInteger(item.totalInputTokens + item.totalOutputTokens)}</div>
84
+ ),
85
+ },
86
+ {
87
+ key: "cacheRate",
88
+ header: "Cache Rate",
89
+ numeric: true,
90
+ render: (item: FolderRowView) => (
91
+ <span className="stats-text-success font-medium">{formatPercent(item.cacheRate)}</span>
92
+ ),
93
+ },
94
+ {
95
+ key: "errorRate",
96
+ header: "Error Rate",
97
+ numeric: true,
98
+ render: (item: FolderRowView) => (
99
+ <StatusPill variant={item.errorRate > 0.1 ? "danger" : item.errorRate > 0 ? "warning" : "success"}>
100
+ {formatPercent(item.errorRate)}
101
+ </StatusPill>
102
+ ),
103
+ },
104
+ {
105
+ key: "avgDuration",
106
+ header: "Avg Duration",
107
+ numeric: true,
108
+ render: (item: FolderRowView) => formatDurationMs(item.avgDuration),
109
+ },
110
+ ],
111
+ [],
112
+ );
113
+
114
+ const renderMobileCard = (item: FolderRowView) => (
115
+ <div className="stats-mobile-card">
116
+ <div className="stats-mobile-card-header mb-2">
117
+ <div className="stats-font-semibold stats-text-primary">{item.folder || "(root)"}</div>
118
+ <StatusPill variant={item.errorRate > 0.1 ? "danger" : item.errorRate > 0 ? "warning" : "success"}>
119
+ {formatPercent(item.errorRate)} Err
120
+ </StatusPill>
121
+ </div>
122
+ <div className="stats-mobile-card-grid">
123
+ <div>
124
+ <div className="stats-mobile-card-label">Requests</div>
125
+ <div className="stats-mobile-card-value font-mono">{formatInteger(item.totalRequests)}</div>
126
+ </div>
127
+ <div>
128
+ <div className="stats-mobile-card-label">Cost</div>
129
+ <div className="stats-mobile-card-value font-mono">{formatCost(item.totalCost)}</div>
130
+ </div>
131
+ <div>
132
+ <div className="stats-mobile-card-label">Cache</div>
133
+ <div className="stats-mobile-card-value">{formatPercent(item.cacheRate)}</div>
134
+ </div>
135
+ <div>
136
+ <div className="stats-mobile-card-label">Duration</div>
137
+ <div className="stats-mobile-card-value">{formatDurationMs(item.avgDuration)}</div>
138
+ </div>
139
+ </div>
140
+ </div>
141
+ );
142
+
143
+ return (
144
+ <div className="stats-route-container">
145
+ <Panel title="Projects & Folders" subtitle="Aggregate proxy metrics grouped by folder path">
146
+ <AsyncBoundary
147
+ loading={loading}
148
+ error={error}
149
+ data={foldersData}
150
+ emptyText="No project folders recorded for this range."
151
+ >
152
+ <DataTable
153
+ columns={columns}
154
+ data={folderRows}
155
+ keyExtractor={item => item.folder}
156
+ renderMobileCard={renderMobileCard}
157
+ emptyText="No project folders recorded for this range."
158
+ />
159
+ </AsyncBoundary>
160
+ </Panel>
161
+ </div>
162
+ );
163
+ }
@@ -0,0 +1,123 @@
1
+ import { useMemo } from "react";
2
+ import { getRecentRequests } from "../api";
3
+ import { formatCost, formatDurationMs, formatInteger, formatRelativeTime } from "../data/formatters";
4
+ import { useResource } from "../data/useResource";
5
+ import type { MessageStats, TimeRange } from "../types";
6
+ import { AsyncBoundary, DataTable, Panel, StatusPill } from "../ui";
7
+
8
+ export interface RequestsRouteProps {
9
+ active: boolean;
10
+ range: TimeRange;
11
+ refreshTrigger: number;
12
+ onRequestClick: (id: number) => void;
13
+ }
14
+
15
+ export function RequestsRoute({ active, refreshTrigger, onRequestClick }: RequestsRouteProps) {
16
+ const {
17
+ data: recentRequests,
18
+ error,
19
+ loading,
20
+ } = useResource(["recent-requests-dense", refreshTrigger], signal => getRecentRequests(50, signal), {
21
+ pollMs: 30000,
22
+ enabled: active,
23
+ });
24
+
25
+ const columns = useMemo(
26
+ () => [
27
+ {
28
+ key: "model",
29
+ header: "Model",
30
+ render: (item: MessageStats) => (
31
+ <div>
32
+ <div className="stats-font-medium stats-text-primary">{item.model}</div>
33
+ <div className="stats-text-xs stats-text-muted">{item.provider}</div>
34
+ </div>
35
+ ),
36
+ },
37
+ {
38
+ key: "timestamp",
39
+ header: "Time",
40
+ render: (item: MessageStats) => formatRelativeTime(item.timestamp),
41
+ },
42
+ {
43
+ key: "tokens",
44
+ header: "Tokens",
45
+ numeric: true,
46
+ render: (item: MessageStats) => formatInteger(item.usage.totalTokens),
47
+ },
48
+ {
49
+ key: "cost",
50
+ header: "Cost",
51
+ numeric: true,
52
+ render: (item: MessageStats) => formatCost(item.usage.cost.total, 4),
53
+ },
54
+ {
55
+ key: "duration",
56
+ header: "Duration",
57
+ numeric: true,
58
+ render: (item: MessageStats) => formatDurationMs(item.duration),
59
+ },
60
+ {
61
+ key: "status",
62
+ header: "Status",
63
+ className: "stats-text-center",
64
+ render: (item: MessageStats) => (
65
+ <StatusPill variant={item.errorMessage ? "danger" : "success"}>
66
+ {item.errorMessage ? "Failed" : "Success"}
67
+ </StatusPill>
68
+ ),
69
+ },
70
+ ],
71
+ [],
72
+ );
73
+
74
+ const renderMobileCard = (item: MessageStats, onClick?: () => void) => (
75
+ <div className="stats-mobile-card" onClick={onClick}>
76
+ <div className="stats-mobile-card-header">
77
+ <div>
78
+ <div className="stats-font-semibold stats-text-primary">{item.model}</div>
79
+ <div className="stats-text-xs stats-text-muted">{item.provider}</div>
80
+ </div>
81
+ <StatusPill variant={item.errorMessage ? "danger" : "success"}>
82
+ {item.errorMessage ? "Failed" : "Success"}
83
+ </StatusPill>
84
+ </div>
85
+ <div className="stats-mobile-card-grid">
86
+ <div>
87
+ <div className="stats-mobile-card-label">Time</div>
88
+ <div className="stats-mobile-card-value">{formatRelativeTime(item.timestamp)}</div>
89
+ </div>
90
+ <div>
91
+ <div className="stats-mobile-card-label">Cost</div>
92
+ <div className="stats-mobile-card-value">{formatCost(item.usage.cost.total, 4)}</div>
93
+ </div>
94
+ <div>
95
+ <div className="stats-mobile-card-label">Tokens</div>
96
+ <div className="stats-mobile-card-value">{formatInteger(item.usage.totalTokens)}</div>
97
+ </div>
98
+ <div>
99
+ <div className="stats-mobile-card-label">Duration</div>
100
+ <div className="stats-mobile-card-value">{formatDurationMs(item.duration)}</div>
101
+ </div>
102
+ </div>
103
+ {item.errorMessage && <div className="stats-mobile-card-error truncate mt-2">{item.errorMessage}</div>}
104
+ </div>
105
+ );
106
+
107
+ return (
108
+ <div className="stats-route-container">
109
+ <Panel title="All Recent Requests" subtitle="Up to 50 most recent requests processed by OMP">
110
+ <AsyncBoundary loading={loading} error={error} data={recentRequests}>
111
+ <DataTable
112
+ columns={columns}
113
+ data={recentRequests || []}
114
+ keyExtractor={item => item.id || `${item.sessionFile}-${item.entryId}`}
115
+ onRowClick={item => item.id && onRequestClick(item.id)}
116
+ renderMobileCard={renderMobileCard}
117
+ emptyText="No recent requests found"
118
+ />
119
+ </AsyncBoundary>
120
+ </Panel>
121
+ </div>
122
+ );
123
+ }
@@ -0,0 +1,7 @@
1
+ export * from "./BehaviorRoute";
2
+ export * from "./CostsRoute";
3
+ export * from "./ErrorsRoute";
4
+ export * from "./ModelsRoute";
5
+ export * from "./OverviewRoute";
6
+ export * from "./ProjectsRoute";
7
+ export * from "./RequestsRoute";