ccus-cli 0.1.19 → 0.1.21
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 +6 -2
- package/dist/cli.js +7 -1
- package/dist/lib/aggregate-dashboard.js +100 -166
- package/dist/lib/aggregate.js +174 -84
- package/dist/lib/chart-assets.js +466 -0
- package/dist/lib/dashboard.js +49 -134
- package/dist/vendor/uplot.LICENSE +21 -0
- package/dist/vendor/uplot.iife.min.js +2 -0
- package/dist/vendor/uplot.min.css +1 -0
- package/package.json +4 -3
package/README.md
CHANGED
|
@@ -147,8 +147,9 @@ ccus aggregate serve --input-dir ./team-exports
|
|
|
147
147
|
|
|
148
148
|
- `5 小时使用量百分比` 是 **展示指标**,来自 Claude 自身字段 `rate_limits.five_hour.used_percentage`
|
|
149
149
|
- 使用率趋势图会在同一张图上叠加两条线:实线为 5 小时使用率(`rate_limits.five_hour`),虚线为 7 天使用率(`rate_limits.seven_day`),两者各自独立按时间桶聚合
|
|
150
|
-
-
|
|
151
|
-
-
|
|
150
|
+
- **折线与纵向柱状图均由 [uPlot](https://github.com/leeoniya/uPlot) 渲染**:鼠标在绘图区**任意位置**悬停即可通过十字线 + **跟随鼠标的 tooltip** 看到该处各条线的当前读数(不必精确落在采样点上;遇到几乎垂直的窄尖峰,把鼠标移到竖线顶端即可读到峰值);纵向柱还会在每根柱顶标注数值。底部为**自绘图例**(实时读数集中在 tooltip 里)。uPlot 库与样式已**内联进生成的 HTML**,`dashboard build` 产物用 `file://` 离线打开也能完整渲染与交互,不依赖任何 CDN
|
|
151
|
+
- 页面新增 **每日用户消息数** 纵向柱状图:按自然日统计的真实用户请求数,口径与导出契约的 `userMessageCount` 一致(来自 `~/.claude/projects/**/*.jsonl`),不是 statusline 采样数,也仅用于页面展示、不进任何导出/聚合契约
|
|
152
|
+
- 跨多天的窗口(如 `this-week` / `last-week`)使用率曲线会自动改用小时桶聚合,避免一周生成上千个点;x 轴刻度由 uPlot 按时间跨度自适应(短窗口显示 `HH:mm` **24 小时制**、跨天的刻度再补一行日期),不用 am/pm
|
|
152
153
|
- 顶部统计卡展示 `Latest 5h usage`、`Peak 5h usage`、`Latest 7d usage`(含峰值)、`用户消息数`(窗口内每日真实用户请求数合计)
|
|
153
154
|
- `--range today / this-week / last-week / 24h` 是 **你要查看的采样历史时间窗口**(`last-week` 指上一个完整周一到周日)
|
|
154
155
|
- statusline 日志本身主要保存 `rawPayload` 与外部补充字段;默认导出时会同时保留原始事件,并额外汇总 `~/.claude/projects/**/*.jsonl` 中的会话 usage
|
|
@@ -183,6 +184,9 @@ ccus aggregate serve --input-dir ./team-exports
|
|
|
183
184
|
- 无有效样本写空(`null`),有样本但无净增长写 `0`;`detail.csv` 不含此列(单事件行不承载区间累计语义)
|
|
184
185
|
- CSV 里所有以 token 计的列(context 与 in/out/cache)都以百万(M)为单位(原始值除以 1,000,000),列名统一带 `M` 后缀;`contextWindowPct` 仍是百分比
|
|
185
186
|
- 想直接查看团队多人 dashboard,可以用 `ccus aggregate serve --input-dir DIR [--port 0] [--host 127.0.0.1]`:默认监听 `127.0.0.1` 上的随机端口,启动后会自动用系统默认浏览器打开,每次请求实时读取目录里的 bundle,不写入任何文件
|
|
187
|
+
- 多人「5h/7d 使用率详细曲线」与「每日用户请求数对比」两张折线同样由 uPlot 渲染,悬停任意位置即可在**跟随 tooltip** 里看到当前位置**各人**的读数。底部图例**一个人只显示一个名字**(小写),点击该名字会同时切换该人 5h + 7d 两条线的显隐。各人按真实时间戳采样、彼此不对齐的曲线会先做**时间戳并集对齐**(缺失点留空而非补 0)再绘制;悬停时每条线的读数与高亮点会**吸附到该人离光标最近的真实采样点**,横向重合的尖峰按鼠标高度做 2D 命中,所以任意位置都能读到每个人的值、峰值圆点也会亮
|
|
188
|
+
- 「周使用量累计对比」是**横向排行榜**,仍保持自绘 SVG(它是带标签的水平进度条排行,uPlot 无原生横向 bar,不纳入迁移)
|
|
189
|
+
- uPlot 库同样内联,离线可用
|
|
186
190
|
|
|
187
191
|
## 定时同步
|
|
188
192
|
|
package/dist/cli.js
CHANGED
|
@@ -486,7 +486,13 @@ async function handleAggregateServe(options) {
|
|
|
486
486
|
if (!Number.isInteger(port) || port < 0 || port > 65535) {
|
|
487
487
|
throw new Error(`Invalid port: ${portOption ?? ""}`);
|
|
488
488
|
}
|
|
489
|
-
const server = node_http_1.default.createServer(async (
|
|
489
|
+
const server = node_http_1.default.createServer(async (request, response) => {
|
|
490
|
+
const url = new URL(request.url ?? "/", "http://localhost");
|
|
491
|
+
if (url.pathname !== "/") {
|
|
492
|
+
response.writeHead(404);
|
|
493
|
+
response.end();
|
|
494
|
+
return;
|
|
495
|
+
}
|
|
490
496
|
try {
|
|
491
497
|
const html = await renderAggregateDashboardHtml(resolvedInputDir);
|
|
492
498
|
response.writeHead(200, {
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.alignTimeSeries = alignTimeSeries;
|
|
3
4
|
exports.summarizePeople = summarizePeople;
|
|
4
5
|
exports.summarizeOverall = summarizeOverall;
|
|
5
6
|
exports.buildAggregateDashboardHtml = buildAggregateDashboardHtml;
|
|
7
|
+
const chart_assets_1 = require("./chart-assets");
|
|
6
8
|
const time_1 = require("./time");
|
|
7
9
|
/** 折线 / 图例统一的调色板,保证同一个人在不同图表里颜色一致。 */
|
|
8
10
|
const CHART_PALETTE = ["#5eead4", "#f59e0b", "#a855f7", "#22c55e", "#f87171", "#60a5fa", "#fbbf24", "#34d399"];
|
|
@@ -26,6 +28,35 @@ function maxOrNull(values) {
|
|
|
26
28
|
const numbers = values.filter((value) => value !== null);
|
|
27
29
|
return numbers.length > 0 ? Math.max(...numbers) : null;
|
|
28
30
|
}
|
|
31
|
+
/**
|
|
32
|
+
* 把多条不规则时间戳的 series 并集对齐到一个统一 x 轴。
|
|
33
|
+
*
|
|
34
|
+
* 多人 5h/7d 详细曲线各人按真实采样时间戳记录、彼此不对齐,而 uPlot 要求所有 series
|
|
35
|
+
* 共用同一个 x 数组。这里取所有 series 时间戳的并集升序作统一 x,每条 series 在自己
|
|
36
|
+
* 没有采样的时间点填 `null`(uPlot 以 gap 呈现 / spanGaps 连接),保证读数对齐而不串位。
|
|
37
|
+
*/
|
|
38
|
+
function alignTimeSeries(seriesList) {
|
|
39
|
+
const xsSet = new Set();
|
|
40
|
+
for (const series of seriesList) {
|
|
41
|
+
for (const point of series) {
|
|
42
|
+
xsSet.add(point.t);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
const xs = [...xsSet].sort((left, right) => left - right);
|
|
46
|
+
const indexOf = new Map();
|
|
47
|
+
xs.forEach((t, index) => indexOf.set(t, index));
|
|
48
|
+
const columns = seriesList.map((series) => {
|
|
49
|
+
const column = new Array(xs.length).fill(null);
|
|
50
|
+
for (const point of series) {
|
|
51
|
+
const index = indexOf.get(point.t);
|
|
52
|
+
if (index !== undefined) {
|
|
53
|
+
column[index] = point.v;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return column;
|
|
57
|
+
});
|
|
58
|
+
return { xs, columns };
|
|
59
|
+
}
|
|
29
60
|
/**
|
|
30
61
|
* 按 personKey 聚合 daily 行,得到每个人的“整段时间”账单。
|
|
31
62
|
*
|
|
@@ -148,52 +179,29 @@ function renderDailyUserRequestChart(people, dailyIndex, dateAxis) {
|
|
|
148
179
|
</section>
|
|
149
180
|
`;
|
|
150
181
|
}
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
const
|
|
154
|
-
const
|
|
155
|
-
const
|
|
156
|
-
const
|
|
157
|
-
const
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
const
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
.map(({ date, index }) => {
|
|
175
|
-
const x = xFor(index);
|
|
176
|
-
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(date.slice(5))}</text></g>`;
|
|
177
|
-
})
|
|
178
|
-
.join("");
|
|
179
|
-
const seriesPaths = seriesData
|
|
180
|
-
.map((series, seriesIndex) => {
|
|
181
|
-
const color = palette[seriesIndex % palette.length];
|
|
182
|
-
const path = series.values
|
|
183
|
-
.map((value, index) => `${index === 0 ? "M" : "L"}${xFor(index).toFixed(2)} ${yFor(value).toFixed(2)}`)
|
|
184
|
-
.join(" ");
|
|
185
|
-
const points = series.values
|
|
186
|
-
.map((value, index) => `<circle cx="${xFor(index).toFixed(2)}" cy="${yFor(value).toFixed(2)}" r="3" fill="${color}"><title>${escapeHtml(series.person.personKey)} · ${escapeHtml(dateAxis[index])} · ${formatNumber(value)} 用户请求</title></circle>`)
|
|
187
|
-
.join("");
|
|
188
|
-
return `<g data-person="${escapeHtml(series.person.personKey)}"><path d="${path}" fill="none" stroke="${color}" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" />${points}</g>`;
|
|
189
|
-
})
|
|
190
|
-
.join("");
|
|
191
|
-
const legend = seriesData
|
|
192
|
-
.map((series, seriesIndex) => {
|
|
193
|
-
const color = palette[seriesIndex % palette.length];
|
|
194
|
-
return `<span class="legend-chip legend-toggle" data-person="${escapeHtml(series.person.personKey)}"><span class="legend-dot" style="background:${color}"></span>${escapeHtml(series.person.personKey)}</span>`;
|
|
195
|
-
})
|
|
196
|
-
.join("");
|
|
182
|
+
// x 复用共享 dateAxis:已对齐,无需并集。用 category x(索引 0..n-1 + 月-日标签),
|
|
183
|
+
// 每个人一条折线,缺失天沿用 0 计数。uPlot 原生 legend 全显示当前日各人值、点击 toggle 显隐。
|
|
184
|
+
const xs = dateAxis.map((_, index) => index);
|
|
185
|
+
const xLabels = dateAxis.map((date) => date.slice(5));
|
|
186
|
+
const seriesSpecs = [];
|
|
187
|
+
const columns = [];
|
|
188
|
+
const legendGroups = [];
|
|
189
|
+
people.forEach((person, index) => {
|
|
190
|
+
const color = CHART_PALETTE[index % CHART_PALETTE.length];
|
|
191
|
+
seriesSpecs.push({ label: person.personKey, stroke: color, width: 2 });
|
|
192
|
+
columns.push(dateAxis.map((date) => dailyIndex.get(`${person.personKey}|${date}`)?.userMessageCount ?? 0));
|
|
193
|
+
// 每人一条 series,图例一项即一个人名(小写)。
|
|
194
|
+
legendGroups.push({ label: person.personKey, color, seriesIdx: [index + 1] });
|
|
195
|
+
});
|
|
196
|
+
const spec = {
|
|
197
|
+
height: 320,
|
|
198
|
+
xType: "category",
|
|
199
|
+
xLabels,
|
|
200
|
+
yMin0: true,
|
|
201
|
+
series: seriesSpecs,
|
|
202
|
+
legendGroups,
|
|
203
|
+
};
|
|
204
|
+
const chart = (0, chart_assets_1.renderUplotChart)("chart-daily-requests", spec, [xs, ...columns]);
|
|
197
205
|
return `
|
|
198
206
|
<section class="panel chart-panel">
|
|
199
207
|
<div class="panel-header">
|
|
@@ -201,14 +209,9 @@ function renderDailyUserRequestChart(people, dailyIndex, dateAxis) {
|
|
|
201
209
|
<p class="eyebrow">Daily User Requests</p>
|
|
202
210
|
<h2>每日用户请求数对比</h2>
|
|
203
211
|
</div>
|
|
204
|
-
<p class="muted">基于每人 daily 汇总中的 userMessageCount(已剔除 tool_result 工具回填;sidechain 子 agent
|
|
212
|
+
<p class="muted">基于每人 daily 汇总中的 userMessageCount(已剔除 tool_result 工具回填;sidechain 子 agent 提示保留)。悬停任意日期可在跟随 tooltip 看到各人请求数,点击图例人名可切换该人曲线显隐。</p>
|
|
205
213
|
</div>
|
|
206
|
-
|
|
207
|
-
<svg viewBox="0 0 ${width} ${height}" class="chart" role="img" aria-label="每日用户请求数对比">
|
|
208
|
-
${ticks}
|
|
209
|
-
${xLabels}
|
|
210
|
-
${seriesPaths}
|
|
211
|
-
</svg>
|
|
214
|
+
${chart}
|
|
212
215
|
</section>
|
|
213
216
|
`;
|
|
214
217
|
}
|
|
@@ -281,41 +284,55 @@ function renderSevenDayCumulativeChart(people) {
|
|
|
281
284
|
</section>
|
|
282
285
|
`;
|
|
283
286
|
}
|
|
284
|
-
/** 把时间戳格式化成图表 X 轴用的本地「MM-DD HH:mm」短标签。 */
|
|
285
|
-
function formatTickTime(t) {
|
|
286
|
-
const d = new Date(t);
|
|
287
|
-
const mm = String(d.getMonth() + 1).padStart(2, "0");
|
|
288
|
-
const dd = String(d.getDate()).padStart(2, "0");
|
|
289
|
-
const hh = String(d.getHours()).padStart(2, "0");
|
|
290
|
-
const mi = String(d.getMinutes()).padStart(2, "0");
|
|
291
|
-
return `${mm}-${dd} ${hh}:${mi}`;
|
|
292
|
-
}
|
|
293
287
|
/**
|
|
294
|
-
* 用事件级 detail 行画出 5h 额度与 7d
|
|
288
|
+
* 用事件级 detail 行画出 5h 额度与 7d 周额度使用率的详细曲线(uPlot 渲染)。
|
|
295
289
|
*
|
|
296
290
|
* 与按天聚合的图不同,这里直接用每条 statusline 采样的真实时间戳,粒度最细,
|
|
297
291
|
* 能看出每个人 5 小时额度在一天里的爬升与重置节奏,同时叠加 7 天周额度的走势。
|
|
298
|
-
*
|
|
292
|
+
* 各人时间戳不规则、彼此不对齐,先用 alignTimeSeries 取并集成统一 x 轴、缺失填 null;
|
|
293
|
+
* 同一个人 5h 用 CHART_PALETTE 实线、7d 用 SEVEN_DAY_PALETTE 对比色虚线,共用 Y 轴。
|
|
294
|
+
* 悬停由跟随 tooltip 显示当前位置各 series 的值(2D 吸附,尖峰按高度命中),点击静态图例 toggle 显隐。
|
|
299
295
|
*/
|
|
300
296
|
function renderFiveHourUsageChart(people, detailRows) {
|
|
301
|
-
const
|
|
297
|
+
const built = people
|
|
302
298
|
.map((person, index) => {
|
|
303
299
|
const personRows = detailRows
|
|
304
300
|
.filter((row) => row.personKey === person.personKey)
|
|
305
301
|
.map((row) => ({ t: new Date(row.timestamp).getTime(), five: row.usagePct, seven: row.sevenDayUsagePct }))
|
|
306
302
|
.filter((point) => Number.isFinite(point.t))
|
|
307
303
|
.sort((left, right) => left.t - right.t);
|
|
304
|
+
// uPlot 时间轴以秒为单位,这里把毫秒时间戳折算成秒。
|
|
308
305
|
const fivePoints = personRows
|
|
309
306
|
.filter((point) => point.five !== null)
|
|
310
|
-
.map((point) => ({ t: point.t, v: point.five }));
|
|
307
|
+
.map((point) => ({ t: Math.floor(point.t / 1000), v: point.five }));
|
|
311
308
|
const sevenPoints = personRows
|
|
312
309
|
.filter((point) => point.seven !== null)
|
|
313
|
-
.map((point) => ({ t: point.t, v: point.seven }));
|
|
310
|
+
.map((point) => ({ t: Math.floor(point.t / 1000), v: point.seven }));
|
|
314
311
|
return { person, index, fivePoints, sevenPoints };
|
|
315
312
|
})
|
|
316
313
|
.filter((entry) => entry.fivePoints.length > 0 || entry.sevenPoints.length > 0);
|
|
317
|
-
|
|
318
|
-
|
|
314
|
+
// 把每个人的 5h / 7d 拆成独立 series,连同对应配色 spec 一起按顺序收集,再整体并集对齐。
|
|
315
|
+
// legendGroups 把同一个人的 5h + 7d series 索引归到一项,图例只显示一个人名、勾选联动两条线。
|
|
316
|
+
const seriesList = [];
|
|
317
|
+
const seriesSpecs = [];
|
|
318
|
+
const legendGroups = [];
|
|
319
|
+
for (const entry of built) {
|
|
320
|
+
const color = CHART_PALETTE[entry.index % CHART_PALETTE.length];
|
|
321
|
+
const sevenColor = SEVEN_DAY_PALETTE[entry.index % SEVEN_DAY_PALETTE.length];
|
|
322
|
+
const seriesIdx = [];
|
|
323
|
+
if (entry.fivePoints.length > 0) {
|
|
324
|
+
seriesList.push(entry.fivePoints);
|
|
325
|
+
seriesSpecs.push({ label: `${entry.person.personKey} · 5h`, stroke: color, width: 2 });
|
|
326
|
+
seriesIdx.push(seriesSpecs.length);
|
|
327
|
+
}
|
|
328
|
+
if (entry.sevenPoints.length > 0) {
|
|
329
|
+
seriesList.push(entry.sevenPoints);
|
|
330
|
+
seriesSpecs.push({ label: `${entry.person.personKey} · 7d`, stroke: sevenColor, dash: [5, 4], width: 2 });
|
|
331
|
+
seriesIdx.push(seriesSpecs.length);
|
|
332
|
+
}
|
|
333
|
+
legendGroups.push({ label: entry.person.personKey, color, seriesIdx });
|
|
334
|
+
}
|
|
335
|
+
if (seriesList.length === 0) {
|
|
319
336
|
return `
|
|
320
337
|
<section class="panel chart-panel">
|
|
321
338
|
<div class="panel-header">
|
|
@@ -328,53 +345,16 @@ function renderFiveHourUsageChart(people, detailRows) {
|
|
|
328
345
|
</section>
|
|
329
346
|
`;
|
|
330
347
|
}
|
|
331
|
-
const
|
|
332
|
-
const
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
const
|
|
341
|
-
const xFor = (t) => paddingX + (spanT === 0 ? 0.5 : (t - minT) / spanT) * innerWidth;
|
|
342
|
-
const yFor = (value) => paddingY + (1 - value / maxValue) * innerHeight;
|
|
343
|
-
const ticks = [0, 0.25, 0.5, 0.75, 1]
|
|
344
|
-
.map((fraction) => {
|
|
345
|
-
const tickValue = maxValue * fraction;
|
|
346
|
-
const y = paddingY + (1 - fraction) * innerHeight;
|
|
347
|
-
return `<g><line x1="${paddingX}" x2="${width - paddingX}" y1="${y}" y2="${y}" class="chart-grid" /><text x="8" y="${y + 4}" class="chart-axis">${tickValue.toFixed(1)}%</text></g>`;
|
|
348
|
-
})
|
|
349
|
-
.join("");
|
|
350
|
-
const labelCount = spanT === 0 ? 1 : 5;
|
|
351
|
-
const xLabels = Array.from({ length: labelCount + 1 }, (_, i) => minT + (spanT * i) / labelCount)
|
|
352
|
-
.map((t) => {
|
|
353
|
-
const x = xFor(t);
|
|
354
|
-
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>`;
|
|
355
|
-
})
|
|
356
|
-
.join("");
|
|
357
|
-
const pathFor = (points) => points.map((point, pointIndex) => `${pointIndex === 0 ? "M" : "L"}${xFor(point.t).toFixed(2)} ${yFor(point.v).toFixed(2)}`).join(" ");
|
|
358
|
-
const seriesPaths = series
|
|
359
|
-
.map((entry) => {
|
|
360
|
-
const color = CHART_PALETTE[entry.index % CHART_PALETTE.length];
|
|
361
|
-
const sevenColor = SEVEN_DAY_PALETTE[entry.index % SEVEN_DAY_PALETTE.length];
|
|
362
|
-
const fivePath = entry.fivePoints.length > 0
|
|
363
|
-
? `<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>`
|
|
364
|
-
: "";
|
|
365
|
-
const sevenPath = entry.sevenPoints.length > 0
|
|
366
|
-
? `<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>`
|
|
367
|
-
: "";
|
|
368
|
-
return `<g data-person="${escapeHtml(entry.person.personKey)}">${fivePath}${sevenPath}</g>`;
|
|
369
|
-
})
|
|
370
|
-
.join("");
|
|
371
|
-
const legend = series
|
|
372
|
-
.map((entry) => {
|
|
373
|
-
const color = CHART_PALETTE[entry.index % CHART_PALETTE.length];
|
|
374
|
-
const sevenColor = SEVEN_DAY_PALETTE[entry.index % SEVEN_DAY_PALETTE.length];
|
|
375
|
-
return `<span class="legend-chip legend-toggle" data-person="${escapeHtml(entry.person.personKey)}"><span class="legend-dot" style="background:${color}"></span><span class="legend-dot" style="background:${sevenColor}"></span>${escapeHtml(entry.person.personKey)}</span>`;
|
|
376
|
-
})
|
|
377
|
-
.join("");
|
|
348
|
+
const { xs, columns } = alignTimeSeries(seriesList);
|
|
349
|
+
const spec = {
|
|
350
|
+
height: 320,
|
|
351
|
+
xType: "time",
|
|
352
|
+
yUnit: "%",
|
|
353
|
+
yMin0: true,
|
|
354
|
+
series: seriesSpecs,
|
|
355
|
+
legendGroups,
|
|
356
|
+
};
|
|
357
|
+
const chart = (0, chart_assets_1.renderUplotChart)("chart-usage-detail", spec, [xs, ...columns]);
|
|
378
358
|
return `
|
|
379
359
|
<section class="panel chart-panel">
|
|
380
360
|
<div class="panel-header">
|
|
@@ -382,18 +362,9 @@ function renderFiveHourUsageChart(people, detailRows) {
|
|
|
382
362
|
<p class="eyebrow">Usage Detail</p>
|
|
383
363
|
<h2>5h / 7d 使用率详细曲线</h2>
|
|
384
364
|
</div>
|
|
385
|
-
<p class="muted">每条 statusline 采样的 5 小时额度(实线)与 7 天周额度(对比色虚线)使用率,按真实时间戳绘制、共用 Y
|
|
386
|
-
</div>
|
|
387
|
-
<div class="legend">
|
|
388
|
-
${legend}
|
|
389
|
-
<span class="legend-chip"><span class="legend-line legend-line-solid"></span>5h 使用率(实线)</span>
|
|
390
|
-
<span class="legend-chip"><span class="legend-line legend-line-dashed"></span>7d 周使用量(对比色虚线)</span>
|
|
365
|
+
<p class="muted">每条 statusline 采样的 5 小时额度(实线)与 7 天周额度(对比色虚线)使用率,按真实时间戳绘制、共用 Y 轴。悬停任意位置可在跟随 tooltip 看到各人 5h/7d 当前值(尖峰把鼠标移到竖线顶端即可读到峰值),点击图例可切换显隐。</p>
|
|
391
366
|
</div>
|
|
392
|
-
|
|
393
|
-
${ticks}
|
|
394
|
-
${xLabels}
|
|
395
|
-
${seriesPaths}
|
|
396
|
-
</svg>
|
|
367
|
+
${chart}
|
|
397
368
|
</section>
|
|
398
369
|
`;
|
|
399
370
|
}
|
|
@@ -687,25 +658,6 @@ function buildAggregateDashboardHtml(detailRows, dailyRows, weeklyRows, generate
|
|
|
687
658
|
.chart-grid { stroke: var(--grid); stroke-width: 1; }
|
|
688
659
|
.chart-axis { fill: var(--muted); font-size: 11px; }
|
|
689
660
|
.chart-axis-line { stroke: var(--grid); stroke-width: 1; }
|
|
690
|
-
.legend {
|
|
691
|
-
display: flex;
|
|
692
|
-
flex-wrap: wrap;
|
|
693
|
-
gap: 10px 18px;
|
|
694
|
-
padding: 14px 24px 0;
|
|
695
|
-
color: var(--muted);
|
|
696
|
-
font-size: 13px;
|
|
697
|
-
}
|
|
698
|
-
.legend-chip { display: inline-flex; align-items: center; gap: 8px; }
|
|
699
|
-
.legend-toggle { cursor: pointer; user-select: none; transition: opacity 0.15s ease; }
|
|
700
|
-
.legend-toggle:hover { color: var(--text); }
|
|
701
|
-
.legend-toggle.is-active { color: var(--text); font-weight: 600; }
|
|
702
|
-
.legend-toggle.is-dimmed { opacity: 0.4; }
|
|
703
|
-
.legend-dot { display: inline-block; width: 10px; height: 10px; border-radius: 50%; }
|
|
704
|
-
svg [data-person] { transition: opacity 0.15s ease; }
|
|
705
|
-
svg [data-person].is-dimmed { opacity: 0.1; }
|
|
706
|
-
svg [data-person].is-active { opacity: 1; }
|
|
707
|
-
.legend-line { display: inline-block; width: 22px; height: 0; border-top-width: 2px; border-top-style: solid; border-top-color: var(--muted); }
|
|
708
|
-
.legend-line-dashed { border-top-style: dashed; }
|
|
709
661
|
.table-panel { margin-top: 22px; }
|
|
710
662
|
.table-wrap { overflow: auto; padding: 16px 20px 22px; }
|
|
711
663
|
table { width: 100%; border-collapse: collapse; min-width: 760px; }
|
|
@@ -739,6 +691,7 @@ function buildAggregateDashboardHtml(detailRows, dailyRows, weeklyRows, generate
|
|
|
739
691
|
.panel-header { flex-direction: column; align-items: start; }
|
|
740
692
|
}
|
|
741
693
|
</style>
|
|
694
|
+
${(0, chart_assets_1.uplotHeadAssets)()}
|
|
742
695
|
</head>
|
|
743
696
|
<body>
|
|
744
697
|
<main class="shell">
|
|
@@ -793,26 +746,7 @@ function buildAggregateDashboardHtml(detailRows, dailyRows, weeklyRows, generate
|
|
|
793
746
|
${renderDailyMatrix(people, dailyIndex, dateAxis)}
|
|
794
747
|
${renderWeeklyTable(weeklyRows)}
|
|
795
748
|
</main>
|
|
796
|
-
|
|
797
|
-
(function () {
|
|
798
|
-
// 点击图例里的人名:高亮该人在所有图表里的曲线,其余淡化;再次点击同名取消高亮。
|
|
799
|
-
var active = null;
|
|
800
|
-
function apply() {
|
|
801
|
-
document.querySelectorAll("[data-person]").forEach(function (el) {
|
|
802
|
-
el.classList.remove("is-active", "is-dimmed");
|
|
803
|
-
if (active === null) return;
|
|
804
|
-
el.classList.add(el.getAttribute("data-person") === active ? "is-active" : "is-dimmed");
|
|
805
|
-
});
|
|
806
|
-
}
|
|
807
|
-
document.querySelectorAll(".legend-toggle").forEach(function (chip) {
|
|
808
|
-
chip.addEventListener("click", function () {
|
|
809
|
-
var person = chip.getAttribute("data-person");
|
|
810
|
-
active = active === person ? null : person;
|
|
811
|
-
apply();
|
|
812
|
-
});
|
|
813
|
-
});
|
|
814
|
-
})();
|
|
815
|
-
</script>
|
|
749
|
+
${(0, chart_assets_1.uplotBodyScripts)()}
|
|
816
750
|
</body>
|
|
817
751
|
</html>`;
|
|
818
752
|
}
|