claude-code-watch 0.2.0 → 0.2.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/package.json +1 -1
- package/public/css/app.css +51 -24
- package/public/index.html +23 -25
- package/public/js/app.js +11 -1
- package/public/js/shared.js +1 -1
- package/public/js/token.js +11 -33
- package/src/server/server.js +41 -1
package/package.json
CHANGED
package/public/css/app.css
CHANGED
|
@@ -203,9 +203,8 @@ body {
|
|
|
203
203
|
#tokens-page { flex: 1; display: flex; flex-direction: column; overflow-y: auto; padding: 20px 24px; gap: 16px; background: var(--bg); }
|
|
204
204
|
.tp-refresh-bar { display: flex; align-items: center; justify-content: flex-end; gap: 12px; padding-bottom: 4px; }
|
|
205
205
|
.tp-refresh-info { font-size: 11px; color: var(--dim); }
|
|
206
|
-
.tp-top { display:
|
|
207
|
-
.tp-
|
|
208
|
-
.tp-right { flex: 1; display: flex; flex-direction: column; gap: 12px; }
|
|
206
|
+
.tp-top { display: grid; grid-template-columns: 1fr 1.5fr 1fr; gap: 16px; }
|
|
207
|
+
.tp-row2 { display: grid; grid-template-columns: 1fr; gap: 16px; margin-top: 16px; }
|
|
209
208
|
.tp-box { background: var(--bg2); border: 1px solid var(--border); border-radius: 8px; padding: 14px 16px; }
|
|
210
209
|
.tp-total-label { font-size: 11px; color: var(--dim); text-transform: uppercase; margin-bottom: 2px; }
|
|
211
210
|
.tp-total-value { font-size: 22px; font-weight: 700; color: var(--white); font-family: monospace; }
|
|
@@ -225,7 +224,7 @@ body {
|
|
|
225
224
|
.tp-h3 { font-size: 12px; color: var(--dim); font-weight: 600; text-transform: uppercase; margin-bottom: 8px; }
|
|
226
225
|
|
|
227
226
|
/* ── Heatmap ── */
|
|
228
|
-
.tp-heatmap { overflow-x: auto; }
|
|
227
|
+
.tp-heatmap { overflow-x: auto; text-align: center; }
|
|
229
228
|
.tp-heatmap-inner { display: inline-flex; flex-direction: column; gap: 2px; }
|
|
230
229
|
.tp-hm-months { display: flex; gap: 0; font-size: 10px; color: var(--dim); margin-bottom: 2px; padding-left: 28px; }
|
|
231
230
|
.tp-hm-row { display: flex; align-items: center; gap: 2px; }
|
|
@@ -233,17 +232,18 @@ body {
|
|
|
233
232
|
.tp-hm-cell { width: 12px; height: 12px; border-radius: 2px; transition: transform 0.15s; cursor: pointer; position: relative; }
|
|
234
233
|
.tp-hm-cell:hover { transform: scale(1.6); z-index: 10; }
|
|
235
234
|
.tp-hm-cell[title]:hover::after { content: attr(title); position: absolute; bottom: calc(100% + 4px); left: 50%; transform: translateX(-50%); background: var(--bg2); color: var(--white); padding: 2px 8px; border-radius: 4px; font-size: 10px; white-space: nowrap; z-index: 100; border: 1px solid var(--border); pointer-events: none; }
|
|
236
|
-
.tp-hm-legend { display: flex; align-items: center; gap: 4px; font-size: 10px; color: var(--dim); margin-top: 6px; justify-content:
|
|
235
|
+
.tp-hm-legend { display: flex; align-items: center; gap: 4px; font-size: 10px; color: var(--dim); margin-top: 6px; justify-content: center; }
|
|
237
236
|
.tp-hm-legend-cell { width: 12px; height: 12px; border-radius: 2px; }
|
|
238
237
|
|
|
239
238
|
/* ── Trend bars ── */
|
|
240
|
-
.tp-trend-bars { display: flex; align-items:
|
|
241
|
-
.tp-trend-bar-wrap { flex: 1; display: flex; flex-direction: column;
|
|
242
|
-
.tp-trend-bar {
|
|
239
|
+
.tp-trend-bars { display: flex; align-items: stretch; gap: 3px; height: 140px; position: relative; }
|
|
240
|
+
.tp-trend-bar-wrap { flex: 1; display: flex; flex-direction: column; min-width: 0; }
|
|
241
|
+
.tp-trend-bar-area { flex: 1; display: flex; flex-direction: column; align-items: stretch; justify-content: flex-end; }
|
|
242
|
+
.tp-trend-bar { position: relative; border-radius: 3px 3px 0 0; transition: all 0.15s; cursor: pointer; min-height: 4px; width: 100%; }
|
|
243
243
|
.tp-trend-bar:hover { filter: brightness(1.3); }
|
|
244
244
|
.tp-trend-bar:hover::after { content: attr(data-tip); position: absolute; bottom: calc(100% + 4px); left: 50%; transform: translateX(-50%); background: var(--bg2); color: var(--white); padding: 2px 8px; border-radius: 4px; font-size: 10px; white-space: nowrap; z-index: 100; border: 1px solid var(--border); pointer-events: none; }
|
|
245
|
-
.tp-trend-label { font-size:
|
|
246
|
-
.tp-trend-grid-lines { position: absolute; inset: 0
|
|
245
|
+
.tp-trend-label { font-size: 11px; color: var(--text); text-align: center; padding-top: 6px; white-space: nowrap; }
|
|
246
|
+
.tp-trend-grid-lines { position: absolute; inset: 0; display: flex; flex-direction: column; justify-content: space-between; pointer-events: none; }
|
|
247
247
|
.tp-trend-grid-line { width: 100%; border-top: 1px dashed var(--border); opacity: 0.4; }
|
|
248
248
|
|
|
249
249
|
/* ── Detail table ── */
|
|
@@ -267,32 +267,34 @@ body {
|
|
|
267
267
|
.tp-chart-title { font-size: 12px; color: var(--dim); font-weight: 600; text-transform: uppercase; margin-bottom: 10px; }
|
|
268
268
|
|
|
269
269
|
/* ── Stacked bar chart (weekly / monthly) ── */
|
|
270
|
-
.tp-stack-bars { display: flex; align-items:
|
|
271
|
-
.tp-stack-wrap { flex: 1; display: flex; flex-direction: column;
|
|
272
|
-
.tp-stack-
|
|
270
|
+
.tp-stack-bars { display: flex; align-items: stretch; gap: 4px; height: 240px; position: relative; }
|
|
271
|
+
.tp-stack-wrap { flex: 1; display: flex; flex-direction: column; min-width: 0; }
|
|
272
|
+
.tp-stack-bar-area { flex: 1; display: flex; align-items: stretch; }
|
|
273
|
+
.tp-stack-seg { transition: all 0.15s; cursor: pointer; min-height: 1px; width: 100%; }
|
|
273
274
|
.tp-stack-seg:hover { filter: brightness(1.3); }
|
|
274
275
|
.tp-stack-bar-group:hover::after { content: attr(data-tip); position: absolute; bottom: calc(100% + 4px); left: 50%; transform: translateX(-50%); background: var(--bg2); color: var(--white); padding: 3px 10px; border-radius: 4px; font-size: 11px; white-space: nowrap; z-index: 100; border: 1px solid var(--border); pointer-events: none; font-family: monospace; }
|
|
275
|
-
.tp-stack-bar-group { display: flex; flex-direction: column; justify-content: flex-end; position: relative;
|
|
276
|
-
.tp-stack-label { font-size:
|
|
276
|
+
.tp-stack-bar-group { display: flex; flex-direction: column; justify-content: flex-end; position: relative; width: 100%; cursor: pointer; }
|
|
277
|
+
.tp-stack-label { font-size: 11px; color: var(--text); text-align: center; padding-top: 6px; white-space: nowrap; }
|
|
277
278
|
.tp-stack-grid { position: absolute; inset: 0 0 22px 0; display: flex; flex-direction: column; justify-content: space-between; pointer-events: none; }
|
|
278
279
|
.tp-stack-grid-line { width: 100%; border-top: 1px dashed var(--border); opacity: 0.3; }
|
|
279
280
|
|
|
280
281
|
/* ── Hourly distribution chart ── */
|
|
281
|
-
.tp-hourly-bars { display: flex; align-items:
|
|
282
|
-
.tp-hourly-bar-wrap { flex: 1; display: flex; flex-direction: column;
|
|
283
|
-
.tp-hourly-bar {
|
|
282
|
+
.tp-hourly-bars { display: flex; align-items: stretch; gap: 2px; height: 200px; position: relative; }
|
|
283
|
+
.tp-hourly-bar-wrap { flex: 1; display: flex; flex-direction: column; min-width: 0; }
|
|
284
|
+
.tp-hourly-bar-area { flex: 1; display: flex; flex-direction: column; align-items: stretch; justify-content: flex-end; }
|
|
285
|
+
.tp-hourly-bar { border-radius: 3px 3px 0 0; transition: all 0.15s; cursor: pointer; min-height: 2px; width: 100%; }
|
|
284
286
|
.tp-hourly-bar:hover { filter: brightness(1.3); }
|
|
285
287
|
.tp-hourly-bar:hover::after { content: attr(data-tip); position: absolute; bottom: calc(100% + 4px); left: 50%; transform: translateX(-50%); background: var(--bg2); color: var(--white); padding: 2px 8px; border-radius: 4px; font-size: 10px; white-space: nowrap; z-index: 100; border: 1px solid var(--border); pointer-events: none; font-family: monospace; }
|
|
286
|
-
.tp-hourly-label { font-size:
|
|
288
|
+
.tp-hourly-label { font-size: 11px; color: var(--text); text-align: center; padding-top: 6px; }
|
|
287
289
|
.tp-hourly-grid { position: absolute; inset: 0 0 22px 0; display: flex; flex-direction: column; justify-content: space-between; pointer-events: none; }
|
|
288
290
|
|
|
289
291
|
/* ── Model proportion doughnut ── */
|
|
290
292
|
.tp-pie-wrap { display: flex; align-items: center; gap: 16px; }
|
|
291
|
-
.tp-pie-ring { position: relative; width:
|
|
292
|
-
.tp-pie-hole { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); width:
|
|
293
|
+
.tp-pie-ring { position: relative; width: 220px; height: 220px; border-radius: 50%; flex-shrink: 0; }
|
|
294
|
+
.tp-pie-hole { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); width: 110px; height: 110px; border-radius: 50%; background: var(--bg2); display: flex; align-items: center; justify-content: center; flex-direction: column; }
|
|
293
295
|
.tp-pie-hole-v { font-size: 14px; font-weight: 700; color: var(--white); font-family: monospace; }
|
|
294
296
|
.tp-pie-hole-l { font-size: 9px; color: var(--dim); }
|
|
295
|
-
.tp-pie-legend { display: flex; flex-direction: column; gap: 4px; overflow-y: auto; max-height:
|
|
297
|
+
.tp-pie-legend { display: flex; flex-direction: column; gap: 4px; overflow-y: auto; max-height: 220px; }
|
|
296
298
|
.tp-pie-legend-item { display: flex; align-items: center; gap: 6px; font-size: 11px; }
|
|
297
299
|
.tp-pie-dot { width: 8px; height: 8px; border-radius: 50%; flex-shrink: 0; }
|
|
298
300
|
.tp-pie-name { color: var(--text); flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
@@ -348,6 +350,31 @@ body {
|
|
|
348
350
|
font-size: 11px; flex-shrink: 0; flex-wrap: wrap;
|
|
349
351
|
}
|
|
350
352
|
#footer .sep { color: var(--dim); margin: 0 2px; }
|
|
353
|
+
.version-update-badge {
|
|
354
|
+
display: inline-flex; align-items: center; gap: 4px;
|
|
355
|
+
color: var(--green, #3fb950); font-weight: 500;
|
|
356
|
+
text-decoration: none; padding: 1px 6px;
|
|
357
|
+
border-radius: 8px; border: 1px solid var(--green, #3fb950);
|
|
358
|
+
transition: background 0.2s, color 0.2s;
|
|
359
|
+
animation: version-badge-pulse 2s ease-in-out infinite;
|
|
360
|
+
}
|
|
361
|
+
.version-update-badge:hover {
|
|
362
|
+
background: var(--green, #3fb950); color: var(--bg, #0d1117);
|
|
363
|
+
}
|
|
364
|
+
.version-update-dot {
|
|
365
|
+
width: 6px; height: 6px; border-radius: 50%;
|
|
366
|
+
background: var(--green, #3fb950);
|
|
367
|
+
display: inline-block;
|
|
368
|
+
animation: version-dot-blink 1.5s ease-in-out infinite;
|
|
369
|
+
}
|
|
370
|
+
@keyframes version-badge-pulse {
|
|
371
|
+
0%, 100% { opacity: 1; }
|
|
372
|
+
50% { opacity: 0.7; }
|
|
373
|
+
}
|
|
374
|
+
@keyframes version-dot-blink {
|
|
375
|
+
0%, 100% { opacity: 1; }
|
|
376
|
+
50% { opacity: 0.3; }
|
|
377
|
+
}
|
|
351
378
|
|
|
352
379
|
/* ── Scrollbar ── */
|
|
353
380
|
::-webkit-scrollbar { width: 6px; height: 6px; }
|
|
@@ -467,8 +494,8 @@ body {
|
|
|
467
494
|
}
|
|
468
495
|
|
|
469
496
|
@media (max-width: 768px) {
|
|
470
|
-
.tp-top {
|
|
471
|
-
.tp-
|
|
497
|
+
.tp-top { grid-template-columns: 1fr; }
|
|
498
|
+
.tp-row2 { grid-template-columns: 1fr; }
|
|
472
499
|
.tp-charts-row { grid-template-columns: 1fr; }
|
|
473
500
|
.tp-pie-wrap { flex-direction: column; align-items: center; }
|
|
474
501
|
}
|
package/public/index.html
CHANGED
|
@@ -13,24 +13,24 @@
|
|
|
13
13
|
<div id="header">
|
|
14
14
|
<button class="btn on" id="tab-stream" onclick="switchTab('stream')">📡 Stream</button>
|
|
15
15
|
<button class="btn" id="tab-tokens" onclick="switchTab('tokens')">📊 Tokens</button>
|
|
16
|
-
<span class="sep">│</span>
|
|
17
|
-
<button class="btn on" id="btn-thinking" onclick="toggleThinking()" data-tooltip="Toggle thinking">🧠 Thinking</button>
|
|
18
|
-
<button class="btn on" id="btn-tool-input" onclick="toggleToolInput()" data-tooltip="Toggle tool input">🔧 Tools</button>
|
|
19
|
-
<button class="btn on" id="btn-tool-output" onclick="toggleToolOutput()" data-tooltip="Toggle tool output">📤 Output</button>
|
|
20
|
-
<button class="btn on" id="btn-text" onclick="toggleText()" data-tooltip="Toggle text responses">💬 Text</button>
|
|
21
|
-
<button class="btn on" id="btn-hook" onclick="toggleHook()" data-tooltip="Toggle hook output">🪝 Hook</button>
|
|
22
|
-
<button class="btn on" id="btn-user-prompt" onclick="toggleUserPrompt()" data-tooltip="Toggle user prompt">👤 Prompt</button>
|
|
23
|
-
<span class="sep">│</span>
|
|
24
|
-
<button class="btn on" id="btn-autoscroll" onclick="toggleAutoScroll()" data-tooltip="Auto-scroll">⇣ Auto</button>
|
|
25
|
-
<button class="btn" id="btn-tree-toggle" onclick="toggleTree()" data-tooltip="Toggle tree panel">📂 Tree</button>
|
|
26
|
-
<span class="sep">│</span>
|
|
27
|
-
<span id="session-info">Connecting...</span>
|
|
16
|
+
<span class="stream-only sep">│</span>
|
|
17
|
+
<button class="btn on stream-only" id="btn-thinking" onclick="toggleThinking()" data-tooltip="Toggle thinking">🧠 Thinking</button>
|
|
18
|
+
<button class="btn on stream-only" id="btn-tool-input" onclick="toggleToolInput()" data-tooltip="Toggle tool input">🔧 Tools</button>
|
|
19
|
+
<button class="btn on stream-only" id="btn-tool-output" onclick="toggleToolOutput()" data-tooltip="Toggle tool output">📤 Output</button>
|
|
20
|
+
<button class="btn on stream-only" id="btn-text" onclick="toggleText()" data-tooltip="Toggle text responses">💬 Text</button>
|
|
21
|
+
<button class="btn on stream-only" id="btn-hook" onclick="toggleHook()" data-tooltip="Toggle hook output">🪝 Hook</button>
|
|
22
|
+
<button class="btn on stream-only" id="btn-user-prompt" onclick="toggleUserPrompt()" data-tooltip="Toggle user prompt">👤 Prompt</button>
|
|
23
|
+
<span class="stream-only sep">│</span>
|
|
24
|
+
<button class="btn on stream-only" id="btn-autoscroll" onclick="toggleAutoScroll()" data-tooltip="Auto-scroll">⇣ Auto</button>
|
|
25
|
+
<button class="btn stream-only" id="btn-tree-toggle" onclick="toggleTree()" data-tooltip="Toggle tree panel">📂 Tree</button>
|
|
26
|
+
<span class="stream-only sep">│</span>
|
|
27
|
+
<span class="stream-only" id="session-info">Connecting...</span>
|
|
28
28
|
<div class="auto">
|
|
29
|
-
<button class="btn btn-icon" id="btn-export" onclick="openExportModal()" data-tooltip="导出 HTML">💾</button>
|
|
29
|
+
<button class="btn btn-icon stream-only" id="btn-export" onclick="openExportModal()" data-tooltip="导出 HTML">💾</button>
|
|
30
30
|
<button class="btn btn-icon" id="btn-theme" onclick="toggleTheme()" data-tooltip="Toggle theme">🌙</button>
|
|
31
|
-
<button class="btn on" id="btn-autodisco" onclick="toggleAutoDiscovery()" data-tooltip="Auto-discover">🔍 Auto</button>
|
|
32
|
-
<span class="sep">│</span>
|
|
33
|
-
<span id="token-info"></span>
|
|
31
|
+
<button class="btn on stream-only" id="btn-autodisco" onclick="toggleAutoDiscovery()" data-tooltip="Auto-discover">🔍 Auto</button>
|
|
32
|
+
<span class="stream-only sep">│</span>
|
|
33
|
+
<span class="stream-only" id="token-info"></span>
|
|
34
34
|
</div>
|
|
35
35
|
</div>
|
|
36
36
|
|
|
@@ -64,15 +64,9 @@
|
|
|
64
64
|
<button class="btn" id="btn-refresh-tokens" onclick="refreshTokenStats()">🔄 刷新数据</button>
|
|
65
65
|
</div>
|
|
66
66
|
<div class="tp-top">
|
|
67
|
-
<div class="tp-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
<div class="tp-box" id="tp-model-rank"></div>
|
|
71
|
-
</div>
|
|
72
|
-
<div class="tp-right">
|
|
73
|
-
<div class="tp-box" id="tp-trend-card"></div>
|
|
74
|
-
<div class="tp-box" id="tp-heatmap-card"></div>
|
|
75
|
-
</div>
|
|
67
|
+
<div class="tp-box" id="tp-total-card"></div>
|
|
68
|
+
<div class="tp-box" id="tp-stats-grid"></div>
|
|
69
|
+
<div class="tp-box" id="tp-model-rank"></div>
|
|
76
70
|
</div>
|
|
77
71
|
<div class="tp-charts-row" id="tp-charts-row">
|
|
78
72
|
<div class="tp-chart-box" id="tp-weekly-chart" role="img" aria-label="Weekly token consumption chart"></div>
|
|
@@ -80,6 +74,10 @@
|
|
|
80
74
|
<div class="tp-chart-box" id="tp-model-pie" role="img" aria-label="Model token proportion chart"></div>
|
|
81
75
|
<div class="tp-chart-box" id="tp-hourly-chart" role="img" aria-label="Active time distribution chart"></div>
|
|
82
76
|
</div>
|
|
77
|
+
<div class="tp-row2">
|
|
78
|
+
<div class="tp-box" id="tp-heatmap-card"></div>
|
|
79
|
+
<div class="tp-box" id="tp-trend-card"></div>
|
|
80
|
+
</div>
|
|
83
81
|
<div class="tp-box">
|
|
84
82
|
<div class="tp-tabs" id="tp-detail-tabs"></div>
|
|
85
83
|
<div id="tp-tc-daily" class="tp-tc"><div class="tp-st" id="tp-daily-table"></div></div>
|
package/public/js/app.js
CHANGED
|
@@ -17,6 +17,7 @@ let lastMsgTime = 0;
|
|
|
17
17
|
let staleCheckTimer = null;
|
|
18
18
|
let currentTab = 'stream';
|
|
19
19
|
let appVersion = '';
|
|
20
|
+
let latestVersion = '';
|
|
20
21
|
|
|
21
22
|
// Cache highlight.js CSS for HTML export
|
|
22
23
|
let hljsDarkCSS = '', hljsLightCSS = '';
|
|
@@ -93,6 +94,7 @@ function handleMessage(msg) {
|
|
|
93
94
|
case 'tokenStats': handleTokenStats(msg.payload); break;
|
|
94
95
|
case 'config':
|
|
95
96
|
if (msg.payload.version) appVersion = msg.payload.version;
|
|
97
|
+
if (msg.payload.latestVersion) { latestVersion = msg.payload.latestVersion; renderFooterVersion(); }
|
|
96
98
|
if (msg.payload.collapseAfter > 0) {
|
|
97
99
|
applyCollapsePolicy(msg.payload.collapseAfter);
|
|
98
100
|
}
|
|
@@ -143,7 +145,11 @@ function renderFooterVersion() {
|
|
|
143
145
|
const vEl = document.getElementById('footer-version');
|
|
144
146
|
if (vEl) {
|
|
145
147
|
const v = appVersion ? `v${appVersion}` : '';
|
|
146
|
-
|
|
148
|
+
const hasUpdate = latestVersion && appVersion && latestVersion !== appVersion;
|
|
149
|
+
const updateBadge = hasUpdate
|
|
150
|
+
? `<a href="https://www.npmjs.com/package/claude-code-watch" target="_blank" rel="noopener" class="version-update-badge" data-tooltip="New version available! Click to view on npm"><span class="version-update-dot"></span>v${latestVersion} ↑</a>`
|
|
151
|
+
: '';
|
|
152
|
+
vEl.innerHTML = `${v ? v + ' ' : ''}${updateBadge}${updateBadge ? ' · ' : ''}<a href="https://github.com/shuxuecode/claude-watch" target="_blank" rel="noopener" style="color:var(--dim);display:inline-flex;align-items:center;gap:3px"><svg viewBox="0 0 16 16" width="12" height="12" fill="currentColor" style="vertical-align:middle"><path d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0016 8c0-4.42-3.58-8-8-8z"/></svg>claude-watch</a>`;
|
|
147
153
|
}
|
|
148
154
|
}
|
|
149
155
|
|
|
@@ -162,6 +168,10 @@ function switchTab(tab) {
|
|
|
162
168
|
document.getElementById('tab-stream').classList.toggle('on', tab === 'stream');
|
|
163
169
|
document.getElementById('tab-tokens').classList.toggle('on', tab === 'tokens');
|
|
164
170
|
document.getElementById('footer').style.display = tab === 'stream' ? 'flex' : 'none';
|
|
171
|
+
// Toggle stream-only header controls
|
|
172
|
+
document.querySelectorAll('.stream-only').forEach(el => {
|
|
173
|
+
el.style.display = tab === 'stream' ? '' : 'none';
|
|
174
|
+
});
|
|
165
175
|
if (tab === 'tokens' && !tokenStatsRendered && tokenStatsData.totals.messages > 0) {
|
|
166
176
|
tokenStatsRendered = true;
|
|
167
177
|
renderTokenPage();
|
package/public/js/shared.js
CHANGED
|
@@ -203,7 +203,7 @@ function aggregateWeekly(dailyKeys, daily) {
|
|
|
203
203
|
const d = new Date(k);
|
|
204
204
|
const wk = getWeekKey(d);
|
|
205
205
|
if (!result[wk]) result[wk] = { messages: 0, input: 0, output: 0, cacheCreation: 0, cacheRead: 0, models: {}, dateRange: k };
|
|
206
|
-
else result[wk].dateRange
|
|
206
|
+
else result[wk].dateRange = result[wk].dateRange.split(' ~ ')[0] + ' ~ ' + k;
|
|
207
207
|
const day = daily[k];
|
|
208
208
|
result[wk].messages += day.messages;
|
|
209
209
|
result[wk].input += day.input;
|
package/public/js/token.js
CHANGED
|
@@ -167,14 +167,14 @@ function buildTrend(daily) {
|
|
|
167
167
|
const label = k.slice(5);
|
|
168
168
|
const tip = `${k}: ${fmtTS(v)}`;
|
|
169
169
|
const color = pct < 30 ? '#0e6b5a' : pct < 60 ? '#12b886' : pct < 80 ? '#34d399' : '#6ee7b7';
|
|
170
|
-
barsHTML += `<div class="tp-trend-bar-wrap"><div class="tp-trend-bar" style="height:${Math.max(pct, 3)}%;background:${color}" data-tip="${esc(tip)}"></div><span class="tp-trend-label">${esc(label)}</span></div>`;
|
|
170
|
+
barsHTML += `<div class="tp-trend-bar-wrap"><div class="tp-trend-bar-area"><div class="tp-trend-bar" style="height:${Math.max(pct, 3)}%;background:${color}" data-tip="${esc(tip)}"></div></div><span class="tp-trend-label">${esc(label)}</span></div>`;
|
|
171
171
|
}
|
|
172
172
|
|
|
173
173
|
const gridLines = `<div class="tp-trend-grid-lines">
|
|
174
174
|
<span style="font-size:9px;color:var(--dim);align-self:flex-start">${fmtTS(maxVal)}</span>
|
|
175
175
|
<div class="tp-trend-grid-line"></div>
|
|
176
176
|
<div class="tp-trend-grid-line"></div>
|
|
177
|
-
<span style="font-size:9px;color:var(--dim);align-self:center">${fmtTS(maxVal * 0.5)}</span>
|
|
177
|
+
<span style="font-size:9px;color:var(--dim);align-self:center">${fmtTS(Math.round(maxVal * 0.5))}</span>
|
|
178
178
|
<div class="tp-trend-grid-line"></div>
|
|
179
179
|
<span style="font-size:9px;color:var(--dim);align-self:flex-end">0</span>
|
|
180
180
|
</div>`;
|
|
@@ -206,9 +206,7 @@ function buildModelRank(mt, totalAll) {
|
|
|
206
206
|
return html;
|
|
207
207
|
}
|
|
208
208
|
|
|
209
|
-
// ──
|
|
210
|
-
const STACK_COLORS = { input: '#58a6ff', output: '#f0883e', cacheRead: '#3fb950', cacheCreation: '#a371f7' };
|
|
211
|
-
const STACK_LABELS = { input: '输入 Input', output: '输出 Output', cacheRead: '缓存读取 Cache Read', cacheCreation: '缓存创建 Cache Create' };
|
|
209
|
+
// ── Bar chart for weekly/monthly token consumption ──
|
|
212
210
|
|
|
213
211
|
function buildStackedChart(dailyKeys, daily, type) {
|
|
214
212
|
const title = type === 'weekly' ? '📊 周 Token 消耗 Weekly Token Consumption' : '📊 月 Token 消耗 Monthly Token Consumption';
|
|
@@ -231,25 +229,11 @@ function buildStackedChart(dailyKeys, daily, type) {
|
|
|
231
229
|
let barsHTML = '';
|
|
232
230
|
for (let i = 0; i < periodKeys.length; i++) {
|
|
233
231
|
const k = periodKeys[i];
|
|
234
|
-
const p = periods[k];
|
|
235
232
|
const total = totals[i];
|
|
236
|
-
|
|
237
|
-
const segs = [
|
|
238
|
-
{ key: 'input', val: p.input },
|
|
239
|
-
{ key: 'output', val: p.output },
|
|
240
|
-
{ key: 'cacheRead', val: p.cacheRead },
|
|
241
|
-
{ key: 'cacheCreation', val: p.cacheCreation },
|
|
242
|
-
];
|
|
243
|
-
let segsHTML = '';
|
|
244
|
-
for (const s of segs) {
|
|
245
|
-
const segPct = maxTotal > 0 ? (s.val / maxTotal * 100) : 0;
|
|
246
|
-
const segH = maxTotal > 0 ? Math.max(segPct, 0.5) : 0;
|
|
247
|
-
segsHTML += `<div class="tp-stack-seg" style="height:${segH}%;background:${STACK_COLORS[s.key]}"></div>`;
|
|
248
|
-
}
|
|
249
|
-
|
|
233
|
+
const pct = maxTotal > 0 ? (total / maxTotal * 100) : 0;
|
|
250
234
|
const label = type === 'weekly' ? k.slice(5) : k.slice(2);
|
|
251
|
-
const tip = `${k}:
|
|
252
|
-
barsHTML += `<div class="tp-stack-wrap"><div class="tp-stack-bar-group" data-tip="${esc(tip)}"
|
|
235
|
+
const tip = `${k}: ${fmtTS(total)} tokens`;
|
|
236
|
+
barsHTML += `<div class="tp-stack-wrap"><div class="tp-stack-bar-area"><div class="tp-stack-bar-group" data-tip="${esc(tip)}"><div class="tp-stack-seg" style="height:${Math.max(pct, 1)}%;background:#58a6ff"></div></div></div><span class="tp-stack-label">${esc(label)}</span></div>`;
|
|
253
237
|
}
|
|
254
238
|
|
|
255
239
|
let gridHTML = '<div class="tp-stack-grid">';
|
|
@@ -260,13 +244,7 @@ function buildStackedChart(dailyKeys, daily, type) {
|
|
|
260
244
|
gridHTML += `<span style="font-size:9px;color:var(--dim);align-self:flex-end">0</span>`;
|
|
261
245
|
gridHTML += '</div>';
|
|
262
246
|
|
|
263
|
-
|
|
264
|
-
for (const [key, color] of Object.entries(STACK_COLORS)) {
|
|
265
|
-
legendHTML += `<span style="display:inline-flex;align-items:center;gap:4px"><span style="width:8px;height:8px;border-radius:2px;background:${color};display:inline-block"></span>${STACK_LABELS[key]}</span>`;
|
|
266
|
-
}
|
|
267
|
-
legendHTML += '</div>';
|
|
268
|
-
|
|
269
|
-
return `<div class="tp-chart-title">${title}</div>${legendHTML}<div class="tp-stack-bars">${gridHTML}${barsHTML}</div>`;
|
|
247
|
+
return `<div class="tp-chart-title">${title}</div><div class="tp-stack-bars">${gridHTML}${barsHTML}</div>`;
|
|
270
248
|
}
|
|
271
249
|
|
|
272
250
|
// ── Model proportion doughnut chart ──
|
|
@@ -332,10 +310,10 @@ function buildHourlyChart(hourly) {
|
|
|
332
310
|
for (let h = 0; h < 24; h++) {
|
|
333
311
|
const calls = hourly[h] || 0;
|
|
334
312
|
const pct = maxCalls > 0 ? (calls / maxCalls * 100) : 0;
|
|
335
|
-
const color = pct < 30 ? 'rgba(
|
|
336
|
-
const borderColor = '
|
|
313
|
+
const color = pct < 30 ? 'rgba(16,185,129,0.25)' : pct < 60 ? 'rgba(16,185,129,0.5)' : pct < 80 ? 'rgba(16,185,129,0.75)' : 'rgb(16,185,129)';
|
|
314
|
+
const borderColor = 'rgb(16,185,129)';
|
|
337
315
|
const tip = `${h}:00 · ${calls.toLocaleString()} 次调用 calls`;
|
|
338
|
-
barsHTML += `<div class="tp-hourly-bar-wrap"><div class="tp-hourly-bar" style="height:${Math.max(pct, 2)}%;background:${color};border:1px solid ${borderColor}" data-tip="${esc(tip)}"></div><span class="tp-hourly-label">${h}</span></div>`;
|
|
316
|
+
barsHTML += `<div class="tp-hourly-bar-wrap"><div class="tp-hourly-bar-area"><div class="tp-hourly-bar" style="height:${Math.max(pct, 2)}%;background:${color};border:1px solid ${borderColor}" data-tip="${esc(tip)}"></div></div><span class="tp-hourly-label">${h}</span></div>`;
|
|
339
317
|
}
|
|
340
318
|
|
|
341
319
|
let gridHTML = '<div class="tp-hourly-grid">';
|
|
@@ -366,7 +344,7 @@ function renderPeriodTable(keys, data, type) {
|
|
|
366
344
|
const k = sorted[i];
|
|
367
345
|
const d = data[k];
|
|
368
346
|
const total = d.input + d.output + d.cacheCreation + d.cacheRead;
|
|
369
|
-
const label = type === 'daily' ? k : k + '<br><small style="color:var(--dim)">' + esc(d.dateRange || '') + '</small>';
|
|
347
|
+
const label = type === 'daily' ? k : type === 'monthly' ? k : k + '<br><small style="color:var(--dim)">' + esc(d.dateRange || '') + '</small>';
|
|
370
348
|
const modelsHtml = Object.entries(d.models).sort((a, b) => {
|
|
371
349
|
const sA = a[1].input + a[1].output + a[1].cacheCreation + a[1].cacheRead;
|
|
372
350
|
const sB = b[1].input + b[1].output + b[1].cacheCreation + b[1].cacheRead;
|
package/src/server/server.js
CHANGED
|
@@ -7,12 +7,35 @@ var os = require('os');
|
|
|
7
7
|
var cp = require('child_process');
|
|
8
8
|
var readline = require('readline');
|
|
9
9
|
var { WebSocketServer } = require('ws');
|
|
10
|
+
var { compareVersions } = require('../cli-helpers');
|
|
10
11
|
var { Watcher, listSessions, listActiveSessions } = require('../watcher/watcher');
|
|
11
12
|
var { setDebugAll, contextWindowFor } = require('../parser/parser');
|
|
12
13
|
var { fullScanTokenUsage } = require('../scanner/scanner');
|
|
13
14
|
|
|
14
15
|
var PACKAGE_VERSION = require('../../package.json').version;
|
|
15
16
|
|
|
17
|
+
function fetchLatestVersion() {
|
|
18
|
+
return new Promise(function(resolve, reject) {
|
|
19
|
+
var opts = {
|
|
20
|
+
hostname: 'registry.npmjs.org',
|
|
21
|
+
path: '/claude-code-watch/latest',
|
|
22
|
+
timeout: 5000,
|
|
23
|
+
};
|
|
24
|
+
var req = require('https').get(opts, function(res) {
|
|
25
|
+
if (res.statusCode !== 200) { reject(new Error('HTTP ' + res.statusCode)); return; }
|
|
26
|
+
var data = '';
|
|
27
|
+
res.on('data', function(chunk) { data += chunk; });
|
|
28
|
+
res.on('end', function() {
|
|
29
|
+
try { resolve(JSON.parse(data).version); }
|
|
30
|
+
catch (err) { reject(err); }
|
|
31
|
+
});
|
|
32
|
+
});
|
|
33
|
+
req.on('error', reject);
|
|
34
|
+
req.on('timeout', function() { req.destroy(); reject(new Error('timeout')); });
|
|
35
|
+
req.end();
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
|
|
16
39
|
var MIME = {
|
|
17
40
|
'.html': 'text/html; charset=utf-8',
|
|
18
41
|
'.css': 'text/css; charset=utf-8',
|
|
@@ -54,6 +77,8 @@ class DashboardServer {
|
|
|
54
77
|
this.wss = null;
|
|
55
78
|
this._heartbeatTimer = null;
|
|
56
79
|
this._allowedPrefix = null;
|
|
80
|
+
this.latestVersion = null;
|
|
81
|
+
this._versionCheckTimer = null;
|
|
57
82
|
|
|
58
83
|
setDebugAll(options.debugAll || false);
|
|
59
84
|
this.debugAll = options.debugAll || false;
|
|
@@ -446,7 +471,17 @@ class DashboardServer {
|
|
|
446
471
|
}
|
|
447
472
|
|
|
448
473
|
sendConfig(ws) {
|
|
449
|
-
this.send(ws, 'config', { collapseAfter: this.collapseAfterMs, version: PACKAGE_VERSION });
|
|
474
|
+
this.send(ws, 'config', { collapseAfter: this.collapseAfterMs, version: PACKAGE_VERSION, latestVersion: this.latestVersion });
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
_checkLatestVersion() {
|
|
478
|
+
fetchLatestVersion().then((latest) => {
|
|
479
|
+
if (compareVersions(latest, PACKAGE_VERSION) > 0) {
|
|
480
|
+
this.latestVersion = latest;
|
|
481
|
+
// Notify all connected clients
|
|
482
|
+
this.broadcast('config', { collapseAfter: this.collapseAfterMs, version: PACKAGE_VERSION, latestVersion: latest });
|
|
483
|
+
}
|
|
484
|
+
}).catch(() => { /* network unavailable, skip */ });
|
|
450
485
|
}
|
|
451
486
|
|
|
452
487
|
setupWatcher(watcherOpts) {
|
|
@@ -670,6 +705,10 @@ class DashboardServer {
|
|
|
670
705
|
this._contextCleanupTimer = setInterval(() => this.cleanupContextMap(), CONTEXT_STALE_MS);
|
|
671
706
|
this._heartbeatTimer = setInterval(() => this.broadcast('heartbeat', null), 30000);
|
|
672
707
|
|
|
708
|
+
// Check for latest version on startup and periodically (every hour)
|
|
709
|
+
this._checkLatestVersion();
|
|
710
|
+
this._versionCheckTimer = setInterval(() => this._checkLatestVersion(), 60 * 60 * 1000);
|
|
711
|
+
|
|
673
712
|
// Start listening and wait for server to be ready before opening browser
|
|
674
713
|
await new Promise((resolve) => {
|
|
675
714
|
this.server.listen(this.port, this.host, () => {
|
|
@@ -702,6 +741,7 @@ class DashboardServer {
|
|
|
702
741
|
stop() {
|
|
703
742
|
if (this._contextCleanupTimer) clearInterval(this._contextCleanupTimer);
|
|
704
743
|
if (this._heartbeatTimer) clearInterval(this._heartbeatTimer);
|
|
744
|
+
if (this._versionCheckTimer) clearInterval(this._versionCheckTimer);
|
|
705
745
|
if (this._flushTimer) {
|
|
706
746
|
clearTimeout(this._flushTimer);
|
|
707
747
|
this._flushTimer = null;
|