ccus-cli 0.1.2 → 0.1.4

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.
@@ -6,6 +6,13 @@ exports.buildAggregateDashboardHtml = buildAggregateDashboardHtml;
6
6
  const time_1 = require("./time");
7
7
  /** 折线 / 图例统一的调色板,保证同一个人在不同图表里颜色一致。 */
8
8
  const CHART_PALETTE = ["#5eead4", "#f59e0b", "#a855f7", "#22c55e", "#f87171", "#60a5fa", "#fbbf24", "#34d399"];
9
+ /**
10
+ * 7d 虚线专用的配套色板,和 CHART_PALETTE 同序号但色相强反差。
11
+ *
12
+ * 5h / 7d 同图叠加时,5h 用 CHART_PALETTE 实线、7d 用这套对比色虚线,
13
+ * 避免两条线颜色太接近看不清;人物对应关系靠图例里的双色点维持。
14
+ */
15
+ const SEVEN_DAY_PALETTE = ["#fb7185", "#38bdf8", "#facc15", "#e879f9", "#4ade80", "#fb923c", "#818cf8", "#f472b6"];
9
16
  /** 所有插入到 HTML 的文本字段都要先转义,避免本地页面被注入。 */
10
17
  function escapeHtml(value) {
11
18
  return value
@@ -119,16 +126,16 @@ function statValue(value, suffix = "%") {
119
126
  */
120
127
  function renderDailyUserRequestChart(people, dailyIndex, dateAxis) {
121
128
  if (people.length === 0 || dateAxis.length === 0) {
122
- return `
123
- <section class="panel chart-panel">
124
- <div class="panel-header">
125
- <div>
126
- <p class="eyebrow">Daily User Requests</p>
127
- <h2>每日用户请求数对比</h2>
128
- </div>
129
- <p class="muted">没有可绘制的日度数据。</p>
130
- </div>
131
- </section>
129
+ return `
130
+ <section class="panel chart-panel">
131
+ <div class="panel-header">
132
+ <div>
133
+ <p class="eyebrow">Daily User Requests</p>
134
+ <h2>每日用户请求数对比</h2>
135
+ </div>
136
+ <p class="muted">没有可绘制的日度数据。</p>
137
+ </div>
138
+ </section>
132
139
  `;
133
140
  }
134
141
  const width = 920;
@@ -177,22 +184,22 @@ function renderDailyUserRequestChart(people, dailyIndex, dateAxis) {
177
184
  return `<span class="legend-chip"><span class="legend-dot" style="background:${color}"></span>${escapeHtml(series.person.personKey)}</span>`;
178
185
  })
179
186
  .join("");
180
- return `
181
- <section class="panel chart-panel">
182
- <div class="panel-header">
183
- <div>
184
- <p class="eyebrow">Daily User Requests</p>
185
- <h2>每日用户请求数对比</h2>
186
- </div>
187
- <p class="muted">基于每人 daily 汇总中的 userMessageCount(已剔除 tool_result 工具回填;sidechain 子 agent 提示保留)。</p>
188
- </div>
189
- <div class="legend">${legend}</div>
190
- <svg viewBox="0 0 ${width} ${height}" class="chart" role="img" aria-label="每日用户请求数对比">
191
- ${ticks}
192
- ${xLabels}
193
- ${seriesPaths}
194
- </svg>
195
- </section>
187
+ return `
188
+ <section class="panel chart-panel">
189
+ <div class="panel-header">
190
+ <div>
191
+ <p class="eyebrow">Daily User Requests</p>
192
+ <h2>每日用户请求数对比</h2>
193
+ </div>
194
+ <p class="muted">基于每人 daily 汇总中的 userMessageCount(已剔除 tool_result 工具回填;sidechain 子 agent 提示保留)。</p>
195
+ </div>
196
+ <div class="legend">${legend}</div>
197
+ <svg viewBox="0 0 ${width} ${height}" class="chart" role="img" aria-label="每日用户请求数对比">
198
+ ${ticks}
199
+ ${xLabels}
200
+ ${seriesPaths}
201
+ </svg>
202
+ </section>
196
203
  `;
197
204
  }
198
205
  /**
@@ -206,16 +213,16 @@ function renderSevenDayPeakChart(people) {
206
213
  .filter((person) => person.sevenDayPeakUsagePct !== null)
207
214
  .sort((left, right) => (right.sevenDayPeakUsagePct ?? 0) - (left.sevenDayPeakUsagePct ?? 0));
208
215
  if (ranked.length === 0) {
209
- return `
210
- <section class="panel chart-panel">
211
- <div class="panel-header">
212
- <div>
213
- <p class="eyebrow">Weekly Peak Usage</p>
214
- <h2>周使用量峰值对比</h2>
215
- </div>
216
- <p class="muted">还没有 7 天额度使用率样本。</p>
217
- </div>
218
- </section>
216
+ return `
217
+ <section class="panel chart-panel">
218
+ <div class="panel-header">
219
+ <div>
220
+ <p class="eyebrow">Weekly Peak Usage</p>
221
+ <h2>周使用量峰值对比</h2>
222
+ </div>
223
+ <p class="muted">还没有 7 天额度使用率样本。</p>
224
+ </div>
225
+ </section>
219
226
  `;
220
227
  }
221
228
  const rowHeight = 38;
@@ -236,28 +243,28 @@ function renderSevenDayPeakChart(people) {
236
243
  const barY = rowTop + (rowHeight - barHeight) / 2;
237
244
  const barWidth = Math.max(2, (pct / maxValue) * trackWidth);
238
245
  const textY = barY + barHeight / 2 + 4;
239
- return `
240
- <g>
241
- <text x="8" y="${textY}" class="bar-label">${escapeHtml(person.personKey)}</text>
242
- <rect x="${trackX}" y="${barY}" width="${trackWidth}" height="${barHeight}" rx="6" class="bar-track" />
243
- <rect x="${trackX}" y="${barY}" width="${barWidth.toFixed(2)}" height="${barHeight}" rx="6" class="bar-fill" />
244
- <text x="${trackX + trackWidth + 10}" y="${textY}" class="bar-value">${pct.toFixed(1)}%</text>
246
+ return `
247
+ <g>
248
+ <text x="8" y="${textY}" class="bar-label">${escapeHtml(person.personKey)}</text>
249
+ <rect x="${trackX}" y="${barY}" width="${trackWidth}" height="${barHeight}" rx="6" class="bar-track" />
250
+ <rect x="${trackX}" y="${barY}" width="${barWidth.toFixed(2)}" height="${barHeight}" rx="6" class="bar-fill" />
251
+ <text x="${trackX + trackWidth + 10}" y="${textY}" class="bar-value">${pct.toFixed(1)}%</text>
245
252
  </g>`;
246
253
  })
247
254
  .join("");
248
- return `
249
- <section class="panel chart-panel">
250
- <div class="panel-header">
251
- <div>
252
- <p class="eyebrow">Weekly Peak Usage</p>
253
- <h2>周使用量峰值对比</h2>
254
- </div>
255
- <p class="muted">每个人 7 天额度使用率(sevenDayPeakUsagePct)的峰值,条越长越接近用满周额度。</p>
256
- </div>
257
- <svg viewBox="0 0 ${width} ${height}" class="chart" role="img" aria-label="周使用量峰值对比">
258
- ${bars}
259
- </svg>
260
- </section>
255
+ return `
256
+ <section class="panel chart-panel">
257
+ <div class="panel-header">
258
+ <div>
259
+ <p class="eyebrow">Weekly Peak Usage</p>
260
+ <h2>周使用量峰值对比</h2>
261
+ </div>
262
+ <p class="muted">每个人 7 天额度使用率(sevenDayPeakUsagePct)的峰值,条越长越接近用满周额度。</p>
263
+ </div>
264
+ <svg viewBox="0 0 ${width} ${height}" class="chart" role="img" aria-label="周使用量峰值对比">
265
+ ${bars}
266
+ </svg>
267
+ </section>
261
268
  `;
262
269
  }
263
270
  /** 把时间戳格式化成图表 X 轴用的本地「MM-DD HH:mm」短标签。 */
@@ -270,35 +277,41 @@ function formatTickTime(t) {
270
277
  return `${mm}-${dd} ${hh}:${mi}`;
271
278
  }
272
279
  /**
273
- * 用事件级 detail 行画出 5h 额度使用率(usagePct)的详细曲线。
280
+ * 用事件级 detail 行画出 5h 额度与 7d 周额度使用率的详细曲线。
274
281
  *
275
282
  * 与按天聚合的图不同,这里直接用每条 statusline 采样的真实时间戳,粒度最细,
276
- * 能看出每个人 5 小时额度在一天里的爬升与重置节奏。X 轴是连续时间,Y 轴是百分比。
283
+ * 能看出每个人 5 小时额度在一天里的爬升与重置节奏,同时叠加 7 天周额度的走势。
284
+ * X 轴是连续时间,Y 轴是百分比;同一个人 5h 用实线、7d 用虚线,共用颜色和 Y 轴。
277
285
  */
278
286
  function renderFiveHourUsageChart(people, detailRows) {
279
287
  const series = people
280
- .map((person, index) => ({
281
- person,
282
- index,
283
- points: detailRows
284
- .filter((row) => row.personKey === person.personKey && row.usagePct !== null)
285
- .map((row) => ({ t: new Date(row.timestamp).getTime(), v: row.usagePct }))
288
+ .map((person, index) => {
289
+ const personRows = detailRows
290
+ .filter((row) => row.personKey === person.personKey)
291
+ .map((row) => ({ t: new Date(row.timestamp).getTime(), five: row.usagePct, seven: row.sevenDayUsagePct }))
286
292
  .filter((point) => Number.isFinite(point.t))
287
- .sort((left, right) => left.t - right.t),
288
- }))
289
- .filter((entry) => entry.points.length > 0);
290
- const allPoints = series.flatMap((entry) => entry.points);
293
+ .sort((left, right) => left.t - right.t);
294
+ const fivePoints = personRows
295
+ .filter((point) => point.five !== null)
296
+ .map((point) => ({ t: point.t, v: point.five }));
297
+ const sevenPoints = personRows
298
+ .filter((point) => point.seven !== null)
299
+ .map((point) => ({ t: point.t, v: point.seven }));
300
+ return { person, index, fivePoints, sevenPoints };
301
+ })
302
+ .filter((entry) => entry.fivePoints.length > 0 || entry.sevenPoints.length > 0);
303
+ const allPoints = series.flatMap((entry) => [...entry.fivePoints, ...entry.sevenPoints]);
291
304
  if (allPoints.length === 0) {
292
- return `
293
- <section class="panel chart-panel">
294
- <div class="panel-header">
295
- <div>
296
- <p class="eyebrow">5h Usage Detail</p>
297
- <h2>5h 使用率详细曲线</h2>
298
- </div>
299
- <p class="muted">还没有带 5h 使用率的 statusline 采样。</p>
300
- </div>
301
- </section>
305
+ return `
306
+ <section class="panel chart-panel">
307
+ <div class="panel-header">
308
+ <div>
309
+ <p class="eyebrow">Usage Detail</p>
310
+ <h2>5h / 7d 使用率详细曲线</h2>
311
+ </div>
312
+ <p class="muted">还没有带使用率的 statusline 采样。</p>
313
+ </div>
314
+ </section>
302
315
  `;
303
316
  }
304
317
  const width = 920;
@@ -327,102 +340,112 @@ function renderFiveHourUsageChart(people, detailRows) {
327
340
  return `<g><line x1="${x}" x2="${x}" y1="${height - paddingY}" y2="${height - paddingY + 6}" class="chart-axis-line" /><text x="${x}" y="${height - 4}" text-anchor="middle" class="chart-axis">${escapeHtml(formatTickTime(t))}</text></g>`;
328
341
  })
329
342
  .join("");
343
+ const pathFor = (points) => points.map((point, pointIndex) => `${pointIndex === 0 ? "M" : "L"}${xFor(point.t).toFixed(2)} ${yFor(point.v).toFixed(2)}`).join(" ");
330
344
  const seriesPaths = series
331
345
  .map((entry) => {
332
346
  const color = CHART_PALETTE[entry.index % CHART_PALETTE.length];
333
- const path = entry.points
334
- .map((point, pointIndex) => `${pointIndex === 0 ? "M" : "L"}${xFor(point.t).toFixed(2)} ${yFor(point.v).toFixed(2)}`)
335
- .join(" ");
336
- return `<g><path d="${path}" fill="none" stroke="${color}" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><title>${escapeHtml(entry.person.personKey)}</title></path></g>`;
347
+ const sevenColor = SEVEN_DAY_PALETTE[entry.index % SEVEN_DAY_PALETTE.length];
348
+ const fivePath = entry.fivePoints.length > 0
349
+ ? `<path d="${pathFor(entry.fivePoints)}" fill="none" stroke="${color}" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><title>${escapeHtml(entry.person.personKey)} · 5h</title></path>`
350
+ : "";
351
+ const sevenPath = entry.sevenPoints.length > 0
352
+ ? `<path d="${pathFor(entry.sevenPoints)}" fill="none" stroke="${sevenColor}" stroke-width="2" stroke-dasharray="5 4" stroke-linecap="round" stroke-linejoin="round"><title>${escapeHtml(entry.person.personKey)} · 7d</title></path>`
353
+ : "";
354
+ return `<g>${fivePath}${sevenPath}</g>`;
337
355
  })
338
356
  .join("");
339
357
  const legend = series
340
358
  .map((entry) => {
341
359
  const color = CHART_PALETTE[entry.index % CHART_PALETTE.length];
342
- return `<span class="legend-chip"><span class="legend-dot" style="background:${color}"></span>${escapeHtml(entry.person.personKey)}</span>`;
360
+ const sevenColor = SEVEN_DAY_PALETTE[entry.index % SEVEN_DAY_PALETTE.length];
361
+ return `<span class="legend-chip"><span class="legend-dot" style="background:${color}"></span><span class="legend-dot" style="background:${sevenColor}"></span>${escapeHtml(entry.person.personKey)}</span>`;
343
362
  })
344
363
  .join("");
345
- return `
346
- <section class="panel chart-panel">
347
- <div class="panel-header">
348
- <div>
349
- <p class="eyebrow">5h Usage Detail</p>
350
- <h2>5h 使用率详细曲线</h2>
351
- </div>
352
- <p class="muted">每条 statusline 采样的 5 小时额度使用率(usagePct),按真实时间戳绘制。</p>
353
- </div>
354
- <div class="legend">${legend}</div>
355
- <svg viewBox="0 0 ${width} ${height}" class="chart" role="img" aria-label="5h 使用率详细曲线">
356
- ${ticks}
357
- ${xLabels}
358
- ${seriesPaths}
359
- </svg>
360
- </section>
364
+ return `
365
+ <section class="panel chart-panel">
366
+ <div class="panel-header">
367
+ <div>
368
+ <p class="eyebrow">Usage Detail</p>
369
+ <h2>5h / 7d 使用率详细曲线</h2>
370
+ </div>
371
+ <p class="muted">每条 statusline 采样的 5 小时额度(实线)与 7 天周额度(对比色虚线)使用率,按真实时间戳绘制、共用 Y 轴。</p>
372
+ </div>
373
+ <div class="legend">
374
+ ${legend}
375
+ <span class="legend-chip"><span class="legend-line legend-line-solid"></span>5h 使用率(实线)</span>
376
+ <span class="legend-chip"><span class="legend-line legend-line-dashed"></span>7d 周使用量(对比色虚线)</span>
377
+ </div>
378
+ <svg viewBox="0 0 ${width} ${height}" class="chart" role="img" aria-label="5h / 7d 使用率详细曲线">
379
+ ${ticks}
380
+ ${xLabels}
381
+ ${seriesPaths}
382
+ </svg>
383
+ </section>
361
384
  `;
362
385
  }
363
386
  /** 按人渲染汇总卡片,给出请求 / 消息 / token / usage 几个核心指标。 */
364
387
  function renderPeopleLeaderboard(people) {
365
388
  if (people.length === 0) {
366
- return `
367
- <section class="panel table-panel">
368
- <div class="panel-header">
369
- <div>
370
- <p class="eyebrow">People</p>
371
- <h2>多人对比</h2>
372
- </div>
373
- <p class="muted">还没有匹配的导出 bundle。</p>
374
- </div>
375
- </section>
389
+ return `
390
+ <section class="panel table-panel">
391
+ <div class="panel-header">
392
+ <div>
393
+ <p class="eyebrow">People</p>
394
+ <h2>多人对比</h2>
395
+ </div>
396
+ <p class="muted">还没有匹配的导出 bundle。</p>
397
+ </div>
398
+ </section>
376
399
  `;
377
400
  }
378
401
  const rows = people
379
- .map((person, index) => `
380
- <tr>
381
- <td class="rank">${index + 1}</td>
382
- <td><strong>${escapeHtml(person.personKey)}</strong></td>
383
- <td>${formatNumber(person.userMessageCount)}</td>
384
- <td>${formatTokensM(person.inputTokens)}</td>
385
- <td>${formatTokensM(person.outputTokens)}</td>
386
- <td>${formatTokensM(person.cacheReadInputTokens)}</td>
387
- <td>${escapeHtml(statValue(person.fiveHourPeakUsagePct))}</td>
388
- <td>${escapeHtml(statValue(person.fiveHourLatestUsagePct))}</td>
389
- <td>${escapeHtml(statValue(person.sevenDayPeakUsagePct))}</td>
390
- <td>${escapeHtml(statValue(person.sevenDayLatestUsagePct))}</td>
391
- <td>${person.activeDays}</td>
392
- <td class="muted-col">${formatNumber(person.apiRequestCount)}</td>
402
+ .map((person, index) => `
403
+ <tr>
404
+ <td class="rank">${index + 1}</td>
405
+ <td><strong>${escapeHtml(person.personKey)}</strong></td>
406
+ <td>${formatNumber(person.userMessageCount)}</td>
407
+ <td>${formatTokensM(person.inputTokens)}</td>
408
+ <td>${formatTokensM(person.outputTokens)}</td>
409
+ <td>${formatTokensM(person.cacheReadInputTokens)}</td>
410
+ <td>${escapeHtml(statValue(person.fiveHourPeakUsagePct))}</td>
411
+ <td>${escapeHtml(statValue(person.fiveHourLatestUsagePct))}</td>
412
+ <td>${escapeHtml(statValue(person.sevenDayPeakUsagePct))}</td>
413
+ <td>${escapeHtml(statValue(person.sevenDayLatestUsagePct))}</td>
414
+ <td>${person.activeDays}</td>
415
+ <td class="muted-col">${formatNumber(person.apiRequestCount)}</td>
393
416
  </tr>`)
394
417
  .join("");
395
- return `
396
- <section class="panel table-panel">
397
- <div class="panel-header">
398
- <div>
399
- <p class="eyebrow">People</p>
400
- <h2>多人对比</h2>
401
- </div>
402
- <p class="muted">按用户消息数降序排列,所有数字直接来自 daily/weekly 汇总。</p>
403
- </div>
404
- <div class="table-wrap">
405
- <table>
406
- <thead>
407
- <tr>
408
- <th>#</th>
409
- <th>personKey</th>
410
- <th>消息</th>
411
- <th>Input tokens</th>
412
- <th>Output tokens</th>
413
- <th>Cache read tokens</th>
414
- <th>5h Peak</th>
415
- <th>5h Latest</th>
416
- <th>7d Peak</th>
417
- <th>7d Latest</th>
418
- <th>活跃天数</th>
419
- <th class="muted-col">API 请求</th>
420
- </tr>
421
- </thead>
422
- <tbody>${rows}</tbody>
423
- </table>
424
- </div>
425
- </section>
418
+ return `
419
+ <section class="panel table-panel">
420
+ <div class="panel-header">
421
+ <div>
422
+ <p class="eyebrow">People</p>
423
+ <h2>多人对比</h2>
424
+ </div>
425
+ <p class="muted">按用户消息数降序排列,所有数字直接来自 daily/weekly 汇总。</p>
426
+ </div>
427
+ <div class="table-wrap">
428
+ <table>
429
+ <thead>
430
+ <tr>
431
+ <th>#</th>
432
+ <th>personKey</th>
433
+ <th>消息</th>
434
+ <th>Input tokens</th>
435
+ <th>Output tokens</th>
436
+ <th>Cache read tokens</th>
437
+ <th>5h Peak</th>
438
+ <th>5h Latest</th>
439
+ <th>7d Peak</th>
440
+ <th>7d Latest</th>
441
+ <th>活跃天数</th>
442
+ <th class="muted-col">API 请求</th>
443
+ </tr>
444
+ </thead>
445
+ <tbody>${rows}</tbody>
446
+ </table>
447
+ </div>
448
+ </section>
426
449
  `;
427
450
  }
428
451
  /** 把 daily 行按 personKey × date 排成一个矩阵表。 */
@@ -447,27 +470,27 @@ function renderDailyMatrix(people, dailyIndex, dateAxis) {
447
470
  return `<tr><th class="row-head">${escapeHtml(person.personKey)}</th>${cells}</tr>`;
448
471
  })
449
472
  .join("");
450
- return `
451
- <section class="panel table-panel">
452
- <div class="panel-header">
453
- <div>
454
- <p class="eyebrow">Daily Matrix</p>
455
- <h2>按天 × 人 矩阵</h2>
456
- </div>
457
- <p class="muted">单元格上方为用户消息数,下方为 API 请求数。</p>
458
- </div>
459
- <div class="table-wrap">
460
- <table class="matrix">
461
- <thead>
462
- <tr>
463
- <th class="row-head">personKey</th>
464
- ${headerCells}
465
- </tr>
466
- </thead>
467
- <tbody>${bodyRows}</tbody>
468
- </table>
469
- </div>
470
- </section>
473
+ return `
474
+ <section class="panel table-panel">
475
+ <div class="panel-header">
476
+ <div>
477
+ <p class="eyebrow">Daily Matrix</p>
478
+ <h2>按天 × 人 矩阵</h2>
479
+ </div>
480
+ <p class="muted">单元格上方为用户消息数,下方为 API 请求数。</p>
481
+ </div>
482
+ <div class="table-wrap">
483
+ <table class="matrix">
484
+ <thead>
485
+ <tr>
486
+ <th class="row-head">personKey</th>
487
+ ${headerCells}
488
+ </tr>
489
+ </thead>
490
+ <tbody>${bodyRows}</tbody>
491
+ </table>
492
+ </div>
493
+ </section>
471
494
  `;
472
495
  }
473
496
  /** 把 weekly 行也照搬出来,便于看每个人每个 ISO 周的总账。 */
@@ -482,51 +505,51 @@ function renderWeeklyTable(weeklyRows) {
482
505
  return left.personKey.localeCompare(right.personKey);
483
506
  });
484
507
  const rows = sorted
485
- .map((row) => `
486
- <tr>
487
- <td>${escapeHtml(row.week)}</td>
488
- <td>${escapeHtml(row.personKey)}</td>
489
- <td>${formatNumber(row.userMessageCount)}</td>
490
- <td>${formatTokensM(row.inputTokens)}</td>
491
- <td>${formatTokensM(row.outputTokens)}</td>
492
- <td>${formatTokensM(row.cacheReadInputTokens)}</td>
493
- <td>${escapeHtml(statValue(row.fiveHourPeakUsagePct))}</td>
494
- <td>${escapeHtml(statValue(row.fiveHourLatestUsagePct))}</td>
495
- <td>${escapeHtml(statValue(row.sevenDayPeakUsagePct))}</td>
496
- <td>${escapeHtml(statValue(row.sevenDayLatestUsagePct))}</td>
497
- <td class="muted-col">${formatNumber(row.apiRequestCount)}</td>
508
+ .map((row) => `
509
+ <tr>
510
+ <td>${escapeHtml(row.week)}</td>
511
+ <td>${escapeHtml(row.personKey)}</td>
512
+ <td>${formatNumber(row.userMessageCount)}</td>
513
+ <td>${formatTokensM(row.inputTokens)}</td>
514
+ <td>${formatTokensM(row.outputTokens)}</td>
515
+ <td>${formatTokensM(row.cacheReadInputTokens)}</td>
516
+ <td>${escapeHtml(statValue(row.fiveHourPeakUsagePct))}</td>
517
+ <td>${escapeHtml(statValue(row.fiveHourLatestUsagePct))}</td>
518
+ <td>${escapeHtml(statValue(row.sevenDayPeakUsagePct))}</td>
519
+ <td>${escapeHtml(statValue(row.sevenDayLatestUsagePct))}</td>
520
+ <td class="muted-col">${formatNumber(row.apiRequestCount)}</td>
498
521
  </tr>`)
499
522
  .join("");
500
- return `
501
- <section class="panel table-panel">
502
- <div class="panel-header">
503
- <div>
504
- <p class="eyebrow">Weekly Rollup</p>
505
- <h2>按周聚合</h2>
506
- </div>
507
- <p class="muted">直接来源于每个 bundle 的 weeklySummary。</p>
508
- </div>
509
- <div class="table-wrap">
510
- <table>
511
- <thead>
512
- <tr>
513
- <th>周起始</th>
514
- <th>personKey</th>
515
- <th>消息</th>
516
- <th>Input tokens</th>
517
- <th>Output tokens</th>
518
- <th>Cache read tokens</th>
519
- <th>5h Peak</th>
520
- <th>5h Latest</th>
521
- <th>7d Peak</th>
522
- <th>7d Latest</th>
523
- <th class="muted-col">API 请求</th>
524
- </tr>
525
- </thead>
526
- <tbody>${rows}</tbody>
527
- </table>
528
- </div>
529
- </section>
523
+ return `
524
+ <section class="panel table-panel">
525
+ <div class="panel-header">
526
+ <div>
527
+ <p class="eyebrow">Weekly Rollup</p>
528
+ <h2>按周聚合</h2>
529
+ </div>
530
+ <p class="muted">直接来源于每个 bundle 的 weeklySummary。</p>
531
+ </div>
532
+ <div class="table-wrap">
533
+ <table>
534
+ <thead>
535
+ <tr>
536
+ <th>周起始</th>
537
+ <th>personKey</th>
538
+ <th>消息</th>
539
+ <th>Input tokens</th>
540
+ <th>Output tokens</th>
541
+ <th>Cache read tokens</th>
542
+ <th>5h Peak</th>
543
+ <th>5h Latest</th>
544
+ <th>7d Peak</th>
545
+ <th>7d Latest</th>
546
+ <th class="muted-col">API 请求</th>
547
+ </tr>
548
+ </thead>
549
+ <tbody>${rows}</tbody>
550
+ </table>
551
+ </div>
552
+ </section>
530
553
  `;
531
554
  }
532
555
  /**
@@ -543,207 +566,209 @@ function buildAggregateDashboardHtml(detailRows, dailyRows, weeklyRows, generate
543
566
  const peakUsage = maxOrNull(people.map((person) => person.fiveHourPeakUsagePct));
544
567
  const peakSevenDay = maxOrNull(people.map((person) => person.sevenDayPeakUsagePct));
545
568
  const rangeLabel = overall.startDate && overall.endDate ? `${overall.startDate} → ${overall.endDate}` : "--";
546
- return `<!doctype html>
547
- <html lang="zh-CN">
548
- <head>
549
- <meta charset="utf-8" />
550
- <meta name="viewport" content="width=device-width, initial-scale=1" />
551
- <title>ccus team dashboard</title>
552
- <style>
553
- :root {
554
- --bg: #0a0d12;
555
- --panel: rgba(16, 21, 31, 0.84);
556
- --panel-border: rgba(120, 141, 173, 0.18);
557
- --text: #ecf3ff;
558
- --muted: #91a0b8;
559
- --accent: #5eead4;
560
- --accent-strong: #22c55e;
561
- --warning: #f59e0b;
562
- --grid: rgba(145, 160, 184, 0.15);
563
- --shadow: 0 24px 80px rgba(0, 0, 0, 0.35);
564
- }
565
- * { box-sizing: border-box; }
566
- body {
567
- margin: 0;
568
- font-family: Georgia, "Times New Roman", serif;
569
- color: var(--text);
570
- background:
571
- radial-gradient(circle at top left, rgba(34, 197, 94, 0.18), transparent 30%),
572
- radial-gradient(circle at top right, rgba(94, 234, 212, 0.16), transparent 28%),
573
- linear-gradient(160deg, #06080c 0%, #0a0d12 48%, #101520 100%);
574
- min-height: 100vh;
575
- }
576
- .shell {
577
- max-width: 1240px;
578
- margin: 0 auto;
579
- padding: 40px 24px 64px;
580
- }
581
- .hero {
582
- display: grid;
583
- gap: 16px;
584
- padding: 28px 0 18px;
585
- }
586
- .eyebrow {
587
- margin: 0 0 8px;
588
- color: var(--accent);
589
- text-transform: uppercase;
590
- letter-spacing: 0.14em;
591
- font-size: 12px;
592
- }
593
- h1, h2, p { margin: 0; }
594
- h1 {
595
- font-size: clamp(36px, 5vw, 60px);
596
- line-height: 0.95;
597
- font-weight: 600;
598
- }
599
- .subtitle {
600
- max-width: 820px;
601
- color: var(--muted);
602
- font-size: 16px;
603
- line-height: 1.6;
604
- }
605
- .hero-meta {
606
- display: flex;
607
- flex-wrap: wrap;
608
- gap: 12px;
609
- color: var(--muted);
610
- font-size: 14px;
611
- }
612
- .hero-chip {
613
- padding: 8px 12px;
614
- border-radius: 999px;
615
- border: 1px solid var(--panel-border);
616
- background: rgba(9, 12, 18, 0.56);
617
- }
618
- .stats {
619
- display: grid;
620
- grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
621
- gap: 16px;
622
- margin: 24px 0 28px;
623
- }
624
- .panel {
625
- background: var(--panel);
626
- backdrop-filter: blur(18px);
627
- border: 1px solid var(--panel-border);
628
- border-radius: 24px;
629
- box-shadow: var(--shadow);
630
- }
631
- .stat-card { padding: 20px; min-height: 128px; }
632
- .stat-card h2 { font-size: 14px; color: var(--muted); font-weight: 500; }
633
- .stat-value { margin-top: 16px; font-size: 36px; line-height: 0.95; }
634
- .stat-note { margin-top: 12px; color: var(--muted); font-size: 13px; }
635
- .panel-header {
636
- display: flex;
637
- align-items: end;
638
- justify-content: space-between;
639
- gap: 16px;
640
- padding: 24px 24px 0;
641
- }
642
- .panel-header h2 { font-size: 28px; line-height: 1; }
643
- .muted { color: var(--muted); font-size: 14px; }
644
- .chart-panel { padding-bottom: 22px; margin-top: 22px; }
645
- .chart { width: 100%; height: auto; display: block; padding: 12px 20px 6px; }
646
- .chart-grid { stroke: var(--grid); stroke-width: 1; }
647
- .chart-axis { fill: var(--muted); font-size: 11px; }
648
- .chart-axis-line { stroke: var(--grid); stroke-width: 1; }
649
- .legend {
650
- display: flex;
651
- flex-wrap: wrap;
652
- gap: 10px 18px;
653
- padding: 14px 24px 0;
654
- color: var(--muted);
655
- font-size: 13px;
656
- }
657
- .legend-chip { display: inline-flex; align-items: center; gap: 8px; }
658
- .legend-dot { display: inline-block; width: 10px; height: 10px; border-radius: 50%; }
659
- .table-panel { margin-top: 22px; }
660
- .table-wrap { overflow: auto; padding: 16px 20px 22px; }
661
- table { width: 100%; border-collapse: collapse; min-width: 760px; }
662
- th, td {
663
- text-align: left;
664
- padding: 12px 10px;
665
- border-bottom: 1px solid rgba(145, 160, 184, 0.12);
666
- vertical-align: top;
667
- }
668
- th {
669
- color: var(--muted);
670
- font-size: 12px;
671
- text-transform: uppercase;
672
- letter-spacing: 0.08em;
673
- font-weight: 500;
674
- }
675
- td { font-size: 14px; }
676
- td.rank { color: var(--muted); width: 32px; }
677
- .muted-col { color: var(--muted); font-size: 12px; }
678
- .bar-label { fill: var(--text); font-size: 13px; }
679
- .bar-value { fill: var(--accent); font-size: 13px; font-weight: 600; }
680
- .bar-track { fill: rgba(145, 160, 184, 0.14); }
681
- .bar-fill { fill: var(--accent); }
682
- table.matrix th, table.matrix td { text-align: center; padding: 8px 10px; }
683
- table.matrix th.row-head { text-align: left; }
684
- table.matrix .cell-primary { display: block; color: var(--text); }
685
- table.matrix .cell-secondary { display: block; color: var(--muted); font-size: 11px; }
686
- table.matrix .muted-cell { color: rgba(145, 160, 184, 0.45); }
687
- @media (max-width: 720px) {
688
- .shell { padding-inline: 16px; }
689
- .panel-header { flex-direction: column; align-items: start; }
690
- }
691
- </style>
692
- </head>
693
- <body>
694
- <main class="shell">
695
- <section class="hero">
696
- <div>
697
- <p class="eyebrow">Claude Code · Team Surface</p>
698
- <h1>ccus team dashboard</h1>
699
- </div>
700
- <p class="subtitle">把目录里所有 export bundle 聚合到一起,看团队里每个人的 Claude Code 使用节奏:消息数、API 请求数、token 用量,以及 5 小时与 7 天额度使用率。</p>
701
- <div class="hero-meta">
702
- <span class="hero-chip">人数:${overall.personCount}</span>
703
- <span class="hero-chip">时间范围:${escapeHtml(rangeLabel)}</span>
704
- <span class="hero-chip">生成时间:${escapeHtml(generatedAt.toISOString())}</span>
705
- </div>
706
- </section>
707
- <section class="stats">
708
- <article class="panel stat-card">
709
- <h2>Total user messages</h2>
710
- <p class="stat-value">${formatNumber(overall.totalUserMessageCount)}</p>
711
- <p class="stat-note">所有人发给 Claude 的非 meta 消息</p>
712
- </article>
713
- <article class="panel stat-card">
714
- <h2>Total tokens (in+out)</h2>
715
- <p class="stat-value">${formatTokensM(totalTokens)}</p>
716
- <p class="stat-note">${formatTokensM(overall.totalInputTokens)} input / ${formatTokensM(overall.totalOutputTokens)} output</p>
717
- </article>
718
- <article class="panel stat-card">
719
- <h2>Cache read tokens</h2>
720
- <p class="stat-value">${formatTokensM(overall.totalCacheReadInputTokens)}</p>
721
- <p class="stat-note">来自 assistant usage 的 cache_read_input_tokens</p>
722
- </article>
723
- <article class="panel stat-card">
724
- <h2>Total API requests</h2>
725
- <p class="stat-value">${formatNumber(overall.totalApiRequestCount)}</p>
726
- <p class="stat-note">所有人 API 请求数合计(次要参考)</p>
727
- </article>
728
- <article class="panel stat-card">
729
- <h2>Peak 5h usage</h2>
730
- <p class="stat-value">${escapeHtml((0, time_1.roundNumber)(peakUsage, 1) === null ? "--" : statValue((0, time_1.roundNumber)(peakUsage, 1)))}</p>
731
- <p class="stat-note">团队内观测到的 5 小时使用率峰值</p>
732
- </article>
733
- <article class="panel stat-card">
734
- <h2>Peak 7d usage</h2>
735
- <p class="stat-value">${escapeHtml((0, time_1.roundNumber)(peakSevenDay, 1) === null ? "--" : statValue((0, time_1.roundNumber)(peakSevenDay, 1)))}</p>
736
- <p class="stat-note">团队内观测到的 7 天额度峰值</p>
737
- </article>
738
- </section>
739
- ${renderPeopleLeaderboard(people)}
740
- ${renderSevenDayPeakChart(people)}
741
- ${renderFiveHourUsageChart(people, detailRows)}
742
- ${renderDailyUserRequestChart(people, dailyIndex, dateAxis)}
743
- ${renderDailyMatrix(people, dailyIndex, dateAxis)}
744
- ${renderWeeklyTable(weeklyRows)}
745
- </main>
746
- </body>
569
+ return `<!doctype html>
570
+ <html lang="zh-CN">
571
+ <head>
572
+ <meta charset="utf-8" />
573
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
574
+ <title>ccus team dashboard</title>
575
+ <style>
576
+ :root {
577
+ --bg: #0a0d12;
578
+ --panel: rgba(16, 21, 31, 0.84);
579
+ --panel-border: rgba(120, 141, 173, 0.18);
580
+ --text: #ecf3ff;
581
+ --muted: #91a0b8;
582
+ --accent: #5eead4;
583
+ --accent-strong: #22c55e;
584
+ --warning: #f59e0b;
585
+ --grid: rgba(145, 160, 184, 0.15);
586
+ --shadow: 0 24px 80px rgba(0, 0, 0, 0.35);
587
+ }
588
+ * { box-sizing: border-box; }
589
+ body {
590
+ margin: 0;
591
+ font-family: Georgia, "Times New Roman", serif;
592
+ color: var(--text);
593
+ background:
594
+ radial-gradient(circle at top left, rgba(34, 197, 94, 0.18), transparent 30%),
595
+ radial-gradient(circle at top right, rgba(94, 234, 212, 0.16), transparent 28%),
596
+ linear-gradient(160deg, #06080c 0%, #0a0d12 48%, #101520 100%);
597
+ min-height: 100vh;
598
+ }
599
+ .shell {
600
+ max-width: 1240px;
601
+ margin: 0 auto;
602
+ padding: 40px 24px 64px;
603
+ }
604
+ .hero {
605
+ display: grid;
606
+ gap: 16px;
607
+ padding: 28px 0 18px;
608
+ }
609
+ .eyebrow {
610
+ margin: 0 0 8px;
611
+ color: var(--accent);
612
+ text-transform: uppercase;
613
+ letter-spacing: 0.14em;
614
+ font-size: 12px;
615
+ }
616
+ h1, h2, p { margin: 0; }
617
+ h1 {
618
+ font-size: clamp(36px, 5vw, 60px);
619
+ line-height: 0.95;
620
+ font-weight: 600;
621
+ }
622
+ .subtitle {
623
+ max-width: 820px;
624
+ color: var(--muted);
625
+ font-size: 16px;
626
+ line-height: 1.6;
627
+ }
628
+ .hero-meta {
629
+ display: flex;
630
+ flex-wrap: wrap;
631
+ gap: 12px;
632
+ color: var(--muted);
633
+ font-size: 14px;
634
+ }
635
+ .hero-chip {
636
+ padding: 8px 12px;
637
+ border-radius: 999px;
638
+ border: 1px solid var(--panel-border);
639
+ background: rgba(9, 12, 18, 0.56);
640
+ }
641
+ .stats {
642
+ display: grid;
643
+ grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
644
+ gap: 16px;
645
+ margin: 24px 0 28px;
646
+ }
647
+ .panel {
648
+ background: var(--panel);
649
+ backdrop-filter: blur(18px);
650
+ border: 1px solid var(--panel-border);
651
+ border-radius: 24px;
652
+ box-shadow: var(--shadow);
653
+ }
654
+ .stat-card { padding: 20px; min-height: 128px; }
655
+ .stat-card h2 { font-size: 14px; color: var(--muted); font-weight: 500; }
656
+ .stat-value { margin-top: 16px; font-size: 36px; line-height: 0.95; }
657
+ .stat-note { margin-top: 12px; color: var(--muted); font-size: 13px; }
658
+ .panel-header {
659
+ display: flex;
660
+ align-items: end;
661
+ justify-content: space-between;
662
+ gap: 16px;
663
+ padding: 24px 24px 0;
664
+ }
665
+ .panel-header h2 { font-size: 28px; line-height: 1; }
666
+ .muted { color: var(--muted); font-size: 14px; }
667
+ .chart-panel { padding-bottom: 22px; margin-top: 22px; }
668
+ .chart { width: 100%; height: auto; display: block; padding: 12px 20px 6px; }
669
+ .chart-grid { stroke: var(--grid); stroke-width: 1; }
670
+ .chart-axis { fill: var(--muted); font-size: 11px; }
671
+ .chart-axis-line { stroke: var(--grid); stroke-width: 1; }
672
+ .legend {
673
+ display: flex;
674
+ flex-wrap: wrap;
675
+ gap: 10px 18px;
676
+ padding: 14px 24px 0;
677
+ color: var(--muted);
678
+ font-size: 13px;
679
+ }
680
+ .legend-chip { display: inline-flex; align-items: center; gap: 8px; }
681
+ .legend-dot { display: inline-block; width: 10px; height: 10px; border-radius: 50%; }
682
+ .legend-line { display: inline-block; width: 22px; height: 0; border-top-width: 2px; border-top-style: solid; border-top-color: var(--muted); }
683
+ .legend-line-dashed { border-top-style: dashed; }
684
+ .table-panel { margin-top: 22px; }
685
+ .table-wrap { overflow: auto; padding: 16px 20px 22px; }
686
+ table { width: 100%; border-collapse: collapse; min-width: 760px; }
687
+ th, td {
688
+ text-align: left;
689
+ padding: 12px 10px;
690
+ border-bottom: 1px solid rgba(145, 160, 184, 0.12);
691
+ vertical-align: top;
692
+ }
693
+ th {
694
+ color: var(--muted);
695
+ font-size: 12px;
696
+ text-transform: uppercase;
697
+ letter-spacing: 0.08em;
698
+ font-weight: 500;
699
+ }
700
+ td { font-size: 14px; }
701
+ td.rank { color: var(--muted); width: 32px; }
702
+ .muted-col { color: var(--muted); font-size: 12px; }
703
+ .bar-label { fill: var(--text); font-size: 13px; }
704
+ .bar-value { fill: var(--accent); font-size: 13px; font-weight: 600; }
705
+ .bar-track { fill: rgba(145, 160, 184, 0.14); }
706
+ .bar-fill { fill: var(--accent); }
707
+ table.matrix th, table.matrix td { text-align: center; padding: 8px 10px; }
708
+ table.matrix th.row-head { text-align: left; }
709
+ table.matrix .cell-primary { display: block; color: var(--text); }
710
+ table.matrix .cell-secondary { display: block; color: var(--muted); font-size: 11px; }
711
+ table.matrix .muted-cell { color: rgba(145, 160, 184, 0.45); }
712
+ @media (max-width: 720px) {
713
+ .shell { padding-inline: 16px; }
714
+ .panel-header { flex-direction: column; align-items: start; }
715
+ }
716
+ </style>
717
+ </head>
718
+ <body>
719
+ <main class="shell">
720
+ <section class="hero">
721
+ <div>
722
+ <p class="eyebrow">Claude Code · Team Surface</p>
723
+ <h1>ccus team dashboard</h1>
724
+ </div>
725
+ <p class="subtitle">把目录里所有 export bundle 聚合到一起,看团队里每个人的 Claude Code 使用节奏:消息数、API 请求数、token 用量,以及 5 小时与 7 天额度使用率。</p>
726
+ <div class="hero-meta">
727
+ <span class="hero-chip">人数:${overall.personCount}</span>
728
+ <span class="hero-chip">时间范围:${escapeHtml(rangeLabel)}</span>
729
+ <span class="hero-chip">生成时间:${escapeHtml(generatedAt.toISOString())}</span>
730
+ </div>
731
+ </section>
732
+ <section class="stats">
733
+ <article class="panel stat-card">
734
+ <h2>Total user messages</h2>
735
+ <p class="stat-value">${formatNumber(overall.totalUserMessageCount)}</p>
736
+ <p class="stat-note">所有人发给 Claude 的非 meta 消息</p>
737
+ </article>
738
+ <article class="panel stat-card">
739
+ <h2>Total tokens (in+out)</h2>
740
+ <p class="stat-value">${formatTokensM(totalTokens)}</p>
741
+ <p class="stat-note">${formatTokensM(overall.totalInputTokens)} input / ${formatTokensM(overall.totalOutputTokens)} output</p>
742
+ </article>
743
+ <article class="panel stat-card">
744
+ <h2>Cache read tokens</h2>
745
+ <p class="stat-value">${formatTokensM(overall.totalCacheReadInputTokens)}</p>
746
+ <p class="stat-note">来自 assistant usage 的 cache_read_input_tokens</p>
747
+ </article>
748
+ <article class="panel stat-card">
749
+ <h2>Total API requests</h2>
750
+ <p class="stat-value">${formatNumber(overall.totalApiRequestCount)}</p>
751
+ <p class="stat-note">所有人 API 请求数合计(次要参考)</p>
752
+ </article>
753
+ <article class="panel stat-card">
754
+ <h2>Peak 5h usage</h2>
755
+ <p class="stat-value">${escapeHtml((0, time_1.roundNumber)(peakUsage, 1) === null ? "--" : statValue((0, time_1.roundNumber)(peakUsage, 1)))}</p>
756
+ <p class="stat-note">团队内观测到的 5 小时使用率峰值</p>
757
+ </article>
758
+ <article class="panel stat-card">
759
+ <h2>Peak 7d usage</h2>
760
+ <p class="stat-value">${escapeHtml((0, time_1.roundNumber)(peakSevenDay, 1) === null ? "--" : statValue((0, time_1.roundNumber)(peakSevenDay, 1)))}</p>
761
+ <p class="stat-note">团队内观测到的 7 天额度峰值</p>
762
+ </article>
763
+ </section>
764
+ ${renderPeopleLeaderboard(people)}
765
+ ${renderSevenDayPeakChart(people)}
766
+ ${renderFiveHourUsageChart(people, detailRows)}
767
+ ${renderDailyUserRequestChart(people, dailyIndex, dateAxis)}
768
+ ${renderDailyMatrix(people, dailyIndex, dateAxis)}
769
+ ${renderWeeklyTable(weeklyRows)}
770
+ </main>
771
+ </body>
747
772
  </html>`;
748
773
  }
749
774
  //# sourceMappingURL=aggregate-dashboard.js.map