@linxiraos/pi-stats 1.0.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.
- package/CHANGELOG.md +276 -0
- package/README.md +82 -0
- package/build.ts +95 -0
- package/dist/client/index.css +1 -0
- package/dist/client/index.html +24 -0
- package/dist/client/index.js +257 -0
- package/dist/client/styles.css +1695 -0
- package/dist/types/aggregator.d.ts +98 -0
- package/dist/types/client/App.d.ts +1 -0
- package/dist/types/client/api.d.ts +22 -0
- package/dist/types/client/app/AppLayout.d.ts +16 -0
- package/dist/types/client/app/NavRail.d.ts +7 -0
- package/dist/types/client/app/RangeControl.d.ts +7 -0
- package/dist/types/client/app/SyncButton.d.ts +14 -0
- package/dist/types/client/app/ThemeToggle.d.ts +1 -0
- package/dist/types/client/app/TopBar.d.ts +15 -0
- package/dist/types/client/app/routes.d.ts +12 -0
- package/dist/types/client/components/AgentTokenShare.d.ts +5 -0
- package/dist/types/client/components/chart-shared.d.ts +173 -0
- package/dist/types/client/components/models-table-shared.d.ts +175 -0
- package/dist/types/client/components/range-meta.d.ts +21 -0
- package/dist/types/client/data/charts.d.ts +1 -0
- package/dist/types/client/data/formatters.d.ts +8 -0
- package/dist/types/client/data/useHashRoute.d.ts +8 -0
- package/dist/types/client/data/useResource.d.ts +13 -0
- package/dist/types/client/data/view-models.d.ts +74 -0
- package/dist/types/client/index.d.ts +2 -0
- package/dist/types/client/routes/BehaviorRoute.d.ts +7 -0
- package/dist/types/client/routes/CostsRoute.d.ts +7 -0
- package/dist/types/client/routes/ErrorsRoute.d.ts +8 -0
- package/dist/types/client/routes/GainRoute.d.ts +7 -0
- package/dist/types/client/routes/ModelsRoute.d.ts +7 -0
- package/dist/types/client/routes/OverviewRoute.d.ts +8 -0
- package/dist/types/client/routes/ProjectsRoute.d.ts +7 -0
- package/dist/types/client/routes/ProvidersRoute.d.ts +7 -0
- package/dist/types/client/routes/RequestsRoute.d.ts +8 -0
- package/dist/types/client/routes/ToolsRoute.d.ts +7 -0
- package/dist/types/client/routes/index.d.ts +10 -0
- package/dist/types/client/types.d.ts +63 -0
- package/dist/types/client/ui/AsyncBoundary.d.ts +12 -0
- package/dist/types/client/ui/DataTable.d.ts +17 -0
- package/dist/types/client/ui/EmptyState.d.ts +7 -0
- package/dist/types/client/ui/ErrorState.d.ts +6 -0
- package/dist/types/client/ui/JsonBlock.d.ts +7 -0
- package/dist/types/client/ui/MetricCluster.d.ts +5 -0
- package/dist/types/client/ui/Panel.d.ts +7 -0
- package/dist/types/client/ui/RequestDrawer.d.ts +5 -0
- package/dist/types/client/ui/SegmentedControl.d.ts +12 -0
- package/dist/types/client/ui/Skeleton.d.ts +8 -0
- package/dist/types/client/ui/StatusPill.d.ts +7 -0
- package/dist/types/client/ui/index.d.ts +11 -0
- package/dist/types/client/useSystemTheme.d.ts +11 -0
- package/dist/types/db.d.ts +159 -0
- package/dist/types/embedded-client.d.ts +18 -0
- package/dist/types/gain-aggregator.d.ts +26 -0
- package/dist/types/index.d.ts +7 -0
- package/dist/types/parser.d.ts +53 -0
- package/dist/types/port-conflict.d.ts +14 -0
- package/dist/types/server.d.ts +12 -0
- package/dist/types/shared-types.d.ts +403 -0
- package/dist/types/sync-worker.d.ts +31 -0
- package/dist/types/types.d.ts +164 -0
- package/dist/types/usage-windows.d.ts +45 -0
- package/dist/types/user-metrics.d.ts +81 -0
- package/package.json +94 -0
- package/src/aggregator.ts +615 -0
- package/src/client/App.tsx +112 -0
- package/src/client/api.ts +118 -0
- package/src/client/app/AppLayout.tsx +93 -0
- package/src/client/app/NavRail.tsx +44 -0
- package/src/client/app/RangeControl.tsx +39 -0
- package/src/client/app/SyncButton.tsx +75 -0
- package/src/client/app/ThemeToggle.tsx +37 -0
- package/src/client/app/TopBar.tsx +73 -0
- package/src/client/app/routes.ts +86 -0
- package/src/client/components/AgentTokenShare.tsx +68 -0
- package/src/client/components/chart-shared.tsx +257 -0
- package/src/client/components/models-table-shared.tsx +255 -0
- package/src/client/components/range-meta.ts +73 -0
- package/src/client/css.d.ts +1 -0
- package/src/client/data/charts.ts +14 -0
- package/src/client/data/formatters.ts +45 -0
- package/src/client/data/useHashRoute.ts +88 -0
- package/src/client/data/useResource.ts +154 -0
- package/src/client/data/view-models.ts +259 -0
- package/src/client/index.tsx +10 -0
- package/src/client/routes/BehaviorRoute.tsx +623 -0
- package/src/client/routes/CostsRoute.tsx +234 -0
- package/src/client/routes/ErrorsRoute.tsx +118 -0
- package/src/client/routes/GainRoute.tsx +226 -0
- package/src/client/routes/ModelsRoute.tsx +430 -0
- package/src/client/routes/OverviewRoute.tsx +342 -0
- package/src/client/routes/ProjectsRoute.tsx +163 -0
- package/src/client/routes/ProvidersRoute.tsx +492 -0
- package/src/client/routes/RequestsRoute.tsx +123 -0
- package/src/client/routes/ToolsRoute.tsx +463 -0
- package/src/client/routes/index.ts +10 -0
- package/src/client/styles.css +1384 -0
- package/src/client/types.ts +80 -0
- package/src/client/ui/AsyncBoundary.tsx +54 -0
- package/src/client/ui/DataTable.tsx +122 -0
- package/src/client/ui/EmptyState.tsx +16 -0
- package/src/client/ui/ErrorState.tsx +25 -0
- package/src/client/ui/JsonBlock.tsx +75 -0
- package/src/client/ui/MetricCluster.tsx +78 -0
- package/src/client/ui/Panel.tsx +24 -0
- package/src/client/ui/RequestDrawer.tsx +208 -0
- package/src/client/ui/SegmentedControl.tsx +36 -0
- package/src/client/ui/Skeleton.tsx +17 -0
- package/src/client/ui/StatusPill.tsx +15 -0
- package/src/client/ui/index.ts +11 -0
- package/src/client/useSystemTheme.ts +87 -0
- package/src/db.ts +1702 -0
- package/src/embedded-client.generated.txt +0 -0
- package/src/embedded-client.ts +26 -0
- package/src/gain-aggregator.ts +311 -0
- package/src/index.ts +194 -0
- package/src/parser.ts +499 -0
- package/src/port-conflict.ts +263 -0
- package/src/server.ts +396 -0
- package/src/shared-types.ts +432 -0
- package/src/sync-worker.ts +40 -0
- package/src/types.ts +171 -0
- package/src/usage-windows.ts +304 -0
- package/src/user-metrics.ts +695 -0
- package/tailwind.config.js +40 -0
|
@@ -0,0 +1,492 @@
|
|
|
1
|
+
import { useMemo, useState } from "react";
|
|
2
|
+
import { Bar } from "react-chartjs-2";
|
|
3
|
+
import { getProviderDashboardStats } from "../api";
|
|
4
|
+
import {
|
|
5
|
+
barDatasetStyle,
|
|
6
|
+
buildSharedPlugins,
|
|
7
|
+
buildSharedScales,
|
|
8
|
+
buildTopNByModelSeries,
|
|
9
|
+
CHART_THEMES,
|
|
10
|
+
MODEL_COLORS,
|
|
11
|
+
styleDatasets,
|
|
12
|
+
} from "../components/chart-shared";
|
|
13
|
+
import {
|
|
14
|
+
formatCompact,
|
|
15
|
+
formatCost,
|
|
16
|
+
formatInteger,
|
|
17
|
+
formatPercent,
|
|
18
|
+
formatRelativeTime,
|
|
19
|
+
formatTokensPerSecond,
|
|
20
|
+
} from "../data/formatters";
|
|
21
|
+
import { useResource } from "../data/useResource";
|
|
22
|
+
import type {
|
|
23
|
+
ProviderAggregate,
|
|
24
|
+
ProviderDashboardStats,
|
|
25
|
+
ProviderHourlyPoint,
|
|
26
|
+
ProviderWindowInsight,
|
|
27
|
+
TimeRange,
|
|
28
|
+
UsageWindowSeries,
|
|
29
|
+
} from "../types";
|
|
30
|
+
import { AsyncBoundary, DataTable, type DataTableColumn, EmptyState, Panel, SegmentedControl } from "../ui";
|
|
31
|
+
import { useSystemTheme } from "../useSystemTheme";
|
|
32
|
+
|
|
33
|
+
export interface ProvidersRouteProps {
|
|
34
|
+
active: boolean;
|
|
35
|
+
range: TimeRange;
|
|
36
|
+
refreshTrigger: number;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function ProvidersRoute({ active, range, refreshTrigger }: ProvidersRouteProps) {
|
|
40
|
+
const {
|
|
41
|
+
data: stats,
|
|
42
|
+
error,
|
|
43
|
+
loading,
|
|
44
|
+
} = useResource(["providers", range, refreshTrigger], signal => getProviderDashboardStats(range, signal), {
|
|
45
|
+
pollMs: 30000,
|
|
46
|
+
enabled: active,
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
return (
|
|
50
|
+
<div className="stats-route-container space-y-6">
|
|
51
|
+
<AsyncBoundary loading={loading} error={error} data={stats}>
|
|
52
|
+
{stats && (
|
|
53
|
+
<>
|
|
54
|
+
<ProviderTotalsPanel providers={stats.providers} />
|
|
55
|
+
<ProviderTrendPanel stats={stats} />
|
|
56
|
+
<PeakHoursPanel hourly={stats.hourly} providers={stats.providers} />
|
|
57
|
+
<WindowInsightsPanel insights={stats.windowInsights} />
|
|
58
|
+
<WindowUtilizationPanel usageSeries={stats.usageSeries} />
|
|
59
|
+
</>
|
|
60
|
+
)}
|
|
61
|
+
</AsyncBoundary>
|
|
62
|
+
</div>
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// ---------------------------------------------------------------------------
|
|
67
|
+
// Provider totals
|
|
68
|
+
// ---------------------------------------------------------------------------
|
|
69
|
+
|
|
70
|
+
function ProviderTotalsPanel({ providers }: { providers: ProviderAggregate[] }) {
|
|
71
|
+
const grandTotal = useMemo(() => providers.reduce((sum, p) => sum + p.totalTokens, 0), [providers]);
|
|
72
|
+
|
|
73
|
+
const columns: DataTableColumn<ProviderAggregate>[] = [
|
|
74
|
+
{ key: "provider", header: "Provider", render: p => <span className="font-medium">{p.provider}</span> },
|
|
75
|
+
{ key: "requests", header: "Requests", numeric: true, render: p => formatInteger(p.totalRequests) },
|
|
76
|
+
{
|
|
77
|
+
key: "errors",
|
|
78
|
+
header: "Error Rate",
|
|
79
|
+
numeric: true,
|
|
80
|
+
render: p => formatPercent(p.totalRequests > 0 ? p.failedRequests / p.totalRequests : 0),
|
|
81
|
+
},
|
|
82
|
+
{ key: "models", header: "Models", numeric: true, render: p => formatInteger(p.models) },
|
|
83
|
+
{
|
|
84
|
+
key: "tokens",
|
|
85
|
+
header: "Tokens",
|
|
86
|
+
numeric: true,
|
|
87
|
+
render: p => (
|
|
88
|
+
<span
|
|
89
|
+
title={`Input ${formatCompact(p.totalInputTokens)} · Output ${formatCompact(p.totalOutputTokens)} · Cache read ${formatCompact(p.totalCacheReadTokens)} · Cache write ${formatCompact(p.totalCacheWriteTokens)}`}
|
|
90
|
+
>
|
|
91
|
+
{formatCompact(p.totalTokens)}
|
|
92
|
+
</span>
|
|
93
|
+
),
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
key: "share",
|
|
97
|
+
header: "Share",
|
|
98
|
+
numeric: true,
|
|
99
|
+
render: p => formatPercent(grandTotal > 0 ? p.totalTokens / grandTotal : 0),
|
|
100
|
+
},
|
|
101
|
+
{ key: "cost", header: "Cost", numeric: true, render: p => formatCost(p.totalCost) },
|
|
102
|
+
{ key: "tps", header: "Tok/s", numeric: true, render: p => formatTokensPerSecond(p.avgTokensPerSecond) },
|
|
103
|
+
];
|
|
104
|
+
|
|
105
|
+
return (
|
|
106
|
+
<Panel title="Provider Totals" subtitle="Token, request, and cost totals per provider over the active range">
|
|
107
|
+
<DataTable
|
|
108
|
+
columns={columns}
|
|
109
|
+
data={providers}
|
|
110
|
+
keyExtractor={p => p.provider}
|
|
111
|
+
emptyText="No requests recorded in this range"
|
|
112
|
+
/>
|
|
113
|
+
</Panel>
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// ---------------------------------------------------------------------------
|
|
118
|
+
// Token / cost trend by provider
|
|
119
|
+
// ---------------------------------------------------------------------------
|
|
120
|
+
|
|
121
|
+
function ProviderTrendPanel({ stats }: { stats: ProviderDashboardStats }) {
|
|
122
|
+
const [metric, setMetric] = useState<"tokens" | "cost">("tokens");
|
|
123
|
+
const theme = useSystemTheme();
|
|
124
|
+
const chartTheme = CHART_THEMES[theme];
|
|
125
|
+
|
|
126
|
+
// buildTopNByModelSeries keys on `model`; feed it the provider name so we
|
|
127
|
+
// get the same top-N + "Other" rollup without a parallel implementation.
|
|
128
|
+
const chartData = useMemo(() => {
|
|
129
|
+
const points = stats.series.map(p => ({ ...p, model: p.provider }));
|
|
130
|
+
return buildTopNByModelSeries<(typeof points)[number], { total: number }>(points, {
|
|
131
|
+
topN: 6,
|
|
132
|
+
rankWeight: p => (metric === "tokens" ? p.totalTokens : p.cost),
|
|
133
|
+
initBucket: () => ({ total: 0 }),
|
|
134
|
+
accumulate: (bucket, p) => {
|
|
135
|
+
bucket.total += metric === "tokens" ? p.totalTokens : p.cost;
|
|
136
|
+
},
|
|
137
|
+
bucketToValue: bucket => bucket.total,
|
|
138
|
+
});
|
|
139
|
+
}, [stats.series, metric]);
|
|
140
|
+
|
|
141
|
+
const formatValue = metric === "tokens" ? formatCompact : (v: number) => formatCost(v);
|
|
142
|
+
const options = useMemo(() => {
|
|
143
|
+
const { sharedScaleBase, yScale } = buildSharedScales({ chartTheme, formatY: formatValue });
|
|
144
|
+
return {
|
|
145
|
+
responsive: true,
|
|
146
|
+
maintainAspectRatio: false,
|
|
147
|
+
interaction: { mode: "index" as const, intersect: false },
|
|
148
|
+
plugins: buildSharedPlugins({
|
|
149
|
+
chartTheme,
|
|
150
|
+
showLegend: true,
|
|
151
|
+
defaultLabel: metric === "tokens" ? "Tokens" : "Cost",
|
|
152
|
+
formatValue,
|
|
153
|
+
footer: items => {
|
|
154
|
+
if (items.length < 2) return undefined;
|
|
155
|
+
const total = items.reduce((sum, item) => sum + (item.parsed.y ?? 0), 0);
|
|
156
|
+
return `Total: ${formatValue(total)}`;
|
|
157
|
+
},
|
|
158
|
+
}),
|
|
159
|
+
scales: {
|
|
160
|
+
x: { ...sharedScaleBase, stacked: true },
|
|
161
|
+
y: { ...yScale, stacked: true },
|
|
162
|
+
},
|
|
163
|
+
};
|
|
164
|
+
}, [chartTheme, metric, formatValue]);
|
|
165
|
+
|
|
166
|
+
const data = useMemo(
|
|
167
|
+
() => ({
|
|
168
|
+
labels: chartData.labels,
|
|
169
|
+
datasets: styleDatasets(chartData, i => barDatasetStyle(MODEL_COLORS[i % MODEL_COLORS.length])),
|
|
170
|
+
}),
|
|
171
|
+
[chartData],
|
|
172
|
+
);
|
|
173
|
+
|
|
174
|
+
return (
|
|
175
|
+
<Panel
|
|
176
|
+
title="Burn by Provider"
|
|
177
|
+
subtitle="Stacked token/cost burn per provider over time"
|
|
178
|
+
actions={
|
|
179
|
+
<SegmentedControl
|
|
180
|
+
options={[
|
|
181
|
+
{ value: "tokens" as const, label: "Tokens" },
|
|
182
|
+
{ value: "cost" as const, label: "Cost" },
|
|
183
|
+
]}
|
|
184
|
+
value={metric}
|
|
185
|
+
onChange={setMetric}
|
|
186
|
+
/>
|
|
187
|
+
}
|
|
188
|
+
>
|
|
189
|
+
<div className="h-[300px]">
|
|
190
|
+
{chartData.labels.length === 0 ? (
|
|
191
|
+
<EmptyState message="No provider activity in this range" />
|
|
192
|
+
) : (
|
|
193
|
+
<Bar data={data} options={options} />
|
|
194
|
+
)}
|
|
195
|
+
</div>
|
|
196
|
+
</Panel>
|
|
197
|
+
);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// ---------------------------------------------------------------------------
|
|
201
|
+
// Peak burn hours
|
|
202
|
+
// ---------------------------------------------------------------------------
|
|
203
|
+
|
|
204
|
+
const ALL_PROVIDERS = "__all__";
|
|
205
|
+
|
|
206
|
+
function PeakHoursPanel({ hourly, providers }: { hourly: ProviderHourlyPoint[]; providers: ProviderAggregate[] }) {
|
|
207
|
+
const [provider, setProvider] = useState(ALL_PROVIDERS);
|
|
208
|
+
const theme = useSystemTheme();
|
|
209
|
+
const chartTheme = CHART_THEMES[theme];
|
|
210
|
+
|
|
211
|
+
const { tokensByHour, peakHour } = useMemo(() => {
|
|
212
|
+
const tokens = new Array<number>(24).fill(0);
|
|
213
|
+
for (const point of hourly) {
|
|
214
|
+
if (provider !== ALL_PROVIDERS && point.provider !== provider) continue;
|
|
215
|
+
tokens[point.hour] += point.totalTokens;
|
|
216
|
+
}
|
|
217
|
+
let peak = 0;
|
|
218
|
+
for (let hour = 1; hour < 24; hour++) {
|
|
219
|
+
if (tokens[hour] > tokens[peak]) peak = hour;
|
|
220
|
+
}
|
|
221
|
+
return { tokensByHour: tokens, peakHour: peak };
|
|
222
|
+
}, [hourly, provider]);
|
|
223
|
+
|
|
224
|
+
const hasData = tokensByHour.some(v => v > 0);
|
|
225
|
+
|
|
226
|
+
const data = useMemo(
|
|
227
|
+
() => ({
|
|
228
|
+
labels: Array.from({ length: 24 }, (_, hour) => `${String(hour).padStart(2, "0")}:00`),
|
|
229
|
+
datasets: [
|
|
230
|
+
{
|
|
231
|
+
label: "Tokens",
|
|
232
|
+
data: tokensByHour,
|
|
233
|
+
...barDatasetStyle(MODEL_COLORS[2]),
|
|
234
|
+
// Highlight the peak hour in the brand accent color.
|
|
235
|
+
backgroundColor: tokensByHour.map((_, hour) => (hour === peakHour ? MODEL_COLORS[0] : MODEL_COLORS[2])),
|
|
236
|
+
},
|
|
237
|
+
],
|
|
238
|
+
}),
|
|
239
|
+
[tokensByHour, peakHour],
|
|
240
|
+
);
|
|
241
|
+
|
|
242
|
+
const options = useMemo(() => {
|
|
243
|
+
const { sharedScaleBase, yScale } = buildSharedScales({ chartTheme, formatY: formatCompact });
|
|
244
|
+
return {
|
|
245
|
+
responsive: true,
|
|
246
|
+
maintainAspectRatio: false,
|
|
247
|
+
plugins: buildSharedPlugins({
|
|
248
|
+
chartTheme,
|
|
249
|
+
showLegend: false,
|
|
250
|
+
defaultLabel: "Tokens",
|
|
251
|
+
formatValue: formatCompact,
|
|
252
|
+
}),
|
|
253
|
+
scales: { x: sharedScaleBase, y: yScale },
|
|
254
|
+
};
|
|
255
|
+
}, [chartTheme]);
|
|
256
|
+
|
|
257
|
+
return (
|
|
258
|
+
<Panel
|
|
259
|
+
title="Peak Burn Hours"
|
|
260
|
+
subtitle={
|
|
261
|
+
hasData
|
|
262
|
+
? `Token burn by local hour of day — peak at ${String(peakHour).padStart(2, "0")}:00`
|
|
263
|
+
: "Token burn by local hour of day"
|
|
264
|
+
}
|
|
265
|
+
actions={
|
|
266
|
+
<select
|
|
267
|
+
className="stats-select"
|
|
268
|
+
value={provider}
|
|
269
|
+
onChange={e => setProvider(e.target.value)}
|
|
270
|
+
aria-label="Provider"
|
|
271
|
+
>
|
|
272
|
+
<option value={ALL_PROVIDERS}>All providers</option>
|
|
273
|
+
{providers.map(p => (
|
|
274
|
+
<option key={p.provider} value={p.provider}>
|
|
275
|
+
{p.provider}
|
|
276
|
+
</option>
|
|
277
|
+
))}
|
|
278
|
+
</select>
|
|
279
|
+
}
|
|
280
|
+
>
|
|
281
|
+
<div className="h-[260px]">
|
|
282
|
+
{hasData ? <Bar data={data} options={options} /> : <EmptyState message="No activity in this range" />}
|
|
283
|
+
</div>
|
|
284
|
+
</Panel>
|
|
285
|
+
);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// ---------------------------------------------------------------------------
|
|
289
|
+
// Subscription window insights
|
|
290
|
+
// ---------------------------------------------------------------------------
|
|
291
|
+
|
|
292
|
+
function WindowInsightsPanel({ insights }: { insights: ProviderWindowInsight[] }) {
|
|
293
|
+
const columns: DataTableColumn<ProviderWindowInsight>[] = [
|
|
294
|
+
{ key: "provider", header: "Provider", render: i => <span className="font-medium">{i.provider}</span> },
|
|
295
|
+
{ key: "window", header: "Window", render: i => i.windowLabel },
|
|
296
|
+
{ key: "accounts", header: "Accounts", numeric: true, render: i => formatInteger(i.accounts) },
|
|
297
|
+
{
|
|
298
|
+
key: "consumed",
|
|
299
|
+
header: "Windows Burned",
|
|
300
|
+
numeric: true,
|
|
301
|
+
render: i => (
|
|
302
|
+
<span title="Subscription-window equivalents consumed in range (sum of used-fraction increases across accounts)">
|
|
303
|
+
{i.fractionConsumed.toFixed(2)}
|
|
304
|
+
</span>
|
|
305
|
+
),
|
|
306
|
+
},
|
|
307
|
+
{
|
|
308
|
+
key: "capacity",
|
|
309
|
+
header: "Est. Tokens / Window",
|
|
310
|
+
numeric: true,
|
|
311
|
+
render: i => (
|
|
312
|
+
<span title="Provider tokens burned in range ÷ windows burned — what one full window is worth">
|
|
313
|
+
{i.estTokensPerWindow !== null ? formatCompact(i.estTokensPerWindow) : "—"}
|
|
314
|
+
</span>
|
|
315
|
+
),
|
|
316
|
+
},
|
|
317
|
+
{
|
|
318
|
+
key: "peak",
|
|
319
|
+
header: "Peak Utilization",
|
|
320
|
+
numeric: true,
|
|
321
|
+
render: i => (
|
|
322
|
+
<span title="Peak of summed used fraction across accounts at any sampled instant">
|
|
323
|
+
{formatPercent(i.peakConcurrentFraction)}
|
|
324
|
+
</span>
|
|
325
|
+
),
|
|
326
|
+
},
|
|
327
|
+
{
|
|
328
|
+
key: "ideal",
|
|
329
|
+
header: "Ideal Accounts",
|
|
330
|
+
numeric: true,
|
|
331
|
+
render: i => (
|
|
332
|
+
<span
|
|
333
|
+
title="Accounts needed to keep peak demand under 90% of fleet capacity"
|
|
334
|
+
className={i.idealAccounts > i.accounts ? "stats-text-warning font-semibold" : undefined}
|
|
335
|
+
>
|
|
336
|
+
{formatInteger(i.idealAccounts)}
|
|
337
|
+
{i.idealAccounts > i.accounts ? ` (have ${i.accounts})` : ""}
|
|
338
|
+
</span>
|
|
339
|
+
),
|
|
340
|
+
},
|
|
341
|
+
{
|
|
342
|
+
key: "exhausted",
|
|
343
|
+
header: "Exhaustions",
|
|
344
|
+
numeric: true,
|
|
345
|
+
render: i => (
|
|
346
|
+
<span className={i.exhaustedEvents > 0 ? "stats-text-warning" : undefined}>
|
|
347
|
+
{formatInteger(i.exhaustedEvents)}
|
|
348
|
+
</span>
|
|
349
|
+
),
|
|
350
|
+
},
|
|
351
|
+
];
|
|
352
|
+
|
|
353
|
+
return (
|
|
354
|
+
<Panel
|
|
355
|
+
title="Subscription Windows"
|
|
356
|
+
subtitle="What each usage window buys you, and how many accounts peak demand needs"
|
|
357
|
+
>
|
|
358
|
+
<DataTable
|
|
359
|
+
columns={columns}
|
|
360
|
+
data={insights}
|
|
361
|
+
keyExtractor={i => `${i.provider}::${i.windowKey}`}
|
|
362
|
+
emptyText="No usage snapshots recorded yet — they accumulate whenever usage is fetched (TUI footer, /usage, omp usage)"
|
|
363
|
+
/>
|
|
364
|
+
</Panel>
|
|
365
|
+
);
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
// ---------------------------------------------------------------------------
|
|
369
|
+
// Window utilization
|
|
370
|
+
// ---------------------------------------------------------------------------
|
|
371
|
+
|
|
372
|
+
const UTILIZATION_COLORS = {
|
|
373
|
+
ok: "#62d394",
|
|
374
|
+
warning: "#f5c14b",
|
|
375
|
+
exhausted: "#ff6b7d",
|
|
376
|
+
} as const;
|
|
377
|
+
|
|
378
|
+
function WindowUtilizationPanel({ usageSeries }: { usageSeries: UsageWindowSeries[] }) {
|
|
379
|
+
const providers = useMemo(() => [...new Set(usageSeries.map(s => s.provider))], [usageSeries]);
|
|
380
|
+
const [selected, setSelected] = useState<string | null>(null);
|
|
381
|
+
const provider = selected !== null && providers.includes(selected) ? selected : (providers[0] ?? null);
|
|
382
|
+
const theme = useSystemTheme();
|
|
383
|
+
const chartTheme = CHART_THEMES[theme];
|
|
384
|
+
|
|
385
|
+
// One row per (window, account): the latest recorded fraction. Snapshot
|
|
386
|
+
// history is bursty (rows appear whenever usage is fetched), so a "how full
|
|
387
|
+
// is each window right now" bar reads far better than a time axis.
|
|
388
|
+
const rows = useMemo(() => {
|
|
389
|
+
return usageSeries
|
|
390
|
+
.filter(s => s.provider === provider)
|
|
391
|
+
.map(s => {
|
|
392
|
+
const latest = [...s.points].reverse().find(p => p.usedFraction !== null);
|
|
393
|
+
return latest
|
|
394
|
+
? {
|
|
395
|
+
label: `${s.windowLabel} · ${s.accountLabel}`,
|
|
396
|
+
fraction: latest.usedFraction ?? 0,
|
|
397
|
+
exhausted: latest.exhausted,
|
|
398
|
+
recordedAt: latest.timestamp,
|
|
399
|
+
}
|
|
400
|
+
: null;
|
|
401
|
+
})
|
|
402
|
+
.filter(row => row !== null)
|
|
403
|
+
.sort((a, b) => b.fraction - a.fraction);
|
|
404
|
+
}, [usageSeries, provider]);
|
|
405
|
+
|
|
406
|
+
const data = useMemo(
|
|
407
|
+
() => ({
|
|
408
|
+
labels: rows.map(r => r.label),
|
|
409
|
+
datasets: [
|
|
410
|
+
{
|
|
411
|
+
label: "Used",
|
|
412
|
+
data: rows.map(r => r.fraction * 100),
|
|
413
|
+
backgroundColor: rows.map(r =>
|
|
414
|
+
r.exhausted
|
|
415
|
+
? UTILIZATION_COLORS.exhausted
|
|
416
|
+
: r.fraction >= 0.8
|
|
417
|
+
? UTILIZATION_COLORS.warning
|
|
418
|
+
: UTILIZATION_COLORS.ok,
|
|
419
|
+
),
|
|
420
|
+
borderWidth: 0,
|
|
421
|
+
borderRadius: 4,
|
|
422
|
+
barThickness: 18,
|
|
423
|
+
},
|
|
424
|
+
],
|
|
425
|
+
}),
|
|
426
|
+
[rows],
|
|
427
|
+
);
|
|
428
|
+
|
|
429
|
+
const options = useMemo(() => {
|
|
430
|
+
const { sharedScaleBase, yScale } = buildSharedScales({ chartTheme, formatY: v => `${Math.round(v)}%` });
|
|
431
|
+
const xMax = Math.max(100, ...rows.map(r => r.fraction * 100));
|
|
432
|
+
const shared = buildSharedPlugins({
|
|
433
|
+
chartTheme,
|
|
434
|
+
showLegend: false,
|
|
435
|
+
defaultLabel: "Used",
|
|
436
|
+
formatValue: v => `${v.toFixed(1)}%`,
|
|
437
|
+
});
|
|
438
|
+
return {
|
|
439
|
+
indexAxis: "y" as const,
|
|
440
|
+
responsive: true,
|
|
441
|
+
maintainAspectRatio: false,
|
|
442
|
+
plugins: {
|
|
443
|
+
...shared,
|
|
444
|
+
tooltip: {
|
|
445
|
+
...shared.tooltip,
|
|
446
|
+
callbacks: {
|
|
447
|
+
label: (ctx: { dataIndex: number; parsed: { x: number | null } }) => {
|
|
448
|
+
const row = rows[ctx.dataIndex];
|
|
449
|
+
const used = `${(ctx.parsed.x ?? 0).toFixed(1)}% used`;
|
|
450
|
+
return row ? `${used} · recorded ${formatRelativeTime(row.recordedAt)}` : used;
|
|
451
|
+
},
|
|
452
|
+
},
|
|
453
|
+
},
|
|
454
|
+
},
|
|
455
|
+
scales: {
|
|
456
|
+
x: { ...yScale, max: xMax },
|
|
457
|
+
y: { ...sharedScaleBase, grid: { display: false } },
|
|
458
|
+
},
|
|
459
|
+
};
|
|
460
|
+
}, [chartTheme, rows]);
|
|
461
|
+
|
|
462
|
+
return (
|
|
463
|
+
<Panel
|
|
464
|
+
title="Window Utilization"
|
|
465
|
+
subtitle="Latest recorded limit utilization per account and window — red bars are exhausted, amber above 80%"
|
|
466
|
+
actions={
|
|
467
|
+
providers.length > 1 ? (
|
|
468
|
+
<select
|
|
469
|
+
className="stats-select"
|
|
470
|
+
value={provider ?? ""}
|
|
471
|
+
onChange={e => setSelected(e.target.value)}
|
|
472
|
+
aria-label="Provider"
|
|
473
|
+
>
|
|
474
|
+
{providers.map(p => (
|
|
475
|
+
<option key={p} value={p}>
|
|
476
|
+
{p}
|
|
477
|
+
</option>
|
|
478
|
+
))}
|
|
479
|
+
</select>
|
|
480
|
+
) : undefined
|
|
481
|
+
}
|
|
482
|
+
>
|
|
483
|
+
<div style={{ height: Math.max(160, rows.length * 34 + 60) }}>
|
|
484
|
+
{rows.length === 0 ? (
|
|
485
|
+
<EmptyState message="No usage snapshots recorded yet — they accumulate whenever usage is fetched" />
|
|
486
|
+
) : (
|
|
487
|
+
<Bar data={data} options={options} />
|
|
488
|
+
)}
|
|
489
|
+
</div>
|
|
490
|
+
</Panel>
|
|
491
|
+
);
|
|
492
|
+
}
|
|
@@ -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
|
+
}
|