返回 DeepSeek-Reasonix
stats.ts
根目录 / workers / crash-report / src / stats.ts
1 import { esc, page } from "./shell";
2 import { type User, userNav } from "./auth";
3 import { clip, reportGroups, statusPill, type CrashRow } from "./stats_reports";
4 export { clip, statusPill } from "./stats_reports";
5
6 type Daily = { date: string; users: number; opens: number };
7 type MetricRow = { signal: string; bucket: string; total: number };
8 type BarRow = { label: string; users: number };
9 type BarListOptions = {
10 limit?: number;
11 className?: string;
12 labelFormatter?: (label: string) => string;
13 };
14
15 type OverviewCounts = {
16 latestAdoptionPct: number | null;
17 openReports: number;
18 newLatestReports: number;
19 regressedReports: number;
20 criticalOpenReports: number;
21 };
22
23 export type StatsModule = "diagnostics" | "usage" | "preferences" | "health";
24
25 function lastDays(rows: Daily[], count: 7 | 30): Daily[] {
26 const byDate = new Map(rows.map((r) => [r.date, r]));
27 const out: Daily[] = [];
28 for (let i = count - 1; i >= 0; i--) {
29 const date = new Date(Date.now() - i * 86400000).toISOString().slice(0, 10);
30 out.push(byDate.get(date) ?? { date, users: 0, opens: 0 });
31 }
32 return out;
33 }
34
35 function chartTickStep(max: number, targetTicks = 4): number {
36 if (max <= targetTicks) return 1;
37 const raw = Math.max(1, max) / targetTicks;
38 const pow = 10 ** Math.floor(Math.log10(raw));
39 const fraction = raw / pow;
40 if (fraction <= 1) return pow;
41 if (fraction <= 2) return 2 * pow;
42 if (fraction <= 5) return 5 * pow;
43 return 10 * pow;
44 }
45
46 function chartTickLabel(n: number): string {
47 if (n >= 1_000_000) return `${Number((n / 1_000_000).toFixed(n % 1_000_000 === 0 ? 0 : 1))}m`;
48 if (n >= 1_000) return `${Number((n / 1_000).toFixed(n % 1_000 === 0 ? 0 : 1))}k`;
49 return String(Math.round(n));
50 }
51
52 export function i18n(en: string, zh: string): string {
53 return `<span data-i18n="en">${esc(en)}</span><span data-i18n="zh">${esc(zh)}</span>`;
54 }
55
56 export function i18nHTML(en: string, zh: string): string {
57 return `<span data-i18n="en">${en}</span><span data-i18n="zh">${zh}</span>`;
58 }
59
60 function dailyChart(days: Daily[]): string {
61 const W = 960;
62 const H = 220;
63 const plotLeft = 50;
64 const plotRight = 8;
65 const plotTop = 16;
66 const baseY = H - 26;
67 const plotH = baseY - plotTop;
68 const slot = (W - plotLeft - plotRight) / days.length;
69 const max = Math.max(1, ...days.map((d) => d.opens));
70 const step = chartTickStep(max);
71 const chartMax = Math.max(step, Math.ceil(max / step) * step);
72 const h = (v: number) => (v / chartMax) * plotH;
73 const ticks: number[] = [];
74 for (let v = 0; v <= chartMax; v += step) ticks.push(v);
75 const grid = ticks
76 .map((v) => {
77 const y = baseY - h(v);
78 return `<g><line x1="${plotLeft}" y1="${y}" x2="${W - plotRight}" y2="${y}" class="gridline"/><text x="${plotLeft - 8}" y="${y + 4}" text-anchor="end" class="ay">${chartTickLabel(v)}</text></g>`;
79 })
80 .join("");
81 const bars = days
82 .map((d, i) => {
83 const x = plotLeft + i * slot;
84 const label = i % 5 === 4 ? `<text x="${x + slot / 2}" y="${H - 8}" text-anchor="middle" class="ax">${d.date.slice(5)}</text>` : "";
85 return `<g><title>${esc(`${d.date} — ${d.users} users · ${d.opens} opens`)}</title>
86 <rect x="${x}" y="${plotTop}" width="${slot}" height="${plotH}" fill="transparent" pointer-events="all"/>
87 <rect x="${x + slot * 0.18}" y="${baseY - h(d.opens)}" width="${slot * 0.64}" height="${h(d.opens)}" rx="3" fill="var(--accent)" opacity="0.22"/>
88 <rect x="${x + slot * 0.3}" y="${baseY - h(d.users)}" width="${slot * 0.4}" height="${h(d.users)}" rx="3" fill="var(--accent)"/>
89 ${label}</g>`;
90 })
91 .join("");
92 return `<svg class="chart" viewBox="0 0 ${W} ${H}" role="img" aria-label="Daily active installs chart"><style>.ax,.ay{font:11px var(--mono);fill:var(--ink-3)}.gridline{stroke:var(--line);stroke-width:1}</style>
93 ${grid}${bars}</svg>`;
94 }
95
96 function bucketDisplayLabel(signal: string, bucket: string): string {
97 if (signal.includes("_model") && bucket.startsWith("custom_")) {
98 const model = bucket.slice("custom_".length).replace(/_/g, " ");
99 return `<span class="bucket-prefix">custom</span><span class="bucket-main">${esc(model)}</span>`;
100 }
101 return esc(bucket);
102 }
103
104 function barRow(r: BarRow, max: number, labelFormatter?: (label: string) => string): string {
105 const label = labelFormatter ? labelFormatter(r.label) : esc(r.label);
106 return `<div class="row" title="${esc(r.label)}"><span class="row-label">${label}</span><div class="row-bar"><div class="bar" style="width:${Math.max(3, Math.round((r.users / max) * 100))}%"></div></div><span class="n">${r.users}</span></div>`;
107 }
108
109 function listBars(rows: BarRow[], options: BarListOptions = {}): string {
110 if (!rows.length) return `<div class="empty">${i18n("No data in this window", "当前时间窗口暂无数据")}</div>`;
111 const max = Math.max(1, ...rows.map((r) => r.users));
112 const limit = options.limit ?? 5;
113 const visible = limit > 0 ? rows.slice(0, limit) : rows;
114 const hidden = limit > 0 ? rows.slice(limit) : [];
115 const className = options.className ? ` ${esc(options.className)}` : "";
116 const visibleRows = visible.map((r) => barRow(r, max, options.labelFormatter)).join("");
117 if (!hidden.length) return `<div class="bars-list${className}">${visibleRows}</div>`;
118 return `<div class="bars-list${className}">${visibleRows}<details class="bars-more"><summary><span class="more-closed">${i18nHTML(
119 `Show ${hidden.length} more`,
120 `展开 ${hidden.length} 项`,
121 )}</span><span class="more-open">${i18nHTML(`Hide ${hidden.length}`, `收起 ${hidden.length} 项`)}</span></summary><div class="bars-more-list">${hidden
122 .map((r) => barRow(r, max, options.labelFormatter))
123 .join("")}</div></details></div>`;
124 }
125
126 function labelizeBucket(bucket: string): string {
127 return bucket.replace(/^n_/, "").replace(/_/g, " ");
128 }
129
130 function sumMetric(rows: MetricRow[], signal: string): number {
131 return rows.filter((r) => r.signal === signal).reduce((sum, r) => sum + r.total, 0);
132 }
133
134 function topMetricBucket(rows: MetricRow[], signal: string): string {
135 const row = rows.filter((r) => r.signal === signal).sort((a, b) => b.total - a.total)[0];
136 return row ? `${labelizeBucket(row.bucket)} · ${row.total}` : "none";
137 }
138
139 function cacheHitRate(rows: MetricRow[]): number | null {
140 const cacheRows = rows.filter((r) => r.signal === "cache_hit");
141 const total = cacheRows.reduce((sum, r) => sum + r.total, 0);
142 if (!total) return null;
143 const weighted = cacheRows.reduce((sum, r) => {
144 const m = r.bucket.match(/^(\d+)_(\d+)$/);
145 const midpoint = m ? (Number(m[1]) + Number(m[2])) / 2 : 0;
146 return sum + midpoint * r.total;
147 }, 0);
148 return weighted / total;
149 }
150
151 function pct(n: number | null): string {
152 if (n === null || !Number.isFinite(n)) return "n/a";
153 return `${Math.round(n)}%`;
154 }
155
156 function ratioPer100(rows: MetricRow[], signal: string): number | null {
157 const turns = sumMetric(rows, "turns");
158 if (!turns) return null;
159 return (sumMetric(rows, signal) / turns) * 100;
160 }
161
162 function deltaLabel(current: number | null, previous: number | null, suffix = ""): string {
163 if (current === null || previous === null) return "new";
164 const delta = current - previous;
165 if (Math.abs(delta) < 0.05) return "flat";
166 const sign = delta > 0 ? "+" : "";
167 const rounded = Math.abs(delta) >= 10 ? Math.round(delta) : Number(delta.toFixed(1));
168 return `${sign}${rounded}${suffix}`;
169 }
170
171 const METRIC_SIGNAL_LABELS: Record<string, { en: string; zh: string }> = {
172 finish_reason: { en: "Finish reason", zh: "结束原因" },
173 empty_final: { en: "Empty final guard", zh: "空回复拦截" },
174 provider_error: { en: "Provider errors", zh: "Provider 错误" },
175 cache_hit: { en: "Cache hit rate", zh: "缓存命中率" },
176 tool_error: { en: "Tool errors", zh: "工具错误" },
177 updater_error: { en: "Updater errors", zh: "更新器错误" },
178 updater_event: { en: "Updater events", zh: "更新器事件" },
179 compaction: { en: "Compactions", zh: "压缩" },
180 turns: { en: "Turns", zh: "轮次" },
181 desktop_hang: { en: "Desktop hangs", zh: "桌面卡死" },
182 desktop_hang_age: { en: "Desktop hang age", zh: "桌面卡死时长" },
183 desktop_exit: { en: "Desktop exits", zh: "桌面退出" },
184 desktop_exit_phase: { en: "Abnormal exit phase", zh: "异常退出阶段" },
185 desktop_uptime: { en: "Uptime before exit", zh: "退出前运行时长" },
186 desktop_install: { en: "Install profile", zh: "安装方式" },
187 desktop_update_transition: { en: "Update transition", zh: "升级阶段" },
188 desktop_restore: { en: "Window restore", zh: "窗口恢复" },
189 desktop_webview2_failure: { en: "WebView2 failures", zh: "WebView2 故障" },
190 desktop_web_runtime_failure: { en: "Web runtime failures", zh: "Web Runtime 故障" },
191 desktop_web_runtime_outcome: { en: "Web runtime outcomes", zh: "Web Runtime 结果" },
192 recovery_failure: { en: "Recovery failures", zh: "恢复失败" },
193 recovery_rule_continue: { en: "Rule recovery continues", zh: "规则恢复继续" },
194 recovery_review_continue: { en: "Review recovery continues", zh: "复核恢复继续" },
195 recovery_human_prompt: { en: "Recovery prompts", zh: "恢复询问" },
196 recovery_human_continue: { en: "Human recovery continues", zh: "人工恢复继续" },
197 recovery_human_revise: { en: "Human recovery revisions", zh: "人工恢复修订" },
198 recovery_review_error: { en: "Recovery review errors", zh: "恢复复核错误" },
199 recovery_repeat_prompt: { en: "Repeated recovery prompts", zh: "重复恢复询问" },
200 recovery_review_latency: { en: "Recovery review latency", zh: "恢复复核耗时" },
201 client_surface: { en: "Client surface", zh: "客户端形态" },
202 client_version: { en: "Client version", zh: "客户端版本" },
203 settings_language: { en: "Settings: language", zh: "设置:语言" },
204 settings_desktop_layout: { en: "Settings: desktop style", zh: "设置:桌面风格" },
205 settings_theme: { en: "Settings: light/dark", zh: "设置:深浅模式" },
206 settings_theme_style: { en: "Settings: theme style", zh: "设置:主题" },
207 settings_close_behavior: { en: "Settings: close behavior", zh: "设置:关闭行为" },
208 settings_display_mode: { en: "Settings: transcript mode", zh: "设置:会话展示" },
209 settings_status_bar_style: { en: "Settings: status bar style", zh: "设置:信息栏样式" },
210 settings_status_bar_items_count: { en: "Settings: status bar items", zh: "设置:信息栏项数" },
211 settings_check_updates: { en: "Settings: update checks", zh: "设置:更新检查" },
212 settings_default_model: { en: "Settings: default model", zh: "设置:默认模型" },
213 settings_planner_model: { en: "Settings: planner model", zh: "设置:规划模型" },
214 settings_subagent_model: { en: "Settings: subagent model", zh: "设置:子代理模型" },
215 settings_subagent_effort: { en: "Settings: subagent effort", zh: "设置:子代理 effort" },
216 settings_reasoning_language: { en: "Settings: reasoning language", zh: "设置:推理语言" },
217 settings_provider_count: { en: "Settings: provider count", zh: "设置:Provider 数量" },
218 settings_provider_access_count: { en: "Settings: enabled providers", zh: "设置:启用 Provider 数量" },
219 settings_provider_access: { en: "Settings: provider access", zh: "设置:Provider 选择" },
220 settings_bot_enabled: { en: "Bot: enabled", zh: "机器人:总开关" },
221 settings_bot_model: { en: "Bot: default model", zh: "机器人:默认模型" },
222 settings_bot_tool_approval: { en: "Bot: tool approval", zh: "机器人:工具审批" },
223 settings_bot_allowlist: { en: "Bot: allowlist", zh: "机器人:白名单" },
224 settings_bot_allow_all: { en: "Bot: allow all", zh: "机器人:允许所有人" },
225 settings_bot_qq_enabled: { en: "Bot: QQ legacy", zh: "机器人:QQ 旧配置" },
226 settings_bot_feishu_enabled: { en: "Bot: Feishu legacy", zh: "机器人:飞书旧配置" },
227 settings_bot_weixin_enabled: { en: "Bot: Weixin legacy", zh: "机器人:微信旧配置" },
228 settings_bot_connection_count: { en: "Bot: connection count", zh: "机器人:连接数量" },
229 settings_bot_connection_provider: { en: "Bot: connection provider", zh: "机器人:连接渠道" },
230 settings_bot_connection_enabled: { en: "Bot: connection enabled", zh: "机器人:连接开关" },
231 settings_bot_connection_status: { en: "Bot: connection status", zh: "机器人:连接状态" },
232 settings_bot_connection_model: { en: "Bot: connection model", zh: "机器人:连接模型" },
233 settings_bot_connection_approval: { en: "Bot: connection approval", zh: "机器人:连接审批" },
234 cli_mode: { en: "CLI mode", zh: "CLI 模式" },
235 cli_profile: { en: "CLI profile", zh: "CLI 配置档" },
236 cli_permission_mode: { en: "CLI permission mode", zh: "CLI 权限模式" },
237 cli_session_mode: { en: "CLI session mode", zh: "CLI 会话模式" },
238 cli_turn_latency: { en: "CLI turn latency", zh: "CLI turn 延迟" },
239 cli_exit: { en: "CLI turn outcome", zh: "CLI turn 结果" },
240 };
241
242 const AGENT_METRIC_SIGNALS = [
243 "finish_reason",
244 "empty_final",
245 "provider_error",
246 "cache_hit",
247 "tool_error",
248 "updater_error",
249 "updater_event",
250 "compaction",
251 "turns",
252 "desktop_hang",
253 "desktop_hang_age",
254 "desktop_exit",
255 "desktop_exit_phase",
256 "desktop_uptime",
257 "desktop_install",
258 "desktop_update_transition",
259 "desktop_restore",
260 "desktop_webview2_failure",
261 "desktop_web_runtime_failure",
262 "desktop_web_runtime_outcome",
263 "cli_turn_latency",
264 "cli_exit",
265 "recovery_failure",
266 "recovery_rule_continue",
267 "recovery_review_continue",
268 "recovery_human_prompt",
269 "recovery_human_continue",
270 "recovery_human_revise",
271 "recovery_review_error",
272 "recovery_repeat_prompt",
273 "recovery_review_latency",
274 ];
275 const DEFAULT_OPEN_SETTING_GROUPS = new Set(["Client", "Models", "Providers"]);
276
277 const SETTINGS_METRIC_GROUPS: { en: string; zh: string; signals: string[] }[] = [
278 {
279 en: "Client",
280 zh: "客户端",
281 signals: ["client_surface", "client_version", "settings_language", "cli_mode", "cli_profile", "cli_permission_mode", "cli_session_mode"],
282 },
283 {
284 en: "Appearance and layout",
285 zh: "外观与布局",
286 signals: [
287 "settings_desktop_layout",
288 "settings_theme",
289 "settings_theme_style",
290 "settings_display_mode",
291 "settings_status_bar_style",
292 "settings_status_bar_items_count",
293 ],
294 },
295 {
296 en: "Models",
297 zh: "模型",
298 signals: [
299 "settings_default_model",
300 "settings_planner_model",
301 "settings_subagent_model",
302 "settings_subagent_effort",
303 "settings_reasoning_language",
304 ],
305 },
306 {
307 en: "Providers",
308 zh: "Provider",
309 signals: ["settings_provider_count", "settings_provider_access_count", "settings_provider_access"],
310 },
311 {
312 en: "Behavior toggles",
313 zh: "行为开关",
314 signals: ["settings_close_behavior", "settings_check_updates"],
315 },
316 {
317 en: "Bots",
318 zh: "机器人",
319 signals: [
320 "settings_bot_enabled",
321 "settings_bot_model",
322 "settings_bot_tool_approval",
323 "settings_bot_allowlist",
324 "settings_bot_allow_all",
325 "settings_bot_qq_enabled",
326 "settings_bot_feishu_enabled",
327 "settings_bot_weixin_enabled",
328 "settings_bot_connection_count",
329 "settings_bot_connection_provider",
330 "settings_bot_connection_enabled",
331 "settings_bot_connection_status",
332 "settings_bot_connection_model",
333 "settings_bot_connection_approval",
334 ],
335 },
336 ];
337
338 function metricSignalLabel(signal: string): string {
339 const label = METRIC_SIGNAL_LABELS[signal];
340 return label ? i18n(label.en, label.zh) : esc(signal);
341 }
342
343 function metricsBySignal(rows: MetricRow[]): Map<string, { label: string; users: number }[]> {
344 const bySignal = new Map<string, { label: string; users: number }[]>();
345 for (const r of rows) {
346 const list = bySignal.get(r.signal) ?? [];
347 list.push({ label: r.bucket, users: r.total });
348 bySignal.set(r.signal, list);
349 }
350 return bySignal;
351 }
352
353 function metricBlocks(bySignal: Map<string, BarRow[]>, signals: string[], options: { barLimit?: number } = {}): string {
354 return signals
355 .filter((signal) => bySignal.has(signal))
356 .map((signal) => {
357 const rows = bySignal.get(signal) ?? [];
358 return `<div class="metric-block"><h3>${metricSignalLabel(signal)}<span>${rows.length}</span></h3>${listBars(rows, {
359 limit: options.barLimit ?? 5,
360 className: "metric-bars",
361 labelFormatter: (label) => bucketDisplayLabel(signal, label),
362 })}</div>`;
363 })
364 .join("");
365 }
366
367 function metricsCards(rows: MetricRow[], signals = AGENT_METRIC_SIGNALS): string {
368 if (!rows.length)
369 return `<div class="empty">${i18n("No metrics yet — flows in once an opt-in build ships", "暂无运行指标 — 等 opt-in 版本发布后有数据")}</div>`;
370 const bySignal = metricsBySignal(rows);
371 const blocks = metricBlocks(bySignal, signals);
372 return blocks ? `<div class="metrics">${blocks}</div>` : `<div class="empty">${i18n("No data in this window", "当前时间窗口暂无数据")}</div>`;
373 }
374
375 function settingsDashboard(rows: MetricRow[], options: { collapseSections?: boolean } = {}): string {
376 const bySignal = metricsBySignal(rows);
377 const sections = SETTINGS_METRIC_GROUPS.map((group) => {
378 const availableSignals = group.signals.filter((signal) => bySignal.has(signal));
379 const blocks = metricBlocks(bySignal, group.signals);
380 if (!blocks) return "";
381 const heading = `<h3>${i18n(group.en, group.zh)}<span>${i18nHTML(`${availableSignals.length} metrics`, `${availableSignals.length} 项指标`)}</span></h3>`;
382 if (options.collapseSections && !DEFAULT_OPEN_SETTING_GROUPS.has(group.en)) {
383 return `<details class="pref-section pref-section-collapsed"><summary>${heading}</summary><div class="metrics pref-metrics">${blocks}</div></details>`;
384 }
385 return `<section class="pref-section">${heading}<div class="metrics pref-metrics">${blocks}</div></section>`;
386 })
387 .filter(Boolean)
388 .join("");
389 if (!sections) return `<div class="empty">${i18n("No settings preference metrics yet", "暂无设置偏好指标")}</div>`;
390 return `<div class="preference-dashboard">${sections}</div>`;
391 }
392
393 function healthLevel(kind: "cache" | "rate", value: number | null): "good" | "warn" | "bad" | "unknown" {
394 if (value === null) return "unknown";
395 if (kind === "cache") {
396 if (value >= 80) return "good";
397 if (value >= 50) return "warn";
398 return "bad";
399 }
400 if (value <= 1) return "good";
401 if (value <= 5) return "warn";
402 return "bad";
403 }
404
405 function countHealthLevel(value: number): "good" | "warn" | "bad" {
406 if (value <= 0) return "good";
407 if (value <= 2) return "warn";
408 return "bad";
409 }
410
411 function levelText(level: "good" | "warn" | "bad" | "unknown"): string {
412 if (level === "good") return i18n("Good", "健康");
413 if (level === "warn") return i18n("Watch", "关注");
414 if (level === "bad") return i18n("Risk", "风险");
415 return i18n("No data", "暂无数据");
416 }
417
418 function healthCard(
419 label: { en: string; zh: string },
420 value: string,
421 level: "good" | "warn" | "bad" | "unknown",
422 deltaHTML: string,
423 detailHTML: string,
424 ): string {
425 return `<div class="health-card ${level}"><div class="health-top"><span>${i18n(label.en, label.zh)}</span><b>${levelText(level)}</b></div>
426 <strong>${esc(value)}</strong><small>${deltaHTML}</small><p>${detailHTML}</p></div>`;
427 }
428
429 function healthDeltaHTML(value: string): string {
430 return i18nHTML(`${esc(value)} vs previous window`, `${esc(value)} 较上一窗口`);
431 }
432
433 function healthDetailHTML(rows: MetricRow[], signal: string): string {
434 return i18nHTML(`${esc(topMetricBucket(rows, signal))} top bucket`, `主要分桶:${esc(topMetricBucket(rows, signal))}`);
435 }
436
437 function agentHealth(rows: MetricRow[], previousRows: MetricRow[]): string {
438 if (!rows.length) return `<div class="empty">${i18n("No agent health metrics yet", "暂无运行健康指标")}</div>`;
439 const cache = cacheHitRate(rows);
440 const prevCache = cacheHitRate(previousRows);
441 const desktopHangs = sumMetric(rows, "desktop_hang");
442 const prevDesktopHangs = sumMetric(previousRows, "desktop_hang");
443 const abnormalExits = rows.filter((r) => r.signal === "desktop_exit" && r.bucket === "abnormal").reduce((sum, r) => sum + r.total, 0);
444 const prevAbnormalExits = previousRows.filter((r) => r.signal === "desktop_exit" && r.bucket === "abnormal").reduce((sum, r) => sum + r.total, 0);
445 const webRuntimeFailures = sumMetric(rows, "desktop_web_runtime_failure") + sumMetric(rows, "desktop_webview2_failure");
446 const prevWebRuntimeFailures = sumMetric(previousRows, "desktop_web_runtime_failure") + sumMetric(previousRows, "desktop_webview2_failure");
447 const rateCard = (signal: string, en: string, zh: string) => {
448 const value = ratioPer100(rows, signal);
449 const prev = ratioPer100(previousRows, signal);
450 return healthCard(
451 { en, zh },
452 value === null ? "n/a" : `${Number(value.toFixed(value < 10 ? 1 : 0))}/100`,
453 healthLevel("rate", value),
454 healthDeltaHTML(deltaLabel(value, prev, "/100")),
455 healthDetailHTML(rows, signal),
456 );
457 };
458 return `<div class="health-grid">
459 ${healthCard(
460 { en: "Cache hit rate", zh: "缓存命中率" },
461 pct(cache),
462 healthLevel("cache", cache),
463 healthDeltaHTML(deltaLabel(cache, prevCache, "pp")),
464 healthDetailHTML(rows, "cache_hit"),
465 )}
466 ${rateCard("provider_error", "Provider errors", "Provider 错误")}
467 ${rateCard("tool_error", "Tool errors", "工具错误")}
468 ${rateCard("empty_final", "Empty final guard", "空回复拦截")}
469 ${rateCard("compaction", "Compactions", "压缩")}
470 ${healthCard(
471 { en: "Desktop hangs", zh: "桌面卡死" },
472 String(desktopHangs),
473 countHealthLevel(desktopHangs),
474 healthDeltaHTML(deltaLabel(desktopHangs, prevDesktopHangs)),
475 healthDetailHTML(rows, "desktop_hang_age"),
476 )}
477 ${healthCard(
478 { en: "Abnormal desktop exits", zh: "桌面异常退出" },
479 String(abnormalExits),
480 countHealthLevel(abnormalExits),
481 healthDeltaHTML(deltaLabel(abnormalExits, prevAbnormalExits)),
482 healthDetailHTML(rows, "desktop_exit_phase"),
483 )}
484 ${healthCard(
485 { en: "Web runtime process failures", zh: "Web Runtime 进程故障" },
486 String(webRuntimeFailures),
487 countHealthLevel(webRuntimeFailures),
488 healthDeltaHTML(deltaLabel(webRuntimeFailures, prevWebRuntimeFailures)),
489 healthDetailHTML(rows, sumMetric(rows, "desktop_web_runtime_failure") ? "desktop_web_runtime_failure" : "desktop_webview2_failure"),
490 )}
491 </div>`;
492 }
493
494 function filterTab(label: string, zhLabel: string, href: string, active: boolean): string {
495 return `<a class="filter-tab${active ? " active" : ""}" href="${esc(href)}">${i18n(label, zhLabel)}</a>`;
496 }
497
498 function facetChip(row: { label: string; users: number }, active: string, hrefFor: (label: string) => string): string {
499 const label = row.label || "legacy";
500 return `<a class="facet-chip${active === row.label ? " active" : ""}" href="${esc(hrefFor(row.label))}" title="${esc(label)}"><span class="facet-label">${esc(label)}</span><b>${row.users}</b></a>`;
501 }
502
503 function facetChips(rows: { label: string; users: number }[], active: string, hrefFor: (label: string) => string, limit = 5): string {
504 if (!rows.length) return `<span class="filter-empty">${i18n("none", "暂无")}</span>`;
505 const visible = rows.slice(0, limit);
506 const activeRow = active ? rows.find((r) => r.label === active) : undefined;
507 if (activeRow && !visible.some((r) => r.label === activeRow.label)) visible.push(activeRow);
508 const visibleKeys = new Set(visible.map((r) => r.label));
509 const hidden = rows.filter((r) => !visibleKeys.has(r.label));
510 const chips = visible.map((r) => facetChip(r, active, hrefFor)).join("");
511 if (!hidden.length) return chips;
512 return `${chips}<details class="facet-more"><summary>${i18nHTML(`More ${hidden.length}`, `更多 ${hidden.length}`)}</summary><div class="facet-more-list">${hidden
513 .map((r) => facetChip(r, active, hrefFor))
514 .join("")}</div></details>`;
515 }
516
517 function statCard(label: { en: string; zh: string }, value: string, note: string, href: string, tone = ""): string {
518 return `<a class="overview-card ${tone}" href="${esc(href)}"><span>${i18n(label.en, label.zh)}</span><strong>${esc(value)}</strong><small>${note}</small></a>`;
519 }
520
521 function latestVersionShare(adoptionPct: number | null): string {
522 return adoptionPct === null ? "n/a" : `${Math.round(adoptionPct)}%`;
523 }
524
525 function topSeverityTone(openReports: number, regressedReports: number, criticalOpenReports: number): string {
526 if (criticalOpenReports || regressedReports) return "bad";
527 if (openReports) return "warn";
528 return "good";
529 }
530
531 function navLink(href: string, label: { en: string; zh: string }, active = false): string {
532 return `<a${active ? ` class="active" aria-current="page"` : ""} href="${esc(href)}">${i18n(label.en, label.zh)}</a>`;
533 }
534
535 export function renderStats(
536 data: {
537 daily: Daily[];
538 versions: { label: string; users: number }[];
539 platforms: { label: string; users: number }[];
540 crashes: CrashRow[];
541 metrics: MetricRow[];
542 previousMetrics: MetricRow[];
543 /** Oldest computed_at in the rollup; empty when the window was queried live. */
544 sources: { label: string; users: number }[];
545 diagnosticFacets?: {
546 osBuilds: BarRow[];
547 osRevisions: BarRow[];
548 distros: BarRow[];
549 distroVersions: BarRow[];
550 kernels: BarRow[];
551 sessions: BarRow[];
552 architectures: BarRow[];
553 channels: BarRow[];
554 runtimes: BarRow[];
555 runtimeEngines: BarRow[];
556 failureKinds: BarRow[];
557 failureReasons: BarRow[];
558 exitCodes: BarRow[];
559 recoveries: BarRow[];
560 gpuStates: BarRow[];
561 };
562 installationLinkedSince?: string;
563 structuredAttributionSince?: string;
564 firebaseStorage?: {
565 active: number;
566 compacted: number;
567 archiving: number;
568 archived: number;
569 reservedBytes: number;
570 budgetBytes: number;
571 outboxCount: number;
572 oldestOutboxSeconds: number;
573 };
574 overview: OverviewCounts;
575 latestVersion: string;
576 filters: {
577 surface: "desktop" | "cli";
578 status: string;
579 source: string;
580 version: string;
581 os: string;
582 platform: string;
583 osBuild?: string;
584 osRevision?: string;
585 distroId?: string;
586 distroVersion?: string;
587 kernelVersion?: string;
588 sessionType?: string;
589 arch?: string;
590 channel?: string;
591 runtimeVersion?: string;
592 runtimeEngine?: string;
593 failureKind?: string;
594 failureReason?: string;
595 exitCode?: string;
596 recovery?: string;
597 gpu?: string;
598 newLatest: boolean;
599 regressed: boolean;
600 windowDays: 7 | 30;
601 };
602 },
603 user: User,
604 activeModule: StatsModule = "usage",
605 ): string {
606 const days = lastDays(data.daily, data.filters.windowDays);
607 const range = data.filters.windowDays;
608 const rangeText = `${range}d`;
609 const diagnosticFacets = data.diagnosticFacets ?? {
610 osBuilds: [], osRevisions: [], distros: [], distroVersions: [], kernels: [], sessions: [],
611 architectures: [], channels: [], runtimes: [], runtimeEngines: [],
612 failureKinds: [], failureReasons: [], exitCodes: [], recoveries: [], gpuStates: [],
613 };
614 const totalUsers = days.at(-1)?.users ?? 0;
615 const anyPing = days.some((d) => d.opens > 0);
616 const agentMetrics = data.metrics.filter((r) => AGENT_METRIC_SIGNALS.includes(r.signal));
617 const previousAgentMetrics = data.previousMetrics.filter((r) => AGENT_METRIC_SIGNALS.includes(r.signal));
618 const isSettingsSignal = (signal: string) =>
619 signal === "client_surface" || signal === "client_version" || signal.startsWith("settings_") ||
620 ["cli_mode", "cli_profile", "cli_permission_mode", "cli_session_mode"].includes(signal);
621 const settingsMetrics = data.metrics.filter((r) => isSettingsSignal(r.signal));
622 const cache = cacheHitRate(agentMetrics);
623 const providerRate = ratioPer100(agentMetrics, "provider_error");
624 const toolRate = ratioPer100(agentMetrics, "tool_error");
625 const desktopHangs = sumMetric(agentMetrics, "desktop_hang");
626 const abnormalExits = agentMetrics
627 .filter((r) => r.signal === "desktop_exit" && r.bucket === "abnormal")
628 .reduce((sum, r) => sum + r.total, 0);
629 const webViewFailures = sumMetric(agentMetrics, "desktop_web_runtime_failure") + sumMetric(agentMetrics, "desktop_webview2_failure");
630 const healthWatchCount =
631 [healthLevel("cache", cache), healthLevel("rate", providerRate), healthLevel("rate", toolRate)].filter((v) => v === "warn" || v === "bad").length +
632 (desktopHangs > 0 ? 1 : 0) +
633 (abnormalExits > 0 ? 1 : 0) +
634 (webViewFailures > 0 ? 1 : 0);
635 const modulePath = (module: StatsModule) => (module === "usage" ? "/stats" : `/stats/${module}`);
636 const filterQS = (patch: Record<string, string>, module: StatsModule = activeModule) => {
637 const params = new URLSearchParams();
638 const put = (k: string, v: string) => {
639 if (v) params.set(k, v);
640 };
641 put("status", data.filters.status);
642 put("source", data.filters.source);
643 put("version", data.filters.version);
644 put("os", data.filters.os);
645 put("platform", data.filters.platform);
646 put("osBuild", data.filters.osBuild ?? "");
647 put("osRevision", data.filters.osRevision ?? "");
648 put("distro", data.filters.distroId ?? "");
649 put("distroVersion", data.filters.distroVersion ?? "");
650 put("kernel", data.filters.kernelVersion ?? "");
651 put("session", data.filters.sessionType ?? "");
652 put("arch", data.filters.arch ?? "");
653 put("channel", data.filters.channel ?? "");
654 put("runtime", data.filters.runtimeVersion ?? "");
655 put("engine", data.filters.runtimeEngine ?? "");
656 put("failureKind", data.filters.failureKind ?? "");
657 put("reason", data.filters.failureReason ?? "");
658 put("exitCode", data.filters.exitCode ?? "");
659 put("recovery", data.filters.recovery ?? "");
660 put("gpu", data.filters.gpu ?? "");
661 put("surface", data.filters.surface === "cli" ? "cli" : "");
662 if (data.filters.newLatest) params.set("new", "latest");
663 if (data.filters.regressed) params.set("regressed", "1");
664 if (data.filters.windowDays === 7) params.set("window", "7d");
665 for (const [k, v] of Object.entries(patch)) {
666 if (v) params.set(k, v);
667 else params.delete(k);
668 }
669 const qs = params.toString();
670 const path = modulePath(module);
671 return qs ? `${path}?${qs}` : path;
672 };
673 const clearFiltersHref = filterQS({ status: "", source: "", version: "", os: "", platform: "", osBuild: "", osRevision: "", distro: "", distroVersion: "", kernel: "", session: "", arch: "", channel: "", engine: "", runtime: "", failureKind: "", reason: "", exitCode: "", recovery: "", gpu: "", new: "", regressed: "" });
674 const hasFilters = Boolean(
675 data.filters.status || data.filters.source || data.filters.version || data.filters.os || data.filters.platform || data.filters.osBuild || data.filters.osRevision || data.filters.distroId || data.filters.distroVersion || data.filters.kernelVersion || data.filters.sessionType || data.filters.arch || data.filters.channel || data.filters.runtimeEngine || data.filters.runtimeVersion || data.filters.failureKind || data.filters.failureReason || data.filters.exitCode || data.filters.recovery || data.filters.gpu || data.filters.newLatest || data.filters.regressed,
676 );
677 const windowControls = `<div class="segmented" aria-label="Time window">
678 <a class="${range === 7 ? "active" : ""}"${range === 7 ? ` aria-current="true"` : ""} href="${esc(filterQS({ window: "7d" }))}">7d</a>
679 <a class="${range === 30 ? "active" : ""}"${range === 30 ? ` aria-current="true"` : ""} href="${esc(filterQS({ window: "" }))}">30d</a>
680 </div>`;
681 const surfaceControls = `<div class="segmented" aria-label="Client surface">
682 <a class="${data.filters.surface === "desktop" ? "active" : ""}"${data.filters.surface === "desktop" ? ` aria-current="true"` : ""} href="${esc(filterQS({ surface: "" }))}">${i18n("Desktop", "桌面端")}</a>
683 <a class="${data.filters.surface === "cli" ? "active" : ""}"${data.filters.surface === "cli" ? ` aria-current="true"` : ""} href="${esc(filterQS({ surface: "cli" }))}">CLI</a>
684 </div>`;
685 const overviewTone = topSeverityTone(data.overview.openReports, data.overview.regressedReports, data.overview.criticalOpenReports);
686 const isDevelopmentDiagnostic = (row: CrashRow) => row.development ?? row.fingerprint.startsWith("dev:");
687 const releaseDiagnostics = data.crashes.filter(
688 (row) => row.kind !== "performance" && row.severity !== "low" && !isDevelopmentDiagnostic(row),
689 );
690 const abnormalExitReports = releaseDiagnostics.filter((row) => row.last_category === "unclean_exit" || row.last_category === "startup_failure");
691 const historicalDiagnostics = releaseDiagnostics.filter((row) => row.last_category === "historical_record");
692 const releaseCrashes = releaseDiagnostics.filter((row) => !abnormalExitReports.includes(row) && !historicalDiagnostics.includes(row));
693 const performanceDiagnostics = data.crashes.filter(
694 (row) => row.kind === "performance" && !isDevelopmentDiagnostic(row),
695 );
696 const developmentDiagnostics = data.crashes.filter(isDevelopmentDiagnostic);
697 const firebaseStorage = data.firebaseStorage ? (() => {
698 const storage = data.firebaseStorage;
699 const percent = storage.budgetBytes > 0 ? storage.reservedBytes / storage.budgetBytes * 100 : 0;
700 const tone = percent >= 100 ? "bad" : percent >= 80 ? "warn" : "good";
701 const waiting = storage.oldestOutboxSeconds > 0
702 ? `${Math.floor(storage.oldestOutboxSeconds / 3600)}h ${Math.floor(storage.oldestOutboxSeconds % 3600 / 60)}m`
703 : "none";
704 return `<section class="module-panel"><h3>${i18n("Firebase Spark storage", "Firebase Spark 存储")}</h3>
705 <div class="overview-grid">
706 ${statCard({ en: "Reserved", zh: "已预留" }, `${(storage.reservedBytes / 1048576).toFixed(1)} MiB`, `${percent.toFixed(1)}% / 700 MiB`, "#", tone)}
707 ${statCard({ en: "Lifecycle", zh: "生命周期" }, `${storage.active}/${storage.compacted}/${storage.archiving}/${storage.archived}`, i18n("active / compacted / archiving / archived", "活跃 / 已压缩 / 归档中 / 已归档"), "#")}
708 ${statCard({ en: "Outbox", zh: "待投递" }, String(storage.outboxCount), i18nHTML(`oldest ${waiting}`, `最老 ${waiting === "none" ? "无" : waiting}`), "#", storage.outboxCount >= 4000 ? "warn" : "good")}
709 </div></section>`;
710 })() : "";
711 const overview = `<section class="overview-grid">
712 ${statCard({ en: "Active today", zh: "今日活跃" }, String(totalUsers), i18n("anonymous installs", "匿名安装"), filterQS({}, "usage"))}
713 ${statCard({ en: "Latest adoption", zh: "最新版本占比" }, latestVersionShare(data.overview.latestAdoptionPct), i18nHTML(`latest ${esc(data.latestVersion || "n/a")}`, `最新 ${esc(data.latestVersion || "n/a")}`), filterQS({}, "usage"))}
714 ${statCard({ en: "Open reports", zh: "未处理报告" }, String(data.overview.openReports), i18n("needs triage", "需要分诊"), filterQS({}, "diagnostics"), overviewTone)}
715 ${statCard({ en: "New in latest", zh: "最新新增" }, String(data.overview.newLatestReports), i18n("first seen on latest", "首次出现在最新版"), filterQS({}, "diagnostics"), data.overview.newLatestReports ? "warn" : "good")}
716 ${statCard({ en: "Regressions", zh: "回归问题" }, String(data.overview.regressedReports), i18n("previously resolved", "曾经解决后复现"), filterQS({}, "diagnostics"), data.overview.regressedReports ? "bad" : "good")}
717 ${statCard({ en: "Agent health", zh: "运行健康" }, healthWatchCount ? String(healthWatchCount) : "OK", i18nHTML(`${pct(cache)} cache · ${providerRate === null ? "n/a" : Number(providerRate.toFixed(1))}/100 provider · ${desktopHangs} hangs`, `${pct(cache)} 缓存 · ${providerRate === null ? "n/a" : Number(providerRate.toFixed(1))}/100 Provider · ${desktopHangs} 次卡死`), filterQS({}, "health"), healthWatchCount ? "warn" : "good")}
718 </section>`;
719 const pageOverview = activeModule === "usage" ? overview : "";
720 const dashboardNav = `<nav class="site-nav" aria-label="Stats navigation">
721 ${navLink(filterQS({}, "usage"), { en: "Home", zh: "主页" }, activeModule === "usage")}
722 ${navLink(filterQS({}, "diagnostics"), { en: "Diagnostics", zh: "诊断分诊" }, activeModule === "diagnostics")}
723 ${navLink(filterQS({}, "preferences"), { en: "Preferences", zh: "设置偏好" }, activeModule === "preferences")}
724 ${navLink(filterQS({}, "health"), { en: "Agent Health", zh: "运行健康" }, activeModule === "health")}
725 </nav>`;
726 const linkedSince = data.installationLinkedSince
727 ? `<p class="muted">${i18n("Installation-linked data available since", "可关联安装数据起始于")} ${esc(data.installationLinkedSince)}</p>`
728 : "";
729 const attributionSince = data.structuredAttributionSince ? `<p class="muted">${i18n("Fault/observer attribution available since", "故障/上报版本归因起始于")} ${esc(data.structuredAttributionSince)}</p>` : "";
730 const filters = `<div class="filter-card"><div class="filter-head"><h2>${i18n("Report filters", "诊断筛选")}</h2><span>${i18nHTML(`latest ${esc(data.latestVersion || "n/a")}`, `最新 ${esc(data.latestVersion || "n/a")}`)}</span></div>${linkedSince}${attributionSince}
731 <div class="filter-tabs">
732 ${filterTab("All", "全部", clearFiltersHref, !hasFilters)}
733 ${filterTab("Open", "未处理", filterQS({ status: "open" }), data.filters.status === "open")}
734 ${filterTab("Resolved", "已解决", filterQS({ status: "resolved" }), data.filters.status === "resolved")}
735 ${filterTab("Ignored", "已忽略", filterQS({ status: "ignored" }), data.filters.status === "ignored")}
736 ${filterTab("New in latest", "最新新增", filterQS({ new: data.filters.newLatest ? "" : "latest" }), data.filters.newLatest)}
737 ${data.latestVersion ? filterTab(`Latest release ${data.latestVersion}`, `最新正式版 ${data.latestVersion}`, filterQS({ version: data.filters.version === data.latestVersion ? "" : data.latestVersion }), data.filters.version === data.latestVersion) : ""}
738 ${filterTab("Regressed", "回归", filterQS({ regressed: data.filters.regressed ? "" : "1" }), data.filters.regressed)}
739 </div>
740 <div class="facet-grid">
741 <section><h3>${i18n("Source", "来源")}</h3><div class="facet-list">${facetChips(data.sources, data.filters.source, (label) => filterQS({ source: label }), 4)}</div></section>
742 <section><h3>${i18n("Fault version", "实际故障版本")}</h3><div class="facet-list">${facetChips(data.versions, data.filters.version, (label) => filterQS({ version: label }), 5)}</div></section>
743 <section><h3>${i18n("Platform", "平台")}</h3><div class="facet-list">${facetChips(data.platforms, data.filters.platform, (label) => filterQS({ platform: label }), 4)}</div></section>
744 <section><h3>${i18n("Windows build / revision", "Windows build / revision")}</h3><div class="facet-list">${facetChips(diagnosticFacets.osBuilds, data.filters.osBuild ?? "", (label) => filterQS({ osBuild: label }), 6)}${facetChips(diagnosticFacets.osRevisions, data.filters.osRevision ?? "", (label) => filterQS({ osRevision: label }), 4)}${data.filters.osBuild !== "17763" ? `<a class="facet-chip" href="${esc(filterQS({ osBuild: "17763" }))}"><span class="facet-label">LTSC 2019 · 17763</span></a>` : ""}</div></section>
745 <section><h3>${i18n("Linux distribution / session", "Linux 发行版 / 会话")}</h3><div class="facet-list">${facetChips(diagnosticFacets.distros, data.filters.distroId ?? "", (label) => filterQS({ distro: label }), 5)}${facetChips(diagnosticFacets.distroVersions, data.filters.distroVersion ?? "", (label) => filterQS({ distroVersion: label }), 4)}${facetChips(diagnosticFacets.kernels, data.filters.kernelVersion ?? "", (label) => filterQS({ kernel: label }), 4)}${facetChips(diagnosticFacets.sessions, data.filters.sessionType ?? "", (label) => filterQS({ session: label }), 4)}</div></section>
746 <section><h3>${i18n("Architecture / channel", "架构 / 渠道")}</h3><div class="facet-list">${facetChips(diagnosticFacets.architectures, data.filters.arch ?? "", (label) => filterQS({ arch: label }), 4)}${facetChips(diagnosticFacets.channels, data.filters.channel ?? "", (label) => filterQS({ channel: label }), 4)}</div></section>
747 <section><h3>Web Runtime</h3><div class="facet-list">${facetChips(diagnosticFacets.runtimeEngines, data.filters.runtimeEngine ?? "", (label) => filterQS({ engine: label }), 3)}${facetChips(diagnosticFacets.runtimes, data.filters.runtimeVersion ?? "", (label) => filterQS({ runtime: label }), 5)}</div></section>
748 <section><h3>${i18n("Failure kind / reason / exit", "故障类型 / 原因 / 退出码")}</h3><div class="facet-list">${facetChips(diagnosticFacets.failureKinds, data.filters.failureKind ?? "", (label) => filterQS({ failureKind: label }), 5)}${facetChips(diagnosticFacets.failureReasons, data.filters.failureReason ?? "", (label) => filterQS({ reason: label }), 5)}${facetChips(diagnosticFacets.exitCodes, data.filters.exitCode ?? "", (label) => filterQS({ exitCode: label }), 4)}</div></section>
749 <section><h3>${i18n("Recovery / GPU", "恢复 / GPU")}</h3><div class="facet-list">${facetChips(diagnosticFacets.recoveries, data.filters.recovery ?? "", (label) => filterQS({ recovery: label }), 4)}${facetChips(diagnosticFacets.gpuStates, data.filters.gpu ?? "", (label) => filterQS({ gpu: label }), 3)}</div></section>
750 </div></div>`;
751 const usageModule = `<section id="usage" class="card full module-card"><div class="module-head"><div><span>${i18n("Module", "模块")}</span><h2>${i18n("Usage distribution", "使用分布")}</h2></div></div>
752 <div class="module-panel wide"><h3>${i18nHTML(`Daily active installs <b>— ${rangeText}</b> (solid: users, faded: opens)`, `每日活跃 <b>— ${rangeText}</b>(实线:用户,淡色:打开次数)`)}</h3>
753 ${anyPing ? dailyChart(days) : `<div class="empty">${i18n("No pings yet — data starts flowing once a telemetry-enabled build ships", "暂无启动 ping — 等带统计的版本发布后这里开始有数据")}</div>`}</div>
754 <div class="module-split">
755 <section class="module-panel"><h3>${i18nHTML(`Versions <b>— ${rangeText}</b>`, `版本分布 <b>— ${rangeText}</b>`)}</h3>${listBars(data.versions)}</section>
756 <section class="module-panel"><h3>${i18nHTML(`Platforms <b>— ${rangeText}</b>`, `平台分布 <b>— ${rangeText}</b>`)}</h3>${listBars(data.platforms)}</section>
757 </div></section>`;
758 const diagnosticsModule = `<section id="diagnostics" class="card full module-card"><div class="module-head"><div><span>${i18n("Module", "模块")}</span><h2>${i18n("Diagnostic triage", "诊断分诊")}</h2></div><a class="module-action" href="#top">${i18n("Back to overview", "回到概览")}</a></div>
759 <p class="sub">${i18n("Installation-linked data is available only from the diagnostics-v2 deployment date; historical device counts are not backfilled.", "可关联安装的数据仅从 diagnostics-v2 部署日起提供;历史设备数不回填。")}</p>
760 ${firebaseStorage}
761 <section class="module-panel"><div class="panel-title"><h3>${i18nHTML("Needs attention <b>— top 10 release crashes and exceptions</b>", "优先处理 <b>— 正式版崩溃与异常 Top 10</b>")}</h3><span class="panel-context">${i18n(`Past ${range} days · ranked by affected installs`, `过去${range}天 · 按受影响安装降序`)}</span></div>${reportGroups(releaseCrashes.slice(0, 10), true, range)}</section>
762 ${abnormalExitReports.length ? `<section class="module-panel"><div class="panel-title"><h3>${i18nHTML("Abnormal exits <b>— cause not yet confirmed</b>", "异常退出 <b>— 原因尚未确定</b>")}</h3><span class="panel-context">${i18n(`Past ${range} days`, `过去${range}天`)}</span></div>${reportGroups(abnormalExitReports.slice(0, 10), true, range)}</section>` : ""}
763 ${historicalDiagnostics.length ? `<section class="module-panel"><div class="panel-title"><h3>${i18nHTML("Historical reports <b>— old versions and retired architecture</b>", "历史补报 <b>— 旧版本与旧架构</b>")}</h3><span class="panel-context">${i18n(`Past ${range} days received`, `过去${range}天接收`)}</span></div>${reportGroups(historicalDiagnostics.slice(0, 10), true, range)}</section>` : ""}
764 ${performanceDiagnostics.length ? `<section class="module-panel"><div class="panel-title"><h3>${i18nHTML("Performance signals <b>— tracked separately from crashes</b>", "性能信号 <b>— 与崩溃分开统计</b>")}</h3><span class="panel-context">${i18n(`Past ${range} days · ranked by affected installs`, `过去${range}天 · 按受影响安装降序`)}</span></div>${reportGroups(performanceDiagnostics.slice(0, 5), true, range)}</section>` : ""}
765 ${developmentDiagnostics.length ? `<section class="module-panel"><div class="panel-title"><h3>${i18nHTML("Development diagnostics <b>— excluded from release priority</b>", "开发版诊断 <b>— 不计入正式版优先级</b>")}</h3><span class="panel-context">${i18n(`Past ${range} days · ranked by affected installs`, `过去${range}天 · 按受影响安装降序`)}</span></div>${reportGroups(developmentDiagnostics.slice(0, 5), true, range)}</section>` : ""}
766 ${filters}
767 <section class="module-panel"><h3>${i18nHTML("All report groups <b>— open, regression, severity, count, recency</b>", "全部诊断分组 <b>— 未处理、回归、严重性、次数和最近出现</b>")}</h3>${reportGroups(data.crashes, false, range)}</section>
768 </section>`;
769 const preferencesModule = `<section id="preferences" class="card full module-card"><div class="module-head"><div><span>${i18n("Module", "模块")}</span><h2>${i18n("Settings preferences", "设置偏好")}</h2></div></div>
770 <section class="module-panel"><h3>${i18nHTML(`Launch/open snapshots <b>— ${rangeText}</b>`, `启动/开启快照 <b>— ${rangeText}</b>`)}</h3>${settingsDashboard(settingsMetrics, { collapseSections: true })}</section></section>`;
771 const healthModule = `<section id="health" class="card full module-card"><div class="module-head"><div><span>${i18n("Module", "模块")}</span><h2>${i18n("Agent health", "运行健康")}</h2></div><div class="module-actions"><a class="module-action" href="${esc(filterQS({}, "preferences"))}">${i18n("Preferences", "设置偏好")}</a></div></div>
772 <section class="module-panel"><h3>${i18nHTML(`Health summary <b>— ${rangeText}, compared with previous window</b>`, `健康摘要 <b>— ${rangeText},对比上一窗口</b>`)}</h3>${agentHealth(agentMetrics, previousAgentMetrics)}</section>
773 <section class="module-panel"><h3>${i18nHTML(`Signal distributions <b>— ${rangeText}, opt-in aggregate</b>`, `信号分布 <b>— ${rangeText},opt-in 汇总</b>`)}</h3>${metricsCards(agentMetrics)}</section>
774 </section>`;
775 const activeModuleHTML: Record<StatsModule, string> = {
776 diagnostics: diagnosticsModule,
777 usage: usageModule,
778 preferences: preferencesModule,
779 health: healthModule,
780 };
781
782 return page(
783 "Reasonix · Crash & Telemetry",
784 "health",
785 `${dashboardNav}
786 <div id="top" class="hero-line"><div><h1>${i18n("Crash & Telemetry", "客户端健康看板")}</h1><p class="sub">${i18nHTML(
787 `${rangeText} window · anonymous launch pings, opt-in aggregate metrics, and user-sent diagnostic reports only`,
788 `${rangeText} 时间窗口 · 仅包含匿名启动 ping、opt-in 汇总指标和用户发送的诊断报告`,
789 )}</p></div><div class="module-actions">${surfaceControls}${windowControls}</div></div>
790 ${pageOverview}
791 <div class="grid">
792 ${activeModuleHTML[activeModule]}
793 </div>`,
794 userNav(user),
795 );
796 }
797
797 lines TYPESCRIPT