claude-session-dashboard 0.1.3 → 0.3.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 (44) hide show
  1. package/README.md +156 -14
  2. package/dist/client/assets/_dashboard-I7m6D7BE.js +1 -0
  3. package/dist/client/assets/_sessionId-DEliIff6.js +12 -0
  4. package/dist/client/assets/app-D7yorIIh.css +1 -0
  5. package/dist/client/assets/{createServerFn-Le0d8Pjz.js → createServerFn-Bn6_ISOt.js} +1 -1
  6. package/dist/client/assets/format-Bsprb3az.js +1 -0
  7. package/dist/client/assets/index-BkqRvnEf.js +1 -0
  8. package/dist/client/assets/{main-CzD8HjLq.js → main-CfJIADCp.js} +7 -7
  9. package/dist/client/assets/sessions.queries-CrJg4dYU.js +1 -0
  10. package/dist/client/assets/settings-C4_lsEzl.js +1 -0
  11. package/dist/client/assets/{settings.types-B4841OLF.js → settings.types-9Qf5WcRY.js} +1 -1
  12. package/dist/client/assets/stats-_r1gmaTe.js +4 -0
  13. package/dist/client/assets/useSessionCost-DPZ-ubM1.js +65 -0
  14. package/dist/client/favicon.svg +3 -0
  15. package/dist/server/assets/_dashboard-TUzgwLqB.js +112 -0
  16. package/dist/server/assets/{_sessionId-BwZK4Ezz.js → _sessionId-C-XZIPqn.js} +57 -35
  17. package/dist/server/assets/_tanstack-start-manifest_v-B51mSkGz.js +4 -0
  18. package/dist/server/assets/{claude-path-CkuljM34.js → claude-path-BdwflgZ1.js} +9 -3
  19. package/dist/server/assets/{format-CGmJnuhZ.js → format-DIZHV7IJ.js} +3 -3
  20. package/dist/server/assets/{index-D4VWrt2z.js → index-CKfH7HpA.js} +28 -60
  21. package/dist/server/assets/project-analytics.server-BkWSd6a8.js +61 -0
  22. package/dist/server/assets/{router-xTSe9UH_.js → router-Cb_hBXHI.js} +62 -31
  23. package/dist/server/assets/{session-detail.server-azkRfON2.js → session-detail.server-DLXl-Pn-.js} +1 -1
  24. package/dist/server/assets/session-scanner-CLfls9u-.js +93 -0
  25. package/dist/server/assets/sessions.queries-B5ZBiVJy.js +42 -0
  26. package/dist/server/assets/{sessions.server-B8zbmvSM.js → sessions.server-CUhasKW2.js} +5 -89
  27. package/dist/server/assets/{settings-ko61yfVs.js → settings-C0_KyVQQ.js} +66 -20
  28. package/dist/server/assets/stats-BtgVene-.js +886 -0
  29. package/dist/server/assets/{stats.server-BZWxV-mC.js → stats.server-qTOvID9-.js} +62 -3
  30. package/dist/server/assets/useSessionCost-CYs5UOX-.js +209 -0
  31. package/dist/server/server.js +13 -10
  32. package/package.json +11 -1
  33. package/dist/client/assets/_dashboard-CYwTENkn.js +0 -1
  34. package/dist/client/assets/_sessionId-Bwfhm_El.js +0 -12
  35. package/dist/client/assets/app-DhZyFob1.css +0 -1
  36. package/dist/client/assets/format-Bf-cSf6L.js +0 -1
  37. package/dist/client/assets/index-DXhX1hdS.js +0 -1
  38. package/dist/client/assets/settings-BSPc79zZ.js +0 -1
  39. package/dist/client/assets/stats-CDIvpOt9.js +0 -4
  40. package/dist/client/assets/useSessionCost-9NP6uhla.js +0 -61
  41. package/dist/server/assets/_dashboard--ukhquwO.js +0 -97
  42. package/dist/server/assets/_tanstack-start-manifest_v-gtQY7f-T.js +0 -4
  43. package/dist/server/assets/stats-DItsFPp5.js +0 -266
  44. package/dist/server/assets/useSessionCost-EB0VxklP.js +0 -76
