@oh-my-pi/omp-stats 18.3.4 → 18.4.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 (192) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/README.md +13 -11
  3. package/build.ts +5 -49
  4. package/dist/client/index.css +1 -1
  5. package/dist/client/index.html +2 -2
  6. package/dist/client/index.js +105 -126
  7. package/dist/types/aggregator.d.ts +25 -22
  8. package/dist/types/client/api.d.ts +11 -7
  9. package/dist/types/client/app/LiveChip.d.ts +2 -0
  10. package/dist/types/client/app/Shell.d.ts +16 -0
  11. package/dist/types/client/app/ThemeToggle.d.ts +1 -0
  12. package/dist/types/client/app/nav.d.ts +18 -0
  13. package/dist/types/client/charts/BarList.d.ts +17 -0
  14. package/dist/types/client/charts/Chart.d.ts +49 -0
  15. package/dist/types/client/charts/Legend.d.ts +16 -0
  16. package/dist/types/client/charts/ShareBar.d.ts +11 -0
  17. package/dist/types/client/charts/Sparkline.d.ts +11 -0
  18. package/dist/types/client/charts/TimeChart.d.ts +9 -0
  19. package/dist/types/client/charts/index.d.ts +7 -0
  20. package/dist/types/client/charts/types.d.ts +25 -0
  21. package/dist/types/client/charts/useWidth.d.ts +3 -0
  22. package/dist/types/client/data/colors.d.ts +25 -0
  23. package/dist/types/client/data/formatters.d.ts +9 -1
  24. package/dist/types/client/data/live.d.ts +29 -0
  25. package/dist/types/client/data/query.d.ts +35 -0
  26. package/dist/types/client/data/range.d.ts +31 -0
  27. package/dist/types/client/data/series.d.ts +28 -0
  28. package/dist/types/client/data/useHashRoute.d.ts +18 -4
  29. package/dist/types/client/data/view-models.d.ts +111 -28
  30. package/dist/types/client/index.d.ts +0 -1
  31. package/dist/types/client/routes/CostsRoute.d.ts +2 -2
  32. package/dist/types/client/routes/ErrorsRoute.d.ts +2 -2
  33. package/dist/types/client/routes/FrustrationRoute.d.ts +7 -0
  34. package/dist/types/client/routes/GainRoute.d.ts +1 -2
  35. package/dist/types/client/routes/ModelsRoute.d.ts +2 -2
  36. package/dist/types/client/routes/OverviewRoute.d.ts +1 -2
  37. package/dist/types/client/routes/ProjectsRoute.d.ts +2 -2
  38. package/dist/types/client/routes/ProvidersRoute.d.ts +2 -2
  39. package/dist/types/client/routes/RequestsRoute.d.ts +1 -2
  40. package/dist/types/client/routes/ToolsRoute.d.ts +2 -2
  41. package/dist/types/client/routes/TracesRoute.d.ts +2 -2
  42. package/dist/types/client/routes/index.d.ts +1 -1
  43. package/dist/types/client/traces/AggregatesPanel.d.ts +4 -3
  44. package/dist/types/client/traces/SummaryStrip.d.ts +2 -2
  45. package/dist/types/client/traces/trace-colors.d.ts +18 -12
  46. package/dist/types/client/ui/Badge.d.ts +19 -0
  47. package/dist/types/client/ui/Card.d.ts +25 -0
  48. package/dist/types/client/ui/Drawer.d.ts +25 -0
  49. package/dist/types/client/ui/JsonBlock.d.ts +2 -2
  50. package/dist/types/client/ui/Modal.d.ts +23 -0
  51. package/dist/types/client/ui/RequestDrawer.d.ts +10 -1
  52. package/dist/types/client/ui/SearchInput.d.ts +8 -0
  53. package/dist/types/client/ui/Segmented.d.ts +15 -0
  54. package/dist/types/client/ui/Stat.d.ts +36 -0
  55. package/dist/types/client/ui/States.d.ts +41 -0
  56. package/dist/types/client/ui/Table.d.ts +48 -0
  57. package/dist/types/client/ui/index.d.ts +9 -10
  58. package/dist/types/db.d.ts +90 -106
  59. package/dist/types/frustration.d.ts +70 -0
  60. package/dist/types/index.d.ts +3 -1
  61. package/dist/types/live.d.ts +30 -0
  62. package/dist/types/rollup.d.ts +115 -0
  63. package/dist/types/server.d.ts +6 -1
  64. package/dist/types/shared-types.d.ts +116 -56
  65. package/dist/types/types.d.ts +4 -0
  66. package/dist/types/user-metrics.d.ts +8 -0
  67. package/package.json +7 -13
  68. package/src/aggregator.ts +276 -211
  69. package/src/client/App.tsx +38 -80
  70. package/src/client/api.ts +65 -7
  71. package/src/client/app/LiveChip.tsx +97 -0
  72. package/src/client/app/Shell.tsx +181 -0
  73. package/src/client/app/ThemeToggle.tsx +5 -2
  74. package/src/client/app/nav.ts +78 -0
  75. package/src/client/charts/BarList.tsx +48 -0
  76. package/src/client/charts/Chart.tsx +388 -0
  77. package/src/client/charts/Legend.tsx +61 -0
  78. package/src/client/charts/ShareBar.tsx +26 -0
  79. package/src/client/charts/Sparkline.tsx +38 -0
  80. package/src/client/charts/TimeChart.tsx +22 -0
  81. package/src/client/charts/index.ts +7 -0
  82. package/src/client/charts/types.ts +27 -0
  83. package/src/client/charts/useWidth.ts +19 -0
  84. package/src/client/data/colors.ts +45 -0
  85. package/src/client/data/formatters.ts +30 -9
  86. package/src/client/data/live.tsx +85 -0
  87. package/src/client/data/query.ts +166 -0
  88. package/src/client/data/range.ts +82 -0
  89. package/src/client/data/series.ts +85 -0
  90. package/src/client/data/useHashRoute.ts +41 -83
  91. package/src/client/data/view-models.ts +347 -140
  92. package/src/client/index.tsx +3 -6
  93. package/src/client/routes/CostsRoute.tsx +394 -222
  94. package/src/client/routes/ErrorsRoute.tsx +401 -98
  95. package/src/client/routes/FrustrationRoute.tsx +924 -0
  96. package/src/client/routes/GainRoute.tsx +209 -206
  97. package/src/client/routes/ModelsRoute.tsx +540 -431
  98. package/src/client/routes/OverviewRoute.tsx +283 -311
  99. package/src/client/routes/ProjectsRoute.tsx +298 -156
  100. package/src/client/routes/ProvidersRoute.tsx +948 -416
  101. package/src/client/routes/RequestsRoute.tsx +295 -107
  102. package/src/client/routes/ToolsRoute.tsx +495 -398
  103. package/src/client/routes/TracesRoute.tsx +136 -117
  104. package/src/client/routes/costs.css +24 -0
  105. package/src/client/routes/errors.css +60 -0
  106. package/src/client/routes/frustration.css +123 -0
  107. package/src/client/routes/index.ts +1 -1
  108. package/src/client/routes/models.css +43 -0
  109. package/src/client/routes/projects.css +6 -0
  110. package/src/client/routes/providers.css +28 -0
  111. package/src/client/routes/tools.css +14 -0
  112. package/src/client/styles.css +1396 -1299
  113. package/src/client/traces/AggregatesPanel.tsx +55 -74
  114. package/src/client/traces/Minimap.tsx +6 -15
  115. package/src/client/traces/SpanDrawer.tsx +128 -194
  116. package/src/client/traces/SummaryStrip.tsx +21 -24
  117. package/src/client/traces/TimelineCanvas.tsx +27 -44
  118. package/src/client/traces/TraceView.tsx +175 -130
  119. package/src/client/traces/TranscriptList.tsx +13 -41
  120. package/src/client/traces/trace-colors.ts +60 -53
  121. package/src/client/traces/traces.css +279 -0
  122. package/src/client/ui/Badge.tsx +29 -0
  123. package/src/client/ui/Card.tsx +61 -0
  124. package/src/client/ui/Drawer.tsx +93 -0
  125. package/src/client/ui/JsonBlock.tsx +26 -44
  126. package/src/client/ui/Modal.tsx +106 -0
  127. package/src/client/ui/RequestDrawer.tsx +163 -188
  128. package/src/client/ui/SearchInput.tsx +25 -0
  129. package/src/client/ui/Segmented.tsx +61 -0
  130. package/src/client/ui/Stat.tsx +104 -0
  131. package/src/client/ui/States.tsx +81 -0
  132. package/src/client/ui/Table.tsx +229 -0
  133. package/src/client/ui/index.ts +9 -10
  134. package/src/client/ui/modal.css +54 -0
  135. package/src/client/ui/request-drawer.css +40 -0
  136. package/src/db.ts +379 -983
  137. package/src/frustration.ts +473 -0
  138. package/src/index.ts +22 -22
  139. package/src/live.ts +259 -0
  140. package/src/parser.ts +55 -66
  141. package/src/rollup.ts +951 -0
  142. package/src/server.ts +123 -40
  143. package/src/shared-types.ts +120 -56
  144. package/src/trace.ts +14 -7
  145. package/src/types.ts +4 -0
  146. package/src/user-metrics.ts +13 -0
  147. package/dist/client/styles.css +0 -1976
  148. package/dist/types/client/app/AppLayout.d.ts +0 -16
  149. package/dist/types/client/app/NavRail.d.ts +0 -7
  150. package/dist/types/client/app/RangeControl.d.ts +0 -7
  151. package/dist/types/client/app/SyncButton.d.ts +0 -14
  152. package/dist/types/client/app/TopBar.d.ts +0 -15
  153. package/dist/types/client/app/routes.d.ts +0 -12
  154. package/dist/types/client/components/AgentTokenShare.d.ts +0 -5
  155. package/dist/types/client/components/chart-shared.d.ts +0 -178
  156. package/dist/types/client/components/models-table-shared.d.ts +0 -175
  157. package/dist/types/client/components/range-meta.d.ts +0 -21
  158. package/dist/types/client/data/charts.d.ts +0 -1
  159. package/dist/types/client/data/useResource.d.ts +0 -13
  160. package/dist/types/client/routes/BehaviorRoute.d.ts +0 -7
  161. package/dist/types/client/ui/AsyncBoundary.d.ts +0 -12
  162. package/dist/types/client/ui/DataTable.d.ts +0 -17
  163. package/dist/types/client/ui/EmptyState.d.ts +0 -7
  164. package/dist/types/client/ui/ErrorState.d.ts +0 -6
  165. package/dist/types/client/ui/MetricCluster.d.ts +0 -5
  166. package/dist/types/client/ui/Panel.d.ts +0 -7
  167. package/dist/types/client/ui/SegmentedControl.d.ts +0 -12
  168. package/dist/types/client/ui/Skeleton.d.ts +0 -8
  169. package/dist/types/client/ui/StatusPill.d.ts +0 -7
  170. package/src/client/app/AppLayout.tsx +0 -93
  171. package/src/client/app/NavRail.tsx +0 -44
  172. package/src/client/app/RangeControl.tsx +0 -39
  173. package/src/client/app/SyncButton.tsx +0 -75
  174. package/src/client/app/TopBar.tsx +0 -73
  175. package/src/client/app/routes.ts +0 -93
  176. package/src/client/components/AgentTokenShare.tsx +0 -68
  177. package/src/client/components/chart-shared.tsx +0 -273
  178. package/src/client/components/models-table-shared.tsx +0 -255
  179. package/src/client/components/range-meta.ts +0 -73
  180. package/src/client/data/charts.ts +0 -14
  181. package/src/client/data/useResource.ts +0 -154
  182. package/src/client/routes/BehaviorRoute.tsx +0 -623
  183. package/src/client/ui/AsyncBoundary.tsx +0 -54
  184. package/src/client/ui/DataTable.tsx +0 -122
  185. package/src/client/ui/EmptyState.tsx +0 -16
  186. package/src/client/ui/ErrorState.tsx +0 -25
  187. package/src/client/ui/MetricCluster.tsx +0 -92
  188. package/src/client/ui/Panel.tsx +0 -24
  189. package/src/client/ui/SegmentedControl.tsx +0 -36
  190. package/src/client/ui/Skeleton.tsx +0 -17
  191. package/src/client/ui/StatusPill.tsx +0 -15
  192. package/tailwind.config.js +0 -40
