@linxiraos/pi-stats 1.1.19 → 1.1.22
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 +4 -0
- package/README.md +13 -11
- package/THIRD-PARTY-NOTICES.txt +3 -40
- package/build.ts +5 -49
- package/dist/client/index.css +1 -1
- package/dist/client/index.html +2 -2
- package/dist/client/index.js +105 -126
- package/dist/types/aggregator.d.ts +25 -22
- package/dist/types/client/api.d.ts +11 -7
- package/dist/types/client/app/LiveChip.d.ts +2 -0
- package/dist/types/client/app/Shell.d.ts +16 -0
- package/dist/types/client/app/ThemeToggle.d.ts +1 -0
- package/dist/types/client/app/nav.d.ts +18 -0
- package/dist/types/client/charts/BarList.d.ts +17 -0
- package/dist/types/client/charts/Chart.d.ts +49 -0
- package/dist/types/client/charts/Legend.d.ts +16 -0
- package/dist/types/client/charts/ShareBar.d.ts +11 -0
- package/dist/types/client/charts/Sparkline.d.ts +11 -0
- package/dist/types/client/charts/TimeChart.d.ts +9 -0
- package/dist/types/client/charts/index.d.ts +7 -0
- package/dist/types/client/charts/types.d.ts +25 -0
- package/dist/types/client/charts/useWidth.d.ts +3 -0
- package/dist/types/client/data/colors.d.ts +25 -0
- package/dist/types/client/data/formatters.d.ts +9 -1
- package/dist/types/client/data/live.d.ts +29 -0
- package/dist/types/client/data/query.d.ts +35 -0
- package/dist/types/client/data/range.d.ts +31 -0
- package/dist/types/client/data/series.d.ts +28 -0
- package/dist/types/client/data/useHashRoute.d.ts +18 -4
- package/dist/types/client/data/view-models.d.ts +111 -28
- package/dist/types/client/index.d.ts +0 -1
- package/dist/types/client/routes/CostsRoute.d.ts +2 -2
- package/dist/types/client/routes/ErrorsRoute.d.ts +2 -2
- package/dist/types/client/routes/FrustrationRoute.d.ts +7 -0
- package/dist/types/client/routes/GainRoute.d.ts +1 -2
- package/dist/types/client/routes/ModelsRoute.d.ts +2 -2
- package/dist/types/client/routes/OverviewRoute.d.ts +1 -2
- package/dist/types/client/routes/ProjectsRoute.d.ts +2 -2
- package/dist/types/client/routes/ProvidersRoute.d.ts +2 -2
- package/dist/types/client/routes/RequestsRoute.d.ts +1 -2
- package/dist/types/client/routes/ToolsRoute.d.ts +2 -2
- package/dist/types/client/routes/TracesRoute.d.ts +2 -2
- package/dist/types/client/routes/index.d.ts +1 -1
- package/dist/types/client/traces/AggregatesPanel.d.ts +4 -3
- package/dist/types/client/traces/SummaryStrip.d.ts +2 -2
- package/dist/types/client/traces/trace-colors.d.ts +18 -12
- package/dist/types/client/ui/Badge.d.ts +19 -0
- package/dist/types/client/ui/Card.d.ts +25 -0
- package/dist/types/client/ui/Drawer.d.ts +25 -0
- package/dist/types/client/ui/JsonBlock.d.ts +2 -2
- package/dist/types/client/ui/Modal.d.ts +23 -0
- package/dist/types/client/ui/RequestDrawer.d.ts +10 -1
- package/dist/types/client/ui/SearchInput.d.ts +8 -0
- package/dist/types/client/ui/Segmented.d.ts +15 -0
- package/dist/types/client/ui/Stat.d.ts +36 -0
- package/dist/types/client/ui/States.d.ts +41 -0
- package/dist/types/client/ui/Table.d.ts +48 -0
- package/dist/types/client/ui/index.d.ts +9 -10
- package/dist/types/db.d.ts +90 -106
- package/dist/types/frustration.d.ts +70 -0
- package/dist/types/index.d.ts +4 -2
- package/dist/types/live.d.ts +30 -0
- package/dist/types/rollup.d.ts +115 -0
- package/dist/types/server.d.ts +6 -1
- package/dist/types/shared-types.d.ts +116 -56
- package/dist/types/types.d.ts +4 -0
- package/dist/types/user-metrics.d.ts +8 -0
- package/package.json +7 -13
- package/src/aggregator.ts +278 -216
- package/src/client/App.tsx +38 -80
- package/src/client/api.ts +65 -7
- package/src/client/app/LiveChip.tsx +97 -0
- package/src/client/app/Shell.tsx +181 -0
- package/src/client/app/ThemeToggle.tsx +5 -2
- package/src/client/app/nav.ts +78 -0
- package/src/client/charts/BarList.tsx +49 -0
- package/src/client/charts/Chart.tsx +388 -0
- package/src/client/charts/Legend.tsx +61 -0
- package/src/client/charts/ShareBar.tsx +26 -0
- package/src/client/charts/Sparkline.tsx +38 -0
- package/src/client/charts/TimeChart.tsx +22 -0
- package/src/client/charts/index.ts +7 -0
- package/src/client/charts/types.ts +27 -0
- package/src/client/charts/useWidth.ts +19 -0
- package/src/client/data/colors.ts +45 -0
- package/src/client/data/formatters.ts +40 -12
- package/src/client/data/live.tsx +85 -0
- package/src/client/data/query.ts +166 -0
- package/src/client/data/range.ts +82 -0
- package/src/client/data/series.ts +85 -0
- package/src/client/data/useHashRoute.ts +41 -83
- package/src/client/data/view-models.ts +347 -140
- package/src/client/index.tsx +3 -6
- package/src/client/routes/CostsRoute.tsx +394 -222
- package/src/client/routes/ErrorsRoute.tsx +401 -98
- package/src/client/routes/FrustrationRoute.tsx +924 -0
- package/src/client/routes/GainRoute.tsx +209 -206
- package/src/client/routes/ModelsRoute.tsx +540 -431
- package/src/client/routes/OverviewRoute.tsx +283 -311
- package/src/client/routes/ProjectsRoute.tsx +298 -156
- package/src/client/routes/ProvidersRoute.tsx +948 -416
- package/src/client/routes/RequestsRoute.tsx +295 -107
- package/src/client/routes/ToolsRoute.tsx +495 -398
- package/src/client/routes/TracesRoute.tsx +136 -117
- package/src/client/routes/costs.css +24 -0
- package/src/client/routes/errors.css +60 -0
- package/src/client/routes/frustration.css +123 -0
- package/src/client/routes/index.ts +1 -1
- package/src/client/routes/models.css +43 -0
- package/src/client/routes/projects.css +6 -0
- package/src/client/routes/providers.css +28 -0
- package/src/client/routes/tools.css +14 -0
- package/src/client/styles.css +1396 -1301
- package/src/client/traces/AggregatesPanel.tsx +55 -74
- package/src/client/traces/Minimap.tsx +6 -15
- package/src/client/traces/SpanDrawer.tsx +128 -194
- package/src/client/traces/SummaryStrip.tsx +21 -24
- package/src/client/traces/TimelineCanvas.tsx +29 -46
- package/src/client/traces/TraceView.tsx +175 -130
- package/src/client/traces/TranscriptList.tsx +13 -41
- package/src/client/traces/trace-colors.ts +60 -53
- package/src/client/traces/traces.css +279 -0
- package/src/client/ui/Badge.tsx +29 -0
- package/src/client/ui/Card.tsx +61 -0
- package/src/client/ui/Drawer.tsx +93 -0
- package/src/client/ui/JsonBlock.tsx +26 -44
- package/src/client/ui/Modal.tsx +106 -0
- package/src/client/ui/RequestDrawer.tsx +163 -188
- package/src/client/ui/SearchInput.tsx +25 -0
- package/src/client/ui/Segmented.tsx +61 -0
- package/src/client/ui/Stat.tsx +104 -0
- package/src/client/ui/States.tsx +81 -0
- package/src/client/ui/Table.tsx +230 -0
- package/src/client/ui/index.ts +9 -10
- package/src/client/ui/modal.css +54 -0
- package/src/client/ui/request-drawer.css +40 -0
- package/src/db.ts +379 -983
- package/src/frustration.ts +473 -0
- package/src/index.ts +23 -29
- package/src/live.ts +259 -0
- package/src/parser.ts +55 -66
- package/src/rollup.ts +951 -0
- package/src/server.ts +143 -87
- package/src/shared-types.ts +120 -56
- package/src/trace.ts +42 -22
- package/src/types.ts +4 -0
- package/src/user-metrics.ts +13 -0
- package/dist/client/styles.css +0 -1976
- package/dist/types/client/app/AppLayout.d.ts +0 -16
- package/dist/types/client/app/NavRail.d.ts +0 -7
- package/dist/types/client/app/RangeControl.d.ts +0 -7
- package/dist/types/client/app/SyncButton.d.ts +0 -14
- package/dist/types/client/app/TopBar.d.ts +0 -15
- package/dist/types/client/app/routes.d.ts +0 -12
- package/dist/types/client/components/AgentTokenShare.d.ts +0 -5
- package/dist/types/client/components/chart-shared.d.ts +0 -178
- package/dist/types/client/components/models-table-shared.d.ts +0 -175
- package/dist/types/client/components/range-meta.d.ts +0 -21
- package/dist/types/client/data/charts.d.ts +0 -1
- package/dist/types/client/data/useResource.d.ts +0 -13
- package/dist/types/client/routes/BehaviorRoute.d.ts +0 -7
- package/dist/types/client/ui/AsyncBoundary.d.ts +0 -12
- package/dist/types/client/ui/DataTable.d.ts +0 -17
- package/dist/types/client/ui/EmptyState.d.ts +0 -7
- package/dist/types/client/ui/ErrorState.d.ts +0 -6
- package/dist/types/client/ui/MetricCluster.d.ts +0 -5
- package/dist/types/client/ui/Panel.d.ts +0 -7
- package/dist/types/client/ui/SegmentedControl.d.ts +0 -12
- package/dist/types/client/ui/Skeleton.d.ts +0 -8
- package/dist/types/client/ui/StatusPill.d.ts +0 -7
- package/src/client/app/AppLayout.tsx +0 -93
- package/src/client/app/NavRail.tsx +0 -44
- package/src/client/app/RangeControl.tsx +0 -39
- package/src/client/app/SyncButton.tsx +0 -75
- package/src/client/app/TopBar.tsx +0 -73
- package/src/client/app/routes.ts +0 -93
- package/src/client/components/AgentTokenShare.tsx +0 -68
- package/src/client/components/chart-shared.tsx +0 -273
- package/src/client/components/models-table-shared.tsx +0 -255
- package/src/client/components/range-meta.ts +0 -73
- package/src/client/data/charts.ts +0 -14
- package/src/client/data/useResource.ts +0 -154
- package/src/client/routes/BehaviorRoute.tsx +0 -623
- package/src/client/ui/AsyncBoundary.tsx +0 -54
- package/src/client/ui/DataTable.tsx +0 -122
- package/src/client/ui/EmptyState.tsx +0 -16
- package/src/client/ui/ErrorState.tsx +0 -25
- package/src/client/ui/MetricCluster.tsx +0 -92
- package/src/client/ui/Panel.tsx +0 -24
- package/src/client/ui/SegmentedControl.tsx +0 -36
- package/src/client/ui/Skeleton.tsx +0 -17
- package/src/client/ui/StatusPill.tsx +0 -15
- package/tailwind.config.js +0 -40
package/src/aggregator.ts
CHANGED
|
@@ -1,19 +1,31 @@
|
|
|
1
1
|
import * as fs from "node:fs";
|
|
2
2
|
import * as path from "node:path";
|
|
3
|
-
import { getStatsDbPath, workerHostEntry } from "@linxiraos/pi-utils";
|
|
3
|
+
import { getAgentDbPath, getStatsDbPath, workerHostEntry } from "@linxiraos/pi-utils";
|
|
4
4
|
import { withFileLock } from "@linxiraos/pi-utils/file-lock";
|
|
5
5
|
import {
|
|
6
|
-
|
|
6
|
+
applySessionParseResults,
|
|
7
7
|
completeSessionSync,
|
|
8
8
|
getRecentErrors as dbGetRecentErrors,
|
|
9
9
|
getRecentRequests as dbGetRecentRequests,
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
getBehaviorTimeSeries,
|
|
13
|
-
getCostTimeSeries,
|
|
14
|
-
getFileOffset,
|
|
10
|
+
type FileOffset,
|
|
11
|
+
getFileOffsets,
|
|
15
12
|
getMessageById,
|
|
16
13
|
getMessageCount,
|
|
14
|
+
initDb,
|
|
15
|
+
markSessionBackfillsComplete,
|
|
16
|
+
type ParsedSession,
|
|
17
|
+
prepareSessionSync,
|
|
18
|
+
} from "./db";
|
|
19
|
+
import {
|
|
20
|
+
getSessionEntry,
|
|
21
|
+
listAllSessionFiles,
|
|
22
|
+
matchesSessionFile,
|
|
23
|
+
type ParseSessionResult,
|
|
24
|
+
parseSessionFile,
|
|
25
|
+
type SessionParserState,
|
|
26
|
+
} from "./parser";
|
|
27
|
+
import {
|
|
28
|
+
getCostTimeSeries,
|
|
17
29
|
getModelPerformanceSeries,
|
|
18
30
|
getModelTimeSeries,
|
|
19
31
|
getOverallStats,
|
|
@@ -27,37 +39,31 @@ import {
|
|
|
27
39
|
getToolStats,
|
|
28
40
|
getToolStatsByModel,
|
|
29
41
|
getToolTimeSeries,
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
prepareSessionSync,
|
|
33
|
-
pruneOrphanSessions,
|
|
34
|
-
} from "./db";
|
|
35
|
-
import {
|
|
36
|
-
getSessionEntry,
|
|
37
|
-
listAllSessionFiles,
|
|
38
|
-
matchesSessionFile,
|
|
39
|
-
type ParseSessionResult,
|
|
40
|
-
parseSessionFile,
|
|
41
|
-
type SessionParserState,
|
|
42
|
-
} from "./parser";
|
|
42
|
+
type RangeWindow,
|
|
43
|
+
} from "./rollup";
|
|
43
44
|
import type { SyncWorkerRequest, SyncWorkerResponse } from "./sync-worker";
|
|
44
45
|
// Coding-agent binary/bundle workers route through the CLI entrypoint with a
|
|
45
46
|
// hidden argv mode, so the compiled binary and npm bundle only need one
|
|
46
|
-
// JavaScript entry. Standalone source `
|
|
47
|
+
// JavaScript entry. Standalone source `omp-stats` keeps using this package's
|
|
47
48
|
// own sync-worker source file.
|
|
48
49
|
import type {
|
|
49
|
-
BehaviorDashboardStats,
|
|
50
50
|
DashboardStats,
|
|
51
51
|
FolderStats,
|
|
52
52
|
MessageStats,
|
|
53
53
|
ProviderDashboardStats,
|
|
54
|
+
ProviderWindowStats,
|
|
54
55
|
RequestDetails,
|
|
55
56
|
ToolDashboardStats,
|
|
56
57
|
} from "./types";
|
|
57
|
-
import { computeUsageWindowStats, fetchUsageData } from "./usage-windows";
|
|
58
|
+
import { computeUsageWindowStats, fetchUsageData, type UsageDataSnapshot } from "./usage-windows";
|
|
58
59
|
|
|
59
60
|
const STATS_SYNC_LOCK_RETRY_MS = 25;
|
|
60
61
|
const STATS_SYNC_LOCK_WAIT_MS = 60 * 60 * 1000;
|
|
62
|
+
// Bound queued results by both file count and derived rows; never hold a SQLite transaction across I/O.
|
|
63
|
+
const SYNC_BATCH_FILES = 512;
|
|
64
|
+
const SYNC_BATCH_ROWS = 8192;
|
|
65
|
+
const SYNC_METADATA_FILES = 128;
|
|
66
|
+
const SYNC_INLINE_READS = 8;
|
|
61
67
|
|
|
62
68
|
/**
|
|
63
69
|
* Serialize stats ingestion and archive reconciliation across processes.
|
|
@@ -73,11 +79,12 @@ export async function withStatsSyncLock<T>(dbPath: string, fn: () => Promise<T>)
|
|
|
73
79
|
retries: Math.ceil(STATS_SYNC_LOCK_WAIT_MS / STATS_SYNC_LOCK_RETRY_MS),
|
|
74
80
|
});
|
|
75
81
|
}
|
|
82
|
+
|
|
76
83
|
/**
|
|
77
84
|
* Progress event emitted after each session file is fully processed.
|
|
78
85
|
* `current` is the number of files completed (skipped + parsed),
|
|
79
86
|
* `total` is the size of the work set. `processed` is the running total
|
|
80
|
-
* of inserted rows.
|
|
87
|
+
* of inserted rows. Parsed files are reported only after their batch commits.
|
|
81
88
|
*/
|
|
82
89
|
export interface SyncProgress {
|
|
83
90
|
current: number;
|
|
@@ -92,9 +99,14 @@ export interface SyncOptions {
|
|
|
92
99
|
/**
|
|
93
100
|
* Worker pool size. Defaults to a sensible value derived from the host
|
|
94
101
|
* (capped to avoid drowning a small machine in workers). Set to `1` to
|
|
95
|
-
*
|
|
102
|
+
* parse on the calling thread without spawning workers. File I/O is pipelined.
|
|
96
103
|
*/
|
|
97
104
|
workers?: number;
|
|
105
|
+
/**
|
|
106
|
+
* Sync only these transcripts (e.g. files a watcher saw change) instead of
|
|
107
|
+
* listing every session. Missing files are skipped.
|
|
108
|
+
*/
|
|
109
|
+
files?: readonly string[];
|
|
98
110
|
}
|
|
99
111
|
|
|
100
112
|
function defaultWorkerCount(): number {
|
|
@@ -222,9 +234,11 @@ export async function smokeTestSyncWorker({ timeoutMs = 5_000 }: { timeoutMs?: n
|
|
|
222
234
|
/**
|
|
223
235
|
* Sync all session files to the database.
|
|
224
236
|
*
|
|
225
|
-
* `workers: 1`
|
|
237
|
+
* `workers: 1` pipelines file reads and parses inline, committing in discovery order.
|
|
238
|
+
* Larger pools fan parsing out across workers
|
|
226
239
|
* (one in-flight job per worker) while DB writes and offset bookkeeping stay on
|
|
227
|
-
* the calling thread
|
|
240
|
+
* the calling thread. Bounded batches commit rows and cursors atomically without
|
|
241
|
+
* holding a database transaction open during file I/O.
|
|
228
242
|
* `onProgress` fires once per completed file (skipped files included so the
|
|
229
243
|
* bar walks at a steady rate).
|
|
230
244
|
*/
|
|
@@ -241,27 +255,58 @@ export async function syncAllSessions(opts?: SyncOptions): Promise<{ processed:
|
|
|
241
255
|
});
|
|
242
256
|
}
|
|
243
257
|
|
|
258
|
+
/** Recency tiers for {@link orderForIngest}, newest first. */
|
|
259
|
+
const INGEST_TIERS_MS = [24 * 60 * 60 * 1000, 7 * 24 * 60 * 60 * 1000];
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* Order a full sync so recent activity lands first: transcripts modified in
|
|
263
|
+
* the last day, then the last week, then the rest. Within a tier files go in
|
|
264
|
+
* creation order, so a fork still follows its parent and the parent keeps
|
|
265
|
+
* ownership of the entries the fork copied (first writer wins). The only
|
|
266
|
+
* exception is a from-scratch ingest where a fork was touched more recently
|
|
267
|
+
* than a parent idle for a day: the fork then owns the copied entries, which
|
|
268
|
+
* changes per-session attribution but never totals.
|
|
269
|
+
*/
|
|
270
|
+
async function orderForIngest(files: string[]): Promise<string[]> {
|
|
271
|
+
const now = Date.now();
|
|
272
|
+
const ranked: { file: string; tier: number; born: number; index: number }[] = [];
|
|
273
|
+
for (let start = 0; start < files.length; start += SYNC_METADATA_FILES) {
|
|
274
|
+
const batch = files.slice(start, start + SYNC_METADATA_FILES);
|
|
275
|
+
const stats = await Promise.all(batch.map(file => fs.promises.stat(file).catch(() => null)));
|
|
276
|
+
for (let i = 0; i < batch.length; i++) {
|
|
277
|
+
const stat = stats[i];
|
|
278
|
+
const age = stat ? now - stat.mtimeMs : Number.POSITIVE_INFINITY;
|
|
279
|
+
const tier = INGEST_TIERS_MS.findIndex(limit => age <= limit);
|
|
280
|
+
ranked.push({
|
|
281
|
+
file: batch[i],
|
|
282
|
+
tier: tier === -1 ? INGEST_TIERS_MS.length : tier,
|
|
283
|
+
born: stat ? stat.birthtimeMs || stat.ctimeMs : 0,
|
|
284
|
+
index: start + i,
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
ranked.sort((a, b) => a.tier - b.tier || a.born - b.born || a.index - b.index);
|
|
289
|
+
return ranked.map(entry => entry.file);
|
|
290
|
+
}
|
|
291
|
+
|
|
244
292
|
async function syncAllSessionsLocked(
|
|
245
293
|
opts?: SyncOptions,
|
|
246
294
|
): Promise<{ processed: number; files: number; reconcile: boolean }> {
|
|
247
295
|
await initDb();
|
|
248
296
|
const replay = prepareSessionSync();
|
|
249
297
|
|
|
250
|
-
const files = await listAllSessionFiles();
|
|
251
|
-
// Drop rows for transcripts that no longer exist on disk (deleted temp/test
|
|
252
|
-
// projects) before parsing, so every aggregate reflects live files only.
|
|
253
|
-
pruneOrphanSessions(files);
|
|
298
|
+
const files = opts?.files ?? (await orderForIngest(await listAllSessionFiles()));
|
|
254
299
|
let totalProcessed = 0;
|
|
255
300
|
let filesProcessed = 0;
|
|
256
301
|
let completed = 0;
|
|
257
302
|
let cursor = 0;
|
|
258
303
|
let reconcile = false;
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
304
|
+
let pending: ParsedSession[] = [];
|
|
305
|
+
let pendingRows = 0;
|
|
306
|
+
let failed = false;
|
|
307
|
+
let metadataStart = 0;
|
|
308
|
+
let metadataEnd = 0;
|
|
309
|
+
let metadata: Promise<{ fileStats?: fs.Stats; stored?: FileOffset }[]> = Promise.resolve([]);
|
|
265
310
|
|
|
266
311
|
const report = (sessionFile: string) => {
|
|
267
312
|
completed++;
|
|
@@ -273,53 +318,117 @@ async function syncAllSessionsLocked(
|
|
|
273
318
|
});
|
|
274
319
|
};
|
|
275
320
|
|
|
276
|
-
const
|
|
277
|
-
|
|
321
|
+
const flush = () => {
|
|
322
|
+
if (pending.length === 0) return;
|
|
323
|
+
const batch = pending;
|
|
324
|
+
const applied = applySessionParseResults(batch);
|
|
325
|
+
pending = [];
|
|
326
|
+
pendingRows = 0;
|
|
327
|
+
totalProcessed += applied.processed;
|
|
328
|
+
filesProcessed += applied.files;
|
|
329
|
+
reconcile ||= applied.reconcile;
|
|
330
|
+
// Report only durable progress: callbacks may interrupt the sync.
|
|
331
|
+
for (const { sessionFile } of batch) report(sessionFile);
|
|
332
|
+
};
|
|
333
|
+
|
|
334
|
+
const finish = () => {
|
|
335
|
+
flush();
|
|
336
|
+
// A targeted sync cannot settle reconciliation or backfills (they need
|
|
337
|
+
// every transcript); leave their markers for the next full sync.
|
|
338
|
+
if (opts?.files) return { processed: totalProcessed, files: filesProcessed, reconcile: false };
|
|
339
|
+
completeSessionSync(reconcile);
|
|
340
|
+
markSessionBackfillsComplete();
|
|
341
|
+
return { processed: totalProcessed, files: filesProcessed, reconcile };
|
|
342
|
+
};
|
|
343
|
+
if (files.length === 0) return finish();
|
|
344
|
+
|
|
345
|
+
const prepareFile = async (
|
|
346
|
+
index: number,
|
|
278
347
|
parse: (
|
|
279
348
|
sessionFile: string,
|
|
280
349
|
fromOffset: number,
|
|
281
350
|
state?: SessionParserState,
|
|
282
351
|
replay?: boolean,
|
|
283
352
|
) => Promise<ParseSessionResult>,
|
|
284
|
-
): Promise<
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
353
|
+
): Promise<ParsedSession | null> => {
|
|
354
|
+
if (index >= metadataEnd) {
|
|
355
|
+
const batch = files.slice(index, index + SYNC_METADATA_FILES);
|
|
356
|
+
const offsets = getFileOffsets(batch);
|
|
357
|
+
metadataStart = index;
|
|
358
|
+
metadataEnd = index + batch.length;
|
|
359
|
+
metadata = Promise.all(
|
|
360
|
+
batch.map(async sessionFile => {
|
|
361
|
+
try {
|
|
362
|
+
const fileStats = await fs.promises.stat(sessionFile);
|
|
363
|
+
return { fileStats, stored: offsets.get(sessionFile) };
|
|
364
|
+
} catch {
|
|
365
|
+
return {};
|
|
366
|
+
}
|
|
367
|
+
}),
|
|
368
|
+
);
|
|
291
369
|
}
|
|
292
|
-
|
|
293
|
-
const
|
|
370
|
+
// Capture the position before awaiting: another worker can start the next metadata batch.
|
|
371
|
+
const metadataIndex = index - metadataStart;
|
|
372
|
+
const { fileStats, stored } = (await metadata)[metadataIndex];
|
|
373
|
+
if (failed || !fileStats) return null;
|
|
294
374
|
if (
|
|
295
375
|
!replay &&
|
|
296
376
|
stored?.parserState &&
|
|
297
|
-
stored.lastModified ===
|
|
377
|
+
stored.lastModified === fileStats.mtimeMs &&
|
|
298
378
|
stored.parserState.size === fileStats.size &&
|
|
299
379
|
matchesSessionFile(stored.parserState, fileStats)
|
|
300
380
|
) {
|
|
301
|
-
|
|
302
|
-
return;
|
|
381
|
+
return null;
|
|
303
382
|
}
|
|
304
383
|
|
|
305
|
-
const
|
|
384
|
+
const sessionFile = files[index];
|
|
385
|
+
const unknownIdentity = stored !== undefined && !stored.parserState;
|
|
306
386
|
const fromOffset = unknownIdentity ? 0 : (stored?.offset ?? 0);
|
|
307
387
|
const result = await parse(sessionFile, fromOffset, stored?.parserState, replay);
|
|
308
388
|
if (unknownIdentity && result.parserState) result.reset = true;
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
389
|
+
return {
|
|
390
|
+
sessionFile,
|
|
391
|
+
result,
|
|
392
|
+
// During a full replay, a changed file can be a rewrite even if its old tail checkpoint matches.
|
|
393
|
+
rebuild:
|
|
394
|
+
!stored?.parserState ||
|
|
395
|
+
(replay &&
|
|
396
|
+
(result.parserState?.size !== stored.parserState.size ||
|
|
397
|
+
result.parserState?.mtimeMs !== stored.parserState.mtimeMs)),
|
|
398
|
+
replay,
|
|
399
|
+
};
|
|
400
|
+
};
|
|
401
|
+
|
|
402
|
+
const acceptFile = (sessionFile: string, parsed: ParsedSession | null) => {
|
|
403
|
+
if (!parsed) {
|
|
404
|
+
report(sessionFile);
|
|
405
|
+
return;
|
|
315
406
|
}
|
|
316
|
-
|
|
407
|
+
pending.push(parsed);
|
|
408
|
+
const { result } = parsed;
|
|
409
|
+
pendingRows +=
|
|
410
|
+
result.stats.length +
|
|
411
|
+
result.userStats.length +
|
|
412
|
+
result.userLinks.length +
|
|
413
|
+
result.toolCalls.length +
|
|
414
|
+
result.toolResults.length;
|
|
415
|
+
if (pending.length >= SYNC_BATCH_FILES || pendingRows >= SYNC_BATCH_ROWS) flush();
|
|
317
416
|
};
|
|
318
417
|
|
|
319
418
|
const requestedWorkers = Math.max(1, Math.floor(opts?.workers ?? defaultWorkerCount()));
|
|
320
419
|
if (requestedWorkers === 1) {
|
|
321
|
-
for (
|
|
322
|
-
await
|
|
420
|
+
for (let start = 0; start < files.length; start += SYNC_INLINE_READS) {
|
|
421
|
+
const results = await Promise.allSettled(
|
|
422
|
+
Array.from({ length: Math.min(SYNC_INLINE_READS, files.length - start) }, (_, offset) =>
|
|
423
|
+
prepareFile(start + offset, parseSessionFile),
|
|
424
|
+
),
|
|
425
|
+
);
|
|
426
|
+
// Preserve fork ownership order and drain reads before a callback can release the sync lock.
|
|
427
|
+
for (let offset = 0; offset < results.length; offset++) {
|
|
428
|
+
const result = results[offset];
|
|
429
|
+
if (result.status === "rejected") throw result.reason;
|
|
430
|
+
acceptFile(files[start + offset], result.value);
|
|
431
|
+
}
|
|
323
432
|
}
|
|
324
433
|
return finish();
|
|
325
434
|
}
|
|
@@ -327,21 +436,30 @@ async function syncAllSessionsLocked(
|
|
|
327
436
|
const poolSize = Math.min(files.length, requestedWorkers);
|
|
328
437
|
|
|
329
438
|
const handles: WorkerHandle[] = [];
|
|
330
|
-
for (let i = 0; i < poolSize; i++) handles.push(spawnWorker());
|
|
331
439
|
|
|
332
440
|
async function drain(handle: WorkerHandle): Promise<void> {
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
441
|
+
try {
|
|
442
|
+
while (!failed) {
|
|
443
|
+
const idx = cursor++;
|
|
444
|
+
if (idx >= files.length) return;
|
|
445
|
+
const parsed = await prepareFile(idx, (file, fromOffset, parserState, replay) =>
|
|
446
|
+
dispatch(handle, { sessionFile: file, fromOffset, parserState, replay }),
|
|
447
|
+
);
|
|
448
|
+
if (!failed) acceptFile(files[idx], parsed);
|
|
449
|
+
}
|
|
450
|
+
} catch (error) {
|
|
451
|
+
failed = true;
|
|
452
|
+
throw error;
|
|
340
453
|
}
|
|
341
454
|
}
|
|
342
455
|
|
|
343
456
|
try {
|
|
344
|
-
|
|
457
|
+
for (let i = 0; i < poolSize; i++) handles.push(spawnWorker());
|
|
458
|
+
// Drain in-flight work before releasing the sync lock, even after a failed batch.
|
|
459
|
+
const results = await Promise.allSettled(handles.map(drain));
|
|
460
|
+
for (const result of results) {
|
|
461
|
+
if (result.status === "rejected") throw result.reason;
|
|
462
|
+
}
|
|
345
463
|
} finally {
|
|
346
464
|
for (const handle of handles) handle.worker.terminate();
|
|
347
465
|
}
|
|
@@ -351,92 +469,31 @@ async function syncAllSessionsLocked(
|
|
|
351
469
|
|
|
352
470
|
const HOUR_MS = 60 * 60 * 1000;
|
|
353
471
|
const DAY_MS = 24 * HOUR_MS;
|
|
354
|
-
const FIVE_MIN_MS = 5 * 60 * 1000;
|
|
355
472
|
|
|
356
473
|
type TimeRange = "1h" | "24h" | "7d" | "30d" | "90d" | "all";
|
|
357
474
|
|
|
358
|
-
interface TimeRangeConfig {
|
|
359
|
-
timeSeriesHours: number;
|
|
360
|
-
timeSeriesBucketMs: number;
|
|
361
|
-
modelSeriesDays: number;
|
|
362
|
-
modelSeriesBucketMs: number;
|
|
363
|
-
modelPerformanceDays: number;
|
|
364
|
-
modelPerformanceBucketMs: number;
|
|
365
|
-
costSeriesDays: number;
|
|
366
|
-
cutoff: number | null;
|
|
367
|
-
}
|
|
368
|
-
|
|
369
475
|
const DEFAULT_TIME_RANGE: TimeRange = "24h";
|
|
370
476
|
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
costSeriesDays: 1,
|
|
380
|
-
},
|
|
381
|
-
"24h": {
|
|
382
|
-
timeSeriesHours: 24,
|
|
383
|
-
timeSeriesBucketMs: HOUR_MS,
|
|
384
|
-
modelSeriesDays: 1,
|
|
385
|
-
modelSeriesBucketMs: HOUR_MS,
|
|
386
|
-
modelPerformanceDays: 1,
|
|
387
|
-
modelPerformanceBucketMs: HOUR_MS,
|
|
388
|
-
costSeriesDays: 1,
|
|
389
|
-
},
|
|
390
|
-
"7d": {
|
|
391
|
-
timeSeriesHours: 24 * 7,
|
|
392
|
-
timeSeriesBucketMs: DAY_MS,
|
|
393
|
-
modelSeriesDays: 7,
|
|
394
|
-
modelSeriesBucketMs: DAY_MS,
|
|
395
|
-
modelPerformanceDays: 7,
|
|
396
|
-
modelPerformanceBucketMs: DAY_MS,
|
|
397
|
-
costSeriesDays: 7,
|
|
398
|
-
},
|
|
399
|
-
"30d": {
|
|
400
|
-
timeSeriesHours: 24 * 30,
|
|
401
|
-
timeSeriesBucketMs: DAY_MS,
|
|
402
|
-
modelSeriesDays: 30,
|
|
403
|
-
modelSeriesBucketMs: DAY_MS,
|
|
404
|
-
modelPerformanceDays: 30,
|
|
405
|
-
modelPerformanceBucketMs: DAY_MS,
|
|
406
|
-
costSeriesDays: 30,
|
|
407
|
-
},
|
|
408
|
-
"90d": {
|
|
409
|
-
timeSeriesHours: 24 * 90,
|
|
410
|
-
timeSeriesBucketMs: DAY_MS,
|
|
411
|
-
modelSeriesDays: 90,
|
|
412
|
-
modelSeriesBucketMs: DAY_MS,
|
|
413
|
-
modelPerformanceDays: 90,
|
|
414
|
-
modelPerformanceBucketMs: DAY_MS,
|
|
415
|
-
costSeriesDays: 90,
|
|
416
|
-
},
|
|
417
|
-
all: {
|
|
418
|
-
timeSeriesHours: 24 * 3650,
|
|
419
|
-
timeSeriesBucketMs: DAY_MS,
|
|
420
|
-
modelSeriesDays: 3650,
|
|
421
|
-
modelSeriesBucketMs: DAY_MS,
|
|
422
|
-
modelPerformanceDays: 3650,
|
|
423
|
-
modelPerformanceBucketMs: DAY_MS,
|
|
424
|
-
costSeriesDays: 3650,
|
|
425
|
-
},
|
|
477
|
+
/** Span and series bucket per range; mirrored by the client's `data/range.ts`. */
|
|
478
|
+
const TIME_RANGES: Record<TimeRange, { spanMs: number | null; bucketMs: number }> = {
|
|
479
|
+
"1h": { spanMs: HOUR_MS, bucketMs: 5 * 60 * 1000 },
|
|
480
|
+
"24h": { spanMs: DAY_MS, bucketMs: HOUR_MS },
|
|
481
|
+
"7d": { spanMs: 7 * DAY_MS, bucketMs: DAY_MS },
|
|
482
|
+
"30d": { spanMs: 30 * DAY_MS, bucketMs: DAY_MS },
|
|
483
|
+
"90d": { spanMs: 90 * DAY_MS, bucketMs: DAY_MS },
|
|
484
|
+
all: { spanMs: null, bucketMs: DAY_MS },
|
|
426
485
|
};
|
|
427
486
|
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
const config = TIME_RANGE_TO_CONFIG[normalized as TimeRange];
|
|
431
|
-
if (config) {
|
|
432
|
-
const cutoff = normalized === "all" ? null : Date.now() - Math.max(1, config.timeSeriesHours * 60 * 60 * 1000);
|
|
433
|
-
return { ...config, cutoff };
|
|
434
|
-
}
|
|
487
|
+
/** Most folders the projects payload carries (busiest first); the tail is thousands of temp dirs. */
|
|
488
|
+
const FOLDER_LIMIT = 2000;
|
|
435
489
|
|
|
436
|
-
|
|
490
|
+
/** Resolve a `?range=` value (default 24h) to its cutoff and series bucket size. */
|
|
491
|
+
export function getTimeRangeConfig(range?: string | null): RangeWindow {
|
|
492
|
+
const normalized = range?.trim().toLowerCase() ?? DEFAULT_TIME_RANGE;
|
|
493
|
+
const config = TIME_RANGES[normalized as TimeRange] ?? TIME_RANGES[DEFAULT_TIME_RANGE];
|
|
437
494
|
return {
|
|
438
|
-
|
|
439
|
-
|
|
495
|
+
cutoff: config.spanMs === null ? null : Date.now() - config.spanMs,
|
|
496
|
+
bucketMs: config.bucketMs,
|
|
440
497
|
};
|
|
441
498
|
}
|
|
442
499
|
|
|
@@ -445,26 +502,16 @@ export function getTimeRangeConfig(range?: string | null): TimeRangeConfig {
|
|
|
445
502
|
*/
|
|
446
503
|
export async function getDashboardStats(range?: string | null): Promise<DashboardStats> {
|
|
447
504
|
await initDb();
|
|
448
|
-
const
|
|
449
|
-
timeSeriesHours,
|
|
450
|
-
timeSeriesBucketMs,
|
|
451
|
-
modelSeriesDays,
|
|
452
|
-
modelSeriesBucketMs,
|
|
453
|
-
modelPerformanceDays,
|
|
454
|
-
modelPerformanceBucketMs,
|
|
455
|
-
costSeriesDays,
|
|
456
|
-
cutoff,
|
|
457
|
-
} = getTimeRangeConfig(range);
|
|
458
|
-
|
|
505
|
+
const window = getTimeRangeConfig(range);
|
|
459
506
|
return {
|
|
460
|
-
overall: getOverallStats(cutoff
|
|
461
|
-
byModel: getStatsByModel(cutoff
|
|
462
|
-
byFolder: getStatsByFolder(cutoff
|
|
463
|
-
byAgentType: getStatsByAgentType(cutoff
|
|
464
|
-
timeSeries: getTimeSeries(
|
|
465
|
-
modelSeries: getModelTimeSeries(
|
|
466
|
-
modelPerformanceSeries: getModelPerformanceSeries(
|
|
467
|
-
costSeries: getCostTimeSeries(
|
|
507
|
+
overall: getOverallStats(window.cutoff),
|
|
508
|
+
byModel: getStatsByModel(window.cutoff),
|
|
509
|
+
byFolder: getStatsByFolder(window.cutoff, FOLDER_LIMIT),
|
|
510
|
+
byAgentType: getStatsByAgentType(window.cutoff),
|
|
511
|
+
timeSeries: getTimeSeries(window),
|
|
512
|
+
modelSeries: getModelTimeSeries(window),
|
|
513
|
+
modelPerformanceSeries: getModelPerformanceSeries(window),
|
|
514
|
+
costSeries: getCostTimeSeries(window.cutoff),
|
|
468
515
|
};
|
|
469
516
|
}
|
|
470
517
|
|
|
@@ -472,12 +519,11 @@ export async function getOverviewStats(
|
|
|
472
519
|
range?: string | null,
|
|
473
520
|
): Promise<Pick<DashboardStats, "overall" | "byAgentType" | "timeSeries">> {
|
|
474
521
|
await initDb();
|
|
475
|
-
const
|
|
476
|
-
|
|
522
|
+
const window = getTimeRangeConfig(range);
|
|
477
523
|
return {
|
|
478
|
-
overall: getOverallStats(cutoff
|
|
479
|
-
byAgentType: getStatsByAgentType(cutoff
|
|
480
|
-
timeSeries: getTimeSeries(
|
|
524
|
+
overall: getOverallStats(window.cutoff),
|
|
525
|
+
byAgentType: getStatsByAgentType(window.cutoff),
|
|
526
|
+
timeSeries: getTimeSeries(window),
|
|
481
527
|
};
|
|
482
528
|
}
|
|
483
529
|
|
|
@@ -485,29 +531,22 @@ export async function getModelDashboardStats(
|
|
|
485
531
|
range?: string | null,
|
|
486
532
|
): Promise<Pick<DashboardStats, "byModel" | "modelSeries" | "modelPerformanceSeries">> {
|
|
487
533
|
await initDb();
|
|
488
|
-
const
|
|
489
|
-
getTimeRangeConfig(range);
|
|
490
|
-
|
|
534
|
+
const window = getTimeRangeConfig(range);
|
|
491
535
|
return {
|
|
492
|
-
byModel: getStatsByModel(cutoff
|
|
493
|
-
modelSeries: getModelTimeSeries(
|
|
494
|
-
modelPerformanceSeries: getModelPerformanceSeries(
|
|
536
|
+
byModel: getStatsByModel(window.cutoff),
|
|
537
|
+
modelSeries: getModelTimeSeries(window),
|
|
538
|
+
modelPerformanceSeries: getModelPerformanceSeries(window),
|
|
495
539
|
};
|
|
496
540
|
}
|
|
497
541
|
|
|
498
542
|
export async function getCostDashboardStats(range?: string | null): Promise<Pick<DashboardStats, "costSeries">> {
|
|
499
543
|
await initDb();
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
return {
|
|
503
|
-
costSeries: getCostTimeSeries(costSeriesDays, cutoff),
|
|
504
|
-
};
|
|
544
|
+
return { costSeries: getCostTimeSeries(getTimeRangeConfig(range).cutoff) };
|
|
505
545
|
}
|
|
506
546
|
|
|
507
547
|
export async function getFolderStats(range?: string | null): Promise<FolderStats[]> {
|
|
508
548
|
await initDb();
|
|
509
|
-
|
|
510
|
-
return getStatsByFolder(cutoff ?? undefined);
|
|
549
|
+
return getStatsByFolder(getTimeRangeConfig(range).cutoff, FOLDER_LIMIT);
|
|
511
550
|
}
|
|
512
551
|
|
|
513
552
|
export async function getRecentRequests(limit?: number): Promise<MessageStats[]> {
|
|
@@ -517,8 +556,7 @@ export async function getRecentRequests(limit?: number): Promise<MessageStats[]>
|
|
|
517
556
|
|
|
518
557
|
export async function getRecentErrors(range?: string | null, limit?: number): Promise<MessageStats[]> {
|
|
519
558
|
await initDb();
|
|
520
|
-
|
|
521
|
-
return dbGetRecentErrors(limit, cutoff);
|
|
559
|
+
return dbGetRecentErrors(limit, getTimeRangeConfig(range).cutoff);
|
|
522
560
|
}
|
|
523
561
|
|
|
524
562
|
export async function getRequestDetails(id: number): Promise<RequestDetails | null> {
|
|
@@ -527,16 +565,12 @@ export async function getRequestDetails(id: number): Promise<RequestDetails | nu
|
|
|
527
565
|
if (!msg) return null;
|
|
528
566
|
|
|
529
567
|
const entry = await getSessionEntry(msg.sessionFile, msg.entryId);
|
|
530
|
-
if (entry?.type !== "message") return null;
|
|
531
|
-
|
|
532
|
-
// TODO: Get parent/context messages?
|
|
533
|
-
// For now we return the single entry which contains the assistant response.
|
|
534
|
-
// The user prompt is likely the parent.
|
|
568
|
+
if (entry?.type !== "message" || !("message" in entry)) return null;
|
|
535
569
|
|
|
536
570
|
return {
|
|
537
571
|
...msg,
|
|
538
572
|
messages: [entry],
|
|
539
|
-
output:
|
|
573
|
+
output: entry.message,
|
|
540
574
|
};
|
|
541
575
|
}
|
|
542
576
|
|
|
@@ -548,51 +582,79 @@ export async function getTotalMessageCount(): Promise<number> {
|
|
|
548
582
|
return getMessageCount();
|
|
549
583
|
}
|
|
550
584
|
|
|
551
|
-
|
|
585
|
+
/**
|
|
586
|
+
* Get the tools dashboard payload: per-tool totals, per-(tool, model)
|
|
587
|
+
* breakdown, and the call time series.
|
|
588
|
+
*/
|
|
589
|
+
export async function getToolDashboardStats(range?: string | null): Promise<ToolDashboardStats> {
|
|
552
590
|
await initDb();
|
|
553
|
-
const
|
|
591
|
+
const window = getTimeRangeConfig(range);
|
|
554
592
|
return {
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
593
|
+
byTool: getToolStats(window.cutoff),
|
|
594
|
+
byToolModel: getToolStatsByModel(window.cutoff),
|
|
595
|
+
series: getToolTimeSeries(window),
|
|
558
596
|
};
|
|
559
597
|
}
|
|
560
598
|
|
|
561
599
|
/**
|
|
562
|
-
* Get the
|
|
563
|
-
*
|
|
600
|
+
* Get the providers dashboard payload: per-provider totals, peak-burn-hours
|
|
601
|
+
* histogram and provider token time series. Subscription windows are served
|
|
602
|
+
* separately by {@link getProviderWindowStats}.
|
|
564
603
|
*/
|
|
565
|
-
export async function
|
|
604
|
+
export async function getProviderDashboardStats(range?: string | null): Promise<ProviderDashboardStats> {
|
|
566
605
|
await initDb();
|
|
567
|
-
const
|
|
606
|
+
const window = getTimeRangeConfig(range);
|
|
568
607
|
return {
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
series:
|
|
608
|
+
providers: getStatsByProvider(window.cutoff),
|
|
609
|
+
hourly: getProviderHourlyBurn(window.cutoff),
|
|
610
|
+
series: getProviderTimeSeries(window),
|
|
572
611
|
};
|
|
573
612
|
}
|
|
574
613
|
|
|
614
|
+
const USAGE_CACHE_TTL_MS = 60_000;
|
|
615
|
+
let usageCache: { key: string; at: number; data: Promise<UsageDataSnapshot> } | null = null;
|
|
616
|
+
|
|
575
617
|
/**
|
|
576
|
-
*
|
|
577
|
-
*
|
|
578
|
-
*
|
|
618
|
+
* Usage snapshots since `sinceMs`, memoized for a minute per hour-aligned
|
|
619
|
+
* cutoff and source database: the broker fetch takes seconds and the
|
|
620
|
+
* dashboard asks on every range switch and live refresh.
|
|
621
|
+
*/
|
|
622
|
+
function cachedUsageData(sinceMs: number): Promise<UsageDataSnapshot> {
|
|
623
|
+
const sinceHour = Math.floor(sinceMs / HOUR_MS) * HOUR_MS;
|
|
624
|
+
const key = `${getAgentDbPath()}:${sinceHour}`;
|
|
625
|
+
const now = Date.now();
|
|
626
|
+
if (usageCache?.key === key && now - usageCache.at < USAGE_CACHE_TTL_MS) {
|
|
627
|
+
return usageCache.data;
|
|
628
|
+
}
|
|
629
|
+
const data = fetchUsageData(sinceHour);
|
|
630
|
+
usageCache = { key, at: now, data };
|
|
631
|
+
data.catch(() => {
|
|
632
|
+
if (usageCache?.data === data) usageCache = null;
|
|
633
|
+
});
|
|
634
|
+
return data;
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
/**
|
|
638
|
+
* Subscription-window analytics derived from recorded usage-limit snapshots:
|
|
639
|
+
* insights for every provider window, plus utilization series for `provider`
|
|
640
|
+
* only (all series can number in the thousands).
|
|
579
641
|
*
|
|
580
642
|
* Window token estimates use broker-held fleet token burn when a broker is
|
|
581
643
|
* configured — the window fractions cover every install sharing the broker's
|
|
582
644
|
* credentials, so dividing them into local-only tokens would undercount.
|
|
583
645
|
*/
|
|
584
|
-
export async function
|
|
646
|
+
export async function getProviderWindowStats(
|
|
647
|
+
range?: string | null,
|
|
648
|
+
provider?: string | null,
|
|
649
|
+
): Promise<ProviderWindowStats> {
|
|
585
650
|
await initDb();
|
|
586
|
-
const {
|
|
587
|
-
const
|
|
588
|
-
const
|
|
589
|
-
|
|
651
|
+
const { cutoff } = getTimeRangeConfig(range);
|
|
652
|
+
const usage = await cachedUsageData(cutoff ?? 0);
|
|
653
|
+
const tokensByProvider =
|
|
654
|
+
usage.fleetTokensByProvider ?? new Map(getStatsByProvider(cutoff).map(p => [p.provider, p.totalTokens]));
|
|
590
655
|
const { usageSeries, windowInsights } = computeUsageWindowStats(usage.rows, tokensByProvider);
|
|
591
656
|
return {
|
|
592
|
-
providers,
|
|
593
|
-
hourly: getProviderHourlyBurn(cutoff ?? undefined),
|
|
594
|
-
series: getProviderTimeSeries(modelSeriesDays, cutoff, modelSeriesBucketMs),
|
|
595
|
-
usageSeries,
|
|
596
657
|
windowInsights,
|
|
658
|
+
usageSeries: provider ? usageSeries.filter(series => series.provider === provider) : [],
|
|
597
659
|
};
|
|
598
660
|
}
|