graphtrack 0.1.1 → 0.1.2
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 +5 -0
- package/dist/graph.js +9 -8
- package/dist/graph.js.map +1 -1
- package/package.json +3 -2
- package/public/images/icon.svg +1 -0
- package/public/index.html +819 -102
package/public/index.html
CHANGED
|
@@ -4,100 +4,674 @@
|
|
|
4
4
|
<meta charset="utf-8" />
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
6
6
|
<meta name="description" content="Graphtrack — visualize Claude Code token usage as an interactive graph." />
|
|
7
|
+
<link rel="icon" type="image/svg+xml" href="images/icon.svg" />
|
|
7
8
|
<title>Graphtrack</title>
|
|
9
|
+
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
10
|
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
11
|
+
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
|
|
8
12
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/vis-network/9.1.9/standalone/umd/vis-network.min.js"></script>
|
|
9
13
|
<style>
|
|
10
14
|
:root {
|
|
11
|
-
--canvas
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
15
|
+
--canvas: #08080a;
|
|
16
|
+
--surface: #111114;
|
|
17
|
+
--card: #18181c;
|
|
18
|
+
--card-hover: #1e1e24;
|
|
19
|
+
--border: #25252a;
|
|
20
|
+
--border-light: #2e2e36;
|
|
21
|
+
--yellow: #faff69;
|
|
22
|
+
--yellow-dim: rgba(250, 255, 105, 0.08);
|
|
23
|
+
--yellow-glow: rgba(250, 255, 105, 0.12);
|
|
24
|
+
--text: #e8e8ec;
|
|
25
|
+
--body: #a0a0ac;
|
|
26
|
+
--muted: #65656f;
|
|
27
|
+
--accent-blue: #5b9cf5;
|
|
28
|
+
--accent-green: #4ade80;
|
|
29
|
+
--accent-red: #f87171;
|
|
30
|
+
--radius: 10px;
|
|
31
|
+
--radius-sm: 6px;
|
|
32
|
+
}
|
|
33
|
+
* { box-sizing: border-box; margin: 0; }
|
|
34
|
+
html, body { height: 100%; }
|
|
35
|
+
body {
|
|
36
|
+
font-family: 'Inter', system-ui, -apple-system, sans-serif;
|
|
37
|
+
background: var(--canvas);
|
|
38
|
+
color: var(--body);
|
|
39
|
+
display: flex;
|
|
40
|
+
flex-direction: column;
|
|
41
|
+
-webkit-font-smoothing: antialiased;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/* ─── NAVBAR ─── */
|
|
45
|
+
header {
|
|
46
|
+
padding: 10px 24px;
|
|
47
|
+
background: var(--surface);
|
|
48
|
+
border-bottom: 1px solid var(--border);
|
|
49
|
+
display: flex;
|
|
50
|
+
align-items: center;
|
|
51
|
+
gap: 10px;
|
|
52
|
+
flex: 0 0 auto;
|
|
53
|
+
flex-wrap: wrap;
|
|
54
|
+
position: relative;
|
|
55
|
+
z-index: 10;
|
|
56
|
+
}
|
|
57
|
+
header::after {
|
|
58
|
+
content: "";
|
|
59
|
+
position: absolute;
|
|
60
|
+
bottom: -1px;
|
|
61
|
+
left: 0;
|
|
62
|
+
right: 0;
|
|
63
|
+
height: 1px;
|
|
64
|
+
background: linear-gradient(90deg, transparent, var(--yellow-dim), transparent);
|
|
65
|
+
}
|
|
66
|
+
.logo {
|
|
67
|
+
display: flex;
|
|
68
|
+
align-items: center;
|
|
69
|
+
gap: 8px;
|
|
70
|
+
flex-shrink: 0;
|
|
71
|
+
}
|
|
72
|
+
.logo img {
|
|
73
|
+
width: 26px;
|
|
74
|
+
height: 26px;
|
|
75
|
+
filter: drop-shadow(0 0 6px rgba(250, 255, 105, 0.25));
|
|
76
|
+
transition: filter 0.3s;
|
|
77
|
+
}
|
|
78
|
+
.logo img:hover {
|
|
79
|
+
filter: drop-shadow(0 0 10px rgba(250, 255, 105, 0.45));
|
|
80
|
+
}
|
|
81
|
+
.logo h1 {
|
|
82
|
+
font-size: 17px;
|
|
83
|
+
color: var(--text);
|
|
84
|
+
letter-spacing: -0.4px;
|
|
85
|
+
font-weight: 700;
|
|
86
|
+
}
|
|
87
|
+
.sub {
|
|
88
|
+
font-size: 11.5px;
|
|
89
|
+
color: var(--muted);
|
|
90
|
+
margin-right: 4px;
|
|
91
|
+
white-space: nowrap;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/* ─── CUSTOM DROPDOWN ─── */
|
|
95
|
+
.dropdown {
|
|
96
|
+
position: relative;
|
|
97
|
+
}
|
|
98
|
+
.dropdown-trigger {
|
|
99
|
+
display: flex;
|
|
100
|
+
align-items: center;
|
|
101
|
+
gap: 8px;
|
|
102
|
+
background: var(--card);
|
|
103
|
+
color: var(--body);
|
|
104
|
+
border: 1px solid var(--border);
|
|
105
|
+
padding: 6px 12px;
|
|
106
|
+
border-radius: var(--radius-sm);
|
|
107
|
+
font-size: 12.5px;
|
|
108
|
+
font-family: inherit;
|
|
109
|
+
cursor: pointer;
|
|
110
|
+
transition: all 0.2s ease;
|
|
111
|
+
max-width: 340px;
|
|
112
|
+
white-space: nowrap;
|
|
113
|
+
overflow: hidden;
|
|
114
|
+
text-overflow: ellipsis;
|
|
115
|
+
min-width: 200px;
|
|
116
|
+
}
|
|
117
|
+
.dropdown-trigger:hover {
|
|
118
|
+
border-color: var(--border-light);
|
|
119
|
+
background: var(--card-hover);
|
|
120
|
+
}
|
|
121
|
+
.dropdown-trigger:focus {
|
|
122
|
+
outline: none;
|
|
123
|
+
border-color: var(--yellow);
|
|
124
|
+
box-shadow: 0 0 0 2px var(--yellow-dim);
|
|
125
|
+
}
|
|
126
|
+
.dropdown-trigger.disabled {
|
|
127
|
+
opacity: 0.4;
|
|
128
|
+
pointer-events: none;
|
|
129
|
+
}
|
|
130
|
+
.dropdown-trigger .dd-label {
|
|
131
|
+
flex: 1;
|
|
132
|
+
overflow: hidden;
|
|
133
|
+
text-overflow: ellipsis;
|
|
134
|
+
text-align: left;
|
|
135
|
+
}
|
|
136
|
+
.dropdown-trigger .dd-label.placeholder {
|
|
137
|
+
color: var(--muted);
|
|
138
|
+
}
|
|
139
|
+
.dropdown-trigger .dd-chevron {
|
|
140
|
+
flex-shrink: 0;
|
|
141
|
+
width: 14px;
|
|
142
|
+
height: 14px;
|
|
143
|
+
color: var(--muted);
|
|
144
|
+
transition: transform 0.2s ease, color 0.2s;
|
|
145
|
+
}
|
|
146
|
+
.dropdown-trigger:hover .dd-chevron { color: var(--body); }
|
|
147
|
+
.dropdown.open .dropdown-trigger {
|
|
148
|
+
border-color: var(--yellow);
|
|
149
|
+
box-shadow: 0 0 0 2px var(--yellow-dim);
|
|
150
|
+
}
|
|
151
|
+
.dropdown.open .dd-chevron {
|
|
152
|
+
transform: rotate(180deg);
|
|
153
|
+
color: var(--yellow);
|
|
154
|
+
}
|
|
155
|
+
.dropdown-panel {
|
|
156
|
+
position: absolute;
|
|
157
|
+
top: calc(100% + 6px);
|
|
158
|
+
left: 0;
|
|
159
|
+
min-width: 100%;
|
|
160
|
+
width: 360px;
|
|
161
|
+
background: var(--card);
|
|
162
|
+
border: 1px solid var(--border-light);
|
|
163
|
+
border-radius: var(--radius);
|
|
164
|
+
box-shadow: 0 12px 40px rgba(0, 0, 0, 0.5), 0 0 0 1px rgba(250, 255, 105, 0.04);
|
|
165
|
+
z-index: 100;
|
|
166
|
+
opacity: 0;
|
|
167
|
+
transform: translateY(-6px) scale(0.98);
|
|
168
|
+
pointer-events: none;
|
|
169
|
+
transition: opacity 0.18s ease, transform 0.18s ease;
|
|
170
|
+
overflow: hidden;
|
|
171
|
+
}
|
|
172
|
+
.dropdown.open .dropdown-panel {
|
|
173
|
+
opacity: 1;
|
|
174
|
+
transform: translateY(0) scale(1);
|
|
175
|
+
pointer-events: auto;
|
|
176
|
+
}
|
|
177
|
+
.dd-search {
|
|
178
|
+
padding: 10px 12px;
|
|
179
|
+
border-bottom: 1px solid var(--border);
|
|
180
|
+
}
|
|
181
|
+
.dd-search input {
|
|
182
|
+
width: 100%;
|
|
183
|
+
background: var(--surface);
|
|
184
|
+
border: 1px solid var(--border);
|
|
185
|
+
border-radius: var(--radius-sm);
|
|
186
|
+
padding: 7px 10px 7px 32px;
|
|
187
|
+
font-size: 12.5px;
|
|
188
|
+
font-family: inherit;
|
|
189
|
+
color: var(--text);
|
|
190
|
+
outline: none;
|
|
191
|
+
transition: border-color 0.2s;
|
|
192
|
+
}
|
|
193
|
+
.dd-search input:focus {
|
|
194
|
+
border-color: var(--yellow);
|
|
195
|
+
}
|
|
196
|
+
.dd-search input::placeholder {
|
|
197
|
+
color: var(--muted);
|
|
198
|
+
}
|
|
199
|
+
.dd-search-icon {
|
|
200
|
+
position: absolute;
|
|
201
|
+
left: 22px;
|
|
202
|
+
top: 50%;
|
|
203
|
+
transform: translateY(-50%);
|
|
204
|
+
width: 14px;
|
|
205
|
+
height: 14px;
|
|
206
|
+
color: var(--muted);
|
|
207
|
+
pointer-events: none;
|
|
208
|
+
}
|
|
209
|
+
.dd-search { position: relative; }
|
|
210
|
+
.dd-list {
|
|
211
|
+
max-height: 300px;
|
|
212
|
+
overflow-y: auto;
|
|
213
|
+
overscroll-behavior: contain;
|
|
214
|
+
padding: 4px;
|
|
215
|
+
}
|
|
216
|
+
.dd-group-label {
|
|
217
|
+
padding: 8px 10px 4px;
|
|
218
|
+
font-size: 10px;
|
|
219
|
+
text-transform: uppercase;
|
|
220
|
+
letter-spacing: 1px;
|
|
221
|
+
font-weight: 600;
|
|
222
|
+
color: var(--muted);
|
|
223
|
+
}
|
|
224
|
+
.dd-item {
|
|
225
|
+
display: flex;
|
|
226
|
+
align-items: center;
|
|
227
|
+
gap: 10px;
|
|
228
|
+
padding: 8px 10px;
|
|
229
|
+
border-radius: var(--radius-sm);
|
|
230
|
+
cursor: pointer;
|
|
231
|
+
transition: background 0.12s, color 0.12s;
|
|
232
|
+
font-size: 12.5px;
|
|
233
|
+
color: var(--body);
|
|
234
|
+
}
|
|
235
|
+
.dd-item:hover {
|
|
236
|
+
background: var(--yellow-dim);
|
|
237
|
+
color: var(--text);
|
|
238
|
+
}
|
|
239
|
+
.dd-item.selected {
|
|
240
|
+
background: rgba(250, 255, 105, 0.1);
|
|
241
|
+
color: var(--yellow);
|
|
242
|
+
}
|
|
243
|
+
.dd-item .dd-dot {
|
|
244
|
+
width: 6px;
|
|
245
|
+
height: 6px;
|
|
246
|
+
border-radius: 50%;
|
|
247
|
+
background: var(--border-light);
|
|
248
|
+
flex-shrink: 0;
|
|
249
|
+
transition: background 0.15s;
|
|
250
|
+
}
|
|
251
|
+
.dd-item.selected .dd-dot {
|
|
252
|
+
background: var(--yellow);
|
|
253
|
+
box-shadow: 0 0 6px rgba(250, 255, 105, 0.4);
|
|
254
|
+
}
|
|
255
|
+
.dd-item-info {
|
|
256
|
+
flex: 1;
|
|
257
|
+
min-width: 0;
|
|
258
|
+
display: flex;
|
|
259
|
+
flex-direction: column;
|
|
260
|
+
gap: 1px;
|
|
261
|
+
}
|
|
262
|
+
.dd-item-id {
|
|
263
|
+
font-family: 'JetBrains Mono', monospace;
|
|
264
|
+
font-size: 12px;
|
|
265
|
+
font-weight: 500;
|
|
266
|
+
color: var(--text);
|
|
267
|
+
overflow: hidden;
|
|
268
|
+
text-overflow: ellipsis;
|
|
269
|
+
white-space: nowrap;
|
|
270
|
+
}
|
|
271
|
+
.dd-item.selected .dd-item-id {
|
|
272
|
+
color: var(--yellow);
|
|
273
|
+
}
|
|
274
|
+
.dd-item-proj {
|
|
275
|
+
font-size: 11px;
|
|
276
|
+
color: var(--muted);
|
|
277
|
+
overflow: hidden;
|
|
278
|
+
text-overflow: ellipsis;
|
|
279
|
+
white-space: nowrap;
|
|
280
|
+
}
|
|
281
|
+
.dd-empty {
|
|
282
|
+
padding: 20px 12px;
|
|
283
|
+
text-align: center;
|
|
284
|
+
color: var(--muted);
|
|
285
|
+
font-size: 12.5px;
|
|
286
|
+
}
|
|
287
|
+
.dd-divider {
|
|
288
|
+
height: 1px;
|
|
289
|
+
background: var(--border);
|
|
290
|
+
margin: 4px 8px;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
.toggle {
|
|
294
|
+
margin-left: auto;
|
|
295
|
+
display: flex;
|
|
296
|
+
gap: 3px;
|
|
297
|
+
background: var(--surface);
|
|
298
|
+
border: 1px solid var(--border);
|
|
299
|
+
border-radius: var(--radius);
|
|
300
|
+
padding: 3px;
|
|
301
|
+
}
|
|
26
302
|
.toggle button {
|
|
27
|
-
background:
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
303
|
+
background: transparent;
|
|
304
|
+
color: var(--muted);
|
|
305
|
+
border: none;
|
|
306
|
+
padding: 6px 14px;
|
|
307
|
+
border-radius: 7px;
|
|
308
|
+
cursor: pointer;
|
|
309
|
+
font-size: 12.5px;
|
|
310
|
+
font-weight: 500;
|
|
311
|
+
font-family: inherit;
|
|
312
|
+
transition: all 0.2s ease;
|
|
313
|
+
position: relative;
|
|
314
|
+
}
|
|
315
|
+
.toggle button:hover {
|
|
316
|
+
color: var(--body);
|
|
317
|
+
background: var(--card);
|
|
318
|
+
}
|
|
319
|
+
.toggle button.active {
|
|
320
|
+
background: var(--yellow);
|
|
321
|
+
color: #0a0a0a;
|
|
322
|
+
font-weight: 600;
|
|
323
|
+
box-shadow: 0 1px 4px rgba(250, 255, 105, 0.2);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
#btnOverview {
|
|
327
|
+
background: var(--card);
|
|
328
|
+
color: var(--body);
|
|
329
|
+
border: 1px solid var(--border);
|
|
330
|
+
padding: 6px 14px;
|
|
331
|
+
border-radius: var(--radius-sm);
|
|
332
|
+
cursor: pointer;
|
|
333
|
+
font-size: 12.5px;
|
|
334
|
+
font-weight: 500;
|
|
335
|
+
font-family: inherit;
|
|
336
|
+
transition: all 0.2s ease;
|
|
337
|
+
}
|
|
338
|
+
#btnOverview:hover {
|
|
339
|
+
border-color: var(--border-light);
|
|
340
|
+
color: var(--text);
|
|
341
|
+
background: var(--card-hover);
|
|
342
|
+
}
|
|
343
|
+
#btnOverview.active {
|
|
344
|
+
background: var(--yellow);
|
|
345
|
+
color: #0a0a0a;
|
|
346
|
+
border-color: var(--yellow);
|
|
347
|
+
font-weight: 600;
|
|
348
|
+
box-shadow: 0 1px 4px rgba(250, 255, 105, 0.2);
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
/* ─── STATS BAR ─── */
|
|
352
|
+
#stats {
|
|
353
|
+
flex: 0 0 auto;
|
|
354
|
+
padding: 10px 24px;
|
|
355
|
+
display: flex;
|
|
356
|
+
gap: 2px;
|
|
357
|
+
flex-wrap: wrap;
|
|
358
|
+
background: var(--surface);
|
|
359
|
+
border-bottom: 1px solid var(--border);
|
|
360
|
+
}
|
|
361
|
+
#stats .stat {
|
|
362
|
+
display: flex;
|
|
363
|
+
flex-direction: column;
|
|
364
|
+
gap: 1px;
|
|
365
|
+
padding: 6px 16px;
|
|
366
|
+
border-radius: var(--radius-sm);
|
|
367
|
+
transition: background 0.2s;
|
|
368
|
+
}
|
|
369
|
+
#stats .stat:hover {
|
|
370
|
+
background: var(--card);
|
|
371
|
+
}
|
|
372
|
+
#stats .stat:first-child {
|
|
373
|
+
padding-left: 0;
|
|
374
|
+
}
|
|
375
|
+
#stats .stat .label {
|
|
376
|
+
color: var(--muted);
|
|
377
|
+
font-size: 10px;
|
|
378
|
+
text-transform: uppercase;
|
|
379
|
+
letter-spacing: 0.8px;
|
|
380
|
+
font-weight: 600;
|
|
381
|
+
}
|
|
382
|
+
#stats .stat .value {
|
|
383
|
+
color: var(--text);
|
|
384
|
+
font-weight: 600;
|
|
385
|
+
font-size: 14px;
|
|
386
|
+
font-variant-numeric: tabular-nums;
|
|
387
|
+
transition: color 0.2s;
|
|
388
|
+
}
|
|
389
|
+
#stats .stat:hover .value {
|
|
390
|
+
color: #fff;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
/* ─── GRAPH / MAIN AREA ─── */
|
|
36
394
|
#graph {
|
|
37
|
-
flex: 1 1 auto;
|
|
395
|
+
flex: 1 1 auto;
|
|
396
|
+
min-height: 0;
|
|
397
|
+
width: 100%;
|
|
38
398
|
background-color: var(--canvas);
|
|
39
399
|
background-image:
|
|
40
|
-
|
|
41
|
-
linear-gradient(
|
|
42
|
-
|
|
400
|
+
radial-gradient(circle 600px at 50% 50%, rgba(250, 255, 105, 0.015), transparent),
|
|
401
|
+
linear-gradient(rgba(255, 255, 255, 0.025) 1px, transparent 1px),
|
|
402
|
+
linear-gradient(90deg, rgba(255, 255, 255, 0.025) 1px, transparent 1px);
|
|
403
|
+
background-size: auto, 48px 48px, 48px 48px;
|
|
43
404
|
position: relative;
|
|
405
|
+
overflow: hidden;
|
|
44
406
|
}
|
|
45
407
|
#graph::after {
|
|
46
|
-
content:"";
|
|
47
|
-
position: absolute;
|
|
48
|
-
|
|
408
|
+
content: "";
|
|
409
|
+
position: absolute;
|
|
410
|
+
inset: 0;
|
|
411
|
+
pointer-events: none;
|
|
412
|
+
background: radial-gradient(ellipse at center, rgba(250, 255, 105, 0.03) 0%, transparent 60%);
|
|
49
413
|
}
|
|
50
414
|
#graph canvas { position: relative; z-index: 2; }
|
|
415
|
+
|
|
416
|
+
/* empty state */
|
|
51
417
|
#graph .empty {
|
|
52
|
-
position: absolute;
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
418
|
+
position: absolute;
|
|
419
|
+
inset: 0;
|
|
420
|
+
display: flex;
|
|
421
|
+
flex-direction: column;
|
|
422
|
+
align-items: center;
|
|
423
|
+
justify-content: center;
|
|
424
|
+
gap: 12px;
|
|
425
|
+
z-index: 2;
|
|
426
|
+
text-align: center;
|
|
427
|
+
padding: 24px;
|
|
428
|
+
}
|
|
429
|
+
#graph .empty .icon {
|
|
430
|
+
width: 56px;
|
|
431
|
+
height: 56px;
|
|
432
|
+
opacity: 0.15;
|
|
433
|
+
margin-bottom: 4px;
|
|
434
|
+
}
|
|
435
|
+
#graph .empty h2 { color: var(--text); font-size: 18px; font-weight: 600; }
|
|
436
|
+
#graph .empty p { color: var(--muted); font-size: 13px; max-width: 440px; line-height: 1.6; }
|
|
437
|
+
#graph .empty code {
|
|
438
|
+
background: var(--card);
|
|
439
|
+
border: 1px solid var(--border);
|
|
440
|
+
padding: 3px 8px;
|
|
441
|
+
border-radius: var(--radius-sm);
|
|
442
|
+
color: var(--yellow);
|
|
443
|
+
font-family: 'JetBrains Mono', monospace;
|
|
444
|
+
font-size: 12px;
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
/* spinner */
|
|
448
|
+
#spinner {
|
|
449
|
+
display: none;
|
|
450
|
+
position: fixed;
|
|
451
|
+
inset: 0;
|
|
452
|
+
z-index: 1000;
|
|
453
|
+
align-items: center;
|
|
454
|
+
justify-content: center;
|
|
455
|
+
background: rgba(8, 8, 10, 0.5);
|
|
456
|
+
backdrop-filter: blur(4px);
|
|
457
|
+
}
|
|
458
|
+
#spinner.on { display: flex; }
|
|
459
|
+
#spinner span {
|
|
460
|
+
color: var(--yellow);
|
|
461
|
+
border: 2.5px solid var(--border);
|
|
462
|
+
border-top-color: var(--yellow);
|
|
463
|
+
border-radius: 50%;
|
|
464
|
+
width: 30px;
|
|
465
|
+
height: 30px;
|
|
466
|
+
animation: spin 0.7s linear infinite;
|
|
467
|
+
}
|
|
61
468
|
@keyframes spin { to { transform: rotate(360deg); } }
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
.tbl-sec {
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
.tables
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
.
|
|
85
|
-
|
|
469
|
+
|
|
470
|
+
/* ─── TABLE VIEW ─── */
|
|
471
|
+
.tables {
|
|
472
|
+
height: 100%;
|
|
473
|
+
overflow: auto;
|
|
474
|
+
padding: 24px 28px 40px;
|
|
475
|
+
}
|
|
476
|
+
.tbl-sec {
|
|
477
|
+
margin-bottom: 32px;
|
|
478
|
+
max-width: 920px;
|
|
479
|
+
}
|
|
480
|
+
.tables h3 {
|
|
481
|
+
color: var(--text);
|
|
482
|
+
margin: 0 0 12px;
|
|
483
|
+
font-size: 12px;
|
|
484
|
+
letter-spacing: 1.2px;
|
|
485
|
+
text-transform: uppercase;
|
|
486
|
+
font-weight: 600;
|
|
487
|
+
display: flex;
|
|
488
|
+
align-items: center;
|
|
489
|
+
gap: 8px;
|
|
490
|
+
}
|
|
491
|
+
.tables h3::after {
|
|
492
|
+
content: "";
|
|
493
|
+
flex: 1;
|
|
494
|
+
height: 1px;
|
|
495
|
+
background: var(--border);
|
|
496
|
+
}
|
|
497
|
+
.tables table {
|
|
498
|
+
width: 100%;
|
|
499
|
+
border-collapse: collapse;
|
|
500
|
+
font-size: 13px;
|
|
501
|
+
}
|
|
502
|
+
.tables th {
|
|
503
|
+
text-align: left;
|
|
504
|
+
color: var(--muted);
|
|
505
|
+
font-size: 10.5px;
|
|
506
|
+
text-transform: uppercase;
|
|
507
|
+
letter-spacing: 0.8px;
|
|
508
|
+
font-weight: 600;
|
|
509
|
+
padding: 8px 14px;
|
|
510
|
+
border-bottom: 1px solid var(--border);
|
|
511
|
+
position: sticky;
|
|
512
|
+
top: 0;
|
|
513
|
+
background: var(--canvas);
|
|
514
|
+
z-index: 1;
|
|
515
|
+
}
|
|
516
|
+
.tables td {
|
|
517
|
+
padding: 8px 14px;
|
|
518
|
+
border-bottom: 1px solid var(--border);
|
|
519
|
+
color: var(--body);
|
|
520
|
+
transition: background 0.15s, color 0.15s;
|
|
521
|
+
}
|
|
522
|
+
.tables tbody tr {
|
|
523
|
+
transition: background 0.15s;
|
|
524
|
+
}
|
|
525
|
+
.tables tbody tr:hover {
|
|
526
|
+
background: var(--card);
|
|
527
|
+
}
|
|
528
|
+
.tables tbody tr:hover td {
|
|
529
|
+
color: var(--text);
|
|
530
|
+
}
|
|
531
|
+
.tables th.num, .tables td.num {
|
|
532
|
+
text-align: right;
|
|
533
|
+
font-variant-numeric: tabular-nums;
|
|
534
|
+
}
|
|
535
|
+
.swatch {
|
|
536
|
+
display: inline-block;
|
|
537
|
+
width: 10px;
|
|
538
|
+
height: 10px;
|
|
539
|
+
border-radius: 3px;
|
|
540
|
+
margin-right: 10px;
|
|
541
|
+
vertical-align: middle;
|
|
542
|
+
box-shadow: 0 0 6px rgba(255, 255, 255, 0.05);
|
|
543
|
+
}
|
|
544
|
+
.bar {
|
|
545
|
+
display: inline-block;
|
|
546
|
+
height: 6px;
|
|
547
|
+
border-radius: 3px;
|
|
548
|
+
min-width: 2px;
|
|
549
|
+
vertical-align: middle;
|
|
550
|
+
transition: width 0.4s ease;
|
|
551
|
+
}
|
|
552
|
+
.tables .muted, .ov-list .muted { color: var(--muted); }
|
|
553
|
+
|
|
554
|
+
/* ─── OVERVIEW ─── */
|
|
555
|
+
.ov-wrap { height: 100%; display: flex; }
|
|
556
|
+
.ov-graph { flex: 1 1 auto; min-width: 0; position: relative; }
|
|
557
|
+
.ov-list {
|
|
558
|
+
flex: 0 0 420px;
|
|
559
|
+
overflow: auto;
|
|
560
|
+
padding: 24px;
|
|
561
|
+
border-left: 1px solid var(--border);
|
|
562
|
+
background: var(--surface);
|
|
563
|
+
}
|
|
564
|
+
.ov-list h3 {
|
|
565
|
+
color: var(--text);
|
|
566
|
+
margin: 0 0 16px;
|
|
567
|
+
font-size: 12px;
|
|
568
|
+
letter-spacing: 1.2px;
|
|
569
|
+
text-transform: uppercase;
|
|
570
|
+
font-weight: 600;
|
|
571
|
+
display: flex;
|
|
572
|
+
align-items: center;
|
|
573
|
+
gap: 8px;
|
|
574
|
+
}
|
|
575
|
+
.ov-list h3::after {
|
|
576
|
+
content: "";
|
|
577
|
+
flex: 1;
|
|
578
|
+
height: 1px;
|
|
579
|
+
background: var(--border);
|
|
580
|
+
}
|
|
581
|
+
.ov-list th {
|
|
582
|
+
text-align: left;
|
|
583
|
+
color: var(--muted);
|
|
584
|
+
font-size: 10.5px;
|
|
585
|
+
text-transform: uppercase;
|
|
586
|
+
letter-spacing: 0.8px;
|
|
587
|
+
font-weight: 600;
|
|
588
|
+
padding: 8px 12px;
|
|
589
|
+
border-bottom: 1px solid var(--border);
|
|
590
|
+
position: sticky;
|
|
591
|
+
top: 0;
|
|
592
|
+
background: var(--surface);
|
|
593
|
+
z-index: 1;
|
|
594
|
+
}
|
|
595
|
+
.ov-list td {
|
|
596
|
+
padding: 10px 12px;
|
|
597
|
+
border-bottom: 1px solid var(--border);
|
|
598
|
+
color: var(--body);
|
|
599
|
+
font-size: 13px;
|
|
600
|
+
transition: background 0.15s, color 0.15s;
|
|
601
|
+
}
|
|
602
|
+
.ov-list th.num, .ov-list td.num {
|
|
603
|
+
text-align: right;
|
|
604
|
+
font-variant-numeric: tabular-nums;
|
|
605
|
+
}
|
|
606
|
+
.ov-list .hint {
|
|
607
|
+
color: var(--muted);
|
|
608
|
+
font-size: 11.5px;
|
|
609
|
+
margin: 16px 0 0;
|
|
610
|
+
padding: 0 12px;
|
|
611
|
+
line-height: 1.5;
|
|
612
|
+
}
|
|
613
|
+
.project-row {
|
|
614
|
+
cursor: pointer;
|
|
615
|
+
transition: background 0.15s;
|
|
616
|
+
}
|
|
617
|
+
.project-row:hover td {
|
|
618
|
+
background: var(--card-hover);
|
|
619
|
+
color: var(--text);
|
|
620
|
+
}
|
|
621
|
+
.proj-name {
|
|
622
|
+
color: var(--text);
|
|
623
|
+
font-family: 'JetBrains Mono', monospace;
|
|
624
|
+
font-size: 12px;
|
|
625
|
+
font-weight: 500;
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
/* ─── GLOW CURSOR EFFECT ─── */
|
|
86
629
|
#glow {
|
|
87
|
-
position: fixed;
|
|
88
|
-
|
|
89
|
-
|
|
630
|
+
position: fixed;
|
|
631
|
+
inset: 0;
|
|
632
|
+
pointer-events: none;
|
|
633
|
+
z-index: 9999;
|
|
634
|
+
background: radial-gradient(circle 180px at var(--gx, -9999px) var(--gy, -9999px),
|
|
635
|
+
rgba(250, 255, 105, 0.07), rgba(250, 255, 105, 0.02) 50%, transparent 75%);
|
|
90
636
|
opacity: 0;
|
|
637
|
+
transition: opacity 0.4s ease;
|
|
91
638
|
}
|
|
92
639
|
#glow.on { opacity: 1; }
|
|
640
|
+
|
|
641
|
+
/* ─── VIEW TRANSITIONS ─── */
|
|
642
|
+
.fade-in { animation: fadeIn 0.25s ease; }
|
|
643
|
+
@keyframes fadeIn {
|
|
644
|
+
from { opacity: 0; transform: translateY(4px); }
|
|
645
|
+
to { opacity: 1; transform: translateY(0); }
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
/* ─── SCROLLBAR ─── */
|
|
649
|
+
::-webkit-scrollbar { width: 6px; height: 6px; }
|
|
650
|
+
::-webkit-scrollbar-track { background: transparent; }
|
|
651
|
+
::-webkit-scrollbar-thumb { background: var(--border-light); border-radius: 3px; }
|
|
652
|
+
::-webkit-scrollbar-thumb:hover { background: var(--muted); }
|
|
93
653
|
</style>
|
|
94
654
|
</head>
|
|
95
655
|
<body>
|
|
96
656
|
<header>
|
|
97
|
-
<
|
|
98
|
-
|
|
657
|
+
<div class="logo">
|
|
658
|
+
<img src="images/icon.svg" alt="" />
|
|
659
|
+
<h1>Graphtrack</h1>
|
|
660
|
+
</div>
|
|
99
661
|
<button id="btnOverview" title="All sessions at a glance">Overview</button>
|
|
100
|
-
<
|
|
662
|
+
<div class="dropdown" id="sessionDropdown">
|
|
663
|
+
<button class="dropdown-trigger disabled" id="sessionTrigger">
|
|
664
|
+
<span class="dd-label placeholder">Loading sessions…</span>
|
|
665
|
+
<svg class="dd-chevron" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="6 9 12 15 18 9"/></svg>
|
|
666
|
+
</button>
|
|
667
|
+
<div class="dropdown-panel">
|
|
668
|
+
<div class="dd-search">
|
|
669
|
+
<svg class="dd-search-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg>
|
|
670
|
+
<input type="text" id="ddSearch" placeholder="Search sessions…" autocomplete="off" />
|
|
671
|
+
</div>
|
|
672
|
+
<div class="dd-list" id="ddList"></div>
|
|
673
|
+
</div>
|
|
674
|
+
</div>
|
|
101
675
|
<div class="toggle">
|
|
102
676
|
<button id="btnAgg" class="active">Aggregate</button>
|
|
103
677
|
<button id="btnChain">Chronological</button>
|
|
@@ -135,11 +709,20 @@
|
|
|
135
709
|
}
|
|
136
710
|
function truncate(s, n) { s = String(s); return s.length > n ? s.slice(0, n - 1) + "…" : s; }
|
|
137
711
|
|
|
138
|
-
|
|
712
|
+
/* ─── Custom dropdown state ─── */
|
|
713
|
+
let sessions = [];
|
|
714
|
+
let selectedSession = null;
|
|
715
|
+
let activeFilter = null; // project slug or null
|
|
716
|
+
let ddOpen = false;
|
|
717
|
+
|
|
718
|
+
const dropdown = $("sessionDropdown");
|
|
719
|
+
const ddTrigger = $("sessionTrigger");
|
|
720
|
+
const ddList = $("ddList");
|
|
721
|
+
const ddSearch = $("ddSearch");
|
|
139
722
|
const spinner = $("spinner");
|
|
140
723
|
const container = $("graph");
|
|
141
|
-
let current = null;
|
|
142
|
-
let overview = null;
|
|
724
|
+
let current = null;
|
|
725
|
+
let overview = null;
|
|
143
726
|
let view = "overview";
|
|
144
727
|
let network = null;
|
|
145
728
|
|
|
@@ -154,9 +737,21 @@
|
|
|
154
737
|
|
|
155
738
|
const options = {
|
|
156
739
|
physics: { enabled: true, solver: "forceAtlas2Based", stabilization: { iterations: 200 } },
|
|
157
|
-
interaction: { hover: true, tooltipDelay: 200 },
|
|
158
|
-
nodes: {
|
|
159
|
-
|
|
740
|
+
interaction: { hover: true, tooltipDelay: 200, zoomView: true, dragView: true },
|
|
741
|
+
nodes: {
|
|
742
|
+
shape: "dot",
|
|
743
|
+
font: { color: "#a0a0ac", size: 12, face: "Inter, system-ui, sans-serif" },
|
|
744
|
+
widthConstraint: { maximum: 140 },
|
|
745
|
+
borderWidth: 2,
|
|
746
|
+
borderWidthSelected: 3,
|
|
747
|
+
shadow: false,
|
|
748
|
+
},
|
|
749
|
+
edges: {
|
|
750
|
+
color: { color: "#25252a", highlight: "#faff69", hover: "#4a4a52" },
|
|
751
|
+
smooth: { enabled: true, type: "dynamic" },
|
|
752
|
+
width: 1.2,
|
|
753
|
+
shadow: false,
|
|
754
|
+
},
|
|
160
755
|
};
|
|
161
756
|
|
|
162
757
|
function showSpinner(on) { spinner.classList.toggle("on", on); }
|
|
@@ -166,7 +761,8 @@
|
|
|
166
761
|
function unmountGraph() { if (network) { network.destroy(); network = null; } }
|
|
167
762
|
function mountGraph(data, el) {
|
|
168
763
|
unmountGraph();
|
|
169
|
-
|
|
764
|
+
const target = el || container;
|
|
765
|
+
network = new vis.Network(target, { nodes: new vis.DataSet(data.nodes), edges: new vis.DataSet(data.edges) }, options);
|
|
170
766
|
}
|
|
171
767
|
|
|
172
768
|
function fillStats(s) {
|
|
@@ -184,11 +780,13 @@
|
|
|
184
780
|
|
|
185
781
|
function showEmpty(title, body) {
|
|
186
782
|
unmountGraph();
|
|
187
|
-
container.innerHTML = '<div class="empty">' +
|
|
783
|
+
container.innerHTML = '<div class="empty">' +
|
|
784
|
+
'<img src="images/icon.svg" class="icon" alt="" />' +
|
|
785
|
+
(title ? "<h2></h2><p></p>" : "") + '</div>';
|
|
188
786
|
if (title) { container.querySelector("h2").textContent = title; container.querySelector("p").textContent = body; }
|
|
189
787
|
}
|
|
190
788
|
|
|
191
|
-
//
|
|
789
|
+
// ─── session views ───
|
|
192
790
|
|
|
193
791
|
function render() {
|
|
194
792
|
if (!current) { showEmpty("No session selected", "Pick a session from the dropdown, or open Overview for all sessions."); return; }
|
|
@@ -216,7 +814,7 @@
|
|
|
216
814
|
<td class="num">${fmt(t.totalTokens)}</td>
|
|
217
815
|
</tr>`).join("");
|
|
218
816
|
container.innerHTML = `
|
|
219
|
-
<div class="tables">
|
|
817
|
+
<div class="tables fade-in">
|
|
220
818
|
<section class="tbl-sec">
|
|
221
819
|
<h3>Tool share</h3>
|
|
222
820
|
<table>
|
|
@@ -240,6 +838,7 @@
|
|
|
240
838
|
const r = await fetch("/api/session/" + encodeURIComponent(slug) + "/" + encodeURIComponent(id));
|
|
241
839
|
if (!r.ok) throw new Error("no data");
|
|
242
840
|
current = await r.json();
|
|
841
|
+
lastTokens = current.session.totalTokens;
|
|
243
842
|
fillStats(current.session);
|
|
244
843
|
view = "aggregate";
|
|
245
844
|
setViewActive("btnAgg");
|
|
@@ -251,7 +850,7 @@
|
|
|
251
850
|
}
|
|
252
851
|
}
|
|
253
852
|
|
|
254
|
-
//
|
|
853
|
+
// ─── overview ───
|
|
255
854
|
|
|
256
855
|
function overviewStats(o) {
|
|
257
856
|
return {
|
|
@@ -269,20 +868,105 @@
|
|
|
269
868
|
}
|
|
270
869
|
|
|
271
870
|
function filterSessions(project) {
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
871
|
+
activeFilter = project;
|
|
872
|
+
renderDdList();
|
|
873
|
+
// Auto-select first matching session
|
|
874
|
+
const filtered = getFilteredSessions();
|
|
875
|
+
if (filtered.length > 0) {
|
|
876
|
+
selectSession(filtered[0]);
|
|
877
|
+
}
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
function getFilteredSessions() {
|
|
881
|
+
let list = sessions.filter((s) => s.project !== "subagents");
|
|
882
|
+
if (activeFilter) list = list.filter((s) => s.project === activeFilter);
|
|
883
|
+
const q = ddSearch.value.trim().toLowerCase();
|
|
884
|
+
if (q) list = list.filter((s) => s.id.toLowerCase().includes(q) || s.project.toLowerCase().includes(q));
|
|
885
|
+
return list;
|
|
275
886
|
}
|
|
276
887
|
|
|
888
|
+
function selectSession(s) {
|
|
889
|
+
selectedSession = s;
|
|
890
|
+
const label = ddTrigger.querySelector(".dd-label");
|
|
891
|
+
label.classList.remove("placeholder");
|
|
892
|
+
label.textContent = s.project + " · " + s.id;
|
|
893
|
+
closeDropdown();
|
|
894
|
+
loadSession(s.project, s.id);
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
function openDropdown() {
|
|
898
|
+
ddOpen = true;
|
|
899
|
+
dropdown.classList.add("open");
|
|
900
|
+
ddSearch.value = "";
|
|
901
|
+
renderDdList();
|
|
902
|
+
requestAnimationFrame(() => ddSearch.focus());
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
function closeDropdown() {
|
|
906
|
+
ddOpen = false;
|
|
907
|
+
dropdown.classList.remove("open");
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
function toggleDropdown() {
|
|
911
|
+
ddOpen ? closeDropdown() : openDropdown();
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
function renderDdList() {
|
|
915
|
+
const filtered = getFilteredSessions();
|
|
916
|
+
if (filtered.length === 0) {
|
|
917
|
+
ddList.innerHTML = '<div class="dd-empty">No sessions found</div>';
|
|
918
|
+
return;
|
|
919
|
+
}
|
|
920
|
+
// Group by project
|
|
921
|
+
const groups = new Map();
|
|
922
|
+
for (const s of filtered) {
|
|
923
|
+
if (!groups.has(s.project)) groups.set(s.project, []);
|
|
924
|
+
groups.get(s.project).push(s);
|
|
925
|
+
}
|
|
926
|
+
let html = "";
|
|
927
|
+
for (const [proj, items] of groups) {
|
|
928
|
+
if (groups.size > 1) {
|
|
929
|
+
html += `<div class="dd-group-label">${escapeHtml(truncate(proj, 36))}</div>`;
|
|
930
|
+
}
|
|
931
|
+
for (const s of items) {
|
|
932
|
+
const sel = selectedSession && selectedSession.id === s.id ? " selected" : "";
|
|
933
|
+
html += `<div class="dd-item${sel}" data-id="${escapeHtml(s.id)}" data-project="${escapeHtml(s.project)}">
|
|
934
|
+
<span class="dd-dot"></span>
|
|
935
|
+
<div class="dd-item-info">
|
|
936
|
+
<span class="dd-item-id">${escapeHtml(s.id)}</span>
|
|
937
|
+
<span class="dd-item-proj">${escapeHtml(s.project)}</span>
|
|
938
|
+
</div>
|
|
939
|
+
</div>`;
|
|
940
|
+
}
|
|
941
|
+
}
|
|
942
|
+
ddList.innerHTML = html;
|
|
943
|
+
ddList.querySelectorAll(".dd-item").forEach((el) =>
|
|
944
|
+
el.addEventListener("click", (e) => {
|
|
945
|
+
e.stopPropagation();
|
|
946
|
+
selectSession({ id: el.dataset.id, project: el.dataset.project });
|
|
947
|
+
})
|
|
948
|
+
);
|
|
949
|
+
}
|
|
950
|
+
|
|
951
|
+
// Close on outside click
|
|
952
|
+
document.addEventListener("click", (e) => {
|
|
953
|
+
if (ddOpen && !dropdown.contains(e.target)) closeDropdown();
|
|
954
|
+
});
|
|
955
|
+
// Close on Escape
|
|
956
|
+
document.addEventListener("keydown", (e) => {
|
|
957
|
+
if (e.key === "Escape" && ddOpen) closeDropdown();
|
|
958
|
+
});
|
|
959
|
+
|
|
277
960
|
function renderOverview() {
|
|
278
961
|
unmountGraph();
|
|
279
962
|
const o = overview;
|
|
280
963
|
if (!o) { showEmpty("No overview data", "The server didn't return an overview."); return; }
|
|
281
964
|
fillStats(overviewStats(o));
|
|
965
|
+
o.projects = o.projects.filter((p) => p.project !== "subagents");
|
|
282
966
|
const projMax = Math.max(1, ...o.projects.map((p) => p.totalTokens));
|
|
283
967
|
const total = o.totals.totalTokens || 1;
|
|
284
|
-
const projRows = o.projects.map((p) => `
|
|
285
|
-
<tr class="project-row" data-project="${escapeHtml(p.project)}">
|
|
968
|
+
const projRows = o.projects.map((p, i) => `
|
|
969
|
+
<tr class="project-row" data-project="${escapeHtml(p.project)}" style="animation-delay:${i * 30}ms">
|
|
286
970
|
<td class="proj-name">${escapeHtml(p.project)}</td>
|
|
287
971
|
<td class="num">${p.sessions}</td>
|
|
288
972
|
<td class="num">${fmt(p.totalTokens)}</td>
|
|
@@ -290,7 +974,7 @@
|
|
|
290
974
|
<td><div class="bar" style="width:${Math.max(2, Math.round(p.totalTokens / projMax * 100))}%;background:var(--yellow)"></div></td>
|
|
291
975
|
</tr>`).join("");
|
|
292
976
|
container.innerHTML = `
|
|
293
|
-
<div class="ov-wrap">
|
|
977
|
+
<div class="ov-wrap fade-in">
|
|
294
978
|
<div class="ov-graph" id="ovGraph"></div>
|
|
295
979
|
<div class="ov-list">
|
|
296
980
|
<h3>Projects · ${o.totals.sessions} sessions</h3>
|
|
@@ -298,7 +982,7 @@
|
|
|
298
982
|
<thead><tr><th>Project</th><th class="num">Sess</th><th class="num">Tokens</th><th class="num">%</th><th style="width:30%"></th></tr></thead>
|
|
299
983
|
<tbody>${projRows || '<tr><td colspan="5" class="muted">No sessions found.</td></tr>'}</tbody>
|
|
300
984
|
</table>
|
|
301
|
-
<p class="hint">Click a project to
|
|
985
|
+
<p class="hint">Click a project to filter sessions in the dropdown above.</p>
|
|
302
986
|
</div>
|
|
303
987
|
</div>`;
|
|
304
988
|
container.querySelectorAll(".project-row").forEach((tr) =>
|
|
@@ -322,7 +1006,7 @@
|
|
|
322
1006
|
}
|
|
323
1007
|
}
|
|
324
1008
|
|
|
325
|
-
//
|
|
1009
|
+
// ─── boot ───
|
|
326
1010
|
|
|
327
1011
|
async function loadSessions() {
|
|
328
1012
|
try {
|
|
@@ -333,15 +1017,12 @@
|
|
|
333
1017
|
"Graphtrack reads session logs from " + data.logsDir + ". Run Claude Code a few times, then restart with: graphtrack --dir <path> if your logs live elsewhere.");
|
|
334
1018
|
return;
|
|
335
1019
|
}
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
opt.textContent = s.project + " · " + s.id;
|
|
343
|
-
sessionSelect.appendChild(opt);
|
|
344
|
-
});
|
|
1020
|
+
sessions = data.sessions;
|
|
1021
|
+
ddTrigger.classList.remove("disabled");
|
|
1022
|
+
const label = ddTrigger.querySelector(".dd-label");
|
|
1023
|
+
label.classList.remove("placeholder");
|
|
1024
|
+
label.textContent = sessions.length + " sessions";
|
|
1025
|
+
ddTrigger.removeAttribute("disabled");
|
|
345
1026
|
loadOverview();
|
|
346
1027
|
} catch (e) {
|
|
347
1028
|
showEmpty("Can't reach the Graphtrack server",
|
|
@@ -349,10 +1030,12 @@
|
|
|
349
1030
|
}
|
|
350
1031
|
}
|
|
351
1032
|
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
1033
|
+
ddTrigger.addEventListener("click", (e) => {
|
|
1034
|
+
e.stopPropagation();
|
|
1035
|
+
toggleDropdown();
|
|
355
1036
|
});
|
|
1037
|
+
ddSearch.addEventListener("input", renderDdList);
|
|
1038
|
+
ddSearch.addEventListener("click", (e) => e.stopPropagation());
|
|
356
1039
|
$("btnOverview").addEventListener("click", () => {
|
|
357
1040
|
view = "overview";
|
|
358
1041
|
setViewActive("btnOverview");
|
|
@@ -362,7 +1045,41 @@
|
|
|
362
1045
|
$("btnChain").addEventListener("click", () => { view = "chronological"; setViewActive("btnChain"); render(); });
|
|
363
1046
|
$("btnTable").addEventListener("click", () => { view = "table"; setViewActive("btnTable"); if (!current) render(); else renderTable(); });
|
|
364
1047
|
|
|
1048
|
+
// ─── auto-refresh every 10s ───
|
|
1049
|
+
let lastTokens = null;
|
|
1050
|
+
async function refreshData() {
|
|
1051
|
+
try {
|
|
1052
|
+
// Refresh session list silently
|
|
1053
|
+
const r = await fetch("/api/sessions");
|
|
1054
|
+
const data = await r.json();
|
|
1055
|
+
if (data.sessions) sessions = data.sessions;
|
|
1056
|
+
|
|
1057
|
+
// Refresh current view — only re-render if data actually changed
|
|
1058
|
+
if (view === "overview") {
|
|
1059
|
+
const ov = await fetch("/api/overview");
|
|
1060
|
+
const newOverview = await ov.json();
|
|
1061
|
+
if (newOverview.totals.totalTokens !== (overview?.totals?.totalTokens)) {
|
|
1062
|
+
overview = newOverview;
|
|
1063
|
+
renderOverview();
|
|
1064
|
+
}
|
|
1065
|
+
} else if (current && selectedSession) {
|
|
1066
|
+
const sr = await fetch("/api/session/" + encodeURIComponent(selectedSession.project) + "/" + encodeURIComponent(selectedSession.id));
|
|
1067
|
+
if (sr.ok) {
|
|
1068
|
+
const newData = await sr.json();
|
|
1069
|
+
const newTotal = newData.session.totalTokens;
|
|
1070
|
+
if (newTotal !== lastTokens) {
|
|
1071
|
+
lastTokens = newTotal;
|
|
1072
|
+
current = newData;
|
|
1073
|
+
fillStats(current.session);
|
|
1074
|
+
render();
|
|
1075
|
+
}
|
|
1076
|
+
}
|
|
1077
|
+
}
|
|
1078
|
+
} catch { /* server may be restarting — skip this tick */ }
|
|
1079
|
+
}
|
|
1080
|
+
setInterval(refreshData, 10000);
|
|
1081
|
+
|
|
365
1082
|
loadSessions();
|
|
366
1083
|
</script>
|
|
367
1084
|
</body>
|
|
368
|
-
</html>
|
|
1085
|
+
</html>
|