claude-session-dashboard 0.2.1 → 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 (31) 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-B0pEGqTk.js → createServerFn-Bn6_ISOt.js} +1 -1
  6. package/dist/client/assets/index-BkqRvnEf.js +1 -0
  7. package/dist/client/assets/{main-CM5g2n-_.js → main-CfJIADCp.js} +7 -7
  8. package/dist/client/assets/{sessions.queries-AUVV0tJj.js → sessions.queries-CrJg4dYU.js} +1 -1
  9. package/dist/client/assets/settings-C4_lsEzl.js +1 -0
  10. package/dist/client/assets/{settings.types-BRNIMHGJ.js → settings.types-9Qf5WcRY.js} +1 -1
  11. package/dist/client/assets/stats-_r1gmaTe.js +4 -0
  12. package/dist/client/assets/{useSessionCost-DgFKglaG.js → useSessionCost-DPZ-ubM1.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-C-XZIPqn.js} +31 -32
  16. package/dist/server/assets/_tanstack-start-manifest_v-B51mSkGz.js +4 -0
  17. package/dist/server/assets/{index-Do0HxVmM.js → index-CKfH7HpA.js} +22 -21
  18. package/dist/server/assets/{router-ChxlsPNU.js → router-Cb_hBXHI.js} +55 -29
  19. package/dist/server/assets/{settings-ko61yfVs.js → settings-C0_KyVQQ.js} +66 -20
  20. package/dist/server/assets/{stats-C9cZXTP5.js → stats-BtgVene-.js} +261 -24
  21. package/dist/server/assets/{stats.server-52mNk2Yw.js → stats.server-qTOvID9-.js} +61 -2
  22. package/dist/server/server.js +12 -12
  23. package/package.json +7 -1
  24. package/dist/client/assets/_dashboard-Bxw4OxIS.js +0 -1
  25. package/dist/client/assets/_sessionId-CNR4Ln7m.js +0 -12
  26. package/dist/client/assets/app-u2nTs9ny.css +0 -1
  27. package/dist/client/assets/index-BbdJ1jMA.js +0 -1
  28. package/dist/client/assets/settings-CIwZDakc.js +0 -1
  29. package/dist/client/assets/stats-CjWSMX3y.js +0 -4
  30. package/dist/server/assets/_dashboard-CAO6-qAS.js +0 -116
  31. package/dist/server/assets/_tanstack-start-manifest_v-C5hwNzs-.js +0 -4
@@ -1,8 +1,9 @@
1
1
  import { jsx, jsxs } from "react/jsx-runtime";
2
- import { useState, useEffect } from "react";
2
+ import { useState } from "react";
3
3
  import { useQuery } from "@tanstack/react-query";
4
4
  import { s as settingsQuery, u as useSettingsMutation } from "./settings.queries-DSQd324O.js";
5
5
  import { a as SUBSCRIPTION_TIERS, b as DEFAULT_PRICING, D as DEFAULT_SETTINGS } from "./settings.types-DntadCHo.js";
6
+ import { u as usePrivacy } from "./router-Cb_hBXHI.js";
6
7
  import "./createSsrRpc-CVg2UDl0.js";
7
8
  import "../server.js";
8
9
  import "@tanstack/history";
@@ -24,7 +25,7 @@ function TierSelector({ value, onChange }) {
24
25
  {
25
26
  type: "button",
26
27
  onClick: () => onChange(tier.id),
27
- className: `rounded-lg border px-3 py-2 text-left transition-colors ${isSelected ? "border-blue-500 bg-blue-500/10 text-white" : "border-gray-800 bg-gray-900/50 text-gray-400 hover:border-gray-700 hover:text-gray-300"}`,
28
+ className: `rounded-lg border px-3 py-2 text-left transition-colors ${isSelected ? "border-brand-500 bg-brand-500/10 text-white" : "border-gray-800 bg-gray-900/50 text-gray-400 hover:border-gray-700 hover:text-gray-300"}`,
28
29
  children: [
29
30
  /* @__PURE__ */ jsx("div", { className: "text-sm font-medium", children: tier.displayName }),
30
31
  /* @__PURE__ */ jsx("div", { className: "mt-0.5 font-mono text-[10px] text-gray-500", children: tier.monthlyUSD !== null ? `$${tier.monthlyUSD}/mo` : "Custom" })
@@ -92,28 +93,31 @@ function PricingTableEditor({ overrides, onChange }) {
92
93
  min: "0",
93
94
  value,
94
95
  onChange: (e) => handleCellChange(model.modelId, f.key, e.target.value),
95
- className: `w-20 rounded border px-2 py-1 text-right font-mono text-xs ${changed ? "border-blue-500/50 bg-blue-500/10 text-blue-400" : "border-gray-700 bg-gray-800 text-gray-300"} focus:border-blue-500 focus:outline-none`
96
+ className: `w-20 rounded border px-2 py-1 text-right font-mono text-xs ${changed ? "border-brand-500/50 bg-brand-500/10 text-brand-400" : "border-gray-700 bg-gray-800 text-gray-300"} focus:border-brand-500 focus:outline-none`
96
97
  }
97
98
  ) }, f.key);
