@vib-rato/stats 0.16.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 (69) hide show
  1. package/README.md +88 -0
  2. package/build.ts +84 -0
  3. package/dist/client/index.css +1 -0
  4. package/dist/client/index.html +13 -0
  5. package/dist/client/index.js +257 -0
  6. package/dist/client/styles.css +1141 -0
  7. package/dist/types/aggregator.d.ts +65 -0
  8. package/dist/types/client/App.d.ts +1 -0
  9. package/dist/types/client/api.d.ts +16 -0
  10. package/dist/types/client/components/BehaviorChart.d.ts +6 -0
  11. package/dist/types/client/components/BehaviorModelsTable.d.ts +7 -0
  12. package/dist/types/client/components/BehaviorSummary.d.ts +7 -0
  13. package/dist/types/client/components/ChartsContainer.d.ts +7 -0
  14. package/dist/types/client/components/CostChart.d.ts +6 -0
  15. package/dist/types/client/components/CostSummary.d.ts +6 -0
  16. package/dist/types/client/components/Header.d.ts +12 -0
  17. package/dist/types/client/components/ModelsTable.d.ts +8 -0
  18. package/dist/types/client/components/RequestDetail.d.ts +6 -0
  19. package/dist/types/client/components/RequestList.d.ts +8 -0
  20. package/dist/types/client/components/StatsGrid.d.ts +6 -0
  21. package/dist/types/client/components/chart-shared.d.ts +190 -0
  22. package/dist/types/client/components/models-table-shared.d.ts +195 -0
  23. package/dist/types/client/components/range-meta.d.ts +21 -0
  24. package/dist/types/client/index.d.ts +1 -0
  25. package/dist/types/client/types.d.ts +62 -0
  26. package/dist/types/client/useSystemTheme.d.ts +2 -0
  27. package/dist/types/compiled-client-assets.d.ts +3 -0
  28. package/dist/types/db.d.ts +93 -0
  29. package/dist/types/index.d.ts +5 -0
  30. package/dist/types/parser.d.ts +40 -0
  31. package/dist/types/server.d.ts +18 -0
  32. package/dist/types/shared-types.d.ts +192 -0
  33. package/dist/types/sync-worker.d.ts +31 -0
  34. package/dist/types/types.d.ts +120 -0
  35. package/dist/types/user-metrics.d.ts +72 -0
  36. package/package.json +92 -0
  37. package/src/aggregator.ts +454 -0
  38. package/src/client/App.tsx +221 -0
  39. package/src/client/api.ts +71 -0
  40. package/src/client/components/BehaviorChart.tsx +189 -0
  41. package/src/client/components/BehaviorModelsTable.tsx +342 -0
  42. package/src/client/components/BehaviorSummary.tsx +95 -0
  43. package/src/client/components/ChartsContainer.tsx +232 -0
  44. package/src/client/components/CostChart.tsx +171 -0
  45. package/src/client/components/CostSummary.tsx +53 -0
  46. package/src/client/components/Header.tsx +73 -0
  47. package/src/client/components/ModelsTable.tsx +265 -0
  48. package/src/client/components/RequestDetail.tsx +172 -0
  49. package/src/client/components/RequestList.tsx +73 -0
  50. package/src/client/components/StatsGrid.tsx +135 -0
  51. package/src/client/components/chart-shared.tsx +331 -0
  52. package/src/client/components/models-table-shared.tsx +275 -0
  53. package/src/client/components/range-meta.ts +72 -0
  54. package/src/client/css.d.ts +1 -0
  55. package/src/client/index.tsx +6 -0
  56. package/src/client/styles.css +318 -0
  57. package/src/client/types.ts +78 -0
  58. package/src/client/useSystemTheme.ts +31 -0
  59. package/src/compiled-client-assets.ts +96 -0
  60. package/src/db.ts +1100 -0
  61. package/src/embedded-client.generated.txt +7 -0
  62. package/src/index.ts +182 -0
  63. package/src/parser.ts +334 -0
  64. package/src/server.ts +332 -0
  65. package/src/shared-types.ts +204 -0
  66. package/src/sync-worker.ts +40 -0
  67. package/src/types.ts +125 -0
  68. package/src/user-metrics.ts +686 -0
  69. package/tailwind.config.js +54 -0
