diff --git a/docs/feature-test-coverage-checklist.html b/docs/feature-test-coverage-checklist.html
new file mode 100644
index 0000000..5305140
--- /dev/null
+++ b/docs/feature-test-coverage-checklist.html
@@ -0,0 +1,340 @@
+
+
+
+
+
+
Pangolin 功能 × 测试覆盖清单
+
逐功能盘点:现有能力 → 已覆盖测试(怎么覆盖)→ 欠缺 → 只能人工。作为「验证五个终端是否正确」的对照工作表。
+
+
+
怎么用:本清单分两部分——
A 共享逻辑 + 后端(控制面 + 数据面),跑一次自动化套件即对全平台生效;
B 平台隧道层,五端各自不同,须逐端按底部验收清单打勾。每行的「覆盖」标签直接告诉你这条是
已自动化、
部分/契约级、
零覆盖 还是
本质上只能人工。
+
+
+
+
+ 自动 CI 里有断言守门,回归即红
+ 部分 只到契约/单元,未覆盖端到端或边界
+ 零 当前无任何测试
+ 人工 本质上只能人工/真设备验证
+
+
「覆盖」列给的是当前状态,不是目标;标 零/部分 的「欠缺」列写清下一步补什么。标 人工 的不是缺陷,是真设备/真出网/真链路这类自动化跑不动、必须人测的部分——集中收在 §A.4 / §B 验收清单。
+
+
+
A · 共享逻辑 + 后端(控制面 + 数据面)
+
这部分代码四端共用(Flutter 业务逻辑 / Go 控制面 / 数据面渲染),自动化跑一次即对五个终端同时生效。验证策略:能自动化的全压到 CI,逐端不重复测。
+
+
+
A.1 控制面(Go 控制面 HTTP API + gRPC,server/internal/*)
+
账号 / 鉴权
+
+| 功能 | 现有能力 | 已覆盖 + 怎么覆盖 | 欠缺 / 只能人工 |
+| 注册 / 登录 / 刷新 自动 |
+邮箱注册、密码登录、refresh token 轮换、登出;/v1/auth/{register,login,refresh,code} |
+auth/service_test · handler_test · token_test · integration_test:真库(SQLite)跑完整 register→login→refresh 链;token 签发/校验/过期纯函数单测;password_test 哈希;helpers_test |
+真邮件投递(SMTP)只 mock;账号枚举/暴力破解的真实速率压测仅 ratelimit_test 逻辑级 |
+| 邮箱验证码 自动 |
+发码、校验、节流;emailcheck |
+auth/emailcheck_test · ratelimit_test:码生成/校验/节流窗口逻辑断言 |
+真 SMTP 通道、到达率、垃圾箱判定 → 人工 |
+| 两步验证 TOTP 自动 |
+TOTP 绑定/校验、备份码 |
+totp/totp_test(算法)+ auth/totp_user_test(绑定/校验流程) |
+真 Authenticator app 互操作 → 人工(一次性) |
+| 管理后台 自动 |
+admin 登录/会话、IP 白名单、配置、加密 |
+admin/{auth,session,mw_ipallow,config,crypto,handlers,services_real}_test:会话签发、IP allowlist 中间件、handler 行为 |
+后台前端页面交互 → 人工 |
+
+
+
业务 / 计费
+
+| 功能 | 现有能力 | 已覆盖 + 怎么覆盖 | 欠缺 / 只能人工 |
+| 兑换码 / 套餐 自动 |
+码生成、兑换、套餐授予;/v1/redeem · /v1/plans |
+codes/{generator,service}_test:码生成唯一性、兑换幂等、过期/已用拒绝 |
+— |
+| 支付 webhook 部分 |
+第三方支付回调 → 充值/开通 |
+codes/webhook_test:回调签名校验 + 入账逻辑(mock 上游) |
+真支付渠道端到端(下单→回调→开通)仅 mock;真渠道沙箱 → 人工 |
+| 激励解锁配额 自动 |
+看广告解锁额度;/v1/ads/unlock |
+usage/ads_test:解锁额度计算/上限 |
+真广告 SDK 回调 → 人工 |
+| 设备管理 自动 |
+设备注册/列举/解绑、上限;/v1/me/devices |
+devices/service_test · devices_integration_test:真库注册→列举→解绑;注册回填 last_seen(修过的真 bug) |
+— |
+
+
+
节点 / 调度 / 供给
+
+| 功能 | 现有能力 | 已覆盖 + 怎么覆盖 | 欠缺 / 只能人工 |
+| 节点注册表 + 连接 自动 |
+列节点、连接/断开、ping;/v1/nodes · /connect · /disconnect |
+nodes/{grpc,hub,lifecycle}_test:gRPC hub 收发、节点生命周期状态机 |
+真节点延迟/可用性探测准确度 → 见数据面 §A.2 |
+| Agent mTLS 接入 自动 |
+CA 签发、CRL 吊销、节点身份、bootstrap token |
+mtls/{ca,crl,identity,bootstrap}_test + nodectl/bootstrap_token_test:证书链签发/校验/吊销 |
+真 mTLS 握手在 §A.2 e2e 覆盖一段 |
+| 自动调度 / 故障替换 自动 |
+探测引擎、熔断、容量、自动替换编排、探针接入 |
+scheduler/detect/engine_test · orchestrate/{breaker,capacity,config,replacer}_test · probe/{ingest,prober_agent}_test · wiring_{lifecycle,provision}_test:健康判定→熔断→替换决策全链逻辑 |
+真云厂商触发的端到端替换 → 人工(贵/慢) |
+| 云供给 provision 部分 |
+cloud-init 渲染、provider 注册表、节点替换 |
+provision/{cloudinit,replace,service}_test · providers/registry_test(fakes 注入) |
+真厂商 API 开机/销毁 → 人工(按需,烧钱) |
+| 告警 自动 |
+Telegram 通知、runbook |
+alert/{notifier,runbook}_test:触发条件 + 去抖逻辑 |
+真 Telegram 投递 → 人工 |
+
+
+
持久层 / 可移植性
+
+| 功能 | 现有能力 | 已覆盖 + 怎么覆盖 | 欠缺 / 只能人工 |
+| 双数据库 SQLite/MySQL 自动 |
+裸 SQL + 方言层;迁移分两套;upsert/行锁中性记法 |
+db/dialect_test · sqlite_smoke_test;store/{sqlite_stores,sqlite_migrate,sqlite_per_device,mysql,mysql_integration}_test:SQLite 实库 + MySQL testcontainers 真库跑同一套断言;ci/scan-portable-sql.sh 扫禁用 MySQL 专属构造 |
+真 512MB VPS 上的并发/锁竞争压测 → 人工 |
+
+
+
+
A.2 数据面(sing-box 配置渲染 + per-user 记账,httpapi/clientconfig · agentd/*)
+
「连上了能不能真出网、记账准不准」的源头逻辑。渲染/解析有测试,真链路靠 e2e 一段 + 人工兜底。
+
+| 功能 | 现有能力 | 已覆盖 + 怎么覆盖 | 欠缺 / 只能人工 |
+| 客户端配置渲染 自动 |
+BuildClientConfig 服务端渲染原样下发:REALITY 出站、TUN 入站(strict_route 杀开关)、DNS 劫持首条规则、国内分流 |
+httpapi/clientconfig_test:断言 REALITY 公钥/short-id/端口、TUN auto_route/strict_route、hijack-dns 规则排在 LAN 前、split_cn 开关 |
+渲染对 ≠ 内核吃得下:真 sing-box 加载该配置并连通 → §B 人工 |
+| 节点配置渲染(agent 侧)自动 |
+agent 渲染 sing-box 服务端配置 + 凭证增删/轮换/吊销 |
+agentd/singbox_test:Upsert/Revoke/Rotate/ApplyConfig 状态机;derive_test 数据口凭证派生;hy2cert_test 证书;command_test |
+真 systemctl restart sing-box 后端口真监听 → 人工 |
+| per-user 流量采集 部分 |
+V2RayUsageSource:agent 读 sing-box v2ray_api StatsService per-user 计数器 → 聚合 → ReportUsage |
+新增 agentd/usage_v2ray_test:表驱动 parseUserStat + loopback 假 StatsService 跑真 Collect(),验聚合/方向不串/全0丢弃/reset=true 窗口语义 |
+假 StatsService ≠ 真 sing-box:真出网流量经真 v2ray_api 的计数准确度 → 人工 |
+| 记账全链 e2e 部分 |
+enroll → ReportUsage → 统计入库 → API 读出 |
+server/test/e2e/smoke_test:进程级 gRPC 全链路(agent enroll→上报→控制面统计真入库真读出),HTTP 段 + miniredis |
+缺 /v1/usage/devices 分设备断言(stats-overhaul 合并后补回);真节点真流量不在 e2e |
+| REALITY 数据口连通 人工 |
+客户端经 REALITY 连节点 443 真出网 |
+—(握手是真协议真节点,自动化跑不动) |
+真握手成功 + 出网 → §B 每端验收清单第 1–2 步 |
+
+
+
+
A.3 客户端共享逻辑(Flutter client/lib/*,四端共用 Dart)
+
+| 功能 | 现有能力 | 已覆盖 + 怎么覆盖 | 欠缺 / 只能人工 |
+| API 客户端 + 契约 自动 |
+ApiClient 取数、错误映射;/v1/me · usage · usage/devices |
+unit/api_client_test;contract/{api_contract,stats_contract}_test 冻结 wire 字段形状;Go 侧 httpapi/contract_test · usage/contract_test · pb/agentv1/contract_test 两侧对齐 |
+— |
+| 登录 / 续登 / 登出流程 自动 |
+AuthNotifier 状态机 + token 存储 |
+unit/flow_auth_test:真控制器 + MockClient 注入,跑登录→重启续登→登出 |
+真 flutter_secure_storage keychain 行为 → 人工(一次性) |
+| 连接状态机 自动 |
+connectionProvider connecting→connected→error;ping 探测 |
+unit/{connection_controller,flow_connect}_test:真控制器 + 假桥,验状态流转;connect_passthrough_test |
+真隧道回调时序(连上才启动 app 等触发条件)→ §B |
+| 统计计算 + 上屏 自动 |
+月 GB/时长聚合、周柱、分设备归因;stats_page |
+unit/{device_usage,format}_test 解析/格式化;新增 widget/stats_page_test:真 wire 形态喂 StatsPage 断言指标卡/周柱/分设备数值真上屏 |
+—(解析对 + 上屏对都已守) |
+| 配额 / 节点列表 自动 |
+quotaProvider · nodesProvider |
+unit/{quota_controller,nodes_provider}_test |
+— |
+| UI 视觉一致性 自动 |
+各页面/组件像素还原(亮/暗 × 中/英 × 手机/平板/桌面) |
+golden/{components,auth_redesign,desktop_pages,tablet_pages}_test Linux 权威基线;widget/{cards,connect_button}_test;responsive/form_factor_test;fonts_test 字体锁 |
+真机不同 DPI/字号/深色模式实机观感 → 人工(抽查) |
+| 桥接口契约 部分 |
+VpnBridge 抽象 + 桌面子进程实现 + mock |
+bridge/{vpn_bridge_mock,kernel_process,desktop_vpn_bridge_m4m5}_test:内核进程查找/启停、mock 桥行为 |
+原生 MethodChannel 两侧真实编解码 → §B(各端原生层) |
+
+
+
设计契约 · 五端 UI 同源
+
五端跑同一个 Flutter 工程(client/lib 共享 Dart),UI 不按平台分叉——颜色/样式/组件不是「约定一致」,是物理上同一份代码。下表是「五端同源」各维度的单源 + 守门状态。两处关键盲区:① token 有漂移闸,logo/app-icon 资产没有;② 「生成物不漂移」有闸,但「screen 是否真用真相源」(依从性) 零闸、且所有闸无反向 case 自检。
+
+| 维度 | 单源(真相源 → 生成/消费) | 守门机制 | 欠缺 / 风险 |
+| 颜色/间距/圆角/字体 自动 |
+design/colors_and_type.css(clay/sand 色板)→ gen_flutter_tokens.mjs → pangolin_tokens.gen.dart(勿手改)→ pangolin_theme.dart 语义层;业务禁硬编码 hex |
+CI codegen-drift 闸(ci/check-codegen-drift.sh):重生成与提交版不一致即红,防「改 CSS 没重生成 / 手改生成物」 |
+—(单源 + 自动闸,最规范的一块) |
+| 组件 自动 |
+client/lib/widgets/ 唯一实现;design/ 禁放 Dart 副本(会漂移);规格在 design/CONTRACT.md + design/preview/ |
+golden 测试:组件 + 各页面 × 明/暗 × 中/英 × 手机/平板/桌面,像素回归即红 |
+— |
+| 图标库 自动 |
+lucide_icons(细线条)全端同一套字体图标 |
+随 golden 像素守 |
+— |
+| 品牌 logo(应用内)部分 |
+design/assets/*.svg(mark / wordmark / app-icon)→ 拷贝一份到 client/assets/,flutter_svg 加载,四端同一份矢量 |
+无自动闸——design↔client 双拷贝靠手动同步(当前 4 个 SVG 已 diff 确认一致) |
+改 design/assets 那份忘同步 client/assets,CI 静默放过 → 待补 drift 闸 |
+| app 启动图标 部分 |
+assets/app-icon.svg → app-icon-ios-1024.png → flutter_launcher_icons 一键铺五端全尺寸(iOS appiconset / Android mipmap / macOS / Windows .ico) |
+无 drift 闸——生成物(各端 PNG/ico,如 iOS 22 张)入库,靠手动 dart run flutter_launcher_icons |
+改 app-icon.svg 忘重跑生成,五端图标与源图分叉,CI 静默放过 → 待补 drift 闸 |
+| 设计契约文档 人工 |
+design/CONTRACT.md(design-distill 蒸馏):token 映射 / 组件映射 / 逐屏像素规格 / 逐屏验收清单(明×暗×中×英) |
+人读契约;像素侧由 golden 兜底 |
+「还原对不对」的逐屏验收是人工抽查(截图 diff 可半自动) |
+screen 依从性 (源自真相源)零 |
+要求 screen/widget 的颜色/字号/间距必须源自 PangolinColors/PangolinText/PangolinSpacing,不得硬编码 hex/数值或绕过 widgets/ 自拼组件 |
+无任何检测。codegen-drift 只守「生成物=CSS 源」不守消费端是否用它;analysis_options.yaml 无禁硬编码规则;design/_adherence.oxlintrc.json 能抓 raw hex/px 但只对 JSX 原型、warn 级、未接 CI |
+实测 lib/screens 已有 62+ 处硬编码 fontSize:/EdgeInsets 数值无人拦(如 account_page.dart:187 fontSize: 16)→ 待补依从性扫描闸(第①档:正则扫 raw 字面量,仿 scan-portable-sql.sh) |
+| 闸的反向 case 自检 零 |
+每个闸应有「注入违规 → 断言闸变红」的负向自检,证明闸非永真(怎么改都绿 = 形同虚设) |
+无任何反向 case(全仓 grep 零结果)——含 codegen-drift 在内的所有闸都没验证过「真能拦住违规」 |
+→ 待补 fixture 自检(备一行违规样本,CI 跑「扫它必须非零退出」;新依从性闸与 codegen-drift 都该配) |
+
+
关键区别:codegen-drift(生成物不漂移)≠ 依从性(消费端真用真相源)。前者保证真相源本身没被改歪,后者保证 screen 真的去用了真相源——当前只有前者有闸,后者零守门、且所有闸都缺反向 case 自检。规划见 plan:UI 真相源依从性收口(本轮仅登记,补闸后续做)。
+
待补(性价比高):仿 ci/check-codegen-drift.sh 给 logo SVG 双拷贝 和 app-icon 生成物 各加一个 drift 闸——重新生成/比对 design↔client,不一致即红,把「图标 logo 五端同源」从靠纪律升成靠闸。这是 token 已有、资产尚缺的一行。
+
+
+
A.4 后端「只能人工」汇总
+
+
+- ☐ 真 SMTP / Telegram / 支付渠道 / 广告 SDK 的真实投递与回调(外部第三方,仅 mock 到逻辑边界)
+- ☐ 真云厂商 provision 开机→销毁→替换端到端(按需手测,烧钱)
+- ☐ 真 sing-box 加载渲染配置后端口真监听、真 v2ray_api 计数准确(渲染/解析已自动,运行时人测)
+- ☐ 512MB VPS 真并发/锁竞争压测
+- ☐ 后台前端页面交互(admin UI)
+
+
+
+
+
B · 平台隧道层(五端逐一)
+
真正逐端不同的只有原生隧道实现——这是测试架构 ⑩ 标注的零自动化盲区。三种策略:
+
+- 策略①内嵌 libbox + 系统 VPN 扩展:iOS / iPad(
NEPacketTunnelProvider + VpnManager.swift)、Android(libbox.aar + PangolinVpnService.kt VpnService)、macOS(PacketTunnel 系统扩展,当前默认关 kUseNativeVpnMacOS=false)
+- 策略②sing-box 子进程 + TUN:macOS(默认,
DesktopVpnBridge + kernel_process.dart)、Windows(sing-box.exe + wintun.dll)、Linux
+- 桥接原生↔Dart:MethodChannel/EventChannel(
VpnEventBus.kt / VpnChannel.swift / StatsClient.swift)传状态与统计——「mac 统计恒为 0」就出在这条没契约守门
+
+
+
各端隧道状态矩阵
+
+| 终端 | 隧道实现 | 已覆盖 | 欠缺 / 验证重点 |
+| Android |
+PangolinVpnService.kt(VpnService) + libbox.aar;DefaultNetworkMonitor.kt 网络监听;VpnEventBus.kt 事件桥 |
+零 原生层无自动化测试 |
+VpnService 授权弹窗、TUN fd 建立、libbox 启动、断网重连、统计回传桥;真机连通 + 出网 |
+| iOS |
+VpnManager.swift(NEPacketTunnelProvider) + 内嵌 libbox;StatsClient.swift 统计 |
+零 原生层无自动化测试 |
+NE 内存 ≤50MB 闸、NE profile 安装授权、libbox 后台队列启动、TestFlight 分发;真机连通 + 出网 |
+| iPad |
+同 iOS 二进制;额外横屏侧栏布局 |
+部分 布局走 golden(tablet_pages),隧道同 iOS 零 |
+同 iOS + 横屏/分屏多任务下隧道与 UI;真机连通 + 出网 |
+| macOS |
+默认子进程(DesktopVpnBridge+sing-box);可选 PacketTunnel 系统扩展(站外 Developer ID + 公证) |
+部分 子进程查找/启停有 kernel_process_test · desktop_vpn_bridge_m4m5_test;sysext realize 零 |
+sysext 能否被 sysextd realize(见踩坑复盘)、CFBundleVersion 递增、公证/staple、三方死锁规避;真机连通 + 出网 |
+| Windows |
+sing-box.exe 子进程 + wintun.dll TUN;kernel_process.dart 管理 |
+部分 进程管理逻辑同 kernel_process_test(跨平台共用);wintun/打包 零 |
+wintun.dll 随 exe 落位、TUN 适配器创建、UAC 提权、安装包;真机连通 + 出网 |
+
+
+
每端真连通验收清单(同一张表,逐端打勾)
+
第 1–7 步对五端是同一份判据,差异只在第 8 步平台专项。用客观信号代替「看着像连上了」。当前节点:107.172.55.251(REALITY 443)。
+
+
+- ☐ 1 流量真走节点 · 连前/连后各查出口 IP(
curl ifconfig.me / ip.sb)。必须从本地 IP 变成节点 IP——没变 = 隧道没真接管,最易自欺的一步。
+- ☐ 2 DNS 劫持 + 路由对 · 浏览器开 youtube / google 能通 → 证明
hijack-dns 首条规则在设备上真生效(缺它「连上也打不开网站」)。
+- ☐ 3 国内分流不绕道 · 开 bilibili 等国内站走直连不进隧道(
split_cn)。
+- ☐ 4 统计记账对 · 跑一段已知流量,对客户端统计页 GB/时长 vs 节点端 v2ray 计数器(咬合 §A.2,验 stats-overhaul 是否真对的天然 E2E)。
+- ☐ 5 断网保护 KillSwitch · 隧道中途断开,确认无明文泄漏(分级见 KillSwitch 设计;TUN
strict_route 已渲染)。
+- ☐ 6 切节点 · 切换后出口 IP 跟着变、不掉线、不卡 connecting。
+- ☐ 7 重连 / 网络切换 · Wi-Fi↔蜂窝切换、息屏/休眠恢复后隧道自愈。
+- ☐ 8 平台专项:
+
+ - iOS/iPad:NE 内存 ≤50MB(Instruments)· profile 授权弹窗 · 后台保活
+ - macOS(sysext):sysextd realize 成功 · 已公证 · CFBundleVersion 已递增
+ - macOS(子进程)/Windows:内核子进程提权(sudo/UAC)· 退出清理 TUN
+ - Android:VpnService 授权弹窗 · 厂商省电杀后台白名单
+ - Windows:wintun.dll 落位 · TUN 适配器创建 · 安装包
+
+
+
+
+
半自动化建议:第 1–3 步(出口 IP / 站点可达 / 分流判定)可写成一个连通探针脚本自动跑,把客观判据固化下来,只留第 4–8 步手测——这是把 §B 盲区往自动化推进的最小一步。
+
+
下一步「功能更完善」可补的自动化(从盲区往里推)
+
+- 原生↔Dart 统计契约:给
pangolin/vpn/stats MethodChannel 两侧加契约测试(防「mac 统计恒为 0」复发)——这是 §B 里唯一能自动化的一块,性价比最高。
+- 连通探针脚本:固化验收清单第 1–3 步(出口 IP 变化 / 墙外站可达 / 国内站直连),CI 之外按需跑真节点。
+- 记账对账:stats-overhaul 合并后,补 e2e
/v1/usage/devices 分设备断言 + 客户端↔节点计数对账口径。
+- 真 sing-box 烟测(重,可选):起真 sing-box 吃渲染配置,验端口监听 + v2ray_api 真计数,替换当前假 StatsService 的一段。
+- 资产 drift 闸(见 §A.3 设计契约):仿
ci/check-codegen-drift.sh,给 logo SVG 双拷贝(design↔client) 和 app-icon 生成物 各加比对闸,把「图标 logo 五端同源」从靠纪律升成靠闸——token 已有、资产尚缺。
+- Flutter 依从性扫描闸(见 §A.3):仿
ci/scan-portable-sql.sh,正则扫 lib/screens+lib/widgets 的 raw 颜色/字号/间距字面量(第①档),命中即红,守「screen 真用真相源」——需先清存量 62+ 处或白名单冻结增量守门。
+- 闸反向 case 自检(见 §A.3):给依从性闸 +
codegen-drift 各配一个违规 fixture,CI 跑「扫它必须非零退出」,证明闸非永真——当前所有闸都缺这一类自检。
+
+
+
维护:新功能上线时同步更新本表对应行的「覆盖」标签与「欠缺」列;与 test-architecture.html ⑩ 已知缺口 互为索引——本表是「逐功能」视角,那里是「分层方法论 + 盲区跟踪」视角。
+
+
+
+
diff --git a/docs/index.html b/docs/index.html
index b223233..a87fc08 100644
--- a/docs/index.html
+++ b/docs/index.html
@@ -93,6 +93,11 @@
多端项目可复用的分层测试方法论(L0 契约→L4 E2E)+ 四类诉求(界面一致/交互/数值/后端记账)→层→工具→量化指标映射 + 五张架构图(金字塔/契约接缝/CI 流水线/E2E 拓扑)+ ⑩「已知缺口/测试盲区」单一跟踪源(没测什么·为什么·何时补)。
KillSwitch 设计与跨平台能力矩阵 HTML
断网保护 L0–L3 分级模型 + 各平台能力天花板 / 当前实现矩阵。KillSwitch 决策依据。
diff --git a/docs/stats-overhaul-plan.html b/docs/stats-overhaul-plan.html
new file mode 100644
index 0000000..f257488
--- /dev/null
+++ b/docs/stats-overhaul-plan.html
@@ -0,0 +1,111 @@
+
+
+
+
+
+统计体系整改 · 实现计划(阅读版)
+
+
+
+
+
+
统计体系整改 · 实现计划
+
todo #5 · 四端实时统计 + 多设备归因 + GB 配额 + 统计页重设计 · 阅读版
+
+
+执行真相源(带 checkbox):docs/superpowers/plans/2026-06-24-stats-overhaul.md。本 HTML 仅供阅读,不驱动执行。
+
+
+
目标
+
四端实时统计真生效(上传/下载/延迟/累计流量)+ 服务端按设备归因并按 GB 账户综合卡控 + 统计页按新设计(上聚合·下分设备)像素级还原。
+
已敲定决策:租户=用户账户(无需层级表);配额改 GB、按账户综合卡;每设备独立 dp_uuid;分三期;页面改动先设计后开发、用真相源、四端统一元素。
+
+
现状关键事实(探查结论)
+
客户端实时管线
+
Dart 契约(pangolin/vpn/stats)与 UI 都正确,缺口全在原生侧:
+
+- iOS:
PacketTunnelProvider 起了 libbox 但 writeStatus/writeGroups 空实现,stats EventChannel 注册了无人 push → 0 数据。
+- macOS 原生:
VpnChannel.swift 硬编码推 0;扩展回调全空。
+- Android:上下行已工作;延迟(
writeGroups urltest)疑似 stale。
+- Windows:上下行已工作(Clash
/connections);延迟解析 /proxies urltest 疑似拿不到 URLTest 组。
+
+
服务端用量/配额/归因
+
+- 记账链路通:agent V2Ray stats(按 dp_uuid)→
ReportUsage → dp_uuid→user_id → usage_daily(user_id,date)。
+- 无法按设备归因:每账户仅一个
users.dp_uuid,devices 无 dp_uuid 列,usage_daily 无设备维度。
+- 配额分钟制(
plans.daily_minutes,连接时凭证 TTL 卡,非持续)。
+- 多租户:user==account 已成立,JWT 全程隔离,无需新表。
+
+
统计页设计/实现
+
现有 stats 设计在 mobile/tablet/desktop 仅聚合;分设备明细任何端都没设计 → Phase 3 必须 design-first。_MetricCard 私有于 stats_page,应提升为公共组件。
+
+
三期方案
+
+
+
Phase 1 · 无 schema 改动
+
四端实时统计修复
+
+- iOS/macOS(核心):主 App 用
LibboxCommandClient 连 App Group 容器订阅扩展 CommandServer 的 writeStatus/writeGroups,转推 stats channel——对齐 Android StatsHandler 模型。
+- Android:修延迟(groups 订阅 + latestUrltest 刷新)。
+- Windows:修
extractUrltestResults() 解析 + 核下发配置含 URLTest 组。
+- 服务端核实:节点 stats API 启用、agent 真上报,使累计流量/周柱有值。
+
+
+
+
+
Phase 2 · 后端重型
+
每设备归因 + GB 综合配额
+
+- Schema(双驱动):
devices.dp_uuid 每设备凭证;新表 usage_device_daily;plans.daily_gb。
+- 每设备 dp_uuid:注册时 mint,connect 按设备下发;控制面
dp_uuid→(user_id,device_id) 解析,双写设备表 + 账户 rollup。
+- GB 综合卡控:账户当日综合 GB 超额即拒;跨阈值 revoke 凭证近持续卡控。
+- API:暴露按设备用量。
+
+
+
+
+
Phase 3 · design-first
+
统计页重设计 + 四端实现
+
+- 设计:走 design-distill,为分设备明细出 mobile/tablet/desktop 原型 + 更新 CONTRACT.md。
+- 实现:提升
metric_card.dart、新增 device_stat_row.dart;四端接分设备 provider;语义 token;红线扫描。
+- 验收:截图 diff + 新增分设备 golden。
+
+
+
+
执行顺序
+
先 Phase 1(见效快、零 schema 风险)→ 合并验收 → Phase 2(后端)→ Phase 3(设计可与 Phase 2 并行起草,实现接线依赖 Phase 2 API)。
+
+
不在本次范围:组织级多租户层级、协议选择(#8)、KillSwitch(#1/#2/#3)。
+
+
+
+
diff --git a/docs/superpowers/plans/2026-06-24-stats-overhaul.md b/docs/superpowers/plans/2026-06-24-stats-overhaul.md
new file mode 100644
index 0000000..2da9585
--- /dev/null
+++ b/docs/superpowers/plans/2026-06-24-stats-overhaul.md
@@ -0,0 +1,163 @@
+# Pangolin 统计体系整改 Implementation Plan(todo #5)
+
+> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
+
+**Goal:** 四端实时统计真生效(上传/下载/延迟/累计流量)+ 服务端按设备归因并按 GB 账户综合卡控 + 统计页按新设计(上聚合·下分设备)像素级还原。
+
+**Architecture:** Flutter UI 全平台共享。实时指标经冻结的 `pangolin/vpn/stats` channel 由各原生侧(libbox CommandClient / Clash API)生产;累计用量由服务端 `/v1/usage`、`/v1/me` 提供。服务端控制面(Go,mysql/sqlite 双驱动)经 agent gRPC `ReportUsage` 按 `dp_uuid` 记账;本计划把记账粒度从「账户」细化到「设备」(每设备独立 dp_uuid),并把配额单位从分钟改 GB、按账户综合卡控。
+
+**Tech Stack:** Flutter/Dart、Swift(iOS/macOS NEPacketTunnelProvider + libbox)、Kotlin(Android VpnService + libbox)、Go(控制面 + agent)、sing-box libbox、SQLite/MySQL(dialect 层)。
+
+**设计依据:** `design/CONTRACT.md`、`design/ui_kits/{mobile,tablet,desktop}`、`docs/ios-ipad-support-design.html`;探查结论见审批计划 `~/.claude/plans/crystalline-exploring-lerdorf.md`。
+
+## Global Constraints
+
+- Channel 契约冻结:`pangolin/vpn/stats` 字段 = uploadBytes/downloadBytes/uploadSpeed/downloadSpeed/urltestResults(`client/lib/bridge/vpn_bridge.dart:27-37`),**不得私改**。
+- 配置由服务端渲染下发,客户端绝不拼配置。
+- 服务端可移植铁律:走 `dialect.Upsert/LockForUpdate`(`server/internal/db/dialect.go`),时间 Go 端算,禁 `UTC_TIMESTAMP()/NOW()/FIELD()` 等 MySQL 专属;迁移 `server/migrations/{mysql,sqlite}` 双份。
+- token 真相源单源:只改 `design/colors_and_type.css` + 跑 `node design/codegen/gen_flutter_tokens.mjs`;`client/lib/pangolin_tokens.gen.dart` 勿手改。
+- 统一元素:复用/提升 `client/lib/widgets/`,禁就地造元素;颜色禁硬编码 hex,走语义 token。
+- 文案禁红线词(VPN/翻墙/科学上网…),用「加速」口径,过 `ci/scan-redline.sh`;双语单显不并排。
+- iOS App Group `group.com.pangolin.pangolinVpn`;macOS App Group `BYL4KQHMTN.com.pangolin.pangolin`。
+
+---
+
+## Phase 1 — 四端实时统计修复(无 schema 改动)
+
+让上传/下载/延迟/累计流量在四端全部可见。优先级最高、风险最低、见效最快。
+
+### Task 1.1: 服务端记账核实(先确认数据源)
+
+**Files:** `server/internal/httpapi/clientconfig.go`、`server/internal/agentd/usage*.go`、`server/internal/nodes/handler_grpc.go`。
+
+- [x] **Step 1:** 已核实——节点配置 `render.go` 启用 `experimental.v2ray_api.stats`(`enabled:true`+`users:statsUsers(creds)`),inbound 用户 `name=dp_uuid`;agent `usage_v2ray.go` 按 `user>>>{dp_uuid}>>>traffic` 精确读取。链路代码完整正确。
+- [x] **Step 2:** 已核实——`handler_grpc.go:282-313` `dp_uuid→user_id` → `store.go AccumulateUsage` 累加 `usage_daily(user_id,date)`,全程 dialect 可移植。
+- [ ] **Step 3(运行时):** 「累计流量没数据」非代码缺陷,是运行时产物(需真实连接产生被记账的流量 / 确认节点 build 含 `with_v2ray_api`)——真机连一次验证。**利好**:dp_uuid 改每设备后这套命名天然按设备拆分,Phase 2 节点+agent 侧近零改动。
+
+### Task 1.2: iOS 实时 stats 生产者(核心)
+
+**Files:** `client/ios/PacketTunnel/PacketTunnelProvider.swift`、`client/ios/Runner/VpnManager.swift`、`client/ios/Runner/AppDelegate.swift`。
+
+**Interfaces:** Produces → `pangolin/vpn/stats` EventChannel 真实帧。
+
+- [x] **Step 1:** 新增 `client/ios/Runner/StatsClient.swift`——`LibboxNewCommandClient`(CommandStatus+CommandGroup,statusInterval 1s)连 App Group 容器(`LibboxSetup` 同扩展路径),含 10 次重试。
+- [x] **Step 2:** `writeStatus` 映射 uplinkTotal/downlinkTotal→uploadBytes/downloadBytes、uplink/downlink→uploadSpeed/downloadSpeed。
+- [x] **Step 3:** `writeGroups` 遍历组成员 `urlTestDelay`→契约 urltestResults。
+- [x] **Step 4:** 经 `VpnStatsStreamHandler.shared.push` 推 EventChannel;`VpnManager` 状态观察 connected→start / disconnected→stop;断开推零帧。pbxproj 四处登记(plutil OK)。
+- [ ] **Step 5(真机):** Release build 装真机,连接后验证上传/下载/延迟实时跳动。
+
+### Task 1.3: macOS 实时 stats 生产者
+
+**Files:** `client/macos/Runner/VpnChannel.swift`、`client/macos/PacketTunnel/PacketTunnelProvider.swift`。
+
+- [x] **Step 1:** 新增 `client/macos/Runner/StatsClient.swift`(镜像 iOS,app group `BYL4KQHMTN.com.pangolin.pangolin`);替换 `VpnChannel.swift` 旧占位(旧帧 schema 也错,推 up/down 非契约字段)。
+- [x] **Step 2:** `VpnChannel` 状态观察驱动 start/stop,`onStats` 回调推 statsSink;`onStatsListen` 去掉占位 Timer。pbxproj 四处登记(plutil OK)。
+- [ ] **Step 2 验证(真机):** 递增 CFBundleVersion + 公证流程(CLAUDE.md),装机验证。
+
+### Task 1.4: Android 延迟修复
+
+**Files:** `client/android/app/src/main/kotlin/com/pangolin/pangolin_vpn/PangolinVpnService.kt`。
+
+- [x] **Step 1(代码核查):** `PangolinVpnService.kt` 已 `addCommand(CommandGroup)`、`writeGroups` 正确解析 `getURLTestDelay()`——读取链路无缺陷。
+- [x] **Step 2(主动探测修复):** 补偿 sing-box urltest 惰性探测——`writeGroups` 捕获 `groupTags`,新增 `startUrlTestTimer()`(`java.util.Timer` 每 12s 对各组 `client.urlTest(tag)`),`doStop` 取消。让延迟稳定及时出现,不等 3min 默认 interval。同样的主动触发也加到了 iOS/macOS `StatsClient`(`urlTestTimer`)。
+- [ ] **Step 3(真机):** 装机验证延迟出现。
+
+### Task 1.5: Windows 延迟修复
+
+**Files:** `client/lib/bridge/kernel_process.dart`、(必要时)`server/internal/httpapi/clientconfig.go`。
+
+- [x] **Step 1(根因定位+修复):** 真正 bug——`getGroupDelay()` 早已实现却**从未被调用**,轮询只读 `/proxies` 缓存,urltest 惰性探测下 `history` 长期为空 → 延迟空白。修复:`_startStatsPoll` 每 `_urlTestEvery=12` 拍对提取出的 `_urltestGroups` fire-and-forget `getGroupDelay()` 主动触发探测;新增 `extractUrltestGroups()` 提取 URLTest/Fallback 组名。
+- [x] **Step 2(单测):** `kernel_process_test.dart` 新增「URLTest extraction」组 3 用例(组名提取/最新 history delay/空组)——`flutter test` 26 全过。
+- [ ] **Step 3(真机):** Windows 装机验证延迟出现。
+
+### Task 1.6: Phase 1 验收
+
+- [ ] `flutter analyze` + `flutter test`(含现有 golden)通过。
+- [ ] 四端连接后:上传/下载/延迟实时跳动;累计流量 + 周柱图有值。
+- [ ] 提交,合并验收后再进 Phase 2。
+
+---
+
+## Phase 2 — 每设备归因 + GB 综合配额(后端重型)
+
+### Task 2.1: Schema 迁移(mysql + sqlite 双份)
+
+**Files:** `server/migrations/{mysql,sqlite}/0000X_*.up/down.sql`。
+
+- [x] **Step 1:** `devices.dp_uuid`(sqlite TEXT / mysql CHAR(36),NULL)+ 独立 `CREATE UNIQUE INDEX idx_devices_dp_uuid`(SQLite ADD COLUMN 不支持内联 UNIQUE,两库对齐)。
+- [x] **Step 2:** 新表 `usage_device_daily`,PK `(device_id, date)`,索引 `(user_id, date)`。
+- [x] **Step 3:** `plans.daily_mb`(MB 整数,支持免费 500MB;与 `daily_minutes` 并存);seed free 500 / pro 102400(100GB) / team 204800(200GB),按用户确认数值。
+- [x] **Step 4:** `000015_per_device_usage.{up,down}.sql` 双驱动;sqlite `TestSQLiteMigrateUpDown` up+down 干净(版本→15、新表/列断言已加),`run_sqlite_test.sh` 全过、`go build ./...` OK。mysql 侧待 `run_mysql_test.sh`(需 docker)。
+
+### Task 2.2: 每设备 dp_uuid 链路
+
+**Files:** `server/internal/devices/service.go`、`server/internal/httpapi/nodes.go`、`server/internal/nodes/store.go`、`server/proto/agent/v1/agent.proto`、`server/internal/nodes/handler_grpc.go`。
+
+- [x] **Step 1:** `store.EnsureDeviceDpUUID(userID, deviceUUID)`——connect 时按设备**懒 mint** `devices.dp_uuid`(`idgen.NewString()`,guarded UPDATE + re-read 处理并发竞态)。
+- [x] **Step 2:** `ConnectNode` 用设备 dp_uuid 建 cred + 渲染 config(`EnsureDeviceDpUUID` 失败则**回退账户 dp_uuid**,不破坏未注册设备的旧客户端)。
+- [x] **Step 3:** `store.UserDeviceByDpUUID(dpUUID) → (user, device)`——优先查 `devices.dp_uuid`,回退 legacy `users.dp_uuid`(device=0)。
+- [x] **Step 4:** `ReportUsage` 双写——账户 `AccumulateUsage` + 设备 `AccumulateDeviceUsage`(仅 device>0)。
+- [x] **Step 5:** 兼容——懒 mint 使存量设备下次 connect 自动补发;legacy 账户 dp_uuid 仍解析(device=0,不归设备)。节点侧零改动:每设备 dp_uuid 作独立 connect_credential → `render.go statsUsers`/inbound 天然按设备拆。
+- [x] **验证:** `sqlite_per_device_test.go` 3 用例(mint 幂等/区分/未知设备报错、dp_uuid→(user,device) 解析+账户回退、设备用量累加分离)+ `grpc_test.go TestReportUsage_PerDevice`(双写)/`TestReportUsage_Accumulates`(账户级 device=0 不写设备)——`go test ./...` 全过。
+
+### Task 2.3: GB 综合配额卡控
+
+**Files:** `server/internal/usage/quota.go`、`server/internal/httpapi/nodes.go`。
+
+- [x] **决策(用户):** 免费=看广告解锁+分钟与 GB **双卡**;免费 500MB/天;付费高 GB 上限(pro 100GB/team 200GB)。
+- [x] **Step 1:** GB 综合卡控落在**真实 connect 路径** `ConnectNode`(`CheckFreeConnect` 实为未接线死代码)——`ent.DailyMB.Valid` 时取 `store.AccountDayBytes`(账户当日 up+down),`≥ daily_mb*1MB` 即拒 403 `QUOTA_EXHAUSTED`;免费/付费统一。`EntitlementForUser` 加载 `daily_mb`(free 回退 500)。
+- [x] **验证:** `TestSQLite_AccountDayBytes`(求和/隔离日期)+ `TestSQLite_EntitlementForUser_DailyMB`(free 回退 500 / pro 订阅 102400)——全过。
+- [ ] **Step 2(后续):** 复用 `ReportUsage` 周期路径跨阈值 revoke 凭证,实现近持续卡控(当前仅 connect 时卡)。
+- [ ] **Step 3(后续):** 完整 ad-unlock 门控接线(ConnectNode 现为 MVP「flat 10min」,未强制看广告);`/v1/me` quota 语义改 GB(并入 Task 2.4)。
+
+### Task 2.4: 按设备用量 API ✅
+
+**Files:** `server/internal/usage/{store,service,handler}.go`、`server/cmd/server/main.go`、`client/lib/models/device_usage.dart`、`client/lib/services/account_api.dart`、`client/lib/state/account_providers.dart`。
+
+- [x] **Step 1:** 新增 `GET /v1/usage/devices?days=N`(独立端点,复用 `auth.RequireAuth` group)。后端:`usage.Store.DeviceUsageRange`(JOIN devices + GROUP BY 设备,窗口求和,busiest-first,可移植 SQL)→ `Service.DeviceUsage` → `DeviceUsageHandler`,返回 `{devices:[{uuid,name,platform,bytes_up,bytes_down,minutes_used}]}`。
+- [x] **Step 2:** 客户端 model `DeviceUsage` + `AccountApi.deviceUsage({days})` + `deviceUsageProvider`(family days)。UI 渲染留到 Phase 3(设计先行)。
+- [x] **验证:** sqlite 实库测试 `TestSQLite_DeviceUsageRange`(分组求和/窗口隔离/账户隔离/busiest-first/JOIN 元数据);handler guard 测试 `TestDeviceUsageHandler_Guards`(401/405/400);Dart `device_usage_test.dart`(fromJson + 默认值)。全量 `go test ./...` 绿、`flutter test` 146 过。
+
+### Task 2.5: Phase 2 验收
+
+- [ ] sqlite 实库测试(`go test ./...` + `./server/run_sqlite_test.sh`)覆盖每设备累加、综合卡控、dp_uuid 解析。
+- [ ] mysql 集成测试(`./server/run_mysql_test.sh`)通过。
+- [ ] 端到端两设备分别连接、用量各归各账;GB 超额被拒。
+
+---
+
+## Phase 3 — 统计页重设计(design-first)+ 四端实现
+
+### Task 3.1: 分设备明细设计(design-source 同步)✅
+
+**Files:** `design/ui_kits/{mobile,tablet,desktop}/*`、`design/CONTRACT.md`。
+
+- [x] **Step 1:** 「设备明细 / By device」section 加入三端原型(mobile `screens.jsx StatsScreen`、tablet `tabapp.jsx TabStats`、desktop `dapp.jsx DStats`):上聚合沿用现 3 指标卡 + 周柱;下分设备列表 = 平台 Lucide 图标盒 38(accent-subtle) + 名称 + 占比迷你条(height 4, accent .85) + 流量(mono)/时长。字串入各端 dict(`byDevice`/`noDeviceUsage`,双语)。
+- [x] **Step 2:** `design/CONTRACT.md` §3.3 增分设备明细像素规格 + 数据源标注。
+
+### Task 3.2: 统一组件提升 ✅
+
+**Files:** `client/lib/widgets/metric_card.dart`(新)、`client/lib/widgets/device_stat_row.dart`(新)。
+
+- [x] **Step 1:** `stats_page.dart` 私有 `_MetricCard` → 公共 `widgets/metric_card.dart`(`MetricCard`)。
+- [x] **Step 2:** 新增 `widgets/device_stat_row.dart`(`DeviceStatRow` + 静态 `iconFor` 平台图标,与 account_screens 同一视觉语言)。
+
+### Task 3.3: 四端 stats_page 接线 ✅
+
+**Files:** `client/lib/screens/stats_page.dart`、`client/lib/l10n/*`。
+
+- [x] **Step 1:** stats_page(mobile/tablet 窄宽分支 + desktop isWide)接 `deviceUsageProvider(30)`;上聚合下分设备(`_DeviceDetail`);占比条按最忙设备归一;全程语义 token(占比条 accent/border,无硬编码 hex)。
+- [x] **Step 2:** 文案入 `l10n/app_text.dart` + `strings_{zh,en}.dart`(`byDevice`/`noDeviceUsage`),双语单显,无红线词(「设备明细」中性)。
+
+### Task 3.4: Phase 3 验收 ✅
+
+- [x] golden 回归闸(CONTRACT §4 既定 pixel gate):tablet 注入 demo 设备用量 → `tablet_stats_{light_zh,light_en,dark_zh}` 重生;desktop 零数据态 → `desktop_stats` 重生(空态 section)。
+- [x] `flutter analyze` 无 error/warning;`flutter test` **146 过**;`go test ./...` 绿。
+- [ ] 真机/模拟器多端目测(待用户验证)。
+
+---
+
+## 不在本次范围
+
+- 组织级多租户层级(父子账户)——user==account 已满足。
+- 协议选择(todo #8)、KillSwitch(#1/#2/#3)。