98
99
  })
99
100
  ] }, model.modelId)) })
100
101
  ] }),
101
- /* @__PURE__ */ jsx("p", { className: "mt-2 text-[10px] text-gray-600", children: "Prices in USD per million tokens. Overridden values shown in blue." })
102
+ /* @__PURE__ */ jsx("p", { className: "mt-2 text-[10px] text-gray-600", children: "Prices in USD per million tokens. Overridden values are highlighted." })
102
103
  ] });
103
104
  }
104
105
  function SettingsPage() {
105
106
  const { data: settings, isLoading } = useQuery(settingsQuery);
107
+ if (isLoading || !settings) {
108
+ return /* @__PURE__ */ jsxs("div", { className: "space-y-4", children: [
109
+ /* @__PURE__ */ jsx("div", { className: "h-8 w-48 animate-pulse rounded bg-gray-800" }),
110
+ /* @__PURE__ */ jsx("div", { className: "h-64 animate-pulse rounded-xl bg-gray-900/50" })
111
+ ] });
112
+ }
113
+ return /* @__PURE__ */ jsx(SettingsForm, { settings });
114
+ }
115
+ function SettingsForm({ settings }) {
106
116
  const mutation = useSettingsMutation();
107
- const [tier, setTier] = useState("pro");
108
- const [overrides, setOverrides] = useState({});
117
+ const { privacyMode, togglePrivacyMode } = usePrivacy();
118
+ const [tier, setTier] = useState(settings.subscriptionTier);
119
+ const [overrides, setOverrides] = useState(settings.pricingOverrides);
109
120
  const [isDirty, setIsDirty] = useState(false);
110
- useEffect(() => {
111
- if (settings) {
112
- setTier(settings.subscriptionTier);
113
- setOverrides(settings.pricingOverrides);
114
- setIsDirty(false);
115
- }
116
- }, [settings]);
117
121
  function handleTierChange(newTier) {
118
122
  setTier(newTier);
119
123
  setIsDirty(true);
@@ -139,15 +143,57 @@ function SettingsPage() {
139
143
  }
140
144
  });
141
145
  }