@@ -1,342 +1,314 @@
1
- import { format } from "@oh-my-pi/pi-utils/dates";
2
- import { useMemo } from "react";
3
- import { Line } from "react-chartjs-2";
1
+ import { ArrowRight } from "lucide-react";
2
+ import { useMemo, useState } from "react";
4
3
  import { getOverviewStats, getRecentRequests } from "../api";
5
- import { AgentTokenShare } from "../components/AgentTokenShare";
6
- import { CHART_THEMES } from "../components/chart-shared";
7
- import { formatDurationMs, formatInteger, formatMessageCost, formatRelativeTime } from "../data/formatters";
8
- import { useResource } from "../data/useResource";
9
- import type { MessageStats, TimeRange } from "../types";
10
- import { AsyncBoundary, DataTable, MetricCluster, Panel, Skeleton, StatusPill } from "../ui";
11
- import { useSystemTheme } from "../useSystemTheme";
4
+ import { Legend, ShareBar, TimeChart } from "../charts";
5
+ import {
6
+ formatCompact,
7
+ formatDurationMs,
8
+ formatEstimatedCost,
9
+ formatInteger,
10
+ formatMessageCost,
11
+ formatPercent,
12
+ formatRelativeTime,
13
+ formatTokensPerSecond,
14
+ } from "../data/formatters";
15
+ import { useQuery } from "../data/query";
16
+ import { bucketAxis, rangeMeta } from "../data/range";
17
+ import { densify } from "../data/series";
18
+ import { buildAgentTokenShare, sumConversationTokens } from "../data/view-models";
19
+ import type { AgentType, MessageStats, TimeRange } from "../types";
20
+ import {
21
+ Badge,
22
+ Card,
23
+ ChartSkeleton,
24
+ Dot,
25
+ LabelCell,
26
+ PageHeader,
27
+ QueryView,
28
+ Segmented,
29
+ Stat,
30
+ StatGrid,
31
+ Table,
32
+ TableSkeleton,
33
+ } from "../ui";
12
34
 
