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.
- package/README.md +156 -14
- package/dist/client/assets/_dashboard-I7m6D7BE.js +1 -0
- package/dist/client/assets/_sessionId-DEliIff6.js +12 -0
- package/dist/client/assets/app-D7yorIIh.css +1 -0
- package/dist/client/assets/{createServerFn-B0pEGqTk.js → createServerFn-Bn6_ISOt.js} +1 -1
- package/dist/client/assets/index-BkqRvnEf.js +1 -0
- package/dist/client/assets/{main-CM5g2n-_.js → main-CfJIADCp.js} +7 -7
- package/dist/client/assets/{sessions.queries-AUVV0tJj.js → sessions.queries-CrJg4dYU.js} +1 -1
- package/dist/client/assets/settings-C4_lsEzl.js +1 -0
- package/dist/client/assets/{settings.types-BRNIMHGJ.js → settings.types-9Qf5WcRY.js} +1 -1
- package/dist/client/assets/stats-_r1gmaTe.js +4 -0
- package/dist/client/assets/{useSessionCost-DgFKglaG.js → useSessionCost-DPZ-ubM1.js} +1 -1
- package/dist/client/favicon.svg +3 -0
- package/dist/server/assets/_dashboard-TUzgwLqB.js +112 -0
- package/dist/server/assets/{_sessionId-BZf2Aqy5.js → _sessionId-C-XZIPqn.js} +31 -32
- package/dist/server/assets/_tanstack-start-manifest_v-B51mSkGz.js +4 -0
- package/dist/server/assets/{index-Do0HxVmM.js → index-CKfH7HpA.js} +22 -21
- package/dist/server/assets/{router-ChxlsPNU.js → router-Cb_hBXHI.js} +55 -29
- package/dist/server/assets/{settings-ko61yfVs.js → settings-C0_KyVQQ.js} +66 -20
- package/dist/server/assets/{stats-C9cZXTP5.js → stats-BtgVene-.js} +261 -24
- package/dist/server/assets/{stats.server-52mNk2Yw.js → stats.server-qTOvID9-.js} +61 -2
- package/dist/server/server.js +12 -12
- package/package.json +7 -1
- package/dist/client/assets/_dashboard-Bxw4OxIS.js +0 -1
- package/dist/client/assets/_sessionId-CNR4Ln7m.js +0 -12
- package/dist/client/assets/app-u2nTs9ny.css +0 -1
- package/dist/client/assets/index-BbdJ1jMA.js +0 -1
- package/dist/client/assets/settings-CIwZDakc.js +0 -1
- package/dist/client/assets/stats-CjWSMX3y.js +0 -4
- package/dist/server/assets/_dashboard-CAO6-qAS.js +0 -116
- 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
|
|
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-
|
|
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-
|
|
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
|
|
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
|
|
108
|
-
const [
|
|
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-
|
|
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,
|
|
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: "#
|
|
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: "#
|
|
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: "#
|
|
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: "#
|
|
63
|
-
border: "1px solid #
|
|
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: "#
|
|
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
|
|
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: "#
|
|
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: "#
|
|
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: "#
|
|
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 = ["#
|
|
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: "#
|
|
314
|
-
border: "1px solid #
|
|
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(
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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(
|
|
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
|
}
|