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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-code-watch",
3
- "version": "0.2.0",
3
+ "version": "0.2.2",
4
4
  "description": "Web-based real-time monitor for Claude Code.",
5
5
  "main": "./src/server/server.js",
6
6
  "bin": {
@@ -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: flex; gap: 16px; }
207
- .tp-left { width: 260px; flex-shrink: 0; display: flex; flex-direction: column; gap: 12px; }
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: flex-end; }
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: flex-end; gap: 3px; height: 140px; position: relative; padding-bottom: 20px; }
241
- .tp-trend-bar-wrap { flex: 1; display: flex; flex-direction: column; position: relative; min-width: 0; }
242
- .tp-trend-bar { position: relative; border-radius: 3px 3px 0 0; transition: all 0.15s; cursor: pointer; min-height: 4px; }
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: 9px; color: var(--dim); text-align: center; position: absolute; bottom: 0; width: 100%; white-space: nowrap; overflow: hidden; }
246
- .tp-trend-grid-lines { position: absolute; inset: 0 0 20px 0; display: flex; flex-direction: column; justify-content: space-between; pointer-events: none; }
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: flex-end; gap: 4px; height: 160px; position: relative; padding-bottom: 22px; }
271
- .tp-stack-wrap { flex: 1; display: flex; flex-direction: column; justify-content: flex-end; position: relative; min-width: 0; height: 100%; }
272
- .tp-stack-seg { transition: all 0.15s; cursor: pointer; min-height: 1px; }
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; height: 100%; cursor: pointer; }
276
- .tp-stack-label { font-size: 9px; color: var(--dim); text-align: center; position: absolute; bottom: 0; width: 100%; white-space: nowrap; overflow: hidden; }
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: flex-end; gap: 2px; height: 140px; position: relative; padding-bottom: 22px; }
282
- .tp-hourly-bar-wrap { flex: 1; display: flex; flex-direction: column; justify-content: flex-end; position: relative; min-width: 0; height: 100%; }
283
- .tp-hourly-bar { border-radius: 3px 3px 0 0; transition: all 0.15s; cursor: pointer; min-height: 2px; }
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: 8px; color: var(--dim); text-align: center; position: absolute; bottom: 0; width: 100%; }
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: 180px; height: 180px; border-radius: 50%; flex-shrink: 0; }
292
- .tp-pie-hole { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); width: 90px; height: 90px; border-radius: 50%; background: var(--bg2); display: flex; align-items: center; justify-content: center; flex-direction: column; }
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: 180px; }
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 { flex-direction: column; }
471
- .tp-left { width: 100%; }
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-left">
68
- <div class="tp-box" id="tp-total-card"></div>
69
- <div class="tp-box" id="tp-stats-grid"></div>
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
- vEl.innerHTML = `${v ? v + ' · ' : ''}<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>`;
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();
@@ -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 += ' ~ ' + k;
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;
@@ -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
- // ── Stacked bar chart for weekly/monthly token consumption ──
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}: 总计 ${fmtTS(total)} | 输入 ${fmtTS(p.input)} 输出 ${fmtTS(p.output)} 缓存读 ${fmtTS(p.cacheRead)} 缓存写 ${fmtTS(p.cacheCreation)}`;
252
- barsHTML += `<div class="tp-stack-wrap"><div class="tp-stack-bar-group" data-tip="${esc(tip)}">${segsHTML}</div><span class="tp-stack-label">${esc(label)}</span></div>`;
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
- let legendHTML = '<div style="display:flex;gap:12px;margin-bottom:6px;font-size:10px;color:var(--dim)">';
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(247,120,186,0.25)' : pct < 60 ? 'rgba(247,120,186,0.5)' : pct < 80 ? 'rgba(247,120,186,0.75)' : 'rgba(247,120,186,1)';
336
- const borderColor = '#f778ba';
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;
@@ -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;