claude-session-dashboard 0.2.1 → 0.3.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 (37) hide show
  1. package/README.md +156 -14
  2. package/dist/client/assets/_dashboard-C-1YOzkf.js +1 -0
  3. package/dist/client/assets/_sessionId-C4jQeEqE.js +12 -0
  4. package/dist/client/assets/app-DNBe9Acr.css +1 -0
  5. package/dist/client/assets/{createServerFn-B0pEGqTk.js → createServerFn-B5mibSc4.js} +1 -1
  6. package/dist/client/assets/index-C83jHUdL.js +1 -0
  7. package/dist/client/assets/{main-CM5g2n-_.js → main-CkUc_xJ0.js} +7 -7
  8. package/dist/client/assets/{sessions.queries-AUVV0tJj.js → sessions.queries-C-HTNzuR.js} +1 -1
  9. package/dist/client/assets/settings-D56cUmNH.js +1 -0
  10. package/dist/client/assets/{settings.types-BRNIMHGJ.js → settings.types-l5MKKuAK.js} +1 -1
  11. package/dist/client/assets/stats-BunIdzj_.js +4 -0
  12. package/dist/client/assets/{useSessionCost-DgFKglaG.js → useSessionCost-BDldLkTA.js} +1 -1
  13. package/dist/client/favicon.svg +3 -0
  14. package/dist/server/assets/_dashboard-TUzgwLqB.js +112 -0
  15. package/dist/server/assets/{_sessionId-BZf2Aqy5.js → _sessionId-BvDwvNyA.js} +162 -145
  16. package/dist/server/assets/_tanstack-start-manifest_v-CVdzOaof.js +4 -0
  17. package/dist/server/assets/{claude-path-BdwflgZ1.js → claude-path-B2oho3NT.js} +2 -2
  18. package/dist/server/assets/{index-Do0HxVmM.js → index-Biupny11.js} +22 -21
  19. package/dist/server/assets/{project-analytics.server-BkWSd6a8.js → project-analytics.server-t1bM6wAa.js} +3 -3
  20. package/dist/server/assets/{router-ChxlsPNU.js → router-kB-tCwY9.js} +55 -29
  21. package/dist/server/assets/{session-detail.server-DLXl-Pn-.js → session-detail.server-IUw67jz-.js} +2 -2
  22. package/dist/server/assets/{session-parser-CAEXxF1D.js → session-parser-CIucKYBT.js} +67 -0
  23. package/dist/server/assets/{session-scanner-CLfls9u-.js → session-scanner-1h9TTTAV.js} +2 -2
  24. package/dist/server/assets/{sessions.server-CUhasKW2.js → sessions.server-Cpffr3MU.js} +3 -3
  25. package/dist/server/assets/{settings-ko61yfVs.js → settings-jxAA3KAS.js} +66 -20
  26. package/dist/server/assets/{stats-C9cZXTP5.js → stats-CzGBAoxT.js} +261 -24
  27. package/dist/server/assets/{stats.server-52mNk2Yw.js → stats.server-DXJiLqey.js} +62 -3
  28. package/dist/server/server.js +17 -17
  29. package/package.json +7 -1
  30. package/dist/client/assets/_dashboard-Bxw4OxIS.js +0 -1
  31. package/dist/client/assets/_sessionId-CNR4Ln7m.js +0 -12
  32. package/dist/client/assets/app-u2nTs9ny.css +0 -1
  33. package/dist/client/assets/index-BbdJ1jMA.js +0 -1
  34. package/dist/client/assets/settings-CIwZDakc.js +0 -1
  35. package/dist/client/assets/stats-CjWSMX3y.js +0 -4
  36. package/dist/server/assets/_dashboard-CAO6-qAS.js +0 -116
  37. package/dist/server/assets/_tanstack-start-manifest_v-C5hwNzs-.js +0 -4
@@ -4,11 +4,12 @@ import { queryOptions, useQuery } from "@tanstack/react-query";
4
4
  import { c as createSsrRpc } from "./createSsrRpc-CVg2UDl0.js";
5
5
  import { c as createServerFn } from "../server.js";