13
35
  export interface OverviewRouteProps {
14
36
  active: boolean;
15
37
  range: TimeRange;
16
- refreshTrigger: number;
17
38
  onRequestClick: (id: number) => void;
18
39
  }
19
40
 
20
- export function OverviewRoute({ active, range, refreshTrigger, onRequestClick }: OverviewRouteProps) {
21
- const {
22
- data: overview,
23
- error: overviewError,
24
- loading: overviewLoading,
25
- } = useResource(["overview", range, refreshTrigger], signal => getOverviewStats(range, signal), {
26
- pollMs: 30000,
27
- enabled: active,
28
- });
41
+ type ActivityMetric = "requests" | "tokens" | "cost";
29
42
 
30
- const {
31
- data: recentRequests,
32
- error: requestsError,
33
- loading: requestsLoading,
34
- } = useResource(["recent-requests", refreshTrigger], signal => getRecentRequests(50, signal), {
35
- pollMs: 30000,
36
- enabled: active,
37
- });
43
+ const ACTIVITY_OPTIONS = [
44
+ { value: "requests" as const, label: "Requests" },
45
+ { value: "tokens" as const, label: "Tokens" },
46
+ { value: "cost" as const, label: "Cost" },
47
+ ];
38
48
 
39
- const theme = useSystemTheme();
40
- const chartTheme = CHART_THEMES[theme];
49
+ const AGENT_LABEL: Record<AgentType, string> = { main: "Main agent", subagent: "Subagents", advisor: "Advisor" };
50
+ const AGENT_COLOR: Record<AgentType, string> = {
51
+ main: "var(--chart-primary)",
52
+ subagent: "var(--chart-secondary)",
53
+ advisor: "#9d7bff",
54
+ };
41
55
 
42
- const chartData = useMemo(() => {
43
- if (!overview?.timeSeries) return { labels: [], datasets: [] };
44
- const labels = overview.timeSeries.map(pt =>
45
- format(new Date(pt.timestamp), range === "1h" || range === "24h" ? "HH:mm" : "MMM d"),
46
- );
47
- // Show point markers when the series is sparse (e.g. a quiet 1h window)
48
- // so a 1-2 point line is still visible instead of an empty plot.
49
- const pointRadius = overview.timeSeries.length <= 2 ? 3 : 0;
50
- return {
51
- labels,
52
- datasets: [
53
- {
54
- label: "Requests",
55
- data: overview.timeSeries.map(pt => pt.requests),
56
- borderColor: "#5ad8e6",
57
- backgroundColor: "rgba(90, 216, 230, 0.12)",
58
- tension: 0.2,
59
- borderWidth: 2,
60
- pointRadius,
61
- pointHoverRadius: 4,
62
- fill: true,
63
- },
64
- {
65
- label: "Errors",
66
- data: overview.timeSeries.map(pt => pt.errors),
67
- borderColor: "#ff6b7d",
68
- backgroundColor: "rgba(255, 107, 125, 0.12)",
69
- tension: 0.2,
70
- borderWidth: 2,
71
- pointRadius,
72
- pointHoverRadius: 4,
73
- fill: true,
74
- },
75
- ],
76
- };
77
- }, [overview?.timeSeries, range]);
56
+ const TOKEN_MIX = [
57
+ { key: "input", label: "Uncached input", color: "#5b8cff" },
58
+ { key: "cacheRead", label: "Cache read", color: "var(--chart-primary)" },
59
+ { key: "cacheWrite", label: "Cache write", color: "#f5b54a" },
60
+ { key: "output", label: "Output", color: "var(--chart-secondary)" },
61
+ ] as const;
78
62
 
79
- const chartOptions = useMemo(() => {
63
+ export function OverviewRoute({ active, range, onRequestClick }: OverviewRouteProps) {
64
+ const overview = useQuery(["overview", range], () => getOverviewStats(range), { enabled: active });
65
+ const recent = useQuery(["recent-requests"], () => getRecentRequests(12), { enabled: active });
66
+ const [metric, setMetric] = useState<ActivityMetric>("requests");
67
+ const meta = rangeMeta(range);
68
+
69
+ const series = useMemo(() => {
70
+ const points = overview.data?.timeSeries ?? [];
71
+ const buckets = bucketAxis(
72
+ range,
73
+ points.map(p => p.timestamp),
74
+ );
80
75
  return {
81
- responsive: true,
82
- maintainAspectRatio: false,
83
- interaction: {
84
- mode: "index" as const,
85
- intersect: false,
86
- },
87
- plugins: {
88
- legend: {
89
- display: true,
90
- position: "top" as const,
91
- align: "end" as const,
92
- labels: {
93
- color: chartTheme.legendLabel,
94
- boxWidth: 8,
95
- usePointStyle: true,
96
- font: { size: 11 },
97
- },
98
- },
99
- tooltip: {
100
- backgroundColor: chartTheme.tooltipBackground,
101
- titleColor: chartTheme.tooltipTitle,
102
- bodyColor: chartTheme.tooltipBody,
103
- borderColor: chartTheme.tooltipBorder,
104
- borderWidth: 1,
105
- cornerRadius: 8,
106
- padding: 10,
107
- },
108
- },
109
- scales: {
110
- x: {
111
- grid: {
112
- color: chartTheme.grid,
113
- drawBorder: false,
114
- },
115
- ticks: {
116
- color: chartTheme.tick,
117
- font: { size: 10 },
118
- },
119
- },
120
- y: {
121
- grid: {
122
- color: chartTheme.grid,
123
- drawBorder: false,
124
- },
125
- ticks: {
126
- color: chartTheme.tick,
127
- font: { size: 10 },
128
- },
129
- min: 0,
130
- },
131
- },
76
+ buckets,
77
+ requests: densify(points, buckets, p => p.requests - p.errors),
78
+ errors: densify(points, buckets, p => p.errors),
79
+ tokens: densify(points, buckets, p => p.tokens),
80
+ cost: densify(points, buckets, p => p.cost),
81
+ all: densify(points, buckets, p => p.requests),
132
82
  };
133
- }, [chartTheme]);
134
-
135
- const columns = useMemo(
136
- () => [
137
- {
138
- key: "model",
139
- header: "Model",
140
- render: (item: MessageStats) => (
141
- <div>
142
- <div className="stats-font-medium stats-text-primary">{item.model}</div>
143
- <div className="stats-text-xs stats-text-muted">{item.provider}</div>
144
- </div>
145
- ),
146
- },
147
- {
148
- key: "timestamp",
149
- header: "Time",
150
- render: (item: MessageStats) => formatRelativeTime(item.timestamp),
151
- },
152
- {
153
- key: "tokens",
154
- header: "Tokens",
155
- numeric: true,
156
- render: (item: MessageStats) => formatInteger(item.usage.totalTokens),
157
- },
158
- {
159
- key: "cost",
160
- header: "API-equivalent estimate",
161
- numeric: true,
162
- render: (item: MessageStats) => formatMessageCost(item, 4),
163
- },
164
- {
165
- key: "duration",
166
- header: "Duration",
167
- numeric: true,
168
- render: (item: MessageStats) => formatDurationMs(item.duration),
169
- },
170
- {
171
- key: "status",
172
- header: "Status",
173
- className: "stats-text-center",
174
- render: (item: MessageStats) => (
175
- <StatusPill variant={item.errorMessage ? "danger" : "success"}>
176
- {item.errorMessage ? "Failed" : "Success"}
177
- </StatusPill>
178
- ),
179
- },
180
- ],
181
- [],
182
- );
183
-
184
- const renderMobileCard = (item: MessageStats, onClick?: () => void) => (
185
- <div className="stats-mobile-card" onClick={onClick}>
186
- <div className="stats-mobile-card-header">
187
- <div>
188
- <div className="stats-font-semibold stats-text-primary">{item.model}</div>
189
- <div className="stats-text-xs stats-text-muted">{item.provider}</div>
190
- </div>
191
- <StatusPill variant={item.errorMessage ? "danger" : "success"}>
192
- {item.errorMessage ? "Failed" : "Success"}
193
- </StatusPill>
194
- </div>
195
- <div className="stats-mobile-card-grid">
196
- <div>
197
- <div className="stats-mobile-card-label">Time</div>
198
- <div className="stats-mobile-card-value">{formatRelativeTime(item.timestamp)}</div>
199
- </div>
200
- <div>
201
- <div className="stats-mobile-card-label">API-equivalent estimate</div>
202
- <div className="stats-mobile-card-value">{formatMessageCost(item, 4)}</div>
203
- </div>
204
- <div>
205
- <div className="stats-mobile-card-label">Tokens</div>
206
- <div className="stats-mobile-card-value">{formatInteger(item.usage.totalTokens)}</div>
207
- </div>
208
- <div>
209
- <div className="stats-mobile-card-label">Duration</div>
210
- <div className="stats-mobile-card-value">{formatDurationMs(item.duration)}</div>
211
- </div>
212
- </div>
213
- {item.errorMessage && <div className="stats-mobile-card-error truncate mt-2">{item.errorMessage}</div>}
214
- </div>
215
- );
83
+ }, [overview.data, range]);
216
84
 
217
- const previewRequests = useMemo(() => {
218
- if (!recentRequests) return [];
219
- return recentRequests.slice(0, 10);
220
- }, [recentRequests]);
85
+ const chartSeries =
86
+ metric === "requests"
87
+ ? [
88
+ { key: "ok", label: "Succeeded", color: "var(--chart-primary)", values: series.requests },
89
+ { key: "err", label: "Failed", color: "var(--bad)", values: series.errors },
90
+ ]
91
+ : metric === "tokens"
92
+ ? [{ key: "tokens", label: "Tokens", color: "var(--chart-primary)", values: series.tokens }]
93
+ : [{ key: "cost", label: "API-equivalent", color: "var(--chart-secondary)", values: series.cost }];
221
94
 
222
95
  return (
223
- <div className="stats-route-container space-y-6">
224
- <AsyncBoundary loading={overviewLoading} error={overviewError} data={overview}>
225
- {overview && <MetricCluster stats={overview.overall} />}
226
- </AsyncBoundary>
96
+ <div className="page">
97
+ <PageHeader title="Overview" description={`Everything omp did across your sessions in ${meta.windowLabel}.`} />
227
98
 
228
- <Panel
229
- title="Conversation Tokens by Agent"
230
- subtitle="Uncached input + cache reads + cache writes + output, grouped by agent type"
231
- >
232
- <AsyncBoundary loading={overviewLoading} error={overviewError} data={overview}>
233
- {overview && <AgentTokenShare stats={overview.byAgentType} />}
234
- </AsyncBoundary>
235
- </Panel>
99
+ <QueryView query={overview} skeleton={<ChartSkeleton height={112} />}>
100
+ {({ overall }) => (
101
+ <div data-stale={overview.stale} className="stack" style={{ gap: 16 }}>
102
+ <StatGrid min={190}>
103
+ <Stat
104
+ label="API-equivalent cost"
105
+ title="What this usage would cost at public API rates"
106
+ value={formatEstimatedCost(overall.totalCost, overall.unpricedRequests)}
107
+ hint={
108
+ overall.unpricedRequests > 0
109
+ ? `${formatInteger(overall.unpricedRequests)} unpriced`
110
+ : undefined
111
+ }
112
+ spark={series.cost}
113
+ sparkColor="var(--chart-secondary)"
114
+ />
115
+ <Stat
116
+ label="Requests"
117
+ value={formatInteger(overall.totalRequests)}
118
+ hint={`${formatInteger(overall.failedRequests)} failed`}
119
+ spark={series.all}
120
+ />
121
+ <Stat
122
+ label="Conversation tokens"
123
+ title="Uncached input + cache reads + cache writes + output"
124
+ value={formatCompact(sumConversationTokens(overall))}
125
+ hint={`${formatCompact(overall.totalOutputTokens)} output`}
126
+ spark={series.tokens}
127
+ />
128
+ <Stat
129
+ label="Cache rate"
130
+ title="Cache reads ÷ (uncached input + cache reads)"
131
+ value={formatPercent(overall.cacheRate)}
132
+ hint={`${formatPercent(overall.cacheSavings)} saved`}
133
+ />
134
+ <Stat
135
+ label="Error rate"
136
+ value={formatPercent(overall.errorRate)}
137
+ hint={`${formatInteger(overall.successfulRequests)} succeeded`}
138
+ spark={series.errors}
139
+ sparkColor="var(--bad)"
140
+ />
141
+ </StatGrid>
142
+ <StatGrid min={140}>
143
+ <Stat size="sm" label="Uncached input" value={formatCompact(overall.totalInputTokens)} />
144
+ <Stat size="sm" label="Cache read" value={formatCompact(overall.totalCacheReadTokens)} />
145
+ <Stat size="sm" label="Cache write" value={formatCompact(overall.totalCacheWriteTokens)} />
146
+ <Stat size="sm" label="Output" value={formatCompact(overall.totalOutputTokens)} />
147
+ <Stat
148
+ size="sm"
149
+ label="Premium requests"
150
+ value={formatInteger(Math.round(overall.totalPremiumRequests * 100) / 100)}
151
+ />
152
+ <Stat size="sm" label="Tokens/s" value={formatTokensPerSecond(overall.avgTokensPerSecond)} />
153
+ <Stat size="sm" label="Avg latency" value={formatDurationMs(overall.avgDuration)} />
154
+ <Stat size="sm" label="Avg TTFT" value={formatDurationMs(overall.avgTtft)} />
155
+ </StatGrid>
156
+ </div>
157
+ )}
158
+ </QueryView>
236
159
 
237
- <div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
238
- <div className="lg:col-span-2">
239
- <Panel title="System Throughput" subtitle="Request volume and errors over time">
240
- <AsyncBoundary loading={overviewLoading} error={overviewError} data={overview}>
241
- <div className="h-[280px]">
242
- {overview?.timeSeries && overview.timeSeries.length > 0 ? (
243
- <Line data={chartData} options={chartOptions} />
244
- ) : (
245
- <div className="h-full flex items-center justify-center text-stats-muted text-sm">
246
- No time-series data available
247
- </div>
248
- )}
249
- </div>
250
- </AsyncBoundary>
251
- </Panel>
252
- </div>
160
+ <div className="grid grid-main-side">
161
+ <Card
162
+ index={1}
163
+ title="Activity"
164
+ description={`Per ${meta.bucketMs < 3_600_000 ? "5 minutes" : meta.bucketMs < 86_400_000 ? "hour" : "day"}`}
165
+ actions={<Segmented size="sm" options={ACTIVITY_OPTIONS} value={metric} onChange={setMetric} />}
166
+ stale={overview.stale}
167
+ >
168
+ <QueryView query={overview} skeleton={<ChartSkeleton height={260} />}>
169
+ {() => (
170
+ <TimeChart
171
+ buckets={series.buckets}
172
+ bucketMs={meta.bucketMs}
173
+ series={chartSeries}
174
+ height={260}
175
+ format={metric === "cost" ? v => formatEstimatedCost(v, 0) : formatCompact}
176
+ />
177
+ )}
178
+ </QueryView>
179
+ </Card>
253
180
 
254
- <div>
255
- <Panel title="Operational Feed" subtitle="Real-time request log">
256
- <AsyncBoundary
257
- loading={requestsLoading}
258
- error={requestsError}
259
- data={recentRequests}
260
- fallback={
261
- <div className="space-y-4">
262
- {Array.from({ length: 5 }).map((_, i) => (
263
- <div key={i} className="flex items-center gap-3">
264
- <Skeleton variant="circle" width={10} height={10} />
265
- <div className="flex-1">
266
- <Skeleton variant="text" width="60%" height={16} />
267
- <Skeleton variant="text" width="40%" height={12} />
268
- </div>
181
+ <Card index={2} title="Token mix" description="Where conversation tokens went" stale={overview.stale}>
182
+ <QueryView query={overview} skeleton={<ChartSkeleton height={260} />}>
183
+ {({ overall, byAgentType }) => {
184
+ const mix = {
185
+ input: overall.totalInputTokens,
186
+ cacheRead: overall.totalCacheReadTokens,
187
+ cacheWrite: overall.totalCacheWriteTokens,
188
+ output: overall.totalOutputTokens,
189
+ };
190
+ const total = sumConversationTokens(overall);
191
+ const agents = buildAgentTokenShare(byAgentType);
192
+ return (
193
+ <div className="stack" style={{ gap: 18 }}>
194
+ <div className="stack" style={{ gap: 10 }}>
195
+ <ShareBar
196
+ segments={TOKEN_MIX.map(t => ({
197
+ key: t.key,
198
+ label: t.label,
199
+ value: mix[t.key],
200
+ color: t.color,
201
+ }))}
202
+ />
203
+ <Legend
204
+ items={TOKEN_MIX.map(t => ({
205
+ key: t.key,
206
+ label: t.label,
207
+ color: t.color,
208
+ value: total > 0 ? formatPercent(mix[t.key] / total, 0) : "–",
209
+ }))}
210
+ />
211
+ </div>
212
+ <div className="stack" style={{ gap: 10 }}>
213
+ <div className="section-label" style={{ marginBottom: 0 }}>
214
+ By agent
269
215
  </div>
270
- ))}
271
- </div>
272
- }
273
- >
274
- <div className="stats-feed-ledger overflow-y-auto max-h-[280px] pr-2">
275
- {previewRequests.map(req => {
276
- const isError = !!req.errorMessage;
277
- return (
278
- <div
279
- key={req.id || `${req.sessionFile}-${req.entryId}`}
280
- className="stats-feed-item flex items-start gap-3 p-2 rounded hover:bg-stats-surface-2 cursor-pointer transition-colors"
281
- onClick={() => req.id && onRequestClick(req.id)}
282
- >
283
- <div
284
- className={`w-2 h-2 mt-1.5 rounded-full flex-shrink-0 ${
285
- isError ? "bg-stats-danger" : "bg-stats-success"
286
- }`}
287
- />
288
- <div className="flex-1 min-w-0">
289
- <div className="flex justify-between items-baseline gap-2">
290
- <div className="stats-font-medium stats-text-primary text-sm truncate">
291
- {req.model}
292
- </div>
293
- <div className="stats-text-xs stats-text-muted whitespace-nowrap">
294
- {formatRelativeTime(req.timestamp)}
295
- </div>
296
- </div>
297
- <div className="flex justify-between items-center text-xs stats-text-muted mt-0.5">
298
- <div>{req.provider}</div>
299
- <div>
300
- {req.duration ? formatDurationMs(req.duration) : ""}{" "}
301
- {req.usage.totalTokens > 0 ? `· ${formatMessageCost(req, 4)}` : ""}
302
- </div>
303
- </div>
304
- {isError && (
305
- <div className="text-xs text-stats-danger truncate mt-1">{req.errorMessage}</div>
306
- )}
216
+ <ShareBar
217
+ segments={agents.segments.map(s => ({
218
+ key: s.agentType,
219
+ label: AGENT_LABEL[s.agentType],
220
+ value: s.tokens,
221
+ color: AGENT_COLOR[s.agentType],
222
+ }))}
223
+ />
224
+ {agents.segments.map(s => (
225
+ <div key={s.agentType} className="row" style={{ justifyContent: "space-between" }}>
226
+ <span className="row">
227
+ <span className="swatch" style={{ background: AGENT_COLOR[s.agentType] }} />
228
+ {AGENT_LABEL[s.agentType]}
229
+ <span className="dim num">{formatInteger(s.requests)} req</span>
230
+ </span>
231
+ <span className="row">
232
+ <span className="dim num">{formatCompact(s.tokens)}</span>
233
+ <span className="num" style={{ minWidth: 48, textAlign: "right" }}>
234
+ {formatPercent(s.share)}
235
+ </span>
236
+ </span>
307
237
  </div>
308
- </div>
309
- );
310
- })}
311
- {previewRequests.length === 0 && (
312
- <div className="py-8 text-center stats-text-muted text-sm">No recent requests found</div>
313
- )}
314
- </div>
315
- </AsyncBoundary>
316
- </Panel>
317
- </div>
238
+ ))}
239
+ </div>
240
+ </div>
241
+ );
242
+ }}
243
+ </QueryView>
244
+ </Card>
318
245
  </div>
319
246
 
320
- <Panel
321
- title="Recent Requests Preview"
322
- subtitle="Latest transactions processed by the proxy"
247
+ <Card
248
+ index={3}
249
+ title={
250
+ <>
251
+ <Dot tone="live" pulse /> Latest requests
252
+ </>
253
+ }
254
+ description="Most recent model calls across every session"
323
255
  actions={
324
- <a href={`#/requests?range=${range}`} className="stats-button stats-button-secondary text-xs">
325
- View All Requests
256
+ <a className="btn" data-size="sm" data-variant="ghost" href={`#/requests?range=${range}`}>
257
+ All requests <ArrowRight size={13} />
326
258
  </a>
327
259
  }
260
+ flush
328
261
  >
329
- <AsyncBoundary loading={requestsLoading} error={requestsError} data={recentRequests}>
330
- <DataTable
331
- columns={columns}
332
- data={previewRequests}
333
- keyExtractor={item => item.id || `${item.sessionFile}-${item.entryId}`}
334
- onRowClick={item => item.id && onRequestClick(item.id)}
335
- renderMobileCard={renderMobileCard}
336
- emptyText="No recent requests found"
337
- />
338
- </AsyncBoundary>
339
- </Panel>
262
+ <QueryView query={recent} skeleton={<TableSkeleton rows={8} />}>
263
+ {rows => (
264
+ <Table
265
+ rows={rows}
266
+ rowKey={row => row.id ?? `${row.sessionFile}:${row.entryId}`}
267
+ onRowClick={row => row.id !== undefined && onRequestClick(row.id)}
268
+ columns={REQUEST_COLUMNS}
269
+ dense
270
+ />
271
+ )}
272
+ </QueryView>
273
+ </Card>
340
274
  </div>
341
275
  );
342
276
  }
277
+
278
+ const REQUEST_COLUMNS = [
279
+ {
280
+ key: "model",
281
+ header: "Model",
282
+ render: (row: MessageStats) => <LabelCell primary={row.model} secondary={row.provider} />,
283
+ },
284
+ {
285
+ key: "time",
286
+ header: "When",
287
+ render: (row: MessageStats) => <span className="muted">{formatRelativeTime(row.timestamp)}</span>,
288
+ },
289
+ {
290
+ key: "tokens",
291
+ header: "Tokens",
292
+ align: "right" as const,
293
+ render: (row: MessageStats) => <span className="num">{formatInteger(row.usage.totalTokens)}</span>,
294
+ },
295
+ {
296
+ key: "cost",
297
+ header: "Cost",
298
+ align: "right" as const,
299
+ render: (row: MessageStats) => <span className="num">{formatMessageCost(row, 4)}</span>,
300
+ },
301
+ {
302
+ key: "duration",
303
+ header: "Duration",
304
+ align: "right" as const,
305
+ render: (row: MessageStats) => <span className="num">{formatDurationMs(row.duration)}</span>,
306
+ },
307
+ {
308
+ key: "status",
309
+ header: "Status",
310
+ align: "right" as const,
311
+ render: (row: MessageStats) =>
312
+ row.errorMessage ? <Badge tone="bad">Failed</Badge> : <Badge tone="ok">OK</Badge>,
313
+ },
314
+ ];