142
- if (isLoading) {
143
- return /* @__PURE__ */ jsxs("div", { className: "space-y-4", children: [
144
- /* @__PURE__ */ jsx("div", { className: "h-8 w-48 animate-pulse rounded bg-gray-800" }),
145
- /* @__PURE__ */ jsx("div", { className: "h-64 animate-pulse rounded-xl bg-gray-900/50" })
146
- ] });
147
- }
148
146
  return /* @__PURE__ */ jsxs("div", { children: [
149
147
  /* @__PURE__ */ jsx("h1", { className: "text-xl font-bold text-white", children: "Settings" }),
150
148
  /* @__PURE__ */ jsx("p", { className: "mt-1 text-xs text-gray-500", children: "Configure your subscription tier and API pricing for cost estimation." }),
149
+ /* @__PURE__ */ jsxs("div", { className: "mt-6", children: [
150
+ /* @__PURE__ */ jsx("h2", { className: "text-sm font-semibold text-gray-300", children: "Privacy Mode" }),
151
+ /* @__PURE__ */ jsx("p", { className: "mt-1 text-[10px] text-gray-500", children: "Hide project names, file paths, and branch names across the dashboard. Useful when screen-sharing or recording demos." }),
152
+ /* @__PURE__ */ jsxs("div", { className: "mt-3 rounded-xl border border-gray-800 bg-gray-900/50 p-4", children: [
153
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between", children: [
154
+ /* @__PURE__ */ jsx("span", { className: "text-xs text-gray-300", children: "Enable privacy mode" }),
155
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
156
+ /* @__PURE__ */ jsx("span", { className: "text-xs text-gray-500", children: privacyMode ? "On" : "Off" }),
157
+ /* @__PURE__ */ jsx(
158
+ "button",
159
+ {
160
+ type: "button",
161
+ role: "switch",
162
+ "aria-checked": privacyMode,
163
+ onClick: togglePrivacyMode,
164
+ className: `relative inline-flex h-5 w-9 shrink-0 cursor-pointer items-center rounded-full transition-colors ${privacyMode ? "bg-brand-600" : "bg-gray-800"}`,
165
+ children: /* @__PURE__ */ jsx(
166
+ "span",
167
+ {
168
+ className: `inline-block h-3.5 w-3.5 rounded-full bg-white transition-transform ${privacyMode ? "translate-x-[18px]" : "translate-x-[3px]"}`
169
+ }
170
+ )
171
+ }
172
+ )
173
+ ] })
174
+ ] }),
175
+ /* @__PURE__ */ jsxs("div", { className: "mt-3 border-t border-gray-800 pt-3", children: [
176
+ /* @__PURE__ */ jsx("p", { className: "text-[10px] font-medium text-gray-400", children: "What gets hidden:" }),
177
+ /* @__PURE__ */ jsxs("ul", { className: "mt-1.5 space-y-1 text-[10px] text-gray-500", children: [
178
+ /* @__PURE__ */ jsxs("li", { children: [
179
+ /* @__PURE__ */ jsx("span", { className: "text-gray-400", children: "Project names" }),
180
+ " ",
181
+ /* @__PURE__ */ jsx("span", { className: "font-mono text-gray-600", children: "→ project-1, project-2, ..." })
182
+ ] }),
183
+ /* @__PURE__ */ jsxs("li", { children: [
184
+ /* @__PURE__ */ jsx("span", { className: "text-gray-400", children: "File paths" }),
185
+ " ",
186
+ /* @__PURE__ */ jsx("span", { className: "font-mono text-gray-600", children: "→ .../project-1" })
187
+ ] }),
188
+ /* @__PURE__ */ jsxs("li", { children: [
189
+ /* @__PURE__ */ jsx("span", { className: "text-gray-400", children: "Branch names" }),
190
+ " ",
191
+ /* @__PURE__ */ jsx("span", { className: "font-mono text-gray-600", children: "→ branch-1, branch-2, ..." })
192
+ ] })
193
+ ] })
194
+ ] })
195
+ ] })
196
+ ] }),
151
197
  /* @__PURE__ */ jsxs("div", { className: "mt-6", children: [
152
198
  /* @__PURE__ */ jsx("h2", { className: "text-sm font-semibold text-gray-300", children: "Subscription Tier" }),
153
199
  /* @__PURE__ */ jsx("p", { className: "mt-1 text-[10px] text-gray-500", children: "Select your Claude subscription plan. This is informational only and does not affect cost calculations." }),
@@ -186,7 +232,7 @@ function SettingsPage() {
186
232
  type: "button",
187
233
  onClick: handleSave,
188
234
  disabled: !isDirty || mutation.isPending,
189
- className: `rounded-lg px-4 py-1.5 text-xs font-medium transition-colors ${isDirty && !mutation.isPending ? "bg-blue-600 text-white hover:bg-blue-500" : "cursor-not-allowed bg-gray-800 text-gray-500"}`,
235
+ className: `rounded-lg px-4 py-1.5 text-xs font-medium transition-colors ${isDirty && !mutation.isPending ? "bg-brand-600 text-white hover:bg-brand-500" : "cursor-not-allowed bg-gray-800 text-gray-500"}`,
190
236
  children: mutation.isPending ? "Saving..." : "Save"
191
237
  }
192
238
  )
@@ -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-Cb_hBXHI.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
3
  import { g as getStatsPath } from "./claude-path-BdwflgZ1.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
  }