code-dash 1.0.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 +55 -0
- package/bin/claudeview.js +91 -0
- package/package.json +37 -0
- package/src/analyzer.js +389 -0
- package/src/dashboard.html +846 -0
|
@@ -0,0 +1,846 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
+
<title>ClaudeView — Session Analytics</title>
|
|
7
|
+
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.4/dist/chart.umd.min.js"></script>
|
|
8
|
+
<style>
|
|
9
|
+
:root {
|
|
10
|
+
--bg: #0a0a0f;
|
|
11
|
+
--bg2: #111118;
|
|
12
|
+
--card: rgba(255,255,255,0.04);
|
|
13
|
+
--card-border: rgba(255,255,255,0.08);
|
|
14
|
+
--card-hover: rgba(255,255,255,0.07);
|
|
15
|
+
--purple: #7c3aed;
|
|
16
|
+
--purple-light: #a78bfa;
|
|
17
|
+
--cyan: #06b6d4;
|
|
18
|
+
--cyan-light: #67e8f9;
|
|
19
|
+
--green: #10b981;
|
|
20
|
+
--amber: #f59e0b;
|
|
21
|
+
--red: #ef4444;
|
|
22
|
+
--text: #e2e8f0;
|
|
23
|
+
--text-muted: #94a3b8;
|
|
24
|
+
--text-dim: #475569;
|
|
25
|
+
--border: rgba(255,255,255,0.08);
|
|
26
|
+
--radius: 12px;
|
|
27
|
+
--radius-sm: 8px;
|
|
28
|
+
}
|
|
29
|
+
* { box-sizing: border-box; margin: 0; padding: 0; }
|
|
30
|
+
html { font-size: 14px; }
|
|
31
|
+
body {
|
|
32
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif;
|
|
33
|
+
background: var(--bg);
|
|
34
|
+
color: var(--text);
|
|
35
|
+
min-height: 100vh;
|
|
36
|
+
overflow-x: hidden;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/* ── Header ── */
|
|
40
|
+
header {
|
|
41
|
+
position: sticky; top: 0; z-index: 100;
|
|
42
|
+
background: rgba(10,10,15,0.85);
|
|
43
|
+
backdrop-filter: blur(12px);
|
|
44
|
+
border-bottom: 1px solid var(--border);
|
|
45
|
+
padding: 0 24px;
|
|
46
|
+
height: 56px;
|
|
47
|
+
display: flex; align-items: center; gap: 16px;
|
|
48
|
+
}
|
|
49
|
+
.logo { display: flex; align-items: center; gap: 8px; text-decoration: none; }
|
|
50
|
+
.logo-icon {
|
|
51
|
+
width: 28px; height: 28px; border-radius: 8px;
|
|
52
|
+
background: linear-gradient(135deg, var(--purple), var(--cyan));
|
|
53
|
+
display: flex; align-items: center; justify-content: center;
|
|
54
|
+
font-size: 14px; font-weight: 800; color: white;
|
|
55
|
+
}
|
|
56
|
+
.logo-text { font-size: 15px; font-weight: 700; color: var(--text); letter-spacing: -0.3px; }
|
|
57
|
+
.header-sep { color: var(--text-dim); }
|
|
58
|
+
.header-account { font-size: 12px; color: var(--text-muted); }
|
|
59
|
+
.header-spacer { flex: 1; }
|
|
60
|
+
.header-updated { font-size: 11px; color: var(--text-dim); }
|
|
61
|
+
.refresh-btn {
|
|
62
|
+
padding: 6px 12px; border-radius: var(--radius-sm);
|
|
63
|
+
background: var(--card); border: 1px solid var(--border);
|
|
64
|
+
color: var(--text-muted); font-size: 12px; cursor: pointer;
|
|
65
|
+
transition: all .15s;
|
|
66
|
+
}
|
|
67
|
+
.refresh-btn:hover { background: var(--card-hover); color: var(--text); }
|
|
68
|
+
|
|
69
|
+
/* ── Layout ── */
|
|
70
|
+
main { max-width: 1400px; margin: 0 auto; padding: 24px; }
|
|
71
|
+
|
|
72
|
+
/* ── Loading ── */
|
|
73
|
+
#loading {
|
|
74
|
+
position: fixed; inset: 0; background: var(--bg);
|
|
75
|
+
display: flex; flex-direction: column; align-items: center; justify-content: center;
|
|
76
|
+
gap: 16px; z-index: 200;
|
|
77
|
+
}
|
|
78
|
+
.spinner {
|
|
79
|
+
width: 36px; height: 36px; border-radius: 50%;
|
|
80
|
+
border: 3px solid var(--border);
|
|
81
|
+
border-top-color: var(--purple);
|
|
82
|
+
animation: spin .8s linear infinite;
|
|
83
|
+
}
|
|
84
|
+
@keyframes spin { to { transform: rotate(360deg); } }
|
|
85
|
+
.loading-text { color: var(--text-muted); font-size: 13px; }
|
|
86
|
+
|
|
87
|
+
/* ── Section ── */
|
|
88
|
+
.section { margin-bottom: 32px; }
|
|
89
|
+
.section-title {
|
|
90
|
+
font-size: 12px; font-weight: 600; text-transform: uppercase;
|
|
91
|
+
letter-spacing: .8px; color: var(--text-muted); margin-bottom: 14px;
|
|
92
|
+
display: flex; align-items: center; gap: 8px;
|
|
93
|
+
}
|
|
94
|
+
.section-title::after {
|
|
95
|
+
content: ''; flex: 1; height: 1px; background: var(--border);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/* ── Cards ── */
|
|
99
|
+
.cards-grid {
|
|
100
|
+
display: grid;
|
|
101
|
+
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
|
|
102
|
+
gap: 12px;
|
|
103
|
+
}
|
|
104
|
+
.card {
|
|
105
|
+
background: var(--card);
|
|
106
|
+
border: 1px solid var(--card-border);
|
|
107
|
+
border-radius: var(--radius);
|
|
108
|
+
padding: 18px;
|
|
109
|
+
transition: border-color .15s, background .15s;
|
|
110
|
+
}
|
|
111
|
+
.card:hover { background: var(--card-hover); border-color: rgba(255,255,255,0.12); }
|
|
112
|
+
.card-label { font-size: 11px; font-weight: 500; color: var(--text-muted); text-transform: uppercase; letter-spacing: .6px; margin-bottom: 8px; }
|
|
113
|
+
.card-value { font-size: 26px; font-weight: 700; letter-spacing: -1px; line-height: 1; margin-bottom: 4px; }
|
|
114
|
+
.card-sub { font-size: 11px; color: var(--text-muted); }
|
|
115
|
+
.card-icon { font-size: 20px; margin-bottom: 10px; }
|
|
116
|
+
.c-purple { color: var(--purple-light); }
|
|
117
|
+
.c-cyan { color: var(--cyan); }
|
|
118
|
+
.c-green { color: var(--green); }
|
|
119
|
+
.c-amber { color: var(--amber); }
|
|
120
|
+
.c-red { color: var(--red); }
|
|
121
|
+
|
|
122
|
+
/* ── Charts grid ── */
|
|
123
|
+
.charts-row { display: grid; gap: 12px; margin-bottom: 12px; }
|
|
124
|
+
.charts-row-2 { grid-template-columns: 1fr 1fr; }
|
|
125
|
+
.charts-row-3 { grid-template-columns: 2fr 1fr; }
|
|
126
|
+
|
|
127
|
+
.chart-card {
|
|
128
|
+
background: var(--card);
|
|
129
|
+
border: 1px solid var(--card-border);
|
|
130
|
+
border-radius: var(--radius);
|
|
131
|
+
padding: 20px;
|
|
132
|
+
}
|
|
133
|
+
.chart-title { font-size: 13px; font-weight: 600; color: var(--text); margin-bottom: 16px; display: flex; align-items: center; justify-content: space-between; }
|
|
134
|
+
.chart-subtitle { font-size: 11px; color: var(--text-muted); font-weight: 400; }
|
|
135
|
+
.chart-wrap { position: relative; }
|
|
136
|
+
.chart-wrap canvas { max-height: 240px; }
|
|
137
|
+
.chart-wrap-tall canvas { max-height: 300px; }
|
|
138
|
+
|
|
139
|
+
/* ── Heatmap ── */
|
|
140
|
+
.heatmap-wrap { overflow-x: auto; padding-bottom: 4px; }
|
|
141
|
+
.heatmap {
|
|
142
|
+
display: grid;
|
|
143
|
+
grid-auto-flow: column;
|
|
144
|
+
grid-template-rows: repeat(7, 12px);
|
|
145
|
+
gap: 3px;
|
|
146
|
+
width: max-content;
|
|
147
|
+
min-width: 100%;
|
|
148
|
+
}
|
|
149
|
+
.hm-cell {
|
|
150
|
+
width: 12px; height: 12px; border-radius: 2px;
|
|
151
|
+
background: rgba(255,255,255,0.05);
|
|
152
|
+
transition: transform .1s;
|
|
153
|
+
}
|
|
154
|
+
.hm-cell:hover { transform: scale(1.4); z-index: 1; }
|
|
155
|
+
.hm-0 { background: rgba(255,255,255,0.04); }
|
|
156
|
+
.hm-1 { background: rgba(124,58,237,0.3); }
|
|
157
|
+
.hm-2 { background: rgba(124,58,237,0.55); }
|
|
158
|
+
.hm-3 { background: rgba(124,58,237,0.75); }
|
|
159
|
+
.hm-4 { background: rgba(124,58,237,1); }
|
|
160
|
+
.hm-labels { display: flex; gap: 4px; margin-top: 6px; }
|
|
161
|
+
.hm-month-labels {
|
|
162
|
+
display: grid; grid-auto-flow: column;
|
|
163
|
+
grid-template-rows: 1fr;
|
|
164
|
+
gap: 3px; width: max-content; min-width: 100%;
|
|
165
|
+
font-size: 10px; color: var(--text-dim); margin-bottom: 4px;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/* ── Tables ── */
|
|
169
|
+
.table-card {
|
|
170
|
+
background: var(--card);
|
|
171
|
+
border: 1px solid var(--card-border);
|
|
172
|
+
border-radius: var(--radius);
|
|
173
|
+
overflow: hidden;
|
|
174
|
+
}
|
|
175
|
+
.table-header {
|
|
176
|
+
padding: 16px 20px;
|
|
177
|
+
border-bottom: 1px solid var(--border);
|
|
178
|
+
display: flex; align-items: center; justify-content: space-between;
|
|
179
|
+
}
|
|
180
|
+
.table-title { font-size: 13px; font-weight: 600; }
|
|
181
|
+
.table-count { font-size: 11px; color: var(--text-muted); }
|
|
182
|
+
.table-wrap { overflow-x: auto; }
|
|
183
|
+
table { width: 100%; border-collapse: collapse; }
|
|
184
|
+
th {
|
|
185
|
+
padding: 10px 20px;
|
|
186
|
+
text-align: left; font-size: 11px; font-weight: 600;
|
|
187
|
+
text-transform: uppercase; letter-spacing: .5px;
|
|
188
|
+
color: var(--text-dim);
|
|
189
|
+
border-bottom: 1px solid var(--border);
|
|
190
|
+
cursor: pointer; user-select: none; white-space: nowrap;
|
|
191
|
+
}
|
|
192
|
+
th:hover { color: var(--text-muted); }
|
|
193
|
+
td { padding: 12px 20px; font-size: 12px; border-bottom: 1px solid rgba(255,255,255,0.04); white-space: nowrap; }
|
|
194
|
+
tr:last-child td { border-bottom: none; }
|
|
195
|
+
tr:hover td { background: rgba(255,255,255,0.02); }
|
|
196
|
+
.td-mono { font-family: 'SF Mono', 'Fira Code', monospace; font-size: 11px; color: var(--text-muted); }
|
|
197
|
+
.td-path { color: var(--text-muted); max-width: 220px; overflow: hidden; text-overflow: ellipsis; }
|
|
198
|
+
.badge {
|
|
199
|
+
display: inline-block; padding: 2px 7px; border-radius: 99px;
|
|
200
|
+
font-size: 10px; font-weight: 600; letter-spacing: .3px;
|
|
201
|
+
}
|
|
202
|
+
.badge-purple { background: rgba(124,58,237,0.2); color: var(--purple-light); }
|
|
203
|
+
.badge-cyan { background: rgba(6,182,212,0.15); color: var(--cyan); }
|
|
204
|
+
.badge-green { background: rgba(16,185,129,0.15); color: var(--green); }
|
|
205
|
+
|
|
206
|
+
/* ── Model tag ── */
|
|
207
|
+
.model-tag {
|
|
208
|
+
display: inline-flex; align-items: center; gap: 4px;
|
|
209
|
+
padding: 2px 8px; border-radius: 99px; font-size: 10px;
|
|
210
|
+
background: rgba(124,58,237,0.15); color: var(--purple-light); border: 1px solid rgba(124,58,237,0.25);
|
|
211
|
+
}
|
|
212
|
+
.model-haiku { background: rgba(6,182,212,0.1); color: var(--cyan); border-color: rgba(6,182,212,0.2); }
|
|
213
|
+
.model-opus { background: rgba(245,158,11,0.1); color: var(--amber); border-color: rgba(245,158,11,0.2); }
|
|
214
|
+
|
|
215
|
+
/* ── Token bar ── */
|
|
216
|
+
.token-bar { display: flex; align-items: center; gap: 8px; }
|
|
217
|
+
.token-bar-track {
|
|
218
|
+
flex: 1; height: 4px; background: rgba(255,255,255,0.06); border-radius: 99px; overflow: hidden;
|
|
219
|
+
}
|
|
220
|
+
.token-bar-fill { height: 100%; border-radius: 99px; }
|
|
221
|
+
|
|
222
|
+
/* ── Empty state ── */
|
|
223
|
+
.empty {
|
|
224
|
+
padding: 60px 20px; text-align: center;
|
|
225
|
+
color: var(--text-dim); font-size: 13px; line-height: 1.8;
|
|
226
|
+
}
|
|
227
|
+
.empty-icon { font-size: 36px; margin-bottom: 12px; }
|
|
228
|
+
|
|
229
|
+
/* ── Tab bar ── */
|
|
230
|
+
.tabs { display: flex; gap: 2px; margin-bottom: 16px; }
|
|
231
|
+
.tab {
|
|
232
|
+
padding: 7px 14px; border-radius: var(--radius-sm);
|
|
233
|
+
font-size: 12px; font-weight: 500; cursor: pointer;
|
|
234
|
+
color: var(--text-muted); border: none; background: transparent;
|
|
235
|
+
transition: all .15s;
|
|
236
|
+
}
|
|
237
|
+
.tab.active { background: var(--card); color: var(--text); }
|
|
238
|
+
.tab:hover:not(.active) { color: var(--text); }
|
|
239
|
+
|
|
240
|
+
/* ── Summary pills ── */
|
|
241
|
+
.pill-row { display: flex; flex-wrap: wrap; gap: 8px; margin-bottom: 20px; }
|
|
242
|
+
.pill {
|
|
243
|
+
display: inline-flex; align-items: center; gap: 5px;
|
|
244
|
+
padding: 5px 10px; border-radius: 99px;
|
|
245
|
+
font-size: 11px; font-weight: 500;
|
|
246
|
+
background: var(--card); border: 1px solid var(--border); color: var(--text-muted);
|
|
247
|
+
}
|
|
248
|
+
.pill-dot { width: 6px; height: 6px; border-radius: 50%; }
|
|
249
|
+
|
|
250
|
+
@media (max-width: 900px) {
|
|
251
|
+
.charts-row-2, .charts-row-3 { grid-template-columns: 1fr; }
|
|
252
|
+
.cards-grid { grid-template-columns: repeat(2, 1fr); }
|
|
253
|
+
main { padding: 16px; }
|
|
254
|
+
}
|
|
255
|
+
@media (max-width: 600px) {
|
|
256
|
+
.cards-grid { grid-template-columns: 1fr 1fr; }
|
|
257
|
+
}
|
|
258
|
+
</style>
|
|
259
|
+
</head>
|
|
260
|
+
<body>
|
|
261
|
+
|
|
262
|
+
<div id="loading">
|
|
263
|
+
<div class="spinner"></div>
|
|
264
|
+
<div class="loading-text">Reading your Claude sessions…</div>
|
|
265
|
+
</div>
|
|
266
|
+
|
|
267
|
+
<header>
|
|
268
|
+
<div class="logo">
|
|
269
|
+
<div class="logo-icon">CV</div>
|
|
270
|
+
<span class="logo-text">ClaudeView</span>
|
|
271
|
+
</div>
|
|
272
|
+
<span class="header-sep">/</span>
|
|
273
|
+
<span class="header-account" id="hdr-account">Loading…</span>
|
|
274
|
+
<div class="header-spacer"></div>
|
|
275
|
+
<span class="header-updated" id="hdr-updated"></span>
|
|
276
|
+
<button class="refresh-btn" onclick="loadData()">↻ Refresh</button>
|
|
277
|
+
</header>
|
|
278
|
+
|
|
279
|
+
<main id="app" style="display:none">
|
|
280
|
+
|
|
281
|
+
<!-- Overview cards -->
|
|
282
|
+
<div class="section">
|
|
283
|
+
<div class="section-title">Overview</div>
|
|
284
|
+
<div class="cards-grid" id="overview-cards"></div>
|
|
285
|
+
</div>
|
|
286
|
+
|
|
287
|
+
<!-- Token timeline + Model usage -->
|
|
288
|
+
<div class="section">
|
|
289
|
+
<div class="section-title">Token Usage</div>
|
|
290
|
+
<div class="charts-row charts-row-3">
|
|
291
|
+
<div class="chart-card">
|
|
292
|
+
<div class="chart-title">Daily Token Usage <span class="chart-subtitle" id="token-range"></span></div>
|
|
293
|
+
<div class="chart-wrap chart-wrap-tall"><canvas id="chart-tokens"></canvas></div>
|
|
294
|
+
</div>
|
|
295
|
+
<div class="chart-card">
|
|
296
|
+
<div class="chart-title">Model Distribution</div>
|
|
297
|
+
<div class="chart-wrap"><canvas id="chart-models"></canvas></div>
|
|
298
|
+
<div id="model-legend" style="margin-top:12px"></div>
|
|
299
|
+
</div>
|
|
300
|
+
</div>
|
|
301
|
+
</div>
|
|
302
|
+
|
|
303
|
+
<!-- Activity heatmap -->
|
|
304
|
+
<div class="section">
|
|
305
|
+
<div class="section-title">Activity Heatmap — last 12 months</div>
|
|
306
|
+
<div class="chart-card">
|
|
307
|
+
<div id="heatmap-months" class="hm-month-labels"></div>
|
|
308
|
+
<div class="heatmap-wrap">
|
|
309
|
+
<div class="heatmap" id="heatmap"></div>
|
|
310
|
+
</div>
|
|
311
|
+
<div style="margin-top:10px; display:flex; align-items:center; gap:6px; font-size:11px; color:var(--text-dim)">
|
|
312
|
+
Less
|
|
313
|
+
<span class="hm-cell hm-0"></span>
|
|
314
|
+
<span class="hm-cell hm-1"></span>
|
|
315
|
+
<span class="hm-cell hm-2"></span>
|
|
316
|
+
<span class="hm-cell hm-3"></span>
|
|
317
|
+
<span class="hm-cell hm-4"></span>
|
|
318
|
+
More
|
|
319
|
+
</div>
|
|
320
|
+
</div>
|
|
321
|
+
</div>
|
|
322
|
+
|
|
323
|
+
<!-- Tool usage + Hourly distribution -->
|
|
324
|
+
<div class="section">
|
|
325
|
+
<div class="section-title">Activity Breakdown</div>
|
|
326
|
+
<div class="charts-row charts-row-2">
|
|
327
|
+
<div class="chart-card">
|
|
328
|
+
<div class="chart-title">Tool Usage</div>
|
|
329
|
+
<div class="chart-wrap chart-wrap-tall"><canvas id="chart-tools"></canvas></div>
|
|
330
|
+
</div>
|
|
331
|
+
<div class="chart-card">
|
|
332
|
+
<div class="chart-title">Hourly Activity Distribution</div>
|
|
333
|
+
<div class="chart-wrap chart-wrap-tall"><canvas id="chart-hours"></canvas></div>
|
|
334
|
+
</div>
|
|
335
|
+
</div>
|
|
336
|
+
</div>
|
|
337
|
+
|
|
338
|
+
<!-- Cache efficiency -->
|
|
339
|
+
<div class="section">
|
|
340
|
+
<div class="section-title">Token Breakdown</div>
|
|
341
|
+
<div class="chart-card">
|
|
342
|
+
<div class="chart-title">Input vs Output vs Cache <span class="chart-subtitle" id="cache-subtitle"></span></div>
|
|
343
|
+
<div class="charts-row" style="grid-template-columns:1fr 1fr; gap:24px; margin:0">
|
|
344
|
+
<div class="chart-wrap"><canvas id="chart-token-breakdown"></canvas></div>
|
|
345
|
+
<div id="token-breakdown-detail" style="display:flex;flex-direction:column;justify-content:center;gap:10px"></div>
|
|
346
|
+
</div>
|
|
347
|
+
</div>
|
|
348
|
+
</div>
|
|
349
|
+
|
|
350
|
+
<!-- Sessions table -->
|
|
351
|
+
<div class="section">
|
|
352
|
+
<div class="section-title">Sessions</div>
|
|
353
|
+
<div class="table-card">
|
|
354
|
+
<div class="table-header">
|
|
355
|
+
<span class="table-title">All Sessions</span>
|
|
356
|
+
<span class="table-count" id="sessions-count"></span>
|
|
357
|
+
</div>
|
|
358
|
+
<div class="table-wrap">
|
|
359
|
+
<table>
|
|
360
|
+
<thead>
|
|
361
|
+
<tr>
|
|
362
|
+
<th>Project</th>
|
|
363
|
+
<th>Date</th>
|
|
364
|
+
<th>Duration</th>
|
|
365
|
+
<th>Messages</th>
|
|
366
|
+
<th>Tokens</th>
|
|
367
|
+
<th>Cost</th>
|
|
368
|
+
<th>Model</th>
|
|
369
|
+
<th>Tools</th>
|
|
370
|
+
</tr>
|
|
371
|
+
</thead>
|
|
372
|
+
<tbody id="sessions-tbody"></tbody>
|
|
373
|
+
</table>
|
|
374
|
+
</div>
|
|
375
|
+
</div>
|
|
376
|
+
</div>
|
|
377
|
+
|
|
378
|
+
<!-- Projects table -->
|
|
379
|
+
<div class="section">
|
|
380
|
+
<div class="section-title">Projects</div>
|
|
381
|
+
<div class="table-card">
|
|
382
|
+
<div class="table-header">
|
|
383
|
+
<span class="table-title">All Projects</span>
|
|
384
|
+
<span class="table-count" id="projects-count"></span>
|
|
385
|
+
</div>
|
|
386
|
+
<div class="table-wrap">
|
|
387
|
+
<table>
|
|
388
|
+
<thead>
|
|
389
|
+
<tr>
|
|
390
|
+
<th>Project</th>
|
|
391
|
+
<th>Sessions</th>
|
|
392
|
+
<th>Messages</th>
|
|
393
|
+
<th>Total Tokens</th>
|
|
394
|
+
<th>Cost</th>
|
|
395
|
+
<th>Top Tool</th>
|
|
396
|
+
<th>Last Active</th>
|
|
397
|
+
</tr>
|
|
398
|
+
</thead>
|
|
399
|
+
<tbody id="projects-tbody"></tbody>
|
|
400
|
+
</table>
|
|
401
|
+
</div>
|
|
402
|
+
</div>
|
|
403
|
+
</div>
|
|
404
|
+
|
|
405
|
+
</main>
|
|
406
|
+
|
|
407
|
+
<script>
|
|
408
|
+
// ── Helpers ──────────────────────────────────────────────────────────────────
|
|
409
|
+
const fmt = {
|
|
410
|
+
num: n => n == null ? '—' : n >= 1e6 ? (n/1e6).toFixed(2)+'M' : n >= 1e3 ? (n/1e3).toFixed(1)+'K' : String(n),
|
|
411
|
+
tokens: n => n == null ? '—' : n >= 1e6 ? (n/1e6).toFixed(2)+'M' : n >= 1e3 ? (n/1e3).toFixed(1)+'K' : n.toFixed(0),
|
|
412
|
+
cost: n => n == null ? '—' : n < 0.001 ? '<$0.001' : '$'+n.toFixed(n < 0.01 ? 4 : n < 1 ? 3 : 2),
|
|
413
|
+
dur: m => m === 0 ? '<1 min' : m < 60 ? m+'m' : (m/60).toFixed(1)+'h',
|
|
414
|
+
date: s => { if (!s) return '—'; const d = new Date(s); return d.toLocaleDateString('en-US', {month:'short',day:'numeric',year:'numeric'}); },
|
|
415
|
+
time: s => { if (!s) return '—'; const d = new Date(s); return d.toLocaleTimeString('en-US', {hour:'2-digit',minute:'2-digit'})+' '+d.toLocaleDateString('en-US', {month:'short',day:'numeric'}); },
|
|
416
|
+
pct: n => (n||0).toFixed(1)+'%',
|
|
417
|
+
shortId: s => s ? s.slice(0,8)+'…' : '—',
|
|
418
|
+
};
|
|
419
|
+
|
|
420
|
+
const COLORS = {
|
|
421
|
+
purple: '#7c3aed', purpleLight: '#a78bfa',
|
|
422
|
+
cyan: '#06b6d4', cyanLight: '#67e8f9',
|
|
423
|
+
green: '#10b981', amber: '#f59e0b', red: '#ef4444',
|
|
424
|
+
palette: ['#7c3aed','#06b6d4','#10b981','#f59e0b','#ef4444','#ec4899','#3b82f6','#8b5cf6'],
|
|
425
|
+
};
|
|
426
|
+
|
|
427
|
+
Chart.defaults.color = '#94a3b8';
|
|
428
|
+
Chart.defaults.borderColor = 'rgba(255,255,255,0.06)';
|
|
429
|
+
Chart.defaults.font.family = "-apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif";
|
|
430
|
+
|
|
431
|
+
const charts = {};
|
|
432
|
+
function destroyChart(id) { if (charts[id]) { charts[id].destroy(); delete charts[id]; } }
|
|
433
|
+
|
|
434
|
+
function modelClass(m) {
|
|
435
|
+
if (!m) return '';
|
|
436
|
+
if (m.includes('haiku')) return 'model-haiku';
|
|
437
|
+
if (m.includes('opus')) return 'model-opus';
|
|
438
|
+
return '';
|
|
439
|
+
}
|
|
440
|
+
function modelShort(m) {
|
|
441
|
+
if (!m) return '—';
|
|
442
|
+
return m.replace('claude-','').replace(/-20\d{6}$/,'');
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
// ── Load Data ────────────────────────────────────────────────────────────────
|
|
446
|
+
let _data = null;
|
|
447
|
+
|
|
448
|
+
async function loadData() {
|
|
449
|
+
document.getElementById('loading').style.display = 'flex';
|
|
450
|
+
document.getElementById('app').style.display = 'none';
|
|
451
|
+
try {
|
|
452
|
+
const res = await fetch('/api/data');
|
|
453
|
+
_data = await res.json();
|
|
454
|
+
render(_data);
|
|
455
|
+
} catch(e) {
|
|
456
|
+
document.getElementById('loading').innerHTML = `
|
|
457
|
+
<div style="color:#ef4444;font-size:13px">Failed to load data: ${e.message}</div>
|
|
458
|
+
`;
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
// ── Render ───────────────────────────────────────────────────────────────────
|
|
463
|
+
function render(d) {
|
|
464
|
+
renderHeader(d);
|
|
465
|
+
renderOverviewCards(d);
|
|
466
|
+
renderTokenChart(d);
|
|
467
|
+
renderModelChart(d);
|
|
468
|
+
renderHeatmap(d);
|
|
469
|
+
renderToolChart(d);
|
|
470
|
+
renderHourlyChart(d);
|
|
471
|
+
renderTokenBreakdown(d);
|
|
472
|
+
renderSessionsTable(d);
|
|
473
|
+
renderProjectsTable(d);
|
|
474
|
+
|
|
475
|
+
document.getElementById('loading').style.display = 'none';
|
|
476
|
+
document.getElementById('app').style.display = 'block';
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
function renderHeader(d) {
|
|
480
|
+
const acc = d.account;
|
|
481
|
+
document.getElementById('hdr-account') .textContent =
|
|
482
|
+
acc.email ? `${acc.email}${acc.organization ? ' · '+acc.organization : ''}` : 'Local sessions';
|
|
483
|
+
document.getElementById('hdr-updated').textContent =
|
|
484
|
+
'Updated ' + new Date(d.meta.analyzedAt).toLocaleTimeString();
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
function renderOverviewCards(d) {
|
|
488
|
+
const ov = d.overview;
|
|
489
|
+
const totalT = ov.tokens.input + ov.tokens.output + ov.tokens.cacheCreation + ov.tokens.cacheRead;
|
|
490
|
+
const avgSessionMsgs = ov.totalSessions > 0
|
|
491
|
+
? (ov.totalMessages.total / ov.totalSessions).toFixed(1)
|
|
492
|
+
: 0;
|
|
493
|
+
|
|
494
|
+
const cards = [
|
|
495
|
+
{ icon: '◈', label: 'Total Tokens', value: fmt.tokens(totalT), sub: `${fmt.tokens(ov.tokens.input)} in · ${fmt.tokens(ov.tokens.output)} out`, cls: 'c-purple' },
|
|
496
|
+
{ icon: '$', label: 'Estimated Cost', value: fmt.cost(ov.costUSD), sub: `across ${ov.totalSessions} sessions`, cls: 'c-green' },
|
|
497
|
+
{ icon: '⌂', label: 'Sessions', value: fmt.num(ov.totalSessions), sub: `${ov.dateRange.activeDays} active days`, cls: 'c-cyan' },
|
|
498
|
+
{ icon: '⬡', label: 'Projects', value: fmt.num(ov.totalProjects), sub: `${fmt.num(ov.totalMessages.total)} total messages`, cls: 'c-amber' },
|
|
499
|
+
{ icon: '⚙', label: 'Tool Calls', value: fmt.num(ov.toolCalls), sub: `${avgSessionMsgs} msgs/session avg`, cls: 'c-purple' },
|
|
500
|
+
{ icon: '⚡', label: 'Cache Hit Rate', value: `${ov.tokens.cacheHitRate}%`, sub: `${fmt.tokens(ov.tokens.cacheRead)} tokens saved`, cls: 'c-cyan' },
|
|
501
|
+
];
|
|
502
|
+
|
|
503
|
+
document.getElementById('overview-cards').innerHTML = cards.map(c => `
|
|
504
|
+
<div class="card">
|
|
505
|
+
<div class="card-label">${c.label}</div>
|
|
506
|
+
<div class="card-value ${c.cls}">${c.value}</div>
|
|
507
|
+
<div class="card-sub">${c.sub}</div>
|
|
508
|
+
</div>
|
|
509
|
+
`).join('');
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
function renderTokenChart(d) {
|
|
513
|
+
destroyChart('tokens');
|
|
514
|
+
const daily = d.dailyActivity;
|
|
515
|
+
if (!daily.length) return;
|
|
516
|
+
|
|
517
|
+
const labels = daily.map(x => {
|
|
518
|
+
const dt = new Date(x.date);
|
|
519
|
+
return dt.toLocaleDateString('en-US', { month: 'short', day: 'numeric' });
|
|
520
|
+
});
|
|
521
|
+
|
|
522
|
+
const inputData = daily.map(x => x.tokens);
|
|
523
|
+
const first = daily[0]?.date, last = daily[daily.length-1]?.date;
|
|
524
|
+
document.getElementById('token-range').textContent =
|
|
525
|
+
first ? `${fmt.date(first)} – ${fmt.date(last)}` : '';
|
|
526
|
+
|
|
527
|
+
const ctx = document.getElementById('chart-tokens').getContext('2d');
|
|
528
|
+
const gradient = ctx.createLinearGradient(0, 0, 0, 280);
|
|
529
|
+
gradient.addColorStop(0, 'rgba(124,58,237,0.35)');
|
|
530
|
+
gradient.addColorStop(1, 'rgba(124,58,237,0)');
|
|
531
|
+
|
|
532
|
+
charts['tokens'] = new Chart(ctx, {
|
|
533
|
+
type: 'line',
|
|
534
|
+
data: {
|
|
535
|
+
labels,
|
|
536
|
+
datasets: [{
|
|
537
|
+
label: 'Tokens',
|
|
538
|
+
data: inputData,
|
|
539
|
+
borderColor: COLORS.purple,
|
|
540
|
+
backgroundColor: gradient,
|
|
541
|
+
borderWidth: 2,
|
|
542
|
+
fill: true,
|
|
543
|
+
tension: 0.4,
|
|
544
|
+
pointRadius: daily.length > 30 ? 0 : 3,
|
|
545
|
+
pointHoverRadius: 5,
|
|
546
|
+
pointBackgroundColor: COLORS.purple,
|
|
547
|
+
}]
|
|
548
|
+
},
|
|
549
|
+
options: {
|
|
550
|
+
responsive: true, maintainAspectRatio: true,
|
|
551
|
+
plugins: {
|
|
552
|
+
legend: { display: false },
|
|
553
|
+
tooltip: {
|
|
554
|
+
callbacks: {
|
|
555
|
+
label: ctx => ` ${fmt.tokens(ctx.raw)} tokens`
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
},
|
|
559
|
+
scales: {
|
|
560
|
+
x: { grid: { display: false }, ticks: { maxTicksLimit: 8, font: { size: 10 } } },
|
|
561
|
+
y: { grid: { color: 'rgba(255,255,255,0.04)' }, ticks: { callback: v => fmt.tokens(v), font: { size: 10 } } }
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
});
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
function renderModelChart(d) {
|
|
568
|
+
destroyChart('models');
|
|
569
|
+
const models = Object.entries(d.models || {});
|
|
570
|
+
if (!models.length) return;
|
|
571
|
+
|
|
572
|
+
const labels = models.map(([m]) => modelShort(m));
|
|
573
|
+
const data = models.map(([,v]) => v.inputTokens + v.outputTokens);
|
|
574
|
+
const colors = models.map((_, i) => COLORS.palette[i % COLORS.palette.length]);
|
|
575
|
+
|
|
576
|
+
const ctx = document.getElementById('chart-models').getContext('2d');
|
|
577
|
+
charts['models'] = new Chart(ctx, {
|
|
578
|
+
type: 'doughnut',
|
|
579
|
+
data: { labels, datasets: [{ data, backgroundColor: colors, borderWidth: 0, hoverOffset: 6 }] },
|
|
580
|
+
options: {
|
|
581
|
+
responsive: true, maintainAspectRatio: true,
|
|
582
|
+
cutout: '65%',
|
|
583
|
+
plugins: {
|
|
584
|
+
legend: { display: false },
|
|
585
|
+
tooltip: {
|
|
586
|
+
callbacks: { label: ctx => ` ${modelShort(ctx.label)}: ${fmt.tokens(ctx.raw)} tokens` }
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
});
|
|
591
|
+
|
|
592
|
+
// Custom legend
|
|
593
|
+
document.getElementById('model-legend').innerHTML = models.map(([m, v], i) => `
|
|
594
|
+
<div style="display:flex;align-items:center;gap:8px;margin-bottom:6px;font-size:11px">
|
|
595
|
+
<span style="width:8px;height:8px;border-radius:50%;background:${colors[i]};flex-shrink:0"></span>
|
|
596
|
+
<span style="color:var(--text-muted);flex:1">${modelShort(m)}</span>
|
|
597
|
+
<span style="color:var(--text)">${fmt.tokens(v.inputTokens + v.outputTokens)}</span>
|
|
598
|
+
</div>
|
|
599
|
+
`).join('');
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
function renderHeatmap(d) {
|
|
603
|
+
const daily = d.dailyActivity || [];
|
|
604
|
+
const dayMap = {};
|
|
605
|
+
for (const x of daily) dayMap[x.date] = x.tokens;
|
|
606
|
+
|
|
607
|
+
const maxTokens = Math.max(...Object.values(dayMap), 1);
|
|
608
|
+
const today = new Date();
|
|
609
|
+
const weeks = 52;
|
|
610
|
+
const totalDays = weeks * 7;
|
|
611
|
+
|
|
612
|
+
// Start from (today - totalDays) aligned to Sunday
|
|
613
|
+
const start = new Date(today);
|
|
614
|
+
start.setDate(start.getDate() - totalDays + 1);
|
|
615
|
+
// Align to Sunday
|
|
616
|
+
const dow = start.getDay();
|
|
617
|
+
start.setDate(start.getDate() - dow);
|
|
618
|
+
|
|
619
|
+
let cells = '';
|
|
620
|
+
let monthLabels = new Array(weeks * 7).fill('');
|
|
621
|
+
|
|
622
|
+
for (let w = 0; w < weeks; w++) {
|
|
623
|
+
for (let day = 0; day < 7; day++) {
|
|
624
|
+
const d2 = new Date(start);
|
|
625
|
+
d2.setDate(start.getDate() + w * 7 + day);
|
|
626
|
+
const dateKey = d2.toISOString().slice(0, 10);
|
|
627
|
+
const tokens = dayMap[dateKey] || 0;
|
|
628
|
+
|
|
629
|
+
const level = tokens === 0 ? 0
|
|
630
|
+
: tokens < maxTokens * 0.25 ? 1
|
|
631
|
+
: tokens < maxTokens * 0.5 ? 2
|
|
632
|
+
: tokens < maxTokens * 0.75 ? 3
|
|
633
|
+
: 4;
|
|
634
|
+
|
|
635
|
+
const title = tokens > 0
|
|
636
|
+
? `${dateKey}: ${fmt.tokens(tokens)} tokens`
|
|
637
|
+
: dateKey;
|
|
638
|
+
|
|
639
|
+
cells += `<div class="hm-cell hm-${level}" title="${title}"></div>`;
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
// Month labels
|
|
644
|
+
const monthNames = ['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec'];
|
|
645
|
+
let monthHTML = '';
|
|
646
|
+
let lastMonth = -1;
|
|
647
|
+
for (let w = 0; w < weeks; w++) {
|
|
648
|
+
const d2 = new Date(start);
|
|
649
|
+
d2.setDate(start.getDate() + w * 7);
|
|
650
|
+
const m = d2.getMonth();
|
|
651
|
+
if (m !== lastMonth) {
|
|
652
|
+
monthHTML += `<span style="grid-column:span 1">${monthNames[m]}</span>`;
|
|
653
|
+
lastMonth = m;
|
|
654
|
+
} else {
|
|
655
|
+
monthHTML += `<span></span>`;
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
document.getElementById('heatmap').innerHTML = cells;
|
|
660
|
+
document.getElementById('heatmap-months').innerHTML = monthHTML;
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
function renderToolChart(d) {
|
|
664
|
+
destroyChart('tools');
|
|
665
|
+
const tools = Object.entries(d.tools || {}).sort((a,b) => b[1]-a[1]).slice(0, 12);
|
|
666
|
+
if (!tools.length) return;
|
|
667
|
+
|
|
668
|
+
const labels = tools.map(([t]) => t);
|
|
669
|
+
const data = tools.map(([,c]) => c);
|
|
670
|
+
|
|
671
|
+
charts['tools'] = new Chart(document.getElementById('chart-tools').getContext('2d'), {
|
|
672
|
+
type: 'bar',
|
|
673
|
+
data: {
|
|
674
|
+
labels,
|
|
675
|
+
datasets: [{
|
|
676
|
+
data,
|
|
677
|
+
backgroundColor: labels.map((_, i) => `rgba(124,58,237,${0.9 - i*0.06})`),
|
|
678
|
+
borderRadius: 5,
|
|
679
|
+
borderSkipped: false,
|
|
680
|
+
}]
|
|
681
|
+
},
|
|
682
|
+
options: {
|
|
683
|
+
indexAxis: 'y',
|
|
684
|
+
responsive: true, maintainAspectRatio: true,
|
|
685
|
+
plugins: {
|
|
686
|
+
legend: { display: false },
|
|
687
|
+
tooltip: { callbacks: { label: ctx => ` ${fmt.num(ctx.raw)} calls` } }
|
|
688
|
+
},
|
|
689
|
+
scales: {
|
|
690
|
+
x: { grid: { color: 'rgba(255,255,255,0.04)' }, ticks: { font: { size: 10 } } },
|
|
691
|
+
y: { grid: { display: false }, ticks: { font: { size: 11 } } }
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
});
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
function renderHourlyChart(d) {
|
|
698
|
+
destroyChart('hours');
|
|
699
|
+
const hourly = d.hourlyDistribution || new Array(24).fill(0);
|
|
700
|
+
const labels = Array.from({length:24}, (_,i) => {
|
|
701
|
+
const h = i % 12 || 12;
|
|
702
|
+
return `${h}${i < 12 ? 'am' : 'pm'}`;
|
|
703
|
+
});
|
|
704
|
+
|
|
705
|
+
const maxH = Math.max(...hourly, 1);
|
|
706
|
+
const colors = hourly.map(v => {
|
|
707
|
+
const intensity = v / maxH;
|
|
708
|
+
return `rgba(6,182,212,${0.2 + intensity * 0.8})`;
|
|
709
|
+
});
|
|
710
|
+
|
|
711
|
+
charts['hours'] = new Chart(document.getElementById('chart-hours').getContext('2d'), {
|
|
712
|
+
type: 'bar',
|
|
713
|
+
data: {
|
|
714
|
+
labels,
|
|
715
|
+
datasets: [{
|
|
716
|
+
data: hourly,
|
|
717
|
+
backgroundColor: colors,
|
|
718
|
+
borderRadius: 4,
|
|
719
|
+
borderSkipped: false,
|
|
720
|
+
}]
|
|
721
|
+
},
|
|
722
|
+
options: {
|
|
723
|
+
responsive: true, maintainAspectRatio: true,
|
|
724
|
+
plugins: {
|
|
725
|
+
legend: { display: false },
|
|
726
|
+
tooltip: { callbacks: { label: ctx => ` ${fmt.num(ctx.raw)} messages` } }
|
|
727
|
+
},
|
|
728
|
+
scales: {
|
|
729
|
+
x: { grid: { display: false }, ticks: { maxTicksLimit: 12, font: { size: 10 } } },
|
|
730
|
+
y: { grid: { color: 'rgba(255,255,255,0.04)' }, ticks: { font: { size: 10 } } }
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
});
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
function renderTokenBreakdown(d) {
|
|
737
|
+
destroyChart('token-breakdown');
|
|
738
|
+
const t = d.overview.tokens;
|
|
739
|
+
const total = t.input + t.output + t.cacheCreation + t.cacheRead || 1;
|
|
740
|
+
|
|
741
|
+
const breakdown = [
|
|
742
|
+
{ label: 'Input', value: t.input, color: COLORS.purple, pct: t.input/total*100 },
|
|
743
|
+
{ label: 'Output', value: t.output, color: COLORS.cyan, pct: t.output/total*100 },
|
|
744
|
+
{ label: 'Cache Creation', value: t.cacheCreation, color: COLORS.amber, pct: t.cacheCreation/total*100 },
|
|
745
|
+
{ label: 'Cache Read', value: t.cacheRead, color: COLORS.green, pct: t.cacheRead/total*100 },
|
|
746
|
+
].filter(x => x.value > 0);
|
|
747
|
+
|
|
748
|
+
document.getElementById('cache-subtitle').textContent =
|
|
749
|
+
`Cache efficiency: ${t.cacheHitRate}% hit rate`;
|
|
750
|
+
|
|
751
|
+
charts['token-breakdown'] = new Chart(
|
|
752
|
+
document.getElementById('chart-token-breakdown').getContext('2d'), {
|
|
753
|
+
type: 'doughnut',
|
|
754
|
+
data: {
|
|
755
|
+
labels: breakdown.map(x => x.label),
|
|
756
|
+
datasets: [{ data: breakdown.map(x => x.value), backgroundColor: breakdown.map(x => x.color), borderWidth: 0, hoverOffset: 4 }]
|
|
757
|
+
},
|
|
758
|
+
options: {
|
|
759
|
+
responsive: true, maintainAspectRatio: true,
|
|
760
|
+
cutout: '60%',
|
|
761
|
+
plugins: {
|
|
762
|
+
legend: { display: false },
|
|
763
|
+
tooltip: { callbacks: { label: ctx => ` ${ctx.label}: ${fmt.tokens(ctx.raw)}` } }
|
|
764
|
+
}
|
|
765
|
+
}
|
|
766
|
+
});
|
|
767
|
+
|
|
768
|
+
document.getElementById('token-breakdown-detail').innerHTML = breakdown.map(x => `
|
|
769
|
+
<div>
|
|
770
|
+
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:4px">
|
|
771
|
+
<span style="display:flex;align-items:center;gap:6px;font-size:12px;color:var(--text-muted)">
|
|
772
|
+
<span style="width:8px;height:8px;border-radius:50%;background:${x.color};flex-shrink:0"></span>
|
|
773
|
+
${x.label}
|
|
774
|
+
</span>
|
|
775
|
+
<span style="font-size:12px;font-weight:600;color:var(--text)">${fmt.tokens(x.value)}</span>
|
|
776
|
+
</div>
|
|
777
|
+
<div class="token-bar-track">
|
|
778
|
+
<div class="token-bar-fill" style="width:${x.pct.toFixed(1)}%;background:${x.color}"></div>
|
|
779
|
+
</div>
|
|
780
|
+
</div>
|
|
781
|
+
`).join('');
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
function renderSessionsTable(d) {
|
|
785
|
+
const sessions = d.sessions || [];
|
|
786
|
+
document.getElementById('sessions-count').textContent = `${sessions.length} sessions`;
|
|
787
|
+
|
|
788
|
+
if (!sessions.length) {
|
|
789
|
+
document.getElementById('sessions-tbody').innerHTML =
|
|
790
|
+
'<tr><td colspan="8"><div class="empty"><div class="empty-icon">📭</div>No sessions found</div></td></tr>';
|
|
791
|
+
return;
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
document.getElementById('sessions-tbody').innerHTML = sessions.map(s => `
|
|
795
|
+
<tr>
|
|
796
|
+
<td>
|
|
797
|
+
<div style="font-weight:500">${s.projectName || '—'}</div>
|
|
798
|
+
<div class="td-path" style="font-size:10px;color:var(--text-dim)">${s.project}</div>
|
|
799
|
+
</td>
|
|
800
|
+
<td style="color:var(--text-muted)">${fmt.time(s.startTime)}</td>
|
|
801
|
+
<td>${fmt.dur(s.durationMinutes)}</td>
|
|
802
|
+
<td>
|
|
803
|
+
<span class="badge badge-purple">${s.userMessages}↑</span>
|
|
804
|
+
<span class="badge badge-cyan" style="margin-left:3px">${s.assistantMessages}↓</span>
|
|
805
|
+
</td>
|
|
806
|
+
<td style="font-variant-numeric:tabular-nums">${fmt.tokens(s.tokens.total)}</td>
|
|
807
|
+
<td style="color:var(--green)">${fmt.cost(s.costUSD)}</td>
|
|
808
|
+
<td><span class="model-tag ${modelClass(s.model)}">${modelShort(s.model)}</span></td>
|
|
809
|
+
<td style="color:var(--text-muted)">${fmt.num(s.toolCalls)}</td>
|
|
810
|
+
</tr>
|
|
811
|
+
`).join('');
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
function renderProjectsTable(d) {
|
|
815
|
+
const projects = d.projects || [];
|
|
816
|
+
document.getElementById('projects-count').textContent = `${projects.length} projects`;
|
|
817
|
+
|
|
818
|
+
if (!projects.length) {
|
|
819
|
+
document.getElementById('projects-tbody').innerHTML =
|
|
820
|
+
'<tr><td colspan="7"><div class="empty"><div class="empty-icon">📂</div>No projects found</div></td></tr>';
|
|
821
|
+
return;
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
document.getElementById('projects-tbody').innerHTML = projects.map(p => {
|
|
825
|
+
const topTool = Object.entries(p.tools || {}).sort((a,b)=>b[1]-a[1])[0];
|
|
826
|
+
return `
|
|
827
|
+
<tr>
|
|
828
|
+
<td>
|
|
829
|
+
<div style="font-weight:500">${p.name}</div>
|
|
830
|
+
<div class="td-path" style="font-size:10px;color:var(--text-dim)">${p.path}</div>
|
|
831
|
+
</td>
|
|
832
|
+
<td>${p.sessions}</td>
|
|
833
|
+
<td>${fmt.num(p.messages)}</td>
|
|
834
|
+
<td style="font-variant-numeric:tabular-nums">${fmt.tokens(p.tokens.total)}</td>
|
|
835
|
+
<td style="color:var(--green)">${fmt.cost(p.costUSD)}</td>
|
|
836
|
+
<td>${topTool ? `<span class="badge badge-purple">${topTool[0]}</span>` : '—'}</td>
|
|
837
|
+
<td style="color:var(--text-muted)">${fmt.date(p.lastActive)}</td>
|
|
838
|
+
</tr>
|
|
839
|
+
`}).join('');
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
// ── Init ─────────────────────────────────────────────────────────────────────
|
|
843
|
+
loadData();
|
|
844
|
+
</script>
|
|
845
|
+
</body>
|
|
846
|
+
</html>
|