claude-session-dashboard 0.1.3 → 0.2.1

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 (39) hide show
  1. package/dist/client/assets/_dashboard-Bxw4OxIS.js +1 -0
  2. package/dist/client/assets/_sessionId-CNR4Ln7m.js +12 -0
  3. package/dist/client/assets/app-u2nTs9ny.css +1 -0
  4. package/dist/client/assets/{createServerFn-Le0d8Pjz.js → createServerFn-B0pEGqTk.js} +1 -1
  5. package/dist/client/assets/format-Bsprb3az.js +1 -0
  6. package/dist/client/assets/index-BbdJ1jMA.js +1 -0
  7. package/dist/client/assets/{main-CzD8HjLq.js → main-CM5g2n-_.js} +7 -7
  8. package/dist/client/assets/sessions.queries-AUVV0tJj.js +1 -0
  9. package/dist/client/assets/{settings-BSPc79zZ.js → settings-CIwZDakc.js} +1 -1
  10. package/dist/client/assets/{settings.types-B4841OLF.js → settings.types-BRNIMHGJ.js} +1 -1
  11. package/dist/client/assets/stats-CjWSMX3y.js +4 -0
  12. package/dist/client/assets/useSessionCost-DgFKglaG.js +65 -0
  13. package/dist/server/assets/{_dashboard--ukhquwO.js → _dashboard-CAO6-qAS.js} +22 -3
  14. package/dist/server/assets/{_sessionId-BwZK4Ezz.js → _sessionId-BZf2Aqy5.js} +30 -7
  15. package/dist/server/assets/_tanstack-start-manifest_v-C5hwNzs-.js +4 -0
  16. package/dist/server/assets/{claude-path-CkuljM34.js → claude-path-BdwflgZ1.js} +9 -3
  17. package/dist/server/assets/{format-CGmJnuhZ.js → format-DIZHV7IJ.js} +3 -3
  18. package/dist/server/assets/{index-D4VWrt2z.js → index-Do0HxVmM.js} +7 -40
  19. package/dist/server/assets/project-analytics.server-BkWSd6a8.js +61 -0
  20. package/dist/server/assets/{router-xTSe9UH_.js → router-ChxlsPNU.js} +12 -7
  21. package/dist/server/assets/{session-detail.server-azkRfON2.js → session-detail.server-DLXl-Pn-.js} +1 -1
  22. package/dist/server/assets/session-scanner-CLfls9u-.js +93 -0
  23. package/dist/server/assets/sessions.queries-B5ZBiVJy.js +42 -0
  24. package/dist/server/assets/{sessions.server-B8zbmvSM.js → sessions.server-CUhasKW2.js} +5 -89
  25. package/dist/server/assets/stats-C9cZXTP5.js +649 -0
  26. package/dist/server/assets/{stats.server-BZWxV-mC.js → stats.server-52mNk2Yw.js} +1 -1
  27. package/dist/server/assets/useSessionCost-CYs5UOX-.js +209 -0
  28. package/dist/server/server.js +19 -16
  29. package/package.json +5 -1
  30. package/dist/client/assets/_dashboard-CYwTENkn.js +0 -1
  31. package/dist/client/assets/_sessionId-Bwfhm_El.js +0 -12
  32. package/dist/client/assets/app-DhZyFob1.css +0 -1
  33. package/dist/client/assets/format-Bf-cSf6L.js +0 -1
  34. package/dist/client/assets/index-DXhX1hdS.js +0 -1
  35. package/dist/client/assets/stats-CDIvpOt9.js +0 -4
  36. package/dist/client/assets/useSessionCost-9NP6uhla.js +0 -61
  37. package/dist/server/assets/_tanstack-start-manifest_v-gtQY7f-T.js +0 -4
  38. package/dist/server/assets/stats-DItsFPp5.js +0 -266
  39. package/dist/server/assets/useSessionCost-EB0VxklP.js +0 -76