@@ -0,0 +1,318 @@
1
+ @import "tailwindcss/index.css";
2
+ :root {
3
+ color-scheme: light;
4
+ --bg-page: #f8fafc;
5
+ --bg-surface: #ffffff;
6
+ --bg-elevated: #f1f5f9;
7
+ --bg-hover: rgba(15, 23, 42, 0.04);
8
+ --bg-active: rgba(15, 23, 42, 0.08);
9
+
10
+ --border-subtle: rgba(15, 23, 42, 0.08);
11
+ --border-default: rgba(15, 23, 42, 0.14);
12
+
13
+ --text-primary: #0f172a;
14
+ --text-secondary: #334155;
15
+ --text-muted: #64748b;
16
+
17
+ --accent-pink: #002f6d;
18
+ --accent-pink-glow: rgba(0, 47, 109, 0.28);
19
+ --accent-cyan: #4f6fa3;
20
+ --accent-cyan-glow: rgba(79, 111, 163, 0.24);
21
+ --accent-violet: #7a8ca8;
22
+ --accent-green: #16a34a;
23
+ --accent-amber: #d97706;
24
+ --accent-red: #dc2626;
25
+
26
+ --tab-active-highlight: inset 0 1px 0 rgba(15, 23, 42, 0.08);
27
+
28
+ --radius-sm: 6px;
29
+ --radius-md: 10px;
30
+ --radius-lg: 14px;
31
+
32
+ --brand-blue: #002f6d;
33
+ --brand-gray: #bcbec0;
34
+ --brand-gradient: linear-gradient(120deg, #003d96 0%, #002f6d 55%, #002d5c 100%);
35
+ --font-title: "NanumSquare", "NanumSquareRound", "Nanum Gothic", -apple-system, "Apple SD Gothic Neo",
36
+ "Malgun Gothic", "Segoe UI", sans-serif;
37
+ --font-body: "KoPubDotum", "KoPub Dotum", "KoPubWorldDotum", "Nanum Gothic", -apple-system, "Apple SD Gothic Neo",
38
+ "Malgun Gothic", "Segoe UI", sans-serif;
39
+ }
40
+
41
+ @media (prefers-color-scheme: dark) {
42
+ :root {
43
+ color-scheme: dark;
44
+ --bg-page: #0b1220;
45
+ --bg-surface: #101a2e;
46
+ --bg-elevated: #16233d;
47
+ --bg-hover: rgba(255, 255, 255, 0.03);
48
+ --bg-active: rgba(255, 255, 255, 0.06);
49
+
50
+ --border-subtle: rgba(127, 166, 230, 0.12);
51
+ --border-default: rgba(127, 166, 230, 0.2);
52
+
53
+ --text-primary: #f8fafc;
54
+ --text-secondary: #94a3b8;
55
+ --text-muted: #64748b;
56
+
57
+ --accent-pink: #7fa6e6;
58
+ --accent-pink-glow: rgba(127, 166, 230, 0.35);
59
+ --accent-cyan: #8fb3e8;
60
+ --accent-cyan-glow: rgba(143, 179, 232, 0.3);
61
+ --accent-violet: #a9b7cc;
62
+ --accent-green: #4ade80;
63
+ --accent-amber: #fbbf24;
64
+ --accent-red: #f87171;
65
+
66
+ --tab-active-highlight: inset 0 1px 0 rgba(255, 255, 255, 0.05);
67
+ }
68
+ }
69
+
70
+ @layer base {
71
+ * {
72
+ box-sizing: border-box;
73
+ }
74
+
75
+ html {
76
+ -webkit-font-smoothing: antialiased;
77
+ -moz-osx-font-smoothing: grayscale;
78
+ }
79
+
80
+ body {
81
+ font-family: var(--font-body);
82
+ background: var(--bg-page);
83
+ color: var(--text-primary);
84
+ margin: 0;
85
+ min-height: 100vh;
86
+ }
87
+
88
+ h1, h2, h3, .title {
89
+ font-family: var(--font-title);
90
+ }
91
+
92
+ /* Custom scrollbar */
93
+ ::-webkit-scrollbar {
94
+ width: 8px;
95
+ height: 8px;
96
+ }
97
+
98
+ ::-webkit-scrollbar-track {
99
+ background: transparent;
100
+ }
101
+
102
+ ::-webkit-scrollbar-thumb {
103
+ background: var(--border-default);
104
+ border-radius: 4px;
105
+ }
106
+
107
+ ::-webkit-scrollbar-thumb:hover {
108
+ background: var(--text-muted);
109
+ }
110
+ }
111
+
112
+ @layer components {
113
+ .surface {
114
+ background: var(--bg-surface);
115
+ border: 1px solid var(--border-subtle);
116
+ border-radius: var(--radius-lg);
117
+ }
118
+
119
+ .surface-elevated {
120
+ background: var(--bg-elevated);
121
+ border: 1px solid var(--border-default);
122
+ border-radius: var(--radius-lg);
123
+ }
124
+
125
+ .btn {
126
+ display: inline-flex;
127
+ align-items: center;
128
+ justify-content: center;
129
+ gap: 8px;
130
+ padding: 8px 16px;
131
+ font-size: 14px;
132
+ font-weight: 500;
133
+ border-radius: var(--radius-md);
134
+ border: none;
135
+ cursor: pointer;
136
+ transition: all 0.15s ease;
137
+ }
138
+
139
+ .btn-primary {
140
+ background: var(--brand-gradient);
141
+ color: white;
142
+ box-shadow: 0 0 20px var(--accent-pink-glow);
143
+ }
144
+
145
+ .btn-primary:hover {
146
+ transform: translateY(-1px);
147
+ box-shadow: 0 4px 24px var(--accent-pink-glow);
148
+ }
149
+
150
+ .btn-primary:disabled {
151
+ opacity: 0.6;
152
+ cursor: not-allowed;
153
+ transform: none;
154
+ }
155
+
156
+ .btn-secondary {
157
+ background: var(--bg-elevated);
158
+ color: var(--text-primary);
159
+ border: 1px solid var(--border-default);
160
+ }
161
+
162
+ .btn-secondary:hover {
163
+ background: var(--bg-hover);
164
+ border-color: var(--border-default);
165
+ }
166
+
167
+ .tab-btn {
168
+ padding: 6px 16px;
169
+ font-size: 14px;
170
+ font-weight: 500;
171
+ color: var(--text-muted);
172
+ background: transparent;
173
+ border: none;
174
+ border-radius: var(--radius-md);
175
+ cursor: pointer;
176
+ transition: all 0.15s ease;
177
+ }
178
+
179
+ .tab-btn:hover {
180
+ color: var(--text-primary);
181
+ background: var(--bg-hover);
182
+ }
183
+
184
+ .tab-btn.active {
185
+ color: var(--text-primary);
186
+ background: var(--bg-elevated);
187
+ box-shadow: var(--tab-active-highlight);
188
+ }
189
+
190
+ .stat-card {
191
+ background: var(--bg-surface);
192
+ border: 1px solid var(--border-subtle);
193
+ border-radius: var(--radius-lg);
194
+ padding: 20px;
195
+ transition: all 0.2s ease;
196
+ }
197
+
198
+ .stat-card:hover {
199
+ border-color: var(--border-default);
200
+ background: var(--bg-elevated);
201
+ }
202
+
203
+ .badge {
204
+ display: inline-flex;
205
+ align-items: center;
206
+ padding: 3px 10px;
207
+ font-size: 12px;
208
+ font-weight: 500;
209
+ border-radius: 100px;
210
+ }
211
+
212
+ .badge-success {
213
+ background: rgba(74, 222, 128, 0.15);
214
+ color: var(--accent-green);
215
+ }
216
+
217
+ .badge-error {
218
+ background: rgba(248, 113, 113, 0.15);
219
+ color: var(--accent-red);
220
+ }
221
+
222
+ .badge-info {
223
+ background: rgba(143, 179, 232, 0.15);
224
+ color: var(--accent-cyan);
225
+ }
226
+
227
+ .badge-warning {
228
+ background: rgba(251, 191, 36, 0.15);
229
+ color: var(--accent-amber);
230
+ }
231
+
232
+ .table-header {
233
+ font-size: 11px;
234
+ font-weight: 600;
235
+ text-transform: uppercase;
236
+ letter-spacing: 0.05em;
237
+ color: var(--text-muted);
238
+ }
239
+
240
+ .table-row {
241
+ transition: background 0.15s ease;
242
+ }
243
+
244
+ .table-row:hover {
245
+ background: var(--bg-hover);
246
+ }
247
+
248
+ .gradient-text {
249
+ background: linear-gradient(135deg, var(--accent-pink) 0%, var(--accent-cyan) 100%);
250
+ -webkit-background-clip: text;
251
+ -webkit-text-fill-color: transparent;
252
+ background-clip: text;
253
+ }
254
+
255
+ .gradient-border {
256
+ position: relative;
257
+ }
258
+
259
+ .gradient-border::before {
260
+ content: '';
261
+ position: absolute;
262
+ inset: 0;
263
+ padding: 1px;
264
+ border-radius: inherit;
265
+ background: linear-gradient(135deg, var(--accent-pink), var(--accent-cyan));
266
+ -webkit-mask: linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0);
267
+ mask: linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0);
268
+ -webkit-mask-composite: xor;
269
+ mask-composite: exclude;
270
+ pointer-events: none;
271
+ }
272
+
273
+ .glow-pink {
274
+ box-shadow: 0 0 30px var(--accent-pink-glow);
275
+ }
276
+
277
+ .glow-cyan {
278
+ box-shadow: 0 0 30px var(--accent-cyan-glow);
279
+ }
280
+ }
281
+
282
+ @layer utilities {
283
+ .text-balance {
284
+ text-wrap: balance;
285
+ }
286
+
287
+ .animate-fade-in {
288
+ animation: fadeIn 0.3s ease-out;
289
+ }
290
+
291
+ .animate-slide-up {
292
+ animation: slideUp 0.3s ease-out;
293
+ }
294
+
295
+ @keyframes fadeIn {
296
+ from { opacity: 0; }
297
+ to { opacity: 1; }
298
+ }
299
+
300
+ @keyframes slideUp {
301
+ from {
302
+ opacity: 0;
303
+ transform: translateY(10px);
304
+ }
305
+ to {
306
+ opacity: 1;
307
+ transform: translateY(0);
308
+ }
309
+ }
310
+
311
+ .spin {
312
+ animation: spin 1s linear infinite;
313
+ }
314
+
315
+ @keyframes spin {
316
+ to { transform: rotate(360deg); }
317
+ }
318
+ }
@@ -0,0 +1,78 @@
1
+ /**
2
+ * Client-side type definitions.
3
+ *
4
+ * Shared shapes (aggregations, time-series, dashboard payloads) live in
5
+ * `../shared-types` and are re-exported here. The types declared inline below
6
+ * are deliberately client-only because:
7
+ * - `Usage` is redeclared locally so the client bundle avoids importing
8
+ * `@vib-rato/ai` (the server-side AI types package).
9
+ * - `MessageStats.stopReason` is widened from the server's `StopReason`
10
+ * enum to `string`, again to keep the client free of pi-ai types.
11
+ * - `TimeRange`, `OverviewStats`, `ModelDashboardStats`,
12
+ * `CostDashboardStats` are UI-only view shapes the server never produces.
13
+ */
14
+
15
+ import type {
16
+ AggregatedStats,
17
+ CostTimeSeriesPoint,
18
+ ModelPerformancePoint,
19
+ ModelStats,
20
+ ModelTimeSeriesPoint,
21
+ TimeSeriesPoint,
22
+ } from "../shared-types";
23
+
24
+ export * from "../shared-types";
25
+
26
+ export interface Usage {
27
+ input: number;
28
+ output: number;
29
+ cacheRead: number;
30
+ cacheWrite: number;
31
+ totalTokens: number;
32
+ premiumRequests?: number;
33
+ cost: {
34
+ input: number;
35
+ output: number;
36
+ cacheRead: number;
37
+ cacheWrite: number;
38
+ total: number;
39
+ };
40
+ }
41
+
42
+ export interface MessageStats {
43
+ id?: number;
44
+ sessionFile: string;
45
+ entryId: string;
46
+ folder: string;
47
+ model: string;
48
+ provider: string;
49
+ api: string;
50
+ timestamp: number;
51
+ duration: number | null;
52
+ ttft: number | null;
53
+ stopReason: string;
54
+ errorMessage: string | null;
55
+ usage: Usage;
56
+ }
57
+
58
+ export interface RequestDetails extends MessageStats {
59
+ messages: unknown[];
60
+ output: unknown;
61
+ }
62
+
63
+ export type TimeRange = "1h" | "24h" | "7d" | "30d" | "90d" | "all";
64
+
65
+ export interface OverviewStats {
66
+ overall: AggregatedStats;
67
+ timeSeries: TimeSeriesPoint[];
68
+ }
69
+
70
+ export interface ModelDashboardStats {
71
+ byModel: ModelStats[];
72
+ modelSeries: ModelTimeSeriesPoint[];
73
+ modelPerformanceSeries: ModelPerformancePoint[];
74
+ }
75
+
76
+ export interface CostDashboardStats {
77
+ costSeries: CostTimeSeriesPoint[];
78
+ }
@@ -0,0 +1,31 @@
1
+ import { useEffect, useState } from "react";
2
+
3
+ export type SystemTheme = "light" | "dark";
4
+
5
+ const DARK_SCHEME_QUERY = "(prefers-color-scheme: dark)";
6
+
7
+ function getSystemTheme(): SystemTheme {
8
+ if (typeof window === "undefined") {
9
+ return "light";
10
+ }
11
+
12
+ return window.matchMedia(DARK_SCHEME_QUERY).matches ? "dark" : "light";
13
+ }
14
+
15
+ export function useSystemTheme(): SystemTheme {
16
+ const [theme, setTheme] = useState<SystemTheme>(() => getSystemTheme());
17
+
18
+ useEffect(() => {
19
+ const media = window.matchMedia(DARK_SCHEME_QUERY);
20
+ const applyTheme = () => {
21
+ setTheme(media.matches ? "dark" : "light");
22
+ };
23
+
24
+ applyTheme();
25
+
26
+ media.addEventListener("change", applyTheme);
27
+ return () => media.removeEventListener("change", applyTheme);
28
+ }, []);
29
+
30
+ return theme;
31
+ }
@@ -0,0 +1,96 @@
1
+ import * as path from "node:path";
2
+
3
+ const INDEX_ASSET = "index.html";
4
+ const UNSAFE_ARCHIVE_NAME = /[\u0000-\u001f\u007f%?#]/u;
5
+ const WINDOWS_ABSOLUTE_PATH = /^[a-zA-Z]:\//;
6
+
7
+ const CONTENT_TYPES: Readonly<Record<string, string>> = Object.freeze({
8
+ ".css": "text/css; charset=utf-8",
9
+ ".gif": "image/gif",
10
+ ".html": "text/html; charset=utf-8",
11
+ ".ico": "image/x-icon",
12
+ ".jpeg": "image/jpeg",
13
+ ".jpg": "image/jpeg",
14
+ ".js": "text/javascript; charset=utf-8",
15
+ ".json": "application/json; charset=utf-8",
16
+ ".map": "application/json; charset=utf-8",
17
+ ".png": "image/png",
18
+ ".svg": "image/svg+xml",
19
+ ".ttf": "font/ttf",
20
+ ".webp": "image/webp",
21
+ ".woff": "font/woff",
22
+ ".woff2": "font/woff2",
23
+ });
24
+
25
+ function normalizeArchiveName(archiveName: string): string {
26
+ const normalized = archiveName.normalize("NFC");
27
+ const segments = normalized.split("/");
28
+ if (
29
+ !normalized ||
30
+ normalized.startsWith("/") ||
31
+ WINDOWS_ABSOLUTE_PATH.test(normalized) ||
32
+ normalized.includes("\\") ||
33
+ UNSAFE_ARCHIVE_NAME.test(normalized) ||
34
+ segments.some(segment => !segment || segment === "." || segment === "..")
35
+ ) {
36
+ throw new Error(`Unsafe compiled stats client archive entry: ${JSON.stringify(archiveName)}`);
37
+ }
38
+ return normalized;
39
+ }
40
+
41
+ function contentType(assetName: string): string {
42
+ return CONTENT_TYPES[path.posix.extname(assetName).toLowerCase()] ?? "application/octet-stream";
43
+ }
44
+
45
+ async function parseArchive(archiveBytes: Uint8Array): Promise<ReadonlyMap<string, Blob>> {
46
+ const files = await new Bun.Archive(archiveBytes).files();
47
+ const assets = new Map<string, Blob>();
48
+ for (const [archiveName, file] of files) {
49
+ const assetName = normalizeArchiveName(archiveName);
50
+ if (assets.has(assetName)) {
51
+ throw new Error(`Duplicate compiled stats client archive entry: ${JSON.stringify(assetName)}`);
52
+ }
53
+ assets.set(assetName, new Blob([file], { type: contentType(assetName) }));
54
+ }
55
+ if (!assets.has(INDEX_ASSET)) {
56
+ throw new Error("Compiled stats client archive is missing index.html");
57
+ }
58
+ return assets;
59
+ }
60
+
61
+ function responseForPath(assets: ReadonlyMap<string, Blob>, requestPath: string): Response {
62
+ const assetName = requestPath === "/" ? INDEX_ASSET : requestPath.replace(/^\//, "");
63
+ const asset = assets.get(assetName);
64
+ if (asset) return new Response(asset);
65
+ return new Response(assets.get(INDEX_ASSET));
66
+ }
67
+
68
+ export function createCompiledClientAssetHandler(
69
+ loadArchiveBytes: () => Uint8Array | null | Promise<Uint8Array | null>,
70
+ ): { response(requestPath: string): Promise<Response> } {
71
+ let initialization: Promise<ReadonlyMap<string, Blob>> | null = null;
72
+
73
+ async function getAssets(): Promise<ReadonlyMap<string, Blob>> {
74
+ if (initialization) return await initialization;
75
+ const attempt = (async () => {
76
+ const archiveBytes = await loadArchiveBytes();
77
+ if (!archiveBytes) {
78
+ throw new Error("Compiled stats client bundle missing. Rebuild binary with embedded stats assets.");
79
+ }
80
+ return await parseArchive(archiveBytes);
81
+ })();
82
+ initialization = attempt;
83
+ try {
84
+ return await attempt;
85
+ } catch (error) {
86
+ if (initialization === attempt) initialization = null;
87
+ throw error;
88
+ }
89
+ }
90
+
91
+ return {
92
+ async response(requestPath) {
93
+ return responseForPath(await getAssets(), requestPath);
94
+ },
95
+ };
96
+ }