claude-code-watch 0.2.0 → 0.2.1
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 +43 -17
- package/public/index.html +23 -25
- package/public/js/app.js +11 -1
- package/public/js/token.js +8 -30
- 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 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; }
|
|
@@ -267,32 +266,34 @@ body {
|
|
|
267
266
|
.tp-chart-title { font-size: 12px; color: var(--dim); font-weight: 600; text-transform: uppercase; margin-bottom: 10px; }
|
|
268
267
|
|
|
269
268
|
/* ── 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-
|
|
269
|
+
.tp-stack-bars { display: flex; align-items: stretch; gap: 4px; height: 240px; position: relative; }
|
|
270
|
+
.tp-stack-wrap { flex: 1; display: flex; flex-direction: column; min-width: 0; }
|
|
271
|
+
.tp-stack-bar-area { flex: 1; display: flex; align-items: stretch; }
|
|
272
|
+
.tp-stack-seg { transition: all 0.15s; cursor: pointer; min-height: 1px; width: 100%; }
|
|
273
273
|
.tp-stack-seg:hover { filter: brightness(1.3); }
|
|
274
274
|
.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:
|
|
275
|
+
.tp-stack-bar-group { display: flex; flex-direction: column; justify-content: flex-end; position: relative; width: 100%; cursor: pointer; }
|
|
276
|
+
.tp-stack-label { font-size: 11px; color: var(--text); text-align: center; padding-top: 6px; white-space: nowrap; }
|
|
277
277
|
.tp-stack-grid { position: absolute; inset: 0 0 22px 0; display: flex; flex-direction: column; justify-content: space-between; pointer-events: none; }
|
|
278
278
|
.tp-stack-grid-line { width: 100%; border-top: 1px dashed var(--border); opacity: 0.3; }
|
|
279
279
|
|
|
280
280
|
/* ── 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 {
|
|
281
|
+
.tp-hourly-bars { display: flex; align-items: stretch; gap: 2px; height: 200px; position: relative; }
|
|
282
|
+
.tp-hourly-bar-wrap { flex: 1; display: flex; flex-direction: column; min-width: 0; }
|
|
283
|
+
.tp-hourly-bar-area { flex: 1; display: flex; flex-direction: column; align-items: stretch; justify-content: flex-end; }
|
|
284
|
+
.tp-hourly-bar { border-radius: 3px 3px 0 0; transition: all 0.15s; cursor: pointer; min-height: 2px; width: 100%; }
|
|
284
285
|
.tp-hourly-bar:hover { filter: brightness(1.3); }
|
|
285
286
|
.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:
|
|
287
|
+
.tp-hourly-label { font-size: 11px; color: var(--text); text-align: center; padding-top: 6px; }
|
|
287
288
|
.tp-hourly-grid { position: absolute; inset: 0 0 22px 0; display: flex; flex-direction: column; justify-content: space-between; pointer-events: none; }
|
|
288
289
|
|
|
289
290
|
/* ── Model proportion doughnut ── */
|
|
290
291
|
.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:
|
|
292
|
+
.tp-pie-ring { position: relative; width: 220px; height: 220px; border-radius: 50%; flex-shrink: 0; }
|
|
293
|
+
.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
294
|
.tp-pie-hole-v { font-size: 14px; font-weight: 700; color: var(--white); font-family: monospace; }
|
|
294
295
|
.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:
|
|
296
|
+
.tp-pie-legend { display: flex; flex-direction: column; gap: 4px; overflow-y: auto; max-height: 220px; }
|
|
296
297
|
.tp-pie-legend-item { display: flex; align-items: center; gap: 6px; font-size: 11px; }
|
|
297
298
|
.tp-pie-dot { width: 8px; height: 8px; border-radius: 50%; flex-shrink: 0; }
|
|
298
299
|
.tp-pie-name { color: var(--text); flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
@@ -348,6 +349,31 @@ body {
|
|
|
348
349
|
font-size: 11px; flex-shrink: 0; flex-wrap: wrap;
|
|
349
350
|
}
|
|
350
351
|
#footer .sep { color: var(--dim); margin: 0 2px; }
|
|
352
|
+
.version-update-badge {
|
|
353
|
+
display: inline-flex; align-items: center; gap: 4px;
|
|
354
|
+
color: var(--green, #3fb950); font-weight: 500;
|
|
355
|
+
text-decoration: none; padding: 1px 6px;
|
|
356
|
+
border-radius: 8px; border: 1px solid var(--green, #3fb950);
|
|
357
|
+
transition: background 0.2s, color 0.2s;
|
|
358
|
+
animation: version-badge-pulse 2s ease-in-out infinite;
|
|
359
|
+
}
|
|
360
|
+
.version-update-badge:hover {
|
|
361
|
+
background: var(--green, #3fb950); color: var(--bg, #0d1117);
|
|
362
|
+
}
|
|
363
|
+
.version-update-dot {
|
|
364
|
+
width: 6px; height: 6px; border-radius: 50%;
|
|
365
|
+
background: var(--green, #3fb950);
|
|
366
|
+
display: inline-block;
|
|
367
|
+
animation: version-dot-blink 1.5s ease-in-out infinite;
|
|
368
|
+
}
|
|
369
|
+
@keyframes version-badge-pulse {
|
|
370
|
+
0%, 100% { opacity: 1; }
|
|
371
|
+
50% { opacity: 0.7; }
|
|
372
|
+
}
|
|
373
|
+
@keyframes version-dot-blink {
|
|
374
|
+
0%, 100% { opacity: 1; }
|
|
375
|
+
50% { opacity: 0.3; }
|
|
376
|
+
}
|
|
351
377
|
|
|
352
378
|
/* ── Scrollbar ── */
|
|
353
379
|
::-webkit-scrollbar { width: 6px; height: 6px; }
|
|
@@ -467,8 +493,8 @@ body {
|
|
|
467
493
|
}
|
|
468
494
|
|
|
469
495
|
@media (max-width: 768px) {
|
|
470
|
-
.tp-top {
|
|
471
|
-
.tp-
|
|
496
|
+
.tp-top { grid-template-columns: 1fr; }
|
|
497
|
+
.tp-row2 { grid-template-columns: 1fr; }
|
|
472
498
|
.tp-charts-row { grid-template-columns: 1fr; }
|
|
473
499
|
.tp-pie-wrap { flex-direction: column; align-items: center; }
|
|
474
500
|
}
|
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-trend-card"></div>
|
|
79
|
+
<div class="tp-box" id="tp-heatmap-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/token.js
CHANGED
|
@@ -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">';
|
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;
|