@@ -0,0 +1,886 @@
1
+ import { jsxs, jsx, Fragment } from "react/jsx-runtime";
2
+ import { useState, useMemo } from "react";
3
+ import { queryOptions, useQuery } from "@tanstack/react-query";
4
+ import { c as createSsrRpc } from "./createSsrRpc-CVg2UDl0.js";
5
+ import { c as createServerFn } from "../server.js";
6
+ import { ResponsiveContainer, BarChart, CartesianGrid, XAxis, YAxis, Tooltip, Bar, AreaChart, Area, PieChart, Pie, Cell, Legend } from "recharts";
7
+ import { format, addDays, getDay, parseISO, startOfISOWeek } from "date-fns";
8
+ import { createPortal } from "react-dom";
9
+ import { f as formatTokenCount, a as formatDuration, b as formatRelativeTime, c as formatUSD } from "./format-DIZHV7IJ.js";
10
+ import { Link } from "@tanstack/react-router";
11
+ import { u as usePrivacy, R as Route } from "./router-Cb_hBXHI.js";
12
+ import { u as useSessionCost, E as ExportDropdown, d as downloadFile, a as dailyActivityToCSV, b as dailyTokensToCSV, m as modelUsageToCSV, s as statsToJSON } from "./useSessionCost-CYs5UOX-.js";
13
+ import "@tanstack/history";
14
+ import "@tanstack/router-core/ssr/client";
15
+ import "@tanstack/router-core";
16
+ import "node:async_hooks";
17
+ import "@tanstack/router-core/ssr/server";
18
+ import "h3-v2";
19
+ import "tiny-invariant";
20
+ import "seroval";
21
+ import "@tanstack/react-router/ssr/server";
22
+ import "zod";
23
+ import "./settings.queries-DSQd324O.js";
24
+ import "./settings.types-DntadCHo.js";
25
+ const getStats = createServerFn({
26
+ method: "GET"
27
+ }).handler(createSsrRpc("4b9a58c176f487b49800a372100037cdf33cf048f3592a449f115c7e3f5ea799"));
28
+ const statsQuery = queryOptions({
29
+ queryKey: ["stats"],
30
+ queryFn: () => getStats(),
31
+ refetchInterval: 6e4
32
+ });
33
+ function ActivityChart({ data }) {
34
+ const chartData = data.map((d) => ({
35
+ ...d,
36
+ dateLabel: format(new Date(d.date), "MMM d")
37
+ }));
38
+ return /* @__PURE__ */ jsxs("div", { className: "rounded-xl border border-gray-800 bg-gray-900/50 p-4", children: [
39
+ /* @__PURE__ */ jsx("h3", { className: "text-sm font-semibold text-gray-300", children: "Daily Activity" }),
40
+ /* @__PURE__ */ jsx("p", { className: "mt-1 text-xs text-gray-500", children: "Messages, sessions, and tool calls per day" }),
41
+ /* @__PURE__ */ jsx("div", { className: "mt-4 h-64", children: /* @__PURE__ */ jsx(ResponsiveContainer, { width: "100%", height: "100%", children: /* @__PURE__ */ jsxs(BarChart, { data: chartData, children: [
42
+ /* @__PURE__ */ jsx(CartesianGrid, { strokeDasharray: "3 3", stroke: "#2a2926" }),
43
+ /* @__PURE__ */ jsx(
44
+ XAxis,
45
+ {
46
+ dataKey: "dateLabel",
47
+ tick: { fill: "#7a7668", fontSize: 10 },
48
+ tickLine: false
49
+ }
50
+ ),
51
+ /* @__PURE__ */ jsx(
52
+ YAxis,
53
+ {
54
+ tick: { fill: "#7a7668", fontSize: 10 },
55
+ tickLine: false,
56
+ axisLine: false
57
+ }
58
+ ),
59
+ /* @__PURE__ */ jsx(
60
+ Tooltip,
61
+ {
62
+ contentStyle: {
63
+ backgroundColor: "#1c1c1a",
64
+ border: "1px solid #3d3b36",
65
+ borderRadius: "8px",
66
+ fontSize: "12px"
67
+ }
68
+ }
69
+ ),
70
+ /* @__PURE__ */ jsx(
71
+ Bar,
72
+ {
73
+ dataKey: "messageCount",
74
+ name: "Messages",
75
+ fill: "#d97757",
76
+ radius: [2, 2, 0, 0]
77
+ }
78
+ ),
79
+ /* @__PURE__ */ jsx(
80
+ Bar,
81
+ {
82
+ dataKey: "toolCallCount",
83
+ name: "Tool Calls",
84
+ fill: "#8b5cf6",
85
+ radius: [2, 2, 0, 0]
86
+ }
87
+ ),
88
+ /* @__PURE__ */ jsx(
89
+ Bar,
90
+ {
91
+ dataKey: "sessionCount",
92
+ name: "Sessions",
93
+ fill: "#10b981",
94
+ radius: [2, 2, 0, 0]
95
+ }
96
+ )
97
+ ] }) }) })
98
+ ] });
99
+ }
100
+ const INTENSITY_COLORS = [
101
+ "#2a2926",
102
+ // Level 0: warm gray-800 (no activity)
103
+ "#3d2a1e",
104
+ // Level 1: dark terracotta
105
+ "#a8512eb3",
106
+ // Level 2: brand-700 at ~70% opacity
107
+ "#d97757cc",
108
+ // Level 3: brand-500 at ~80% opacity
109
+ "#e09070"
110
+ // Level 4: brand-400 (most intense)
111
+ ];
112
+ const DAY_LABELS = ["Mon", "", "Wed", "", "Fri", "", ""];
113
+ const CELL_SIZE = 10;
114
+ const CELL_GAP = 2;
115
+ const DAY_LABEL_WIDTH = 28;
116
+ function computePercentiles(values) {
117
+ if (values.length === 0) return { p25: 0, p50: 0, p75: 0 };
118
+ const sorted = [...values].sort((a, b) => a - b);
119
+ const percentile = (p) => {
120
+ const idx = p / 100 * (sorted.length - 1);
121
+ const lower = Math.floor(idx);
122
+ const upper = Math.ceil(idx);
123
+ if (lower === upper) return sorted[lower];
124
+ return sorted[lower] + (sorted[upper] - sorted[lower]) * (idx - lower);
125
+ };
126
+ return {
127
+ p25: percentile(25),
128
+ p50: percentile(50),
129
+ p75: percentile(75)
130
+ };
131
+ }
132
+ function getIntensityLevel(tokens, percentiles) {
133
+ if (tokens === 0) return 0;
134
+ if (tokens <= percentiles.p25) return 1;
135
+ if (tokens <= percentiles.p50) return 2;
136
+ if (tokens <= percentiles.p75) return 3;
137
+ return 4;
138
+ }
139
+ function toMondayFirst(jsDay) {
140
+ return jsDay === 0 ? 6 : jsDay - 1;
141
+ }
142
+ function ContributionHeatmap({
143
+ dailyActivity,
144
+ dailyModelTokens
145
+ }) {
146
+ const [tooltip, setTooltip] = useState(null);
147
+ const { days, monthLabels } = useMemo(() => {
148
+ const dataMap = /* @__PURE__ */ new Map();
149
+ for (const entry of dailyActivity) {
150
+ const existing = dataMap.get(entry.date) ?? { sessionCount: 0, totalTokens: 0 };
151
+ existing.sessionCount = entry.sessionCount;
152
+ dataMap.set(entry.date, existing);
153
+ }
154
+ for (const entry of dailyModelTokens) {
155
+ const existing = dataMap.get(entry.date) ?? { sessionCount: 0, totalTokens: 0 };
156
+ existing.totalTokens = Object.values(entry.tokensByModel).reduce((sum, t) => sum + t, 0);
157
+ dataMap.set(entry.date, existing);
158
+ }
159
+ const today = /* @__PURE__ */ new Date();
160
+ today.setHours(0, 0, 0, 0);
161
+ let startDate = addDays(today, -364);
162
+ const startDayOfWeek = toMondayFirst(getDay(startDate));
163
+ if (startDayOfWeek !== 0) {
164
+ startDate = addDays(startDate, -startDayOfWeek);
165
+ }
166
+ const allDays = [];
167
+ let currentDate = startDate;
168
+ while (currentDate <= today) {
169
+ const dateStr = format(currentDate, "yyyy-MM-dd");
170
+ const dayOfWeek = toMondayFirst(getDay(currentDate));
171
+ const daysSinceStart = Math.floor(
172
+ (currentDate.getTime() - startDate.getTime()) / (1e3 * 60 * 60 * 24)
173
+ );
174
+ const weekIndex = Math.floor(daysSinceStart / 7);
175
+ const data = dataMap.get(dateStr);
176
+ allDays.push({
177
+ date: dateStr,
178
+ dateFormatted: format(currentDate, "MMM d, yyyy"),
179
+ sessionCount: data?.sessionCount ?? 0,
180
+ totalTokens: data?.totalTokens ?? 0,
181
+ intensity: 0,
182
+ // computed below
183
+ weekIndex,
184
+ dayOfWeek
185
+ });
186
+ currentDate = addDays(currentDate, 1);
187
+ }
188
+ const nonZeroTokens = allDays.filter((d) => d.totalTokens > 0).map((d) => d.totalTokens);
189
+ const percentiles = computePercentiles(nonZeroTokens);
190
+ for (const day of allDays) {
191
+ day.intensity = getIntensityLevel(day.totalTokens, percentiles);
192
+ }
193
+ const labels = [];
194
+ let lastMonth = -1;
195
+ for (const day of allDays) {
196
+ if (day.dayOfWeek === 0) {
197
+ const parsed = parseISO(day.date);
198
+ const month = parsed.getMonth();
199
+ if (month !== lastMonth) {
200
+ labels.push({
201
+ label: format(parsed, "MMM"),
202
+ weekIndex: day.weekIndex
203
+ });
204
+ lastMonth = month;
205
+ }
206
+ }
207
+ }
208
+ return { days: allDays, monthLabels: labels };
209
+ }, [dailyActivity, dailyModelTokens]);
210
+ if (days.length === 0) {
211
+ return /* @__PURE__ */ jsxs("div", { className: "rounded-xl border border-gray-800 bg-gray-900/50 p-4", children: [
212
+ /* @__PURE__ */ jsx("h3", { className: "text-sm font-semibold text-gray-300", children: "Activity" }),
213
+ /* @__PURE__ */ jsx("div", { className: "flex h-32 items-center justify-center", children: /* @__PURE__ */ jsx("p", { className: "text-sm text-gray-500", children: "No activity data available" }) })
214
+ ] });
215
+ }
216
+ return /* @__PURE__ */ jsxs("div", { className: "rounded-xl border border-gray-800 bg-gray-900/50 p-4", children: [
217
+ /* @__PURE__ */ jsx("h3", { className: "text-sm font-semibold text-gray-300", children: "Activity" }),
218
+ /* @__PURE__ */ jsx("p", { className: "mt-1 text-xs text-gray-500", children: "Token usage intensity over the past year" }),
219
+ /* @__PURE__ */ jsx("div", { className: "mt-4 overflow-x-auto", children: /* @__PURE__ */ jsx("div", { className: "relative w-full", children: /* @__PURE__ */ jsxs("div", { className: "flex", children: [
220
+ /* @__PURE__ */ jsxs(
221
+ "div",
222
+ {
223
+ className: "flex flex-col text-[10px] text-gray-500",
224
+ style: { width: DAY_LABEL_WIDTH },
225
+ children: [
226
+ /* @__PURE__ */ jsx("div", { style: { height: 16, marginBottom: CELL_GAP } }),
227
+ DAY_LABELS.map((label, i) => /* @__PURE__ */ jsx(
228
+ "div",
229
+ {
230
+ className: "flex items-center",
231
+ style: { height: CELL_SIZE, marginTop: i > 0 ? CELL_GAP : 0 },
232
+ children: label
233
+ },
234
+ i
235
+ ))
236
+ ]
237
+ }
238
+ ),
239
+ /* @__PURE__ */ jsxs("div", { className: "flex flex-1 flex-col", children: [
240
+ /* @__PURE__ */ jsx(
241
+ "div",
242
+ {
243
+ className: "grid text-[10px] text-gray-500",
244
+ style: {
245
+ gridAutoFlow: "column",
246
+ gridAutoColumns: `minmax(${CELL_SIZE}px, 1fr)`,
247
+ gap: CELL_GAP,
248
+ height: 16,
249
+ marginBottom: CELL_GAP
250
+ },
251
+ children: monthLabels.map((ml, i) => /* @__PURE__ */ jsx(
252
+ "div",
253
+ {
254
+ className: "flex items-end",
255
+ style: { gridColumn: ml.weekIndex + 1 },
256
+ children: ml.label
257
+ },
258
+ `${ml.label}-${i}`
259
+ ))
260
+ }
261
+ ),
262
+ /* @__PURE__ */ jsx(
263
+ "div",
264
+ {
265
+ className: "relative grid",
266
+ style: {
267
+ gridTemplateRows: `repeat(7, ${CELL_SIZE}px)`,
268
+ gridAutoFlow: "column",
269
+ gridAutoColumns: `minmax(${CELL_SIZE}px, 1fr)`,
270
+ gap: CELL_GAP
271
+ },
272
+ children: days.map((day) => /* @__PURE__ */ jsx(
273
+ "div",
274
+ {
275
+ className: "rounded-sm transition-colors",
276
+ style: {
277
+ width: CELL_SIZE,
278
+ height: CELL_SIZE,
279
+ backgroundColor: INTENSITY_COLORS[day.intensity],
280
+ gridRow: day.dayOfWeek + 1,
281
+ gridColumn: day.weekIndex + 1
282
+ },
283
+ onMouseEnter: (e) => {
284
+ const rect = e.target.getBoundingClientRect();
285
+ setTooltip({
286
+ date: day.date,
287
+ dateFormatted: day.dateFormatted,
288
+ sessionCount: day.sessionCount,
289
+ totalTokens: day.totalTokens,
290
+ x: rect.left + rect.width / 2,
291
+ y: rect.top - 8
292
+ });
293
+ },
294
+ onMouseLeave: () => setTooltip(null)
295
+ },
296
+ day.date
297
+ ))
298
+ }
299
+ )
300
+ ] })
301
+ ] }) }) }),
302
+ tooltip && createPortal(
303
+ /* @__PURE__ */ jsxs(
304
+ "div",
305
+ {
306
+ className: "pointer-events-none fixed z-50 rounded-lg border border-gray-700 bg-gray-800 px-3 py-2 text-xs shadow-lg",
307
+ style: {
308
+ left: tooltip.x,
309
+ top: tooltip.y,
310
+ transform: "translate(-50%, -100%)",
311
+ whiteSpace: "nowrap"
312
+ },
313
+ children: [
314
+ /* @__PURE__ */ jsx("p", { className: "font-medium text-gray-300", children: tooltip.dateFormatted }),
315
+ tooltip.totalTokens > 0 || tooltip.sessionCount > 0 ? /* @__PURE__ */ jsxs("div", { className: "mt-1 space-y-0.5 text-gray-400", children: [
316
+ /* @__PURE__ */ jsxs("p", { children: [
317
+ tooltip.sessionCount,
318
+ " ",
319
+ tooltip.sessionCount === 1 ? "session" : "sessions"
320
+ ] }),
321
+ /* @__PURE__ */ jsxs("p", { children: [
322
+ formatTokenCount(tooltip.totalTokens),
323
+ " tokens"
324
+ ] })
325
+ ] }) : /* @__PURE__ */ jsx("p", { className: "mt-1 text-gray-500", children: "No activity" })
326
+ ]
327
+ }
328
+ ),
329
+ document.body
330
+ )
331
+ ] });
332
+ }
333
+ const COLORS$1 = ["#d97757", "#8b5cf6", "#10b981", "#f59e0b", "#ef4444", "#b07cc5"];
334
+ function normalizeModelName(model) {
335
+ return model.replace(/^claude-/, "").split("-202")[0];
336
+ }
337
+ function getTopModels(data, limit) {
338
+ const totals = {};
339
+ for (const day of data) {
340
+ for (const [model, tokens] of Object.entries(day.tokensByModel)) {
341
+ const normalized = normalizeModelName(model);
342
+ totals[normalized] = (totals[normalized] ?? 0) + tokens;
343
+ }
344
+ }
345
+ const sorted = Object.entries(totals).sort((a, b) => b[1] - a[1]);
346
+ const topModels = sorted.slice(0, limit).map(([name]) => name);
347
+ const hasOther = sorted.length > limit;
348
+ return { topModels, hasOther };
349
+ }
350
+ function processDaily(data, topModels, hasOther) {
351
+ return data.map((day) => {
352
+ const entry = {
353
+ dateLabel: format(parseISO(day.date), "MMM d"),
354
+ sortKey: day.date
355
+ };
356
+ for (const model of topModels) {
357
+ entry[model] = 0;
358
+ }
359
+ if (hasOther) {
360
+ entry["Other"] = 0;
361
+ }
362
+ for (const [rawModel, tokens] of Object.entries(day.tokensByModel)) {
363
+ const normalized = normalizeModelName(rawModel);
364
+ if (topModels.includes(normalized)) {
365
+ entry[normalized] = entry[normalized] + tokens;
366
+ } else if (hasOther) {
367
+ entry["Other"] = entry["Other"] + tokens;
368
+ }
369
+ }
370
+ return entry;
371
+ });
372
+ }
373
+ function processWeekly(data, topModels, hasOther) {
374
+ const weekMap = /* @__PURE__ */ new Map();
375
+ for (const day of data) {
376
+ const weekStart = startOfISOWeek(parseISO(day.date));
377
+ const weekKey = format(weekStart, "yyyy-MM-dd");
378
+ if (!weekMap.has(weekKey)) {
379
+ const entry2 = {
380
+ dateLabel: `Week of ${format(weekStart, "MMM d")}`,
381
+ sortKey: weekKey
382
+ };
383
+ for (const model of topModels) {
384
+ entry2[model] = 0;
385
+ }
386
+ if (hasOther) {
387
+ entry2["Other"] = 0;
388
+ }
389
+ weekMap.set(weekKey, entry2);
390
+ }
391
+ const entry = weekMap.get(weekKey);
392
+ for (const [rawModel, tokens] of Object.entries(day.tokensByModel)) {
393
+ const normalized = normalizeModelName(rawModel);
394
+ if (topModels.includes(normalized)) {
395
+ entry[normalized] = entry[normalized] + tokens;
396
+ } else if (hasOther) {
397
+ entry["Other"] = entry["Other"] + tokens;
398
+ }
399
+ }
400
+ }
401
+ return Array.from(weekMap.values()).sort(
402
+ (a, b) => a.sortKey.localeCompare(b.sortKey)
403
+ );
404
+ }
405
+ function CustomTooltip({ active, payload, label }) {
406
+ if (!active || !payload || payload.length === 0) return null;
407
+ const total = payload.reduce((sum, entry) => sum + entry.value, 0);
408
+ return /* @__PURE__ */ jsxs("div", { className: "rounded-lg border border-gray-700 bg-gray-800 p-3 text-xs shadow-lg", children: [
409
+ /* @__PURE__ */ jsx("p", { className: "mb-2 font-medium text-gray-300", children: label }),
410
+ payload.filter((entry) => entry.value > 0).sort((a, b) => b.value - a.value).map((entry) => /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between gap-4", children: [
411
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1.5", children: [
412
+ /* @__PURE__ */ jsx(
413
+ "div",
414
+ {
415
+ className: "h-2 w-2 rounded-full",
416
+ style: { backgroundColor: entry.color }
417
+ }
418
+ ),
419
+ /* @__PURE__ */ jsx("span", { className: "text-gray-400", children: entry.name })
420
+ ] }),
421
+ /* @__PURE__ */ jsx("span", { className: "font-mono text-gray-300", children: formatTokenCount(entry.value) })
422
+ ] }, entry.name)),
423
+ /* @__PURE__ */ jsxs("div", { className: "mt-1.5 border-t border-gray-700 pt-1.5 flex justify-between", children: [
424
+ /* @__PURE__ */ jsx("span", { className: "text-gray-400", children: "Total" }),
425
+ /* @__PURE__ */ jsx("span", { className: "font-mono font-medium text-white", children: formatTokenCount(total) })
426
+ ] })
427
+ ] });
428
+ }
429
+ function TokenTrendChart({ data }) {
430
+ const [granularity, setGranularity] = useState("daily");
431
+ const { topModels, hasOther } = useMemo(
432
+ () => getTopModels(data, 5),
433
+ [data]
434
+ );
435
+ const allModelKeys = useMemo(() => {
436
+ const keys = [...topModels];
437
+ if (hasOther) keys.push("Other");
438
+ return keys;
439
+ }, [topModels, hasOther]);
440
+ const chartData = useMemo(() => {
441
+ if (granularity === "weekly") {
442
+ return processWeekly(data, topModels, hasOther);
443
+ }
444
+ return processDaily(data, topModels, hasOther);
445
+ }, [data, topModels, hasOther, granularity]);
446
+ if (data.length === 0) {
447
+ return /* @__PURE__ */ jsxs("div", { className: "rounded-xl border border-gray-800 bg-gray-900/50 p-4", children: [
448
+ /* @__PURE__ */ jsx("h3", { className: "text-sm font-semibold text-gray-300", children: "Token Usage Over Time" }),
449
+ /* @__PURE__ */ jsx("p", { className: "mt-1 text-xs text-gray-500", children: "Total tokens by model per day" }),
450
+ /* @__PURE__ */ jsx("div", { className: "flex h-64 items-center justify-center", children: /* @__PURE__ */ jsx("p", { className: "text-sm text-gray-500", children: "No data available" }) })
451
+ ] });
452
+ }
453
+ return /* @__PURE__ */ jsxs("div", { className: "rounded-xl border border-gray-800 bg-gray-900/50 p-4", children: [
454
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between", children: [
455
+ /* @__PURE__ */ jsxs("div", { children: [
456
+ /* @__PURE__ */ jsx("h3", { className: "text-sm font-semibold text-gray-300", children: "Token Usage Over Time" }),
457
+ /* @__PURE__ */ jsx("p", { className: "mt-1 text-xs text-gray-500", children: "Total tokens by model per day" })
458
+ ] }),
459
+ /* @__PURE__ */ jsxs("div", { className: "flex rounded-lg border border-gray-700 text-xs", children: [
460
+ /* @__PURE__ */ jsx(
461
+ "button",
462
+ {
463
+ type: "button",
464
+ onClick: () => setGranularity("daily"),
465
+ className: `rounded-l-lg px-3 py-1 ${granularity === "daily" ? "bg-gray-700 text-white" : "text-gray-400 hover:text-gray-300"}`,
466
+ children: "Daily"
467
+ }
468
+ ),
469
+ /* @__PURE__ */ jsx(
470
+ "button",
471
+ {
472
+ type: "button",
473
+ onClick: () => setGranularity("weekly"),
474
+ className: `rounded-r-lg px-3 py-1 ${granularity === "weekly" ? "bg-gray-700 text-white" : "text-gray-400 hover:text-gray-300"}`,
475
+ children: "Weekly"
476
+ }
477
+ )
478
+ ] })
479
+ ] }),
480
+ /* @__PURE__ */ jsx("div", { className: "mt-4 h-72", children: /* @__PURE__ */ jsx(ResponsiveContainer, { width: "100%", height: "100%", children: /* @__PURE__ */ jsxs(AreaChart, { data: chartData, children: [
481
+ /* @__PURE__ */ jsx(CartesianGrid, { strokeDasharray: "3 3", stroke: "#2a2926" }),
482
+ /* @__PURE__ */ jsx(
483
+ XAxis,
484
+ {
485
+ dataKey: "dateLabel",
486
+ tick: { fill: "#7a7668", fontSize: 10 },
487
+ tickLine: false
488
+ }
489
+ ),
490
+ /* @__PURE__ */ jsx(
491
+ YAxis,
492
+ {
493
+ tick: { fill: "#7a7668", fontSize: 10 },
494
+ tickLine: false,
495
+ axisLine: false,
496
+ tickFormatter: (value) => formatTokenCount(value)
497
+ }
498
+ ),
499
+ /* @__PURE__ */ jsx(Tooltip, { content: /* @__PURE__ */ jsx(CustomTooltip, {}) }),
500
+ allModelKeys.map((model, i) => /* @__PURE__ */ jsx(
501
+ Area,
502
+ {
503
+ type: "monotone",
504
+ dataKey: model,
505
+ stackId: "1",
506
+ stroke: COLORS$1[i % COLORS$1.length],
507
+ fill: COLORS$1[i % COLORS$1.length],
508
+ fillOpacity: 0.6
509
+ },
510
+ model
511
+ ))
512
+ ] }) }) })
513
+ ] });
514
+ }
515
+ const COLORS = ["#d97757", "#8b5cf6", "#10b981", "#f59e0b", "#ef4444", "#b07cc5"];
516
+ function ModelUsageChart({ data }) {
517
+ const chartData = Object.entries(data).map(([model, usage]) => ({
518
+ name: model.replace(/^claude-/, "").split("-202")[0],
519
+ fullName: model,
520
+ totalTokens: usage.inputTokens + usage.outputTokens,
521
+ outputTokens: usage.outputTokens
522
+ }));
523
+ chartData.sort((a, b) => b.totalTokens - a.totalTokens);
524
+ return /* @__PURE__ */ jsxs("div", { className: "rounded-xl border border-gray-800 bg-gray-900/50 p-4", children: [
525
+ /* @__PURE__ */ jsx("h3", { className: "text-sm font-semibold text-gray-300", children: "Model Usage" }),
526
+ /* @__PURE__ */ jsx("p", { className: "mt-1 text-xs text-gray-500", children: "Token usage by model" }),
527
+ /* @__PURE__ */ jsx("div", { className: "mt-4 h-64", children: /* @__PURE__ */ jsx(ResponsiveContainer, { width: "100%", height: "100%", children: /* @__PURE__ */ jsxs(PieChart, { children: [
528
+ /* @__PURE__ */ jsx(
529
+ Pie,
530
+ {
531
+ data: chartData,
532
+ cx: "50%",
533
+ cy: "50%",
534
+ innerRadius: 50,
535
+ outerRadius: 80,
536
+ dataKey: "totalTokens",
537
+ nameKey: "name",
538
+ strokeWidth: 0,
539
+ children: chartData.map((_, i) => /* @__PURE__ */ jsx(Cell, { fill: COLORS[i % COLORS.length], opacity: 0.8 }, i))
540
+ }
541
+ ),
542
+ /* @__PURE__ */ jsx(
543
+ Tooltip,
544
+ {
545
+ formatter: (value) => formatTokenCount(value),
546
+ contentStyle: {
547
+ backgroundColor: "#1c1c1a",
548
+ border: "1px solid #3d3b36",
549
+ borderRadius: "8px",
550
+ fontSize: "12px"
551
+ }
552
+ }
553
+ ),
554
+ /* @__PURE__ */ jsx(
555
+ Legend,
556
+ {
557
+ wrapperStyle: { fontSize: "11px" }
558
+ }
559
+ )
560
+ ] }) }) })
561
+ ] });
562
+ }
563
+ function HourlyDistribution({
564
+ hourCounts
565
+ }) {
566
+ const hours = Array.from({ length: 24 }, (_, i) => ({
567
+ hour: i,
568
+ label: `${i.toString().padStart(2, "0")}:00`,
569
+ count: hourCounts[String(i)] ?? 0
570
+ }));
571
+ const maxCount = Math.max(...hours.map((h) => h.count), 1);
572
+ return /* @__PURE__ */ jsxs("div", { className: "rounded-xl border border-gray-800 bg-gray-900/50 p-4", children: [
573
+ /* @__PURE__ */ jsx("h3", { className: "text-sm font-semibold text-gray-300", children: "Hourly Distribution" }),
574
+ /* @__PURE__ */ jsx("p", { className: "mt-1 text-xs text-gray-500", children: "Session starts by hour of day" }),
575
+ /* @__PURE__ */ jsx("div", { className: "mt-4 flex items-end gap-0.5", style: { height: "120px" }, children: hours.map((h) => {
576
+ const height = h.count > 0 ? Math.max(h.count / maxCount * 100, 4) : 0;
577
+ const intensity = h.count / maxCount;
578
+ return /* @__PURE__ */ jsxs(
579
+ "div",
580
+ {
581
+ className: "group relative flex-1",
582
+ style: { height: "100%" },
583
+ children: [
584
+ /* @__PURE__ */ jsx(
585
+ "div",
586
+ {
587
+ className: "absolute bottom-0 w-full rounded-t transition-colors",
588
+ style: {
589
+ height: `${height}%`,
590
+ backgroundColor: `rgba(217, 119, 87, ${0.2 + intensity * 0.6})`
591
+ }
592
+ }
593
+ ),
594
+ /* @__PURE__ */ jsxs("div", { className: "absolute -top-8 left-1/2 hidden -translate-x-1/2 whitespace-nowrap rounded bg-gray-800 px-1.5 py-0.5 text-[10px] text-gray-300 group-hover:block", children: [
595
+ h.label,
596
+ ": ",
597
+ h.count
598
+ ] })
599
+ ]
600
+ },
601
+ h.hour
602
+ );
603
+ }) }),
604
+ /* @__PURE__ */ jsxs("div", { className: "mt-1 flex justify-between text-[10px] text-gray-600", children: [
605
+ /* @__PURE__ */ jsx("span", { children: "00:00" }),
606
+ /* @__PURE__ */ jsx("span", { children: "06:00" }),
607
+ /* @__PURE__ */ jsx("span", { children: "12:00" }),
608
+ /* @__PURE__ */ jsx("span", { children: "18:00" }),
609
+ /* @__PURE__ */ jsx("span", { children: "23:00" })
610
+ ] })
611
+ ] });
612
+ }
613
+ const getProjectAnalytics = createServerFn({
614
+ method: "GET"
615
+ }).handler(createSsrRpc("64052f224a1d6696436e5d3deeee2b798f0742e1292ffabd038c3a7bf75e6fcb"));
616
+ const projectAnalyticsQuery = queryOptions({
617
+ queryKey: ["projects", "analytics"],
618
+ queryFn: () => getProjectAnalytics(),
619
+ refetchInterval: 6e4
620
+ });
621
+ const COLUMNS = [
622
+ { key: "projectName", label: "Project" },
623
+ { key: "totalSessions", label: "Sessions", align: "right" },
624
+ { key: "totalMessages", label: "Messages", align: "right" },
625
+ { key: "totalDurationMs", label: "Duration", align: "right" },
626
+ { key: "lastSessionAt", label: "Last Active", align: "right" }
627
+ ];
628
+ function ProjectTable({ projects }) {
629
+ const { anonymizeProjectName } = usePrivacy();
630
+ const [sortField, setSortField] = useState("lastSessionAt");
631
+ const [sortDir, setSortDir] = useState("desc");
632
+ const sorted = useMemo(() => {
633
+ const copy = [...projects];
634
+ copy.sort((a, b) => {
635
+ let cmp = 0;
636
+ switch (sortField) {
637
+ case "projectName":
638
+ cmp = a.projectName.localeCompare(b.projectName);
639
+ break;
640
+ case "totalSessions":
641
+ cmp = a.totalSessions - b.totalSessions;
642
+ break;
643
+ case "totalMessages":
644
+ cmp = a.totalMessages - b.totalMessages;
645
+ break;
646
+ case "totalDurationMs":
647
+ cmp = a.totalDurationMs - b.totalDurationMs;
648
+ break;
649
+ case "lastSessionAt":
650
+ cmp = a.lastSessionAt.localeCompare(b.lastSessionAt);
651
+ break;
652
+ }
653
+ return sortDir === "asc" ? cmp : -cmp;
654
+ });
655
+ return copy;
656
+ }, [projects, sortField, sortDir]);
657
+ function handleSort(field) {
658
+ if (field === sortField) {
659
+ setSortDir((prev) => prev === "asc" ? "desc" : "asc");
660
+ } else {
661
+ setSortField(field);
662
+ setSortDir("desc");
663
+ }
664
+ }
665
+ function renderSortIndicator(field) {
666
+ if (field !== sortField) return null;
667
+ return /* @__PURE__ */ jsx("span", { className: "ml-1", children: sortDir === "asc" ? "▲" : "▼" });
668
+ }
669
+ return /* @__PURE__ */ jsx("div", { className: "overflow-x-auto rounded-xl border border-gray-800 bg-gray-900/50", children: /* @__PURE__ */ jsxs("table", { className: "w-full", children: [
670
+ /* @__PURE__ */ jsx("thead", { children: /* @__PURE__ */ jsx("tr", { className: "border-b border-gray-800", children: COLUMNS.map((col) => /* @__PURE__ */ jsxs(
671
+ "th",
672
+ {
673
+ onClick: () => handleSort(col.key),
674
+ className: `cursor-pointer px-4 py-3 text-xs font-medium text-gray-400 hover:text-gray-200 ${col.align === "right" ? "text-right" : "text-left"}`,
675
+ children: [
676
+ col.label,
677
+ renderSortIndicator(col.key)
678
+ ]
679
+ },
680
+ col.key
681
+ )) }) }),
682
+ /* @__PURE__ */ jsx("tbody", { children: sorted.map((project) => /* @__PURE__ */ jsxs(
683
+ "tr",
684
+ {
685
+ className: "border-b border-gray-800/50 transition-colors hover:bg-gray-800/30",
686
+ children: [
687
+ /* @__PURE__ */ jsxs("td", { className: "px-4 py-3", children: [
688
+ /* @__PURE__ */ jsx(
689
+ Link,
690
+ {
691
+ to: "/sessions",
692
+ search: { project: project.projectName },
693
+ className: "text-sm text-brand-300 hover:underline",
694
+ children: anonymizeProjectName(project.projectName)
695
+ }
696
+ ),
697
+ project.activeSessions > 0 && /* @__PURE__ */ jsxs("span", { className: "ml-2 rounded-full bg-emerald-500/20 px-1.5 py-0.5 text-[10px] font-medium text-emerald-400", children: [
698
+ project.activeSessions,
699
+ " active"
700
+ ] })
701
+ ] }),
702
+ /* @__PURE__ */ jsx("td", { className: "px-4 py-3 text-right font-mono text-sm text-gray-300", children: project.totalSessions }),
703
+ /* @__PURE__ */ jsx("td", { className: "px-4 py-3 text-right font-mono text-sm text-gray-300", children: project.totalMessages.toLocaleString() }),
704
+ /* @__PURE__ */ jsx("td", { className: "px-4 py-3 text-right font-mono text-sm text-gray-300", children: formatDuration(project.totalDurationMs) }),
705
+ /* @__PURE__ */ jsx("td", { className: "px-4 py-3 text-right text-sm text-gray-400", children: formatRelativeTime(project.lastSessionAt) })
706
+ ]
707
+ },
708
+ project.projectPath
709
+ )) })
710
+ ] }) });
711
+ }
712
+ function ProjectAnalytics() {
713
+ const { anonymizeProjectName } = usePrivacy();
714
+ const { data, isLoading } = useQuery(projectAnalyticsQuery);
715
+ if (isLoading) {
716
+ return /* @__PURE__ */ jsxs("div", { className: "space-y-4", children: [
717
+ /* @__PURE__ */ jsx("div", { className: "grid grid-cols-2 gap-3 md:grid-cols-3", children: Array.from({ length: 3 }).map((_, i) => /* @__PURE__ */ jsx(
718
+ "div",
719
+ {
720
+ className: "h-20 animate-pulse rounded-xl bg-gray-900/50"
721
+ },
722
+ i
723
+ )) }),
724
+ /* @__PURE__ */ jsx("div", { className: "h-64 animate-pulse rounded-xl bg-gray-900/50" })
725
+ ] });
726
+ }
727
+ const projects = data?.projects ?? [];
728
+ if (projects.length === 0) {
729
+ return /* @__PURE__ */ jsx("div", { className: "py-12 text-center text-sm text-gray-500", children: "No projects found. Sessions will appear here once scanned." });
730
+ }
731
+ const totalSessions = projects.reduce((sum, p) => sum + p.totalSessions, 0);
732
+ const totalDurationMs = projects.reduce((sum, p) => sum + p.totalDurationMs, 0);
733
+ const mostActive = projects.reduce(
734
+ (max, p) => p.totalSessions > max.totalSessions ? p : max
735
+ );
736
+ return /* @__PURE__ */ jsxs("div", { className: "space-y-6", children: [
737
+ /* @__PURE__ */ jsxs("div", { className: "grid grid-cols-2 gap-3 md:grid-cols-4", children: [
738
+ /* @__PURE__ */ jsx(SummaryCard, { label: "Total Projects", value: String(projects.length) }),
739
+ /* @__PURE__ */ jsx(
740
+ SummaryCard,
741
+ {
742
+ label: "Total Sessions",
743
+ value: totalSessions.toLocaleString()
744
+ }
745
+ ),
746
+ /* @__PURE__ */ jsx(
747
+ SummaryCard,
748
+ {
749
+ label: "Total Duration",
750
+ value: formatDuration(totalDurationMs)
751
+ }
752
+ ),
753
+ /* @__PURE__ */ jsx(
754
+ SummaryCard,
755
+ {
756
+ label: "Most Active",
757
+ value: anonymizeProjectName(mostActive.projectName),
758
+ sub: `${mostActive.totalSessions} sessions`
759
+ }
760
+ )
761
+ ] }),
762
+ /* @__PURE__ */ jsx(ProjectTable, { projects })
763
+ ] });
764
+ }
765
+ function SummaryCard({
766
+ label,
767
+ value,
768
+ sub
769
+ }) {
770
+ return /* @__PURE__ */ jsxs("div", { className: "rounded-xl border border-gray-800 bg-gray-900/50 p-4", children: [
771
+ /* @__PURE__ */ jsx("p", { className: "text-xs text-gray-400", children: label }),
772
+ /* @__PURE__ */ jsx("p", { className: "mt-1 truncate text-xl font-bold text-white", children: value }),
773
+ sub && /* @__PURE__ */ jsx("p", { className: "mt-0.5 text-xs text-gray-500", children: sub })
774
+ ] });
775
+ }
776
+ const EMPTY_TOKENS_BY_MODEL = {};
777
+ function StatsPage() {
778
+ const {
779
+ tab
780
+ } = Route.useSearch();
781
+ const navigate = Route.useNavigate();
782
+ const {
783
+ data: stats,
784
+ isLoading
785
+ } = useQuery(statsQuery);
786
+ const tokensByModel = useMemo(() => {
787
+ if (!stats) return EMPTY_TOKENS_BY_MODEL;
788
+ const result = {};
789
+ for (const [model, usage] of Object.entries(stats.modelUsage)) {
790
+ result[model] = {
791
+ inputTokens: usage.inputTokens,
792
+ outputTokens: usage.outputTokens,
793
+ cacheReadInputTokens: usage.cacheReadInputTokens,
794
+ cacheCreationInputTokens: usage.cacheCreationInputTokens
795
+ };
796
+ }
797
+ return result;
798
+ }, [stats]);
799
+ const {
800
+ cost
801
+ } = useSessionCost(tokensByModel);
802
+ return /* @__PURE__ */ jsxs("div", { children: [
803
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between", children: [
804
+ /* @__PURE__ */ jsxs("div", { children: [
805
+ /* @__PURE__ */ jsx("h1", { className: "text-2xl font-bold text-white", children: "Stats" }),
806
+ /* @__PURE__ */ jsx("p", { className: "mt-1 text-sm text-gray-400", children: "Usage analytics and project insights" })
807
+ ] }),
808
+ tab === "overview" && stats && /* @__PURE__ */ jsx(ExportDropdown, { options: [{
809
+ label: "Daily Activity (CSV)",
810
+ onClick: () => downloadFile(dailyActivityToCSV(stats), "daily-activity.csv", "text/csv")
811
+ }, {
812
+ label: "Token Usage (CSV)",
813
+ onClick: () => downloadFile(dailyTokensToCSV(stats), "daily-tokens.csv", "text/csv")
814
+ }, {
815
+ label: "Model Usage (CSV)",
816
+ onClick: () => downloadFile(modelUsageToCSV(stats), "model-usage.csv", "text/csv")
817
+ }, {
818
+ label: "Full Stats (JSON)",
819
+ onClick: () => downloadFile(statsToJSON(stats), "stats.json", "application/json")
820
+ }] })
821
+ ] }),
822
+ /* @__PURE__ */ jsxs("div", { className: "mt-4 flex gap-1 border-b border-gray-800", children: [
823
+ /* @__PURE__ */ jsx("button", { onClick: () => navigate({
824
+ search: {
825
+ tab: "overview"
826
+ }
827
+ }), className: `px-4 py-2 text-sm border-b-2 transition-colors ${tab === "overview" ? "border-brand-500 text-white" : "border-transparent text-gray-400 hover:text-gray-200"}`, children: "Overview" }),
828
+ /* @__PURE__ */ jsx("button", { onClick: () => navigate({
829
+ search: {
830
+ tab: "projects"
831
+ }
832
+ }), className: `px-4 py-2 text-sm border-b-2 transition-colors ${tab === "projects" ? "border-brand-500 text-white" : "border-transparent text-gray-400 hover:text-gray-200"}`, children: "Projects" })
833
+ ] }),
834
+ tab === "overview" ? /* @__PURE__ */ jsx(StatsOverview, { stats, isLoading, cost }) : /* @__PURE__ */ jsx("div", { className: "mt-6", children: /* @__PURE__ */ jsx(ProjectAnalytics, {}) })
835
+ ] });
836
+ }
837
+ function StatsOverview({
838
+ stats,
839
+ isLoading,
840
+ cost
841
+ }) {
842
+ if (isLoading) {
843
+ return /* @__PURE__ */ jsxs("div", { className: "mt-6 space-y-4", children: [
844
+ /* @__PURE__ */ jsx("div", { className: "grid grid-cols-2 gap-3 md:grid-cols-5", children: Array.from({
845
+ length: 5
846
+ }).map((_, i) => /* @__PURE__ */ jsx("div", { className: "h-20 animate-pulse rounded-xl bg-gray-900/50" }, i)) }),
847
+ Array.from({
848
+ length: 3
849
+ }).map((_, i) => /* @__PURE__ */ jsx("div", { className: "h-64 animate-pulse rounded-xl bg-gray-900/50" }, i))
850
+ ] });
851
+ }
852
+ if (!stats) {
853
+ return /* @__PURE__ */ jsx("div", { className: "py-12 text-center text-sm text-gray-500", children: "No stats data found. Check ~/.claude/stats-cache.json" });
854
+ }
855
+ const totalTokens = Object.values(stats.modelUsage).reduce((sum, m) => sum + m.inputTokens + m.outputTokens, 0);
856
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
857
+ /* @__PURE__ */ jsxs("div", { className: "mt-6 grid grid-cols-2 gap-3 md:grid-cols-5", children: [
858
+ /* @__PURE__ */ jsx(StatCard, { label: "Total Sessions", value: String(stats.totalSessions) }),
859
+ /* @__PURE__ */ jsx(StatCard, { label: "Total Messages", value: stats.totalMessages.toLocaleString() }),
860
+ /* @__PURE__ */ jsx(StatCard, { label: "Total Tokens", value: formatTokenCount(totalTokens), sub: cost ? `~${formatUSD(cost.totalUSD)}` : void 0 }),
861
+ /* @__PURE__ */ jsx(StatCard, { label: "Total Estimated Cost", value: cost ? `~${formatUSD(cost.totalUSD)}` : "N/A" }),
862
+ /* @__PURE__ */ jsx(StatCard, { label: "Longest Session", value: formatDuration(stats.longestSession.duration), sub: `${stats.longestSession.messageCount} messages` })
863
+ ] }),
864
+ /* @__PURE__ */ jsx("div", { className: "mt-6", children: /* @__PURE__ */ jsx(ContributionHeatmap, { dailyActivity: stats.dailyActivity, dailyModelTokens: stats.dailyModelTokens }) }),
865
+ /* @__PURE__ */ jsx("div", { className: "mt-4", children: /* @__PURE__ */ jsx(ActivityChart, { data: stats.dailyActivity }) }),
866
+ /* @__PURE__ */ jsx("div", { className: "mt-4", children: /* @__PURE__ */ jsx(TokenTrendChart, { data: stats.dailyModelTokens }) }),
867
+ /* @__PURE__ */ jsxs("div", { className: "mt-4 grid grid-cols-1 gap-4 md:grid-cols-2", children: [
868
+ /* @__PURE__ */ jsx(ModelUsageChart, { data: stats.modelUsage }),
869
+ /* @__PURE__ */ jsx(HourlyDistribution, { hourCounts: stats.hourCounts })
870
+ ] })
871
+ ] });
872
+ }
873
+ function StatCard({
874
+ label,
875
+ value,
876
+ sub
877
+ }) {
878
+ return /* @__PURE__ */ jsxs("div", { className: "rounded-xl border border-gray-800 bg-gray-900/50 p-4", children: [
879
+ /* @__PURE__ */ jsx("p", { className: "text-xs text-gray-400", children: label }),
880
+ /* @__PURE__ */ jsx("p", { className: "mt-1 text-xl font-bold text-white", children: value }),
881
+ sub && /* @__PURE__ */ jsx("p", { className: "mt-0.5 text-xs text-gray-500", children: sub })
882
+ ] });
883
+ }
884
+ export {
885
+ StatsPage as component
886
+ };