@@ -0,0 +1,649 @@
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, startOfISOWeek, parseISO } from "date-fns";
8
+ import { f as formatTokenCount, a as formatDuration, b as formatRelativeTime, c as formatUSD } from "./format-DIZHV7IJ.js";
9
+ import { Link } from "@tanstack/react-router";
10
+ 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";
11
+ import { R as Route } from "./router-ChxlsPNU.js";
12
+ import "@tanstack/history";
13
+ import "@tanstack/router-core/ssr/client";
14
+ import "@tanstack/router-core";
15
+ import "node:async_hooks";
16
+ import "@tanstack/router-core/ssr/server";
17
+ import "h3-v2";
18
+ import "tiny-invariant";
19
+ import "seroval";
20
+ import "@tanstack/react-router/ssr/server";
21
+ import "./settings.queries-DSQd324O.js";
22
+ import "./settings.types-DntadCHo.js";
23
+ import "zod";
24
+ const getStats = createServerFn({
25
+ method: "GET"
26
+ }).handler(createSsrRpc("4b9a58c176f487b49800a372100037cdf33cf048f3592a449f115c7e3f5ea799"));
27
+ const statsQuery = queryOptions({
28
+ queryKey: ["stats"],
29
+ queryFn: () => getStats(),
30
+ refetchInterval: 6e4
31
+ });
32
+ function ActivityChart({ data }) {
33
+ const chartData = data.map((d) => ({
34
+ ...d,
35
+ dateLabel: format(new Date(d.date), "MMM d")
36
+ }));
37
+ return /* @__PURE__ */ jsxs("div", { className: "rounded-xl border border-gray-800 bg-gray-900/50 p-4", children: [
38
+ /* @__PURE__ */ jsx("h3", { className: "text-sm font-semibold text-gray-300", children: "Daily Activity" }),
39
+ /* @__PURE__ */ jsx("p", { className: "mt-1 text-xs text-gray-500", children: "Messages, sessions, and tool calls per day" }),
40
+ /* @__PURE__ */ jsx("div", { className: "mt-4 h-64", children: /* @__PURE__ */ jsx(ResponsiveContainer, { width: "100%", height: "100%", children: /* @__PURE__ */ jsxs(BarChart, { data: chartData, children: [
41
+ /* @__PURE__ */ jsx(CartesianGrid, { strokeDasharray: "3 3", stroke: "#1f2937" }),
42
+ /* @__PURE__ */ jsx(
43
+ XAxis,
44
+ {
45
+ dataKey: "dateLabel",
46
+ tick: { fill: "#6b7280", fontSize: 10 },
47
+ tickLine: false
48
+ }
49
+ ),
50
+ /* @__PURE__ */ jsx(
51
+ YAxis,
52
+ {
53
+ tick: { fill: "#6b7280", fontSize: 10 },
54
+ tickLine: false,
55
+ axisLine: false
56
+ }
57
+ ),
58
+ /* @__PURE__ */ jsx(
59
+ Tooltip,
60
+ {
61
+ contentStyle: {
62
+ backgroundColor: "#111827",
63
+ border: "1px solid #374151",
64
+ borderRadius: "8px",
65
+ fontSize: "12px"
66
+ }
67
+ }
68
+ ),
69
+ /* @__PURE__ */ jsx(
70
+ Bar,
71
+ {
72
+ dataKey: "messageCount",
73
+ name: "Messages",
74
+ fill: "#3b82f6",
75
+ radius: [2, 2, 0, 0]
76
+ }
77
+ ),
78
+ /* @__PURE__ */ jsx(
79
+ Bar,
80
+ {
81
+ dataKey: "toolCallCount",
82
+ name: "Tool Calls",
83
+ fill: "#8b5cf6",
84
+ radius: [2, 2, 0, 0]
85
+ }
86
+ ),
87
+ /* @__PURE__ */ jsx(
88
+ Bar,
89
+ {
90
+ dataKey: "sessionCount",
91
+ name: "Sessions",
92
+ fill: "#10b981",
93
+ radius: [2, 2, 0, 0]
94
+ }
95
+ )
96
+ ] }) }) })
97
+ ] });
98
+ }
99
+ const COLORS$1 = ["#3b82f6", "#8b5cf6", "#10b981", "#f59e0b", "#ef4444", "#6366f1"];
100
+ function normalizeModelName(model) {
101
+ return model.replace(/^claude-/, "").split("-202")[0];
102
+ }
103
+ function getTopModels(data, limit) {
104
+ const totals = {};
105
+ for (const day of data) {
106
+ for (const [model, tokens] of Object.entries(day.tokensByModel)) {
107
+ const normalized = normalizeModelName(model);
108
+ totals[normalized] = (totals[normalized] ?? 0) + tokens;
109
+ }
110
+ }
111
+ const sorted = Object.entries(totals).sort((a, b) => b[1] - a[1]);
112
+ const topModels = sorted.slice(0, limit).map(([name]) => name);
113
+ const hasOther = sorted.length > limit;
114
+ return { topModels, hasOther };
115
+ }
116
+ function processDaily(data, topModels, hasOther) {
117
+ return data.map((day) => {
118
+ const entry = {
119
+ dateLabel: format(parseISO(day.date), "MMM d"),
120
+ sortKey: day.date
121
+ };
122
+ for (const model of topModels) {
123
+ entry[model] = 0;
124
+ }
125
+ if (hasOther) {
126
+ entry["Other"] = 0;
127
+ }
128
+ for (const [rawModel, tokens] of Object.entries(day.tokensByModel)) {
129
+ const normalized = normalizeModelName(rawModel);
130
+ if (topModels.includes(normalized)) {
131
+ entry[normalized] = entry[normalized] + tokens;
132
+ } else if (hasOther) {
133
+ entry["Other"] = entry["Other"] + tokens;
134
+ }
135
+ }
136
+ return entry;
137
+ });
138
+ }
139
+ function processWeekly(data, topModels, hasOther) {
140
+ const weekMap = /* @__PURE__ */ new Map();
141
+ for (const day of data) {
142
+ const weekStart = startOfISOWeek(parseISO(day.date));
143
+ const weekKey = format(weekStart, "yyyy-MM-dd");
144
+ if (!weekMap.has(weekKey)) {
145
+ const entry2 = {
146
+ dateLabel: `Week of ${format(weekStart, "MMM d")}`,
147
+ sortKey: weekKey
148
+ };
149
+ for (const model of topModels) {
150
+ entry2[model] = 0;
151
+ }
152
+ if (hasOther) {
153
+ entry2["Other"] = 0;
154
+ }
155
+ weekMap.set(weekKey, entry2);
156
+ }
157
+ const entry = weekMap.get(weekKey);
158
+ for (const [rawModel, tokens] of Object.entries(day.tokensByModel)) {
159
+ const normalized = normalizeModelName(rawModel);
160
+ if (topModels.includes(normalized)) {
161
+ entry[normalized] = entry[normalized] + tokens;
162
+ } else if (hasOther) {
163
+ entry["Other"] = entry["Other"] + tokens;
164
+ }
165
+ }
166
+ }
167
+ return Array.from(weekMap.values()).sort(
168
+ (a, b) => a.sortKey.localeCompare(b.sortKey)
169
+ );
170
+ }
171
+ function CustomTooltip({ active, payload, label }) {
172
+ if (!active || !payload || payload.length === 0) return null;
173
+ const total = payload.reduce((sum, entry) => sum + entry.value, 0);
174
+ return /* @__PURE__ */ jsxs("div", { className: "rounded-lg border border-gray-700 bg-gray-800 p-3 text-xs shadow-lg", children: [
175
+ /* @__PURE__ */ jsx("p", { className: "mb-2 font-medium text-gray-300", children: label }),
176
+ 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: [
177
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1.5", children: [
178
+ /* @__PURE__ */ jsx(
179
+ "div",
180
+ {
181
+ className: "h-2 w-2 rounded-full",
182
+ style: { backgroundColor: entry.color }
183
+ }
184
+ ),
185
+ /* @__PURE__ */ jsx("span", { className: "text-gray-400", children: entry.name })
186
+ ] }),
187
+ /* @__PURE__ */ jsx("span", { className: "font-mono text-gray-300", children: formatTokenCount(entry.value) })
188
+ ] }, entry.name)),
189
+ /* @__PURE__ */ jsxs("div", { className: "mt-1.5 border-t border-gray-700 pt-1.5 flex justify-between", children: [
190
+ /* @__PURE__ */ jsx("span", { className: "text-gray-400", children: "Total" }),
191
+ /* @__PURE__ */ jsx("span", { className: "font-mono font-medium text-white", children: formatTokenCount(total) })
192
+ ] })
193
+ ] });
194
+ }
195
+ function TokenTrendChart({ data }) {
196
+ const [granularity, setGranularity] = useState("daily");
197
+ const { topModels, hasOther } = useMemo(
198
+ () => getTopModels(data, 5),
199
+ [data]
200
+ );
201
+ const allModelKeys = useMemo(() => {
202
+ const keys = [...topModels];
203
+ if (hasOther) keys.push("Other");
204
+ return keys;
205
+ }, [topModels, hasOther]);
206
+ const chartData = useMemo(() => {
207
+ if (granularity === "weekly") {
208
+ return processWeekly(data, topModels, hasOther);
209
+ }
210
+ return processDaily(data, topModels, hasOther);
211
+ }, [data, topModels, hasOther, granularity]);
212
+ if (data.length === 0) {
213
+ return /* @__PURE__ */ jsxs("div", { className: "rounded-xl border border-gray-800 bg-gray-900/50 p-4", children: [
214
+ /* @__PURE__ */ jsx("h3", { className: "text-sm font-semibold text-gray-300", children: "Token Usage Over Time" }),
215
+ /* @__PURE__ */ jsx("p", { className: "mt-1 text-xs text-gray-500", children: "Total tokens by model per day" }),
216
+ /* @__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" }) })
217
+ ] });
218
+ }
219
+ return /* @__PURE__ */ jsxs("div", { className: "rounded-xl border border-gray-800 bg-gray-900/50 p-4", children: [
220
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between", children: [
221
+ /* @__PURE__ */ jsxs("div", { children: [
222
+ /* @__PURE__ */ jsx("h3", { className: "text-sm font-semibold text-gray-300", children: "Token Usage Over Time" }),
223
+ /* @__PURE__ */ jsx("p", { className: "mt-1 text-xs text-gray-500", children: "Total tokens by model per day" })
224
+ ] }),
225
+ /* @__PURE__ */ jsxs("div", { className: "flex rounded-lg border border-gray-700 text-xs", children: [
226
+ /* @__PURE__ */ jsx(
227
+ "button",
228
+ {
229
+ type: "button",
230
+ onClick: () => setGranularity("daily"),
231
+ className: `rounded-l-lg px-3 py-1 ${granularity === "daily" ? "bg-gray-700 text-white" : "text-gray-400 hover:text-gray-300"}`,
232
+ children: "Daily"
233
+ }
234
+ ),
235
+ /* @__PURE__ */ jsx(
236
+ "button",
237
+ {
238
+ type: "button",
239
+ onClick: () => setGranularity("weekly"),
240
+ className: `rounded-r-lg px-3 py-1 ${granularity === "weekly" ? "bg-gray-700 text-white" : "text-gray-400 hover:text-gray-300"}`,
241
+ children: "Weekly"
242
+ }
243
+ )
244
+ ] })
245
+ ] }),
246
+ /* @__PURE__ */ jsx("div", { className: "mt-4 h-72", children: /* @__PURE__ */ jsx(ResponsiveContainer, { width: "100%", height: "100%", children: /* @__PURE__ */ jsxs(AreaChart, { data: chartData, children: [
247
+ /* @__PURE__ */ jsx(CartesianGrid, { strokeDasharray: "3 3", stroke: "#1f2937" }),
248
+ /* @__PURE__ */ jsx(
249
+ XAxis,
250
+ {
251
+ dataKey: "dateLabel",
252
+ tick: { fill: "#6b7280", fontSize: 10 },
253
+ tickLine: false
254
+ }
255
+ ),
256
+ /* @__PURE__ */ jsx(
257
+ YAxis,
258
+ {
259
+ tick: { fill: "#6b7280", fontSize: 10 },
260
+ tickLine: false,
261
+ axisLine: false,
262
+ tickFormatter: (value) => formatTokenCount(value)
263
+ }
264
+ ),
265
+ /* @__PURE__ */ jsx(Tooltip, { content: /* @__PURE__ */ jsx(CustomTooltip, {}) }),
266
+ allModelKeys.map((model, i) => /* @__PURE__ */ jsx(
267
+ Area,
268
+ {
269
+ type: "monotone",
270
+ dataKey: model,
271
+ stackId: "1",
272
+ stroke: COLORS$1[i % COLORS$1.length],
273
+ fill: COLORS$1[i % COLORS$1.length],
274
+ fillOpacity: 0.6
275
+ },
276
+ model
277
+ ))
278
+ ] }) }) })
279
+ ] });
280
+ }
281
+ const COLORS = ["#3b82f6", "#8b5cf6", "#10b981", "#f59e0b", "#ef4444", "#6366f1"];
282
+ function ModelUsageChart({ data }) {
283
+ const chartData = Object.entries(data).map(([model, usage]) => ({
284
+ name: model.replace(/^claude-/, "").split("-202")[0],
285
+ fullName: model,
286
+ totalTokens: usage.inputTokens + usage.outputTokens,
287
+ outputTokens: usage.outputTokens
288
+ }));
289
+ chartData.sort((a, b) => b.totalTokens - a.totalTokens);
290
+ return /* @__PURE__ */ jsxs("div", { className: "rounded-xl border border-gray-800 bg-gray-900/50 p-4", children: [
291
+ /* @__PURE__ */ jsx("h3", { className: "text-sm font-semibold text-gray-300", children: "Model Usage" }),
292
+ /* @__PURE__ */ jsx("p", { className: "mt-1 text-xs text-gray-500", children: "Token usage by model" }),
293
+ /* @__PURE__ */ jsx("div", { className: "mt-4 h-64", children: /* @__PURE__ */ jsx(ResponsiveContainer, { width: "100%", height: "100%", children: /* @__PURE__ */ jsxs(PieChart, { children: [
294
+ /* @__PURE__ */ jsx(
295
+ Pie,
296
+ {
297
+ data: chartData,
298
+ cx: "50%",
299
+ cy: "50%",
300
+ innerRadius: 50,
301
+ outerRadius: 80,
302
+ dataKey: "totalTokens",
303
+ nameKey: "name",
304
+ strokeWidth: 0,
305
+ children: chartData.map((_, i) => /* @__PURE__ */ jsx(Cell, { fill: COLORS[i % COLORS.length], opacity: 0.8 }, i))
306
+ }
307
+ ),
308
+ /* @__PURE__ */ jsx(
309
+ Tooltip,
310
+ {
311
+ formatter: (value) => formatTokenCount(value),
312
+ contentStyle: {
313
+ backgroundColor: "#111827",
314
+ border: "1px solid #374151",
315
+ borderRadius: "8px",
316
+ fontSize: "12px"
317
+ }
318
+ }
319
+ ),
320
+ /* @__PURE__ */ jsx(
321
+ Legend,
322
+ {
323
+ wrapperStyle: { fontSize: "11px" }
324
+ }
325
+ )
326
+ ] }) }) })
327
+ ] });
328
+ }
329
+ function HourlyDistribution({
330
+ hourCounts
331
+ }) {
332
+ const hours = Array.from({ length: 24 }, (_, i) => ({
333
+ hour: i,
334
+ label: `${i.toString().padStart(2, "0")}:00`,
335
+ count: hourCounts[String(i)] ?? 0
336
+ }));
337
+ const maxCount = Math.max(...hours.map((h) => h.count), 1);
338
+ return /* @__PURE__ */ jsxs("div", { className: "rounded-xl border border-gray-800 bg-gray-900/50 p-4", children: [
339
+ /* @__PURE__ */ jsx("h3", { className: "text-sm font-semibold text-gray-300", children: "Hourly Distribution" }),
340
+ /* @__PURE__ */ jsx("p", { className: "mt-1 text-xs text-gray-500", children: "Session starts by hour of day" }),
341
+ /* @__PURE__ */ jsx("div", { className: "mt-4 flex items-end gap-0.5", style: { height: "120px" }, children: hours.map((h) => {
342
+ const height = h.count > 0 ? Math.max(h.count / maxCount * 100, 4) : 0;
343
+ const intensity = h.count / maxCount;
344
+ return /* @__PURE__ */ jsxs(
345
+ "div",
346
+ {
347
+ className: "group relative flex-1",
348
+ style: { height: "100%" },
349
+ children: [
350
+ /* @__PURE__ */ jsx(
351
+ "div",
352
+ {
353
+ className: "absolute bottom-0 w-full rounded-t transition-colors",
354
+ style: {
355
+ height: `${height}%`,
356
+ backgroundColor: `rgba(59, 130, 246, ${0.2 + intensity * 0.6})`
357
+ }
358
+ }
359
+ ),
360
+ /* @__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: [
361
+ h.label,
362
+ ": ",
363
+ h.count
364
+ ] })
365
+ ]
366
+ },
367
+ h.hour
368
+ );
369
+ }) }),
370
+ /* @__PURE__ */ jsxs("div", { className: "mt-1 flex justify-between text-[10px] text-gray-600", children: [
371
+ /* @__PURE__ */ jsx("span", { children: "00:00" }),
372
+ /* @__PURE__ */ jsx("span", { children: "06:00" }),
373
+ /* @__PURE__ */ jsx("span", { children: "12:00" }),
374
+ /* @__PURE__ */ jsx("span", { children: "18:00" }),
375
+ /* @__PURE__ */ jsx("span", { children: "23:00" })
376
+ ] })
377
+ ] });
378
+ }
379
+ const getProjectAnalytics = createServerFn({
380
+ method: "GET"
381
+ }).handler(createSsrRpc("64052f224a1d6696436e5d3deeee2b798f0742e1292ffabd038c3a7bf75e6fcb"));
382
+ const projectAnalyticsQuery = queryOptions({
383
+ queryKey: ["projects", "analytics"],
384
+ queryFn: () => getProjectAnalytics(),
385
+ refetchInterval: 6e4
386
+ });
387
+ const COLUMNS = [
388
+ { key: "projectName", label: "Project" },
389
+ { key: "totalSessions", label: "Sessions", align: "right" },
390
+ { key: "totalMessages", label: "Messages", align: "right" },
391
+ { key: "totalDurationMs", label: "Duration", align: "right" },
392
+ { key: "lastSessionAt", label: "Last Active", align: "right" }
393
+ ];
394
+ function ProjectTable({ projects }) {
395
+ const [sortField, setSortField] = useState("lastSessionAt");
396
+ const [sortDir, setSortDir] = useState("desc");
397
+ const sorted = useMemo(() => {
398
+ const copy = [...projects];
399
+ copy.sort((a, b) => {
400
+ let cmp = 0;
401
+ switch (sortField) {
402
+ case "projectName":
403
+ cmp = a.projectName.localeCompare(b.projectName);
404
+ break;
405
+ case "totalSessions":
406
+ cmp = a.totalSessions - b.totalSessions;
407
+ break;
408
+ case "totalMessages":
409
+ cmp = a.totalMessages - b.totalMessages;
410
+ break;
411
+ case "totalDurationMs":
412
+ cmp = a.totalDurationMs - b.totalDurationMs;
413
+ break;
414
+ case "lastSessionAt":
415
+ cmp = a.lastSessionAt.localeCompare(b.lastSessionAt);
416
+ break;
417
+ }
418
+ return sortDir === "asc" ? cmp : -cmp;
419
+ });
420
+ return copy;
421
+ }, [projects, sortField, sortDir]);
422
+ function handleSort(field) {
423
+ if (field === sortField) {
424
+ setSortDir((prev) => prev === "asc" ? "desc" : "asc");
425
+ } else {
426
+ setSortField(field);
427
+ setSortDir("desc");
428
+ }
429
+ }
430
+ function renderSortIndicator(field) {
431
+ if (field !== sortField) return null;
432
+ return /* @__PURE__ */ jsx("span", { className: "ml-1", children: sortDir === "asc" ? "▲" : "▼" });
433
+ }
434
+ 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: [
435
+ /* @__PURE__ */ jsx("thead", { children: /* @__PURE__ */ jsx("tr", { className: "border-b border-gray-800", children: COLUMNS.map((col) => /* @__PURE__ */ jsxs(
436
+ "th",
437
+ {
438
+ onClick: () => handleSort(col.key),
439
+ 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"}`,
440
+ children: [
441
+ col.label,
442
+ renderSortIndicator(col.key)
443
+ ]
444
+ },
445
+ col.key
446
+ )) }) }),
447
+ /* @__PURE__ */ jsx("tbody", { children: sorted.map((project) => /* @__PURE__ */ jsxs(
448
+ "tr",
449
+ {
450
+ className: "border-b border-gray-800/50 transition-colors hover:bg-gray-800/30",
451
+ children: [
452
+ /* @__PURE__ */ jsxs("td", { className: "px-4 py-3", children: [
453
+ /* @__PURE__ */ jsx(
454
+ Link,
455
+ {
456
+ to: "/sessions",
457
+ search: { project: project.projectName },
458
+ className: "text-sm text-blue-400 hover:underline",
459
+ children: project.projectName
460
+ }
461
+ ),
462
+ project.activeSessions > 0 && /* @__PURE__ */ jsxs("span", { className: "ml-2 rounded-full bg-green-500/20 px-1.5 py-0.5 text-[10px] font-medium text-green-400", children: [
463
+ project.activeSessions,
464
+ " active"
465
+ ] })
466
+ ] }),
467
+ /* @__PURE__ */ jsx("td", { className: "px-4 py-3 text-right font-mono text-sm text-gray-300", children: project.totalSessions }),
468
+ /* @__PURE__ */ jsx("td", { className: "px-4 py-3 text-right font-mono text-sm text-gray-300", children: project.totalMessages.toLocaleString() }),
469
+ /* @__PURE__ */ jsx("td", { className: "px-4 py-3 text-right font-mono text-sm text-gray-300", children: formatDuration(project.totalDurationMs) }),
470
+ /* @__PURE__ */ jsx("td", { className: "px-4 py-3 text-right text-sm text-gray-400", children: formatRelativeTime(project.lastSessionAt) })
471
+ ]
472
+ },
473
+ project.projectPath
474
+ )) })
475
+ ] }) });
476
+ }
477
+ function ProjectAnalytics() {
478
+ const { data, isLoading } = useQuery(projectAnalyticsQuery);
479
+ if (isLoading) {
480
+ return /* @__PURE__ */ jsxs("div", { className: "space-y-4", children: [
481
+ /* @__PURE__ */ jsx("div", { className: "grid grid-cols-2 gap-3 md:grid-cols-3", children: Array.from({ length: 3 }).map((_, i) => /* @__PURE__ */ jsx(
482
+ "div",
483
+ {
484
+ className: "h-20 animate-pulse rounded-xl bg-gray-900/50"
485
+ },
486
+ i
487
+ )) }),
488
+ /* @__PURE__ */ jsx("div", { className: "h-64 animate-pulse rounded-xl bg-gray-900/50" })
489
+ ] });
490
+ }
491
+ const projects = data?.projects ?? [];
492
+ if (projects.length === 0) {
493
+ return /* @__PURE__ */ jsx("div", { className: "py-12 text-center text-sm text-gray-500", children: "No projects found. Sessions will appear here once scanned." });
494
+ }
495
+ const totalSessions = projects.reduce((sum, p) => sum + p.totalSessions, 0);
496
+ const totalDurationMs = projects.reduce((sum, p) => sum + p.totalDurationMs, 0);
497
+ const mostActive = projects.reduce(
498
+ (max, p) => p.totalSessions > max.totalSessions ? p : max
499
+ );
500
+ return /* @__PURE__ */ jsxs("div", { className: "space-y-6", children: [
501
+ /* @__PURE__ */ jsxs("div", { className: "grid grid-cols-2 gap-3 md:grid-cols-4", children: [
502
+ /* @__PURE__ */ jsx(SummaryCard, { label: "Total Projects", value: String(projects.length) }),
503
+ /* @__PURE__ */ jsx(
504
+ SummaryCard,
505
+ {
506
+ label: "Total Sessions",
507
+ value: totalSessions.toLocaleString()
508
+ }
509
+ ),
510
+ /* @__PURE__ */ jsx(
511
+ SummaryCard,
512
+ {
513
+ label: "Total Duration",
514
+ value: formatDuration(totalDurationMs)
515
+ }
516
+ ),
517
+ /* @__PURE__ */ jsx(
518
+ SummaryCard,
519
+ {
520
+ label: "Most Active",
521
+ value: mostActive.projectName,
522
+ sub: `${mostActive.totalSessions} sessions`
523
+ }
524
+ )
525
+ ] }),
526
+ /* @__PURE__ */ jsx(ProjectTable, { projects })
527
+ ] });
528
+ }
529
+ function SummaryCard({
530
+ label,
531
+ value,
532
+ sub
533
+ }) {
534
+ return /* @__PURE__ */ jsxs("div", { className: "rounded-xl border border-gray-800 bg-gray-900/50 p-4", children: [
535
+ /* @__PURE__ */ jsx("p", { className: "text-xs text-gray-400", children: label }),
536
+ /* @__PURE__ */ jsx("p", { className: "mt-1 truncate text-xl font-bold text-white", children: value }),
537
+ sub && /* @__PURE__ */ jsx("p", { className: "mt-0.5 text-xs text-gray-500", children: sub })
538
+ ] });
539
+ }
540
+ const EMPTY_TOKENS_BY_MODEL = {};
541
+ function StatsPage() {
542
+ const {
543
+ tab
544
+ } = Route.useSearch();
545
+ const navigate = Route.useNavigate();
546
+ const {
547
+ data: stats,
548
+ isLoading
549
+ } = useQuery(statsQuery);
550
+ const tokensByModel = useMemo(() => {
551
+ if (!stats) return EMPTY_TOKENS_BY_MODEL;
552
+ const result = {};
553
+ for (const [model, usage] of Object.entries(stats.modelUsage)) {
554
+ result[model] = {
555
+ inputTokens: usage.inputTokens,
556
+ outputTokens: usage.outputTokens,
557
+ cacheReadInputTokens: usage.cacheReadInputTokens,
558
+ cacheCreationInputTokens: usage.cacheCreationInputTokens
559
+ };
560
+ }
561
+ return result;
562
+ }, [stats]);
563
+ const {
564
+ cost
565
+ } = useSessionCost(tokensByModel);
566
+ return /* @__PURE__ */ jsxs("div", { children: [
567
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between", children: [
568
+ /* @__PURE__ */ jsxs("div", { children: [
569
+ /* @__PURE__ */ jsx("h1", { className: "text-2xl font-bold text-white", children: "Stats" }),
570
+ /* @__PURE__ */ jsx("p", { className: "mt-1 text-sm text-gray-400", children: "Usage analytics and project insights" })
571
+ ] }),
572
+ tab === "overview" && stats && /* @__PURE__ */ jsx(ExportDropdown, { options: [{
573
+ label: "Daily Activity (CSV)",
574
+ onClick: () => downloadFile(dailyActivityToCSV(stats), "daily-activity.csv", "text/csv")
575
+ }, {
576
+ label: "Token Usage (CSV)",
577
+ onClick: () => downloadFile(dailyTokensToCSV(stats), "daily-tokens.csv", "text/csv")
578
+ }, {
579
+ label: "Model Usage (CSV)",
580
+ onClick: () => downloadFile(modelUsageToCSV(stats), "model-usage.csv", "text/csv")
581
+ }, {
582
+ label: "Full Stats (JSON)",
583
+ onClick: () => downloadFile(statsToJSON(stats), "stats.json", "application/json")
584
+ }] })
585
+ ] }),
586
+ /* @__PURE__ */ jsxs("div", { className: "mt-4 flex gap-1 border-b border-gray-800", children: [
587
+ /* @__PURE__ */ jsx("button", { onClick: () => navigate({
588
+ search: {
589
+ tab: "overview"
590
+ }
591
+ }), className: `px-4 py-2 text-sm border-b-2 transition-colors ${tab === "overview" ? "border-blue-500 text-white" : "border-transparent text-gray-400 hover:text-gray-200"}`, children: "Overview" }),
592
+ /* @__PURE__ */ jsx("button", { onClick: () => navigate({
593
+ search: {
594
+ tab: "projects"
595
+ }
596
+ }), className: `px-4 py-2 text-sm border-b-2 transition-colors ${tab === "projects" ? "border-blue-500 text-white" : "border-transparent text-gray-400 hover:text-gray-200"}`, children: "Projects" })
597
+ ] }),
598
+ tab === "overview" ? /* @__PURE__ */ jsx(StatsOverview, { stats, isLoading, cost }) : /* @__PURE__ */ jsx("div", { className: "mt-6", children: /* @__PURE__ */ jsx(ProjectAnalytics, {}) })
599
+ ] });
600
+ }
601
+ function StatsOverview({
602
+ stats,
603
+ isLoading,
604
+ cost
605
+ }) {
606
+ if (isLoading) {
607
+ return /* @__PURE__ */ jsxs("div", { className: "mt-6 space-y-4", children: [
608
+ /* @__PURE__ */ jsx("div", { className: "grid grid-cols-2 gap-3 md:grid-cols-5", children: Array.from({
609
+ length: 5
610
+ }).map((_, i) => /* @__PURE__ */ jsx("div", { className: "h-20 animate-pulse rounded-xl bg-gray-900/50" }, i)) }),
611
+ Array.from({
612
+ length: 3
613
+ }).map((_, i) => /* @__PURE__ */ jsx("div", { className: "h-64 animate-pulse rounded-xl bg-gray-900/50" }, i))
614
+ ] });
615
+ }
616
+ if (!stats) {
617
+ return /* @__PURE__ */ jsx("div", { className: "py-12 text-center text-sm text-gray-500", children: "No stats data found. Check ~/.claude/stats-cache.json" });
618
+ }
619
+ const totalTokens = Object.values(stats.modelUsage).reduce((sum, m) => sum + m.inputTokens + m.outputTokens, 0);
620
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
621
+ /* @__PURE__ */ jsxs("div", { className: "mt-6 grid grid-cols-2 gap-3 md:grid-cols-5", children: [
622
+ /* @__PURE__ */ jsx(StatCard, { label: "Total Sessions", value: String(stats.totalSessions) }),
623
+ /* @__PURE__ */ jsx(StatCard, { label: "Total Messages", value: stats.totalMessages.toLocaleString() }),
624
+ /* @__PURE__ */ jsx(StatCard, { label: "Total Tokens", value: formatTokenCount(totalTokens), sub: cost ? `~${formatUSD(cost.totalUSD)}` : void 0 }),
625
+ /* @__PURE__ */ jsx(StatCard, { label: "Total Estimated Cost", value: cost ? `~${formatUSD(cost.totalUSD)}` : "N/A" }),
626
+ /* @__PURE__ */ jsx(StatCard, { label: "Longest Session", value: formatDuration(stats.longestSession.duration), sub: `${stats.longestSession.messageCount} messages` })
627
+ ] }),
628
+ /* @__PURE__ */ jsx("div", { className: "mt-6", children: /* @__PURE__ */ jsx(ActivityChart, { data: stats.dailyActivity }) }),
629
+ /* @__PURE__ */ jsx("div", { className: "mt-4", children: /* @__PURE__ */ jsx(TokenTrendChart, { data: stats.dailyModelTokens }) }),
630
+ /* @__PURE__ */ jsxs("div", { className: "mt-4 grid grid-cols-1 gap-4 md:grid-cols-2", children: [
631
+ /* @__PURE__ */ jsx(ModelUsageChart, { data: stats.modelUsage }),
632
+ /* @__PURE__ */ jsx(HourlyDistribution, { hourCounts: stats.hourCounts })
633
+ ] })
634
+ ] });
635
+ }
636
+ function StatCard({
637
+ label,
638
+ value,
639
+ sub
640
+ }) {
641
+ return /* @__PURE__ */ jsxs("div", { className: "rounded-xl border border-gray-800 bg-gray-900/50 p-4", children: [
642
+ /* @__PURE__ */ jsx("p", { className: "text-xs text-gray-400", children: label }),
643
+ /* @__PURE__ */ jsx("p", { className: "mt-1 text-xl font-bold text-white", children: value }),
644
+ sub && /* @__PURE__ */ jsx("p", { className: "mt-0.5 text-xs text-gray-500", children: sub })
645
+ ] });
646
+ }
647
+ export {
648
+ StatsPage as component
649
+ };
@@ -1,6 +1,6 @@
1
1
  import { c as createServerRpc } from "./createServerRpc-Bd3B-Ah9.js";
2
2
  import * as fs from "node:fs";
3
- import { a as getStatsPath } from "./claude-path-CkuljM34.js";
3
+ import { g as getStatsPath } from "./claude-path-BdwflgZ1.js";
4
4
  import { z } from "zod";
5
5
  import { c as createServerFn } from "../server.js";
6
6
  import "node:path";