6
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";
7
+ import { format, addDays, getDay, parseISO, startOfISOWeek } from "date-fns";
8
+ import { createPortal } from "react-dom";
8
9
  import { f as formatTokenCount, a as formatDuration, b as formatRelativeTime, c as formatUSD } from "./format-DIZHV7IJ.js";
9
10
  import { Link } from "@tanstack/react-router";
11
+ import { u as usePrivacy, R as Route } from "./router-kB-tCwY9.js";
10
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";
11
- import { R as Route } from "./router-ChxlsPNU.js";
12
13
  import "@tanstack/history";
13
14
  import "@tanstack/router-core/ssr/client";
14
15
  import "@tanstack/router-core";
@@ -18,9 +19,9 @@ import "h3-v2";
18
19
  import "tiny-invariant";
19
20
  import "seroval";
20
21
  import "@tanstack/react-router/ssr/server";
22
+ import "zod";
21
23
  import "./settings.queries-DSQd324O.js";
22
24
  import "./settings.types-DntadCHo.js";
23
- import "zod";
24
25
  const getStats = createServerFn({
25
26
  method: "GET"
26
27
  }).handler(createSsrRpc("4b9a58c176f487b49800a372100037cdf33cf048f3592a449f115c7e3f5ea799"));
@@ -38,19 +39,19 @@ function ActivityChart({ data }) {
38
39
  /* @__PURE__ */ jsx("h3", { className: "text-sm font-semibold text-gray-300", children: "Daily Activity" }),
39
40
  /* @__PURE__ */ jsx("p", { className: "mt-1 text-xs text-gray-500", children: "Messages, sessions, and tool calls per day" }),
40
41
  /* @__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(CartesianGrid, { strokeDasharray: "3 3", stroke: "#2a2926" }),
42
43
  /* @__PURE__ */ jsx(
43
44
  XAxis,
44
45
  {
45
46
  dataKey: "dateLabel",
46
- tick: { fill: "#6b7280", fontSize: 10 },
47
+ tick: { fill: "#7a7668", fontSize: 10 },
47
48
  tickLine: false
48
49
  }
49
50
  ),
50
51
  /* @__PURE__ */ jsx(
51
52
  YAxis,
52
53
  {
53
- tick: { fill: "#6b7280", fontSize: 10 },
54
+ tick: { fill: "#7a7668", fontSize: 10 },
54
55
  tickLine: false,
55
56
  axisLine: false
56
57
  }
@@ -59,8 +60,8 @@ function ActivityChart({ data }) {
59
60
  Tooltip,
60
61
  {
61
62
  contentStyle: {
62
- backgroundColor: "#111827",
63
- border: "1px solid #374151",
63
+ backgroundColor: "#1c1c1a",
64
+ border: "1px solid #3d3b36",
64
65
  borderRadius: "8px",
65
66
  fontSize: "12px"
66
67
  }
@@ -71,7 +72,7 @@ function ActivityChart({ data }) {
71
72
  {
72
73
  dataKey: "messageCount",
73
74
  name: "Messages",
74
- fill: "#3b82f6",
75
+ fill: "#d97757",
75
76
  radius: [2, 2, 0, 0]
76
77
  }
77
78
  ),
@@ -96,7 +97,240 @@ function ActivityChart({ data }) {
96
97
  ] }) }) })
97
98
  ] });
98
99
  }
99
- const COLORS$1 = ["#3b82f6", "#8b5cf6", "#10b981", "#f59e0b", "#ef4444", "#6366f1"];
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"];
100
334
  function normalizeModelName(model) {
101
335
  return model.replace(/^claude-/, "").split("-202")[0];
102
336
  }
@@ -244,19 +478,19 @@ function TokenTrendChart({ data }) {
244
478
  ] })
245
479
  ] }),
246
480
  /* @__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" }),
481
+ /* @__PURE__ */ jsx(CartesianGrid, { strokeDasharray: "3 3", stroke: "#2a2926" }),
248
482
  /* @__PURE__ */ jsx(
249
483
  XAxis,
250
484
  {
251
485
  dataKey: "dateLabel",
252
- tick: { fill: "#6b7280", fontSize: 10 },
486
+ tick: { fill: "#7a7668", fontSize: 10 },
253
487
  tickLine: false
254
488
  }
255
489
  ),
