ccus-cli 0.1.21 → 0.1.22

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -146,11 +146,11 @@ ccus aggregate serve --input-dir ./team-exports
146
146
  其中:
147
147
 
148
148
  - `5 小时使用量百分比` 是 **展示指标**,来自 Claude 自身字段 `rate_limits.five_hour.used_percentage`
149
- - 使用率趋势图会在同一张图上叠加两条线:实线为 5 小时使用率(`rate_limits.five_hour`),虚线为 7 天使用率(`rate_limits.seven_day`),两者各自独立按时间桶聚合
149
+ - 使用率趋势图会在同一张图上叠加三条线:实线为 5 小时使用率(`rate_limits.five_hour`)、虚线为 7 天使用率(`rate_limits.seven_day`),两者各自独立按时间桶聚合、共用固定 0–100% 的左侧 Y 轴;紫色「7d 分区叠加累计」是把 7 天额度锯齿波还原成的累计真实使用量(口径同团队看板的 `sevenDayCumulativeUsagePct`),量纲可超 100%、单独走**右侧自适应 Y 轴**,按分段峰谷和单调非递减地累积上升,终点即顶部卡片的累计值
150
150
  - **折线与纵向柱状图均由 [uPlot](https://github.com/leeoniya/uPlot) 渲染**:鼠标在绘图区**任意位置**悬停即可通过十字线 + **跟随鼠标的 tooltip** 看到该处各条线的当前读数(不必精确落在采样点上;遇到几乎垂直的窄尖峰,把鼠标移到竖线顶端即可读到峰值);纵向柱还会在每根柱顶标注数值。底部为**自绘图例**(实时读数集中在 tooltip 里)。uPlot 库与样式已**内联进生成的 HTML**,`dashboard build` 产物用 `file://` 离线打开也能完整渲染与交互,不依赖任何 CDN
151
151
  - 页面新增 **每日用户消息数** 纵向柱状图:按自然日统计的真实用户请求数,口径与导出契约的 `userMessageCount` 一致(来自 `~/.claude/projects/**/*.jsonl`),不是 statusline 采样数,也仅用于页面展示、不进任何导出/聚合契约
152
152
  - 跨多天的窗口(如 `this-week` / `last-week`)使用率曲线会自动改用小时桶聚合,避免一周生成上千个点;x 轴刻度由 uPlot 按时间跨度自适应(短窗口显示 `HH:mm` **24 小时制**、跨天的刻度再补一行日期),不用 am/pm
153
- - 顶部统计卡展示 `Latest 5h usage`、`Peak 5h usage`、`Latest 7d usage`(含峰值)、`用户消息数`(窗口内每日真实用户请求数合计)
153
+ - 顶部统计卡展示 `Latest 5h usage`、`Peak 5h usage`、`7d 分区叠加累计`(主数值为 7 天额度分区叠加累计真实使用量,小字补充峰值与最新值)、`用户消息数`(窗口内每日真实用户请求数合计)
154
154
  - `--range today / this-week / last-week / 24h` 是 **你要查看的采样历史时间窗口**(`last-week` 指上一个完整周一到周日)
155
155
  - statusline 日志本身主要保存 `rawPayload` 与外部补充字段;默认导出时会同时保留原始事件,并额外汇总 `~/.claude/projects/**/*.jsonl` 中的会话 usage
156
156
 
@@ -4,7 +4,10 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.loadWeeklyExportBundles = loadWeeklyExportBundles;
7
+ exports.computeCumulativeSevenDayCurve = computeCumulativeSevenDayCurve;
7
8
  exports.computeCumulativeSevenDay = computeCumulativeSevenDay;
9
+ exports.deburrSevenDayEvents = deburrSevenDayEvents;
10
+ exports.buildSevenDayCurveFromEvents = buildSevenDayCurveFromEvents;
8
11
  exports.buildPersonSevenDayCurve = buildPersonSevenDayCurve;
9
12
  exports.buildAggregatedDetailRows = buildAggregatedDetailRows;
10
13
  exports.buildAggregatedDailyRows = buildAggregatedDailyRows;
@@ -265,7 +268,8 @@ function bundleEventsByDate(bundle) {
265
268
  */
266
269
  const SEVEN_DAY_RESET_RATIO = 0.5;
267
270
  /**
268
- * 7 天额度累计真实使用量:对一组事件取非 null `sevenDayUsagePct`、按时间升序,用**分段峰谷和**还原。
271
+ * 7 天额度累计真实使用量的**逐点曲线**:对一组事件取非 null `sevenDayUsagePct`、按时间升序,用**分段峰谷和**
272
+ * 还原,并在每个样本处输出「截至此刻的累计值」,得到一条单调非递减曲线。
269
273
  *
270
274
  * 把曲线按 reset(样本跌破当前段峰值的 {@link SEVEN_DAY_RESET_RATIO})切成若干上升段,
271
275
  * 每段贡献「段内峰值 − 段内谷值」,累计 = 各段贡献之和。等价于「正增量累加,但忽略未跌破段峰一半的小回落」。
@@ -274,21 +278,23 @@ const SEVEN_DAY_RESET_RATIO = 0.5;
274
278
  * 朴素累加会把每次上抖都计成真实增长,导致严重高估(实测 gaoshang 102 vs 分段 50)。
275
279
  * 分段峰谷和对抖动 / aging 回落鲁棒,又能正确累计「涨到峰 → 归零 → 再涨」的多段真实使用。
276
280
  *
277
- * 无有效样本返回 null(区别于 0:0 表示有样本但无净增长);单样本返回 0。
281
+ * 每个点的累计 = 已锁定的各完整段贡献 + 当前段 (segMax − segMin);段内更新 / reset 都不会让它回落,
282
+ * 所以曲线单调非递减、终点恒等于 {@link computeCumulativeSevenDay} 的标量返回值。输入建议先经 deburr 去毛刺。
283
+ * 无有效样本返回空数组;单样本返回单点 0。
278
284
  */
279
- function computeCumulativeSevenDay(events) {
280
- const values = [...events]
281
- .sort((left, right) => new Date(left.timestamp).getTime() - new Date(right.timestamp).getTime())
282
- .map((event) => event.sevenDayUsagePct)
283
- .filter((value) => value !== null);
284
- if (values.length === 0) {
285
- return null;
285
+ function computeCumulativeSevenDayCurve(events) {
286
+ const sorted = [...events]
287
+ .filter((event) => event.sevenDayUsagePct !== null)
288
+ .sort((left, right) => new Date(left.timestamp).getTime() - new Date(right.timestamp).getTime());
289
+ if (sorted.length === 0) {
290
+ return [];
286
291
  }
292
+ const points = [{ timestamp: sorted[0].timestamp, cumulative: 0 }];
287
293
  let cumulative = 0;
288
- let segMin = values[0];
289
- let segMax = values[0];
290
- for (let index = 1; index < values.length; index += 1) {
291
- const value = values[index];
294
+ let segMin = sorted[0].sevenDayUsagePct;
295
+ let segMax = sorted[0].sevenDayUsagePct;
296
+ for (let index = 1; index < sorted.length; index += 1) {
297
+ const value = sorted[index].sevenDayUsagePct;
292
298
  if (value > segMax) {
293
299
  segMax = value;
294
300
  }
@@ -302,9 +308,18 @@ function computeCumulativeSevenDay(events) {
302
308
  // 段内小回落(抖动 / aging):只更新谷值,不分段、不重复计数。
303
309
  segMin = value;
304
310
  }
311
+ points.push({ timestamp: sorted[index].timestamp, cumulative: (0, time_1.roundNumber)(cumulative + (segMax - segMin), 1) ?? 0 });
305
312
  }
306
- cumulative += segMax - segMin;
307
- return (0, time_1.roundNumber)(cumulative, 1);
313
+ return points;
314
+ }
315
+ /**
316
+ * 7 天额度累计真实使用量(标量):取 {@link computeCumulativeSevenDayCurve} 曲线终点。
317
+ *
318
+ * 无有效样本返回 null(区别于 0:0 表示有样本但无净增长);单样本返回 0。
319
+ */
320
+ function computeCumulativeSevenDay(events) {
321
+ const curve = computeCumulativeSevenDayCurve(events);
322
+ return curve.length === 0 ? null : curve[curve.length - 1].cumulative;
308
323
  }
309
324
  /** 7d 曲线读数去毛刺的最小持续时长:短于此的中间读数段视为 stale / 瞬时异常。 */
310
325
  const SEVEN_DAY_MIN_HOLD_MS = 2 * 60 * 1000;
@@ -312,10 +327,14 @@ const SEVEN_DAY_MIN_HOLD_MS = 2 * 60 * 1000;
312
327
  * 7d 曲线读数去毛刺:真实 7d 信号变化慢、每个档位会被高频采样连续覆盖几十上百个样本,
313
328
  * 而 stale 缓存读数 / 瞬时异常只会短暂出现(秒级,如 baseline 很低时偶发跳到 30 又落回)。
314
329
  *
315
- * 把**中间**持续短于 {@link SEVEN_DAY_MIN_HOLD_MS} 的读数段(下一段起始 − 本段起始)替换为最近的已保留前值,
316
- * 抹掉这些尖峰;首尾段无条件保留(端点缺上下文判断持续性)。这等价于人眼在曲线图上自动忽略短毛刺。
330
+ * 把**中间**持续短于 {@link SEVEN_DAY_MIN_HOLD_MS} 的读数段替换为最近的已保留前值,抹掉这些尖峰;
331
+ * 首尾段无条件保留(端点缺上下文判断持续性)。这等价于人眼在曲线图上自动忽略短毛刺。
317
332
  *
318
- * 仅对密集采样的真实曲线生效:稀疏数据(每个值只有一两个样本、间隔很大)的中间段持续时长通常远超阈值,
333
+ * 持续时长默认按「下一段起始 − 本段起始」度量(密集采样下约等于本段实际持续);但当**多样本**段后面紧跟一段
334
+ * 比阈值还大的**采集间隙**时,该度量会把间隙也算进去、把只持续几十秒的短尖峰“撑”过阈值而漏抹,
335
+ * 此时改用**段内真实跨度(最后样本 − 第一样本)**判定。单样本段无段内跨度、无法据此判断,仍走原度量。
336
+ *
337
+ * 仅对密集采样的真实曲线生效:稀疏数据(每个值只有一两个样本、间隔很大)的单样本中间段仍按原度量保留,
319
338
  * 不会被误删,所以 spec 的稀疏示例与单样本段不受影响。输入须按 timestamp 升序。
320
339
  */
321
340
  function deburrSevenDayEvents(events, minHoldMs = SEVEN_DAY_MIN_HOLD_MS) {
@@ -329,14 +348,28 @@ function deburrSevenDayEvents(events, minHoldMs = SEVEN_DAY_MIN_HOLD_MS) {
329
348
  while (end + 1 < events.length && events[end + 1].sevenDayUsagePct === events[index].sevenDayUsagePct) {
330
349
  end += 1;
331
350
  }
332
- runs.push({ value: events[index].sevenDayUsagePct, startIdx: index, endIdx: end, startTime: new Date(events[index].timestamp).getTime() });
351
+ runs.push({
352
+ value: events[index].sevenDayUsagePct,
353
+ startIdx: index,
354
+ endIdx: end,
355
+ startTime: new Date(events[index].timestamp).getTime(),
356
+ endTime: new Date(events[end].timestamp).getTime(),
357
+ });
333
358
  index = end + 1;
334
359
  }
335
360
  const result = [...events];
336
361
  let lastKept = runs[0].value;
337
362
  for (let k = 0; k < runs.length; k += 1) {
338
363
  const isEdge = k === 0 || k === runs.length - 1;
339
- const holdMs = k + 1 < runs.length ? runs[k + 1].startTime - runs[k].startTime : Number.POSITIVE_INFINITY;
364
+ // 默认持续时长 = 下一段起始 本段起始(密集采样下约等于本段实际持续)。
365
+ const toNext = k + 1 < runs.length ? runs[k + 1].startTime - runs[k].startTime : Number.POSITIVE_INFINITY;
366
+ // 本段最后样本到下一段起始的采集间隙:密集采样下很小,遇到数据采集中断时会变大。
367
+ const gapAfter = k + 1 < runs.length ? runs[k + 1].startTime - runs[k].endTime : Number.POSITIVE_INFINITY;
368
+ // 多样本段(段内本身有跨度)后面若紧跟一段比阈值还大的采集间隙,说明 toNext 把间隙也算进了持续时长,
369
+ // 会把本只持续几十秒的短尖峰“撑”过阈值而漏抹;此时改用段内真实跨度判定,正确识别这类 stale 尖峰。
370
+ // 单样本段(无段内跨度)无法据此判断真实持续,仍走 toNext,避免误伤稀疏单样本数据。
371
+ const multiSample = runs[k].endIdx > runs[k].startIdx;
372
+ const holdMs = multiSample && gapAfter > minHoldMs ? runs[k].endTime - runs[k].startTime : toNext;
340
373
  if (isEdge || holdMs >= minHoldMs) {
341
374
  lastKept = runs[k].value;
342
375
  continue;
@@ -347,6 +380,28 @@ function deburrSevenDayEvents(events, minHoldMs = SEVEN_DAY_MIN_HOLD_MS) {
347
380
  }
348
381
  return result;
349
382
  }
383
+ /**
384
+ * 把一组事件整理成一条可用于累计计算的 7d 曲线:取非 null `sevenDayUsagePct`、按时间升序、
385
+ * 对完全相同 timestamp 去重,再做读数去毛刺。
386
+ *
387
+ * 单台机器的本地日志(dashboard 个人看板)与多机合并(aggregate)都走这同一套整理逻辑,
388
+ * 保证两边的累计口径不会漂移。
389
+ */
390
+ function buildSevenDayCurveFromEvents(events) {
391
+ const sorted = [...events]
392
+ .filter((event) => event.sevenDayUsagePct !== null)
393
+ .sort((left, right) => new Date(left.timestamp).getTime() - new Date(right.timestamp).getTime());
394
+ const seen = new Set();
395
+ const deduped = [];
396
+ for (const event of sorted) {
397
+ if (seen.has(event.timestamp)) {
398
+ continue;
399
+ }
400
+ seen.add(event.timestamp);
401
+ deduped.push(event);
402
+ }
403
+ return deburrSevenDayEvents(deduped);
404
+ }
350
405
  /**
351
406
  * 同一 personKey 的 7d 累计曲线:把该人**所有 bundle**(非仅 winner)的 rawEvents 计算成事件、
352
407
  * 取非 null `sevenDayUsagePct`、按 timestamp 升序合并、对完全相同 timestamp 去重,再做读数去毛刺,
@@ -380,17 +435,7 @@ function buildPersonSevenDayCurve(bundles) {
380
435
  }
381
436
  const curves = new Map();
382
437
  for (const [personKey, events] of collected.entries()) {
383
- const sorted = [...events].sort((left, right) => new Date(left.timestamp).getTime() - new Date(right.timestamp).getTime());
384
- const seen = new Set();
385
- const deduped = [];
386
- for (const event of sorted) {
387
- if (seen.has(event.timestamp)) {
388
- continue;
389
- }
390
- seen.add(event.timestamp);
391
- deduped.push(event);
392
- }
393
- curves.set(personKey, deburrSevenDayEvents(deduped));
438
+ curves.set(personKey, buildSevenDayCurveFromEvents(events));
394
439
  }
395
440
  personSevenDayCurveCache.set(bundles, curves);
396
441
  return curves;
@@ -324,6 +324,7 @@ function uplotBodyScripts() {
324
324
  };
325
325
  if (s.fill) ser.fill = s.fill;
326
326
  if (s.dash) ser.dash = s.dash;
327
+ if (s.scale) ser.scale = s.scale;
327
328
  if (spec.bars) {
328
329
  ser.paths = uPlot.paths.bars({ size: [0.62, 48], align: 0 });
329
330
  ser.points = { show: false };
@@ -379,12 +380,30 @@ function uplotBodyScripts() {
379
380
  };
380
381
  }
381
382
 
383
+ var axes = [xAxis, yAxis];
384
+ if (spec.y2) {
385
+ var y2unit = spec.y2.unit || "";
386
+ scales[spec.y2.scale] = spec.y2.range
387
+ ? { range: spec.y2.range }
388
+ : (spec.y2.min0
389
+ ? { range: function (u, min, max) { var hi = max == null || max <= 0 ? 1 : max; return [0, hi]; } }
390
+ : {});
391
+ axes.push({
392
+ scale: spec.y2.scale,
393
+ side: 1,
394
+ stroke: THEME.axis,
395
+ grid: { show: false },
396
+ ticks: { stroke: THEME.grid, width: 1 },
397
+ values: function (u, splits) { return splits.map(function (v) { return round1(v) + y2unit; }); },
398
+ });
399
+ }
400
+
382
401
  var opts = {
383
402
  width: el.clientWidth || 920,
384
403
  height: spec.height || 280,
385
404
  scales: scales,
386
405
  series: series,
387
- axes: [xAxis, yAxis],
406
+ axes: axes,
388
407
  // 关掉 uPlot 原生底部 legend(每条 series 一行),改用 buildLegend 自绘「每人一项」图例。
389
408
  legend: { show: false },
390
409
  // dataIdx 2D 吸附:横向重合的尖峰按鼠标高度命中峰顶,稀疏区/缺失退回最近真实点。
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.summarizeEvents = summarizeEvents;
4
4
  exports.bucketizeEvents = bucketizeEvents;
5
5
  exports.buildDashboardHtml = buildDashboardHtml;
6
+ const aggregate_1 = require("./aggregate");
6
7
  const chart_assets_1 = require("./chart-assets");
7
8
  const time_1 = require("./time");
8
9
  /** 所有插入到 HTML/SVG 的动态文本都先转义,避免本地页面被注入内容。 */
@@ -32,11 +33,14 @@ function summarizeEvents(events) {
32
33
  const latestSevenDayUsagePct = [...events]
33
34
  .sort((left, right) => new Date(right.timestamp).getTime() - new Date(left.timestamp).getTime())
34
35
  .find((event) => event.sevenDayUsagePct !== null)?.sevenDayUsagePct ?? null;
36
+ // 7d 分区叠加累计:与 aggregate 同一套「去毛刺 + 分段峰谷和」,口径与团队看板一致。
37
+ const sevenDayCumulativeUsagePct = (0, aggregate_1.computeCumulativeSevenDay)((0, aggregate_1.buildSevenDayCurveFromEvents)(events));
35
38
  return {
36
39
  fiveHourLatestUsagePct: latestUsagePct,
37
40
  fiveHourPeakUsagePct: usages.length > 0 ? (0, time_1.roundNumber)(Math.max(...usages), 1) : null,
38
41
  sevenDayLatestUsagePct: sevenDayUsages.length > 0 ? latestSevenDayUsagePct : null,
39
42
  sevenDayPeakUsagePct: sevenDayUsages.length > 0 ? (0, time_1.roundNumber)(Math.max(...sevenDayUsages), 1) : null,
43
+ sevenDayCumulativeUsagePct,
40
44
  sampleCount: events.length,
41
45
  uniqueSessions: new Set(events.map((event) => event.sessionId).filter(Boolean)).size,
42
46
  uniqueWorkspaces: new Set(events.map((event) => event.workspaceDir).filter(Boolean)).size,
@@ -72,12 +76,24 @@ function bucketizeEvents(events, start, end, bucketMinutes = 5) {
72
76
  slot.sevenDay.push(event.sevenDayUsagePct);
73
77
  }
74
78
  }
79
+ // 7d 累计曲线(去毛刺 + 分段峰谷和的逐点版本)映射到时间桶:曲线已按时间升序,
80
+ // 同桶内后写覆盖前写,最终每个桶留下落在其区间内最后一个累计点的值(单调,等于桶内最大累计)。
81
+ const cumulativeByBucket = new Map();
82
+ for (const point of (0, aggregate_1.computeCumulativeSevenDayCurve)((0, aggregate_1.buildSevenDayCurveFromEvents)(events))) {
83
+ const ts = new Date(point.timestamp).getTime();
84
+ if (ts < start.getTime() || ts > end.getTime()) {
85
+ continue;
86
+ }
87
+ const bucketStart = start.getTime() + Math.floor((ts - start.getTime()) / bucketMs) * bucketMs;
88
+ cumulativeByBucket.set(bucketStart, point.cumulative);
89
+ }
75
90
  return [...buckets.entries()].map(([bucketStart, slot]) => ({
76
91
  bucketStart: new Date(bucketStart).toISOString(),
77
92
  avgUsagePct: slot.fiveHour.length > 0 ? (0, time_1.roundNumber)(average(slot.fiveHour), 1) : null,
78
93
  maxUsagePct: slot.fiveHour.length > 0 ? (0, time_1.roundNumber)(Math.max(...slot.fiveHour), 1) : null,
79
94
  minUsagePct: slot.fiveHour.length > 0 ? (0, time_1.roundNumber)(Math.min(...slot.fiveHour), 1) : null,
80
95
  avgSevenDayUsagePct: slot.sevenDay.length > 0 ? (0, time_1.roundNumber)(average(slot.sevenDay), 1) : null,
96
+ cumulativeSevenDayPct: cumulativeByBucket.has(bucketStart) ? cumulativeByBucket.get(bucketStart) : null,
81
97
  sampleCount: slot.fiveHour.length,
82
98
  }));
83
99
  }
@@ -90,29 +106,34 @@ function bucketizeEvents(events, start, end, bucketMinutes = 5) {
90
106
  * 提供十字线 + legend 跟随显示两条线当前值。
91
107
  */
92
108
  function renderChart(buckets) {
93
- // 每条曲线只连有数据的桶:取「5h 7d 任一非空」的桶作为统一 x 轴,缺失填 null。
94
- const valued = buckets.filter((bucket) => bucket.maxUsagePct !== null || bucket.avgSevenDayUsagePct !== null);
109
+ // 每条曲线只连有数据的桶:取「5h / 7d / 累计 任一非空」的桶作为统一 x 轴,缺失填 null。
110
+ const valued = buckets.filter((bucket) => bucket.maxUsagePct !== null || bucket.avgSevenDayUsagePct !== null || bucket.cumulativeSevenDayPct !== null);
95
111
  const noData = valued.length === 0
96
112
  ? `<div class="empty-state">当前时间窗口里还没有可绘制的 Claude 使用率样本。</div>`
97
113
  : "";
98
114
  const xs = valued.map((bucket) => Math.floor(new Date(bucket.bucketStart).getTime() / 1000));
99
115
  const fiveHour = valued.map((bucket) => bucket.maxUsagePct);
100
116
  const sevenDay = valued.map((bucket) => bucket.avgSevenDayUsagePct);
117
+ // 7d 分区叠加累计:量纲可远超 0–100%,单独走右侧 Y 轴(y2 自适应),避免压扁固定 0–100 的 5h/7d 主轴。
118
+ const cumulative = valued.map((bucket) => bucket.cumulativeSevenDayPct);
101
119
  const spec = {
102
120
  height: 280,
103
121
  xType: "time",
104
122
  yUnit: "%",
105
123
  yRange: [0, 100],
124
+ y2: { scale: "y2", unit: "%", min0: true },
106
125
  series: [
107
126
  { label: "5 小时使用率", stroke: "#5eead4", fill: "rgba(94, 234, 212, 0.14)", width: 3 },
108
127
  { label: "7 天使用率", stroke: "#f59e0b", dash: [6, 4], width: 2 },
128
+ { label: "7d 分区叠加累计", stroke: "#a855f7", width: 2, scale: "y2" },
109
129
  ],
110
130
  legendGroups: [
111
131
  { label: "5 小时使用率", color: "#5eead4", seriesIdx: [1] },
112
132
  { label: "7 天使用率", color: "#f59e0b", seriesIdx: [2] },
133
+ { label: "7d 分区叠加累计", color: "#a855f7", seriesIdx: [3] },
113
134
  ],
114
135
  };
115
- const chart = valued.length > 0 ? (0, chart_assets_1.renderUplotChart)("chart-usage", spec, [xs, fiveHour, sevenDay]) : "";
136
+ const chart = valued.length > 0 ? (0, chart_assets_1.renderUplotChart)("chart-usage", spec, [xs, fiveHour, sevenDay, cumulative]) : "";
116
137
  return `
117
138
  <section class="panel chart-panel">
118
139
  <div class="panel-header">
@@ -120,7 +141,7 @@ function renderChart(buckets) {
120
141
  <p class="eyebrow">Recent Trend</p>
121
142
  <h2>Claude 使用率趋势</h2>
122
143
  </div>
123
- <p class="muted">按采样时间聚合,5h 来自 rate_limits.five_hour,7d 来自 rate_limits.seven_day</p>
144
+ <p class="muted">按采样时间聚合:5h(实线)来自 rate_limits.five_hour,7d(虚线)来自 rate_limits.seven_day;紫色「7d 分区叠加累计」走右侧 Y 轴,是把 7d 锯齿波还原成的累计真实使用量(口径同团队看板)</p>
124
145
  </div>
125
146
  ${noData}
126
147
  ${chart}
@@ -422,9 +443,9 @@ function buildDashboardHtml(events, rangeLabel, start, end, dailyUserMessages =
422
443
  <p class="stat-note">窗口内观测到的 5 小时使用率峰值</p>
423
444
  </article>
424
445
  <article class="panel stat-card">
425
- <h2>Latest 7d usage</h2>
426
- <p class="stat-value">${escapeHtml(statValue(summary.sevenDayLatestUsagePct))}</p>
427
- <p class="stat-note">最新 7 usage 样本,峰值 ${escapeHtml(statValue(summary.sevenDayPeakUsagePct))}</p>
446
+ <h2>7d 分区叠加累计</h2>
447
+ <p class="stat-value">${escapeHtml(statValue(summary.sevenDayCumulativeUsagePct))}</p>
448
+ <p class="stat-note">7 天额度分区叠加累计真实使用量 · 峰值 ${escapeHtml(statValue(summary.sevenDayPeakUsagePct))} · 最新 ${escapeHtml(statValue(summary.sevenDayLatestUsagePct))}</p>
428
449
  </article>
429
450
  <article class="panel stat-card">
430
451
  <h2>用户消息数</h2>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ccus-cli",
3
- "version": "0.1.21",
3
+ "version": "0.1.22",
4
4
  "description": "Claude Code statusline usage logger and dashboard CLI",
5
5
  "type": "commonjs",
6
6
  "bin": {