256
490
  /* @__PURE__ */ jsx(
257
491
  YAxis,
258
492
  {
259
- tick: { fill: "#6b7280", fontSize: 10 },
493
+ tick: { fill: "#7a7668", fontSize: 10 },
260
494
  tickLine: false,
261
495
  axisLine: false,
262
496
  tickFormatter: (value) => formatTokenCount(value)
@@ -278,7 +512,7 @@ function TokenTrendChart({ data }) {
278
512
  ] }) }) })
279
513
  ] });
280
514
  }
281
- const COLORS = ["#3b82f6", "#8b5cf6", "#10b981", "#f59e0b", "#ef4444", "#6366f1"];
515
+ const COLORS = ["#d97757", "#8b5cf6", "#10b981", "#f59e0b", "#ef4444", "#b07cc5"];
282
516
  function ModelUsageChart({ data }) {
283
517
  const chartData = Object.entries(data).map(([model, usage]) => ({
284
518
  name: model.replace(/^claude-/, "").split("-202")[0],
@@ -310,8 +544,8 @@ function ModelUsageChart({ data }) {
310
544
  {
311
545
  formatter: (value) => formatTokenCount(value),
312
546
  contentStyle: {
313
- backgroundColor: "#111827",
314
- border: "1px solid #374151",
547
+ backgroundColor: "#1c1c1a",
548
+ border: "1px solid #3d3b36",
315
549
  borderRadius: "8px",
316
550
  fontSize: "12px"
317
551
  }
@@ -353,7 +587,7 @@ function HourlyDistribution({
353
587
  className: "absolute bottom-0 w-full rounded-t transition-colors",
354
588
  style: {
355
589
  height: `${height}%`,
356
- backgroundColor: `rgba(59, 130, 246, ${0.2 + intensity * 0.6})`
590
+ backgroundColor: `rgba(217, 119, 87, ${0.2 + intensity * 0.6})`
357
591
  }
358
592
  }
359
593
  ),
@@ -392,6 +626,7 @@ const COLUMNS = [
392
626
  { key: "lastSessionAt", label: "Last Active", align: "right" }
393
627
  ];
394
628
  function ProjectTable({ projects }) {
629
+ const { anonymizeProjectName } = usePrivacy();
395
630
  const [sortField, setSortField] = useState("lastSessionAt");
396
631
  const [sortDir, setSortDir] = useState("desc");
397
632
  const sorted = useMemo(() => {
@@ -455,11 +690,11 @@ function ProjectTable({ projects }) {
455
690
  {
456
691
  to: "/sessions",
457
692
  search: { project: project.projectName },
458
- className: "text-sm text-blue-400 hover:underline",
459
- children: project.projectName
693
+ className: "text-sm text-brand-300 hover:underline",
694
+ children: anonymizeProjectName(project.projectName)
460
695
  }
461
696
  ),
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: [
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: [
463
698
  project.activeSessions,
464
699
  " active"
465
700
  ] })
@@ -475,6 +710,7 @@ function ProjectTable({ projects }) {
475
710
  ] }) });
476
711
  }
477
712
  function ProjectAnalytics() {
713
+ const { anonymizeProjectName } = usePrivacy();
478
714
  const { data, isLoading } = useQuery(projectAnalyticsQuery);
479
715
  if (isLoading) {
480
716
  return /* @__PURE__ */ jsxs("div", { className: "space-y-4", children: [
@@ -518,7 +754,7 @@ function ProjectAnalytics() {
518
754
  SummaryCard,
519
755
  {
520
756
  label: "Most Active",
521
- value: mostActive.projectName,
757
+ value: anonymizeProjectName(mostActive.projectName),
522
758
  sub: `${mostActive.totalSessions} sessions`
523
759
  }
524
760
  )
@@ -588,12 +824,12 @@ function StatsPage() {
588
824
  search: {
589
825
  tab: "overview"
590
826
  }
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" }),
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" }),
592
828
  /* @__PURE__ */ jsx("button", { onClick: () => navigate({
593
829
  search: {
594
830
  tab: "projects"
595
831
  }
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" })
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" })
597
833
  ] }),
598
834
  tab === "overview" ? /* @__PURE__ */ jsx(StatsOverview, { stats, isLoading, cost }) : /* @__PURE__ */ jsx("div", { className: "mt-6", children: /* @__PURE__ */ jsx(ProjectAnalytics, {}) })
599
835
  ] });
@@ -625,7 +861,8 @@ function StatsOverview({
625
861
  /* @__PURE__ */ jsx(StatCard, { label: "Total Estimated Cost", value: cost ? `~${formatUSD(cost.totalUSD)}` : "N/A" }),
626
862
  /* @__PURE__ */ jsx(StatCard, { label: "Longest Session", value: formatDuration(stats.longestSession.duration), sub: `${stats.longestSession.messageCount} messages` })
627
863
  ] }),
628
- /* @__PURE__ */ jsx("div", { className: "mt-6", children: /* @__PURE__ */ jsx(ActivityChart, { data: stats.dailyActivity }) }),
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 }) }),
629
866
  /* @__PURE__ */ jsx("div", { className: "mt-4", children: /* @__PURE__ */ jsx(TokenTrendChart, { data: stats.dailyModelTokens }) }),
630
867
  /* @__PURE__ */ jsxs("div", { className: "mt-4 grid grid-cols-1 gap-4 md:grid-cols-2", children: [
631
868
  /* @__PURE__ */ jsx(ModelUsageChart, { data: stats.modelUsage }),
@@ -1,10 +1,10 @@
1
1
  import { c as createServerRpc } from "./createServerRpc-Bd3B-Ah9.js";
2
2
  import * as fs from "node:fs";
3
- import { g as getStatsPath } from "./claude-path-BdwflgZ1.js";
3
+ import { a as getStatsPath } from "./claude-path-B2oho3NT.js";
4
+ import * as path from "node:path";
5
+ import * as os from "node:os";
4
6
  import { z } from "zod";
5
7
  import { c as createServerFn } from "../server.js";
6
- import "node:path";
7
- import "node:os";
8
8
  import "@tanstack/history";
9
9
  import "@tanstack/router-core/ssr/client";
10
10
  import "@tanstack/router-core";
@@ -16,6 +16,59 @@ import "seroval";
16
16
  import "react/jsx-runtime";
17
17
  import "@tanstack/react-router/ssr/server";
18
18
  import "@tanstack/react-router";
19
+ const CACHE_VERSION = 1;
20
+ function getCacheDir() {
21
+ return path.join(os.homedir(), ".claude-dashboard", "cache");
22
+ }
23
+ function getCachePath(cacheKey) {
24
+ return path.join(getCacheDir(), `${cacheKey}.cache.json`);
25
+ }
26
+ function readDiskCache(cacheKey, sourceMtimeMs, schema) {
27
+ try {
28
+ const cachePath = getCachePath(cacheKey);
29
+ if (!fs.existsSync(cachePath)) {
30
+ return null;
31
+ }
32
+ const raw = fs.readFileSync(cachePath, "utf-8");
33
+ const parsed = JSON.parse(raw);
34
+ if (parsed.version !== CACHE_VERSION) {
35
+ return null;
36
+ }
37
+ if (parsed.sourceMtimeMs !== sourceMtimeMs) {
38
+ return null;
39
+ }
40
+ const result = schema.safeParse(parsed.data);
41
+ if (!result.success) {
42
+ console.warn(`[disk-cache] Zod validation failed for "${cacheKey}":`, result.error.message);
43
+ return null;
44
+ }
45
+ return result.data;
46
+ } catch (error) {
47
+ const message = error instanceof Error ? error.message : String(error);
48
+ console.warn(`[disk-cache] Read failed for "${cacheKey}":`, message);
49
+ return null;
50
+ }
51
+ }
52
+ function writeDiskCache(cacheKey, sourceFile, sourceMtimeMs, data) {
53
+ try {
54
+ const cacheDir = getCacheDir();
55
+ fs.mkdirSync(cacheDir, { recursive: true });
56
+ const cachePath = getCachePath(cacheKey);
57
+ const tmpPath = `${cachePath}.tmp`;
58
+ const entry = {
59
+ version: CACHE_VERSION,
60
+ sourceFile,
61
+ sourceMtimeMs,
62
+ cachedAt: (/* @__PURE__ */ new Date()).toISOString(),
63
+ data
64
+ };
65
+ fs.writeFileSync(tmpPath, JSON.stringify(entry), "utf-8");
66
+ fs.renameSync(tmpPath, cachePath);
67
+ } catch (error) {
68
+ const message = error instanceof Error ? error.message : String(error);
69
+ console.warn(`[disk-cache] Write failed for "${cacheKey}":`, message);
70
+ }
71
+ }
19
72
  const DailyActivitySchema = z.object({
20
73
  date: z.string(),
21
74
  messageCount: z.number(),
@@ -64,9 +117,15 @@ async function parseStats() {
64
117
  if (cachedStats && cachedStats.mtimeMs === stat.mtimeMs) {
65
118
  return cachedStats.data;
66
119
  }
120
+ const diskResult = readDiskCache("stats", stat.mtimeMs, StatsCacheSchema);
121
+ if (diskResult) {
122
+ cachedStats = { mtimeMs: stat.mtimeMs, data: diskResult };
123
+ return diskResult;
124
+ }
67
125
  const raw = await fs.promises.readFile(statsPath, "utf-8");
68
126
  const parsed = JSON.parse(raw);
69
127
  const result = StatsCacheSchema.parse(parsed);
128
+ writeDiskCache("stats", statsPath, stat.mtimeMs, result);
70
129
  cachedStats = { mtimeMs: stat.mtimeMs, data: result };
71
130
  return result;
72
131
  }
@@ -423,7 +423,7 @@ function getResponse() {
423
423
  return event.res;
424
424
  }
425
425
  async function getStartManifest(matchedRoutes) {
426
- const { tsrStartManifest } = await import("./assets/_tanstack-start-manifest_v-C5hwNzs-.js");
426
+ const { tsrStartManifest } = await import("./assets/_tanstack-start-manifest_v-CVdzOaof.js");
427
427
  const startManifest = tsrStartManifest();
428
428
  const rootRoute = startManifest.routes[rootRouteId] = startManifest.routes[rootRouteId] || {};
429
429
  rootRoute.assets = rootRoute.assets || [];
@@ -577,30 +577,30 @@ function createMultiplexedStream(jsonStream, rawStreams) {
577
577
  }
578
578
  });
579
579
  }
580
- const manifest = { "4b9a58c176f487b49800a372100037cdf33cf048f3592a449f115c7e3f5ea799": {
581
- functionName: "getStats_createServerFn_handler",
582
- importer: () => import("./assets/stats.server-52mNk2Yw.js")
583
- }, "ff8a3161afdfa175e9c519e4146a56ab5bce6e80745e99cfc2191ebbb7a859bb": {
580
+ const manifest = { "ff8a3161afdfa175e9c519e4146a56ab5bce6e80745e99cfc2191ebbb7a859bb": {
584
581
  functionName: "getSessionDetail_createServerFn_handler",
585
- importer: () => import("./assets/session-detail.server-DLXl-Pn-.js")
586
- }, "bf8e4a7901f1843bdc9c46be1ad5ad59c615b8bbe611b73eb3ff28f20e43ee0d": {
587
- functionName: "getSessionList_createServerFn_handler",
588
- importer: () => import("./assets/sessions.server-CUhasKW2.js")
589
- }, "839d29fe93dfa2a6d506af7b48ca25197190a5ff4c796e970ddfdc6e8c98827f": {
590
- functionName: "getActiveSessionList_createServerFn_handler",
591
- importer: () => import("./assets/sessions.server-CUhasKW2.js")
592
- }, "a3f42f9012fd83586787da8f7cb90649da739dd947d867eb67572f68735ff495": {
593
- functionName: "getPaginatedSessions_createServerFn_handler",
594
- importer: () => import("./assets/sessions.server-CUhasKW2.js")
582
+ importer: () => import("./assets/session-detail.server-IUw67jz-.js")
583
+ }, "4b9a58c176f487b49800a372100037cdf33cf048f3592a449f115c7e3f5ea799": {
584
+ functionName: "getStats_createServerFn_handler",
585
+ importer: () => import("./assets/stats.server-DXJiLqey.js")
595
586
  }, "810657681a273df5b4e58f0d8fcc6a5451598b489431b9bcaa98eea0ad815da8": {
596
587
  functionName: "getSettings_createServerFn_handler",
597
588
  importer: () => import("./assets/settings.server-6B2PvLgf.js")
598
589
  }, "3050115d92ca91ab1fd8fd698e33076328aae80dc64ca27c088eee16cebccc1a": {
599
590
  functionName: "saveSettings_createServerFn_handler",
600
591
  importer: () => import("./assets/settings.server-6B2PvLgf.js")
592
+ }, "bf8e4a7901f1843bdc9c46be1ad5ad59c615b8bbe611b73eb3ff28f20e43ee0d": {
593
+ functionName: "getSessionList_createServerFn_handler",
594
+ importer: () => import("./assets/sessions.server-Cpffr3MU.js")
595
+ }, "839d29fe93dfa2a6d506af7b48ca25197190a5ff4c796e970ddfdc6e8c98827f": {
596
+ functionName: "getActiveSessionList_createServerFn_handler",
597
+ importer: () => import("./assets/sessions.server-Cpffr3MU.js")
598
+ }, "a3f42f9012fd83586787da8f7cb90649da739dd947d867eb67572f68735ff495": {
599
+ functionName: "getPaginatedSessions_createServerFn_handler",
600
+ importer: () => import("./assets/sessions.server-Cpffr3MU.js")
601
601
  }, "64052f224a1d6696436e5d3deeee2b798f0742e1292ffabd038c3a7bf75e6fcb": {
602
602
  functionName: "getProjectAnalytics_createServerFn_handler",
603
- importer: () => import("./assets/project-analytics.server-BkWSd6a8.js")
603
+ importer: () => import("./assets/project-analytics.server-t1bM6wAa.js")
604
604
  } };
605
605
  async function getServerFnById(id) {
606
606
  const serverFnInfo = manifest[id];
@@ -1016,7 +1016,7 @@ let entriesPromise;
1016
1016
  let baseManifestPromise;
1017
1017
  let cachedFinalManifestPromise;
1018
1018
  async function loadEntries() {
1019
- const routerEntry = await import("./assets/router-ChxlsPNU.js").then((n) => n.r);
1019
+ const routerEntry = await import("./assets/router-kB-tCwY9.js").then((n) => n.r);
1020
1020
  const startEntry = await import("./assets/start-HYkvq4Ni.js");
1021
1021
  return { startEntry, routerEntry };
1022
1022
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-session-dashboard",
3
- "version": "0.2.1",
3
+ "version": "0.3.1",
4
4
  "description": "Local observability dashboard for Claude Code sessions",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -42,6 +42,7 @@
42
42
  "e2e": "playwright test",
43
43
  "e2e:ui": "playwright test --ui",
44
44
  "e2e:headed": "playwright test --headed",
45
+ "lint": "eslint src/",
45
46
  "prepublishOnly": "npm run build"
46
47
  },
47
48
  "dependencies": {
@@ -55,6 +56,7 @@
55
56
  "zod": "^3.24.0"
56
57
  },
57
58
  "devDependencies": {
59
+ "@eslint/js": "^9.39.2",
58
60
  "@playwright/test": "^1.58.2",
59
61
  "@tailwindcss/vite": "^4.1.0",
60
62
  "@testing-library/react": "^16.3.2",
@@ -63,10 +65,14 @@
63
65
  "@types/react-dom": "^19.1.0",
64
66
  "@vitejs/plugin-react": "^5.1.0",
65
67
  "@vitest/ui": "^4.0.18",
68
+ "eslint": "^9.39.2",
69
+ "eslint-plugin-react-hooks": "^7.0.1",
70
+ "eslint-plugin-react-refresh": "^0.5.0",
66
71
  "happy-dom": "^20.6.1",
67
72
  "jsdom": "^28.0.0",
68
73
  "tailwindcss": "^4.1.0",
69
74
  "typescript": "^5.8.0",
75
+ "typescript-eslint": "^8.55.0",
70
76
  "vite": "^7.3.0",
71
77
  "vite-tsconfig-paths": "^4.3.0",
72
78
  "vitest": "^4.0.18"
@@ -1 +0,0 @@
1
- import{u as l,j as e,a as c,L as r,O as i}from"./main-CM5g2n-_.js";import{u as x}from"./createServerFn-B0pEGqTk.js";import{a as d}from"./sessions.queries-AUVV0tJj.js";function u(){const{privacyMode:s,togglePrivacyMode:t}=l();return e.jsxs("button",{type:"button",onClick:t,title:s?"Privacy mode on":"Privacy mode off",className:`flex items-center gap-2 rounded-lg px-3 py-1.5 text-xs transition-colors ${s?"bg-blue-600 text-white":"bg-gray-800 text-gray-400 hover:text-gray-200"}`,children:[e.jsxs("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 20 20",fill:"currentColor",className:"h-3.5 w-3.5",children:[s?e.jsx("path",{fillRule:"evenodd",d:"M3.28 2.22a.75.75 0 00-1.06 1.06l14.5 14.5a.75.75 0 101.06-1.06l-1.745-1.745a10.029 10.029 0 003.3-4.38 1.651 1.651 0 000-1.185A10.004 10.004 0 009.999 3a9.956 9.956 0 00-4.744 1.194L3.28 2.22zM7.752 6.69l1.092 1.092a2.5 2.5 0 013.374 3.373l1.092 1.092a4 4 0 00-5.558-5.558z",clipRule:"evenodd"}):e.jsx("path",{d:"M10 12.5a2.5 2.5 0 100-5 2.5 2.5 0 000 5z"}),s?e.jsx("path",{d:"M10.748 13.93l2.523 2.523a9.987 9.987 0 01-3.27.547c-4.258 0-7.894-2.66-9.337-6.41a1.651 1.651 0 010-1.186A10.007 10.007 0 012.839 6.02L6.07 9.252a4 4 0 004.678 4.678z"}):e.jsx("path",{fillRule:"evenodd",d:"M.458 10a9.996 9.996 0 019.542-6c4.258 0 7.894 2.66 9.337 6.41a1.651 1.651 0 010 1.186A10.004 10.004 0 0110 17.5c-4.258 0-7.894-2.66-9.337-6.41a1.651 1.651 0 010-1.186L.458 10zM14 10a4 4 0 11-8 0 4 4 0 018 0z",clipRule:"evenodd"})]}),"Privacy"]})}function h(){const{data:s}=x(d),t=s?.length??0;return t===0?null:e.jsx("span",{className:"ml-auto rounded-full bg-green-500/20 px-1.5 py-0.5 text-[10px] font-medium text-green-400",children:t})}const m=[{to:"/sessions",label:"Sessions",icon:">"},{to:"/stats",label:"Stats",icon:"#"},{to:"/settings",label:"Settings",icon:"*"}];function g({children:s}){const t=c(),o=t[t.length-1]?.pathname??"";return e.jsxs("div",{className:"flex min-h-screen",children:[e.jsxs("aside",{className:"flex w-56 shrink-0 flex-col border-r border-gray-800 bg-gray-950",children:[e.jsx("div",{className:"flex h-14 items-center border-b border-gray-800 px-4",children:e.jsxs(r,{to:"/sessions",className:"text-sm font-bold text-white",children:[e.jsx("span",{className:"text-blue-400",children:"Claude"})," Dashboard"]})}),e.jsx("nav",{className:"flex-1 p-3",children:m.map(a=>{const n=o.startsWith(a.to);return e.jsxs(r,{to:a.to,className:`flex items-center gap-2.5 rounded-lg px-3 py-2 text-sm transition-colors ${n?"bg-gray-800 text-white":"text-gray-400 hover:bg-gray-800/50 hover:text-gray-200"}`,children:[e.jsx("span",{className:"font-mono text-xs text-gray-500",children:a.icon}),a.label,a.to==="/sessions"&&e.jsx(h,{})]},a.to)})}),e.jsxs("div",{className:"border-t border-gray-800 p-3",children:[e.jsx(u,{}),e.jsx("p",{className:"mt-2 text-xs text-gray-600",children:"Read-only observer"})]})]}),e.jsx("main",{className:"flex-1 overflow-auto",children:e.jsx("div",{className:"mx-auto max-w-5xl px-6 py-6",children:s})})]})}function f(){return e.jsx(g,{children:e.jsx(i,{})})}export{f as component};