Commit Graph

4 Commits

Author SHA1 Message Date
ericwyuan
32588085a1 perf: 今日页首屏 394KB/2.0s → 51.5KB/0.82s
逐个接口计时后的两处改动:

- 健康摘要不再下发空值。一天 40 项指标里大部分是这块表没有的传感器,
  全按 null 发出去占了约两成体积。客户端本来就把「键不存在」和 null
  当同一回事。
- 今日页首屏由 365 天改为 60 天,往前翻越界时再加载 180 天。
  一次取一年是为了让翻页不发请求,代价是首屏 394 KB,手机上不划算。
  日历选到窗口外的日期同样会自动加载。

顺带把加载逻辑收成一个 loadFrom:原来初始 effect 的依赖是空数组,
日历里改 from 不会触发重新拉取,是个还没被触发的 bug。

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-08-24 04:34:32 +08:00
ericwyuan
bfda1cd017 [阶段8] 新增每日数据模块;趋势改为全指标并列 + 周期聚合
每日数据(新页面 /daily):
- 按活动/能量/心率/压力/睡眠/血氧呼吸/训练七组,列出全部 40 项指标
- 日期选择器 + 前后一天翻页;"只显示有数据的指标"开关
- 附当天的运动记录明细
- 标题处显示当天记录到多少项,缺数据一目了然

趋势(重写):
- 15 组指标全部并列展示,不再一次只能看一个
- 标签可逐个隐藏/显示,选择存入 localStorage(每次刷新都重置的
  选择算不上偏好);提供全选/全不选
- 两级筛选:范围(一月/一季/半年/一年/两年)× 周期(每天/每 7 天/
  每月/每季度)。周期选项按范围过滤,避免出现"近一月按季度聚合"
- 所有图表共用同一份聚合结果,一行筛选器统摄全部图表

聚合口径(lib/aggregate.ts):
- 无论累计型还是速率型指标,一律折算为"周期内日均",这样 30 天的
  月份和 31 天的月份不会仅因日历差 3%
- 周按最新一天往回切,而不是按自然周一 —— 否则开头会出现一个半空
  的桶,看起来像低谷,其实只是窗口起点
- 累计型指标的统计标签写作"日均"而非"平均",读者不必猜口径

fix(viz): 聚合后柱形/面积图掩盖了变化
- 柱形与面积都以延展量编码大小,必须从 0 起;而一年的月均步数都在
  9,590~12,701 之间,画出来几乎一样高,恰恰看不见要看的变化
- 聚合视图改用折线:折线编码位置而非延展量,非零轴是正当的。
  改后 y 轴自动落在 9350~12750,走势清晰可读

fix(health): 运动记录按日期筛选时 500
- get_activities 复用了按 date 列过滤的子句,但 activities 表只有
  start_time,报 "Unknown column 'date'"。此前唯一的调用方不传
  日期,所以一直没暴露,每日数据页一传就炸
- 上界改用次日零点的开区间:SQLite 按字符串比较,而存储的分隔符
  可能是 'T'(0x54) 也可能是空格(0x20),写成 "end 23:59:59" 会让
  当天 18:30 的记录排在上界之后而被排除在自己那天之外

tests (+6, 共 305): 运动记录的单日范围、上下界闭合、仅起点/仅终点、
不传范围返回全部、端点级验证

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-08-23 22:33:59 +08:00
ericwyuan
cbbff61082 [阶段6] 同步 Garmin 全量数据:31 项日指标 + 奖励 + 个人纪录
原来每天只存 7 个指标,而 get_user_summary 一次就返回 60+ 字段,
另有睡眠分期、训练准备度、耐力分等独立端点从未被调用。

db.py:
- health_data 新增 31 列(距离/活动卡路里/基础代谢/爬楼/强度分钟/
  久坐时长/最高最低心率/最大压力/身体电量四项/血氧/呼吸/
  睡眠深浅REM清醒分期/睡眠血氧/睡眠呼吸/睡眠压力/训练准备度/
  VO2max/耐力分)
- 新增 badges 与 personal_records 两张表,均以 (user_id, garmin_id)
  为主键,重复同步更新而非累积
- 新增增量迁移: CREATE TABLE IF NOT EXISTS 对已存在的表不生效,
  新列必须显式 ALTER,否则生产库上永远不会出现。按列名比对后
  逐个补齐,SQLite 与 MariaDB 都幂等

services/garmin.py:
- _extract_daily 改为汇总 user_summary + sleep + hrv +
  training_readiness + training_status + endurance_score 五个端点
- 每个可选端点用 _safe 包裹:某项设备不记录时留 NULL,不影响当天其余数据
- 新增 sync_badges / sync_personal_records(账号级,每次同步取一次)

fix(garmin): 个人纪录整批写入失败
- Garmin 在同一份数据里混用 ISO 字符串和 Unix 毫秒时间戳,
  prStartTimeGmt 是 1570961412000,写进 DATETIME 列被 MariaDB
  以 1292 拒绝,导致 11 项个人纪录一条都没存进去
- 新增 _to_datetime 统一处理 ISO / 毫秒 / 秒三种形状,并优先取
  Garmin 自己提供的 *Formatted 字段

services/ai.py:
- 送给模型的 CSV 从 7 列扩到 23 列,纳入身体电量、血氧、呼吸、
  训练准备度、耐力分和睡眠分期

接口: GET /api/health/badges、/api/health/personal-records

tests (+13, 共 292):
- 徽章/纪录的往返、重复同步不累积、按用户隔离
- 两个用户可持有同一个 Garmin 徽章 id 而不冲突
- 时间戳三种形状的归一化及无效值不抛异常

NAS 实测: 7 天数据每天 31 项指标、65 个奖励、11 项个人纪录

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-08-23 20:48:49 +08:00
ericwyuan
8e37e5a551 [阶段3.1] pytest 测试基建 + auth/health/analysis 单元测试 - 102 用例全绿
测试基建:
- pytest.ini / requirements-dev.txt
- tests/conftest.py: 每个用例独立 SQLite 库,提供
  db / app / client / user / auth / seed_health 六个 fixture

tests/test_auth.py (38 通过, 1 跳过):
- 密码哈希: 加盐唯一性、格式自描述、明文不入库、畸形哈希不抛异常
- 回归用例: 无 scrypt 的解释器上也能哈希(覆盖上一个 commit 的 bug)
- JWT: 过期/换密钥/篡改签名均拒绝
- 登录错误不区分"邮箱不存在"与"密码错误"(防用户枚举)
- require_auth: 缺失/畸形/过期 header 一律 401 而非 500

tests/test_health.py (30 通过):
- 日期范围上下界均为闭区间
- 数据按 user_id 隔离,查不到他人数据
- 重复 upsert 同一天不产生重复行
- 各指标端点正确剔除 NULL 行

tests/test_analysis.py (34 通过):
- 5 条建议规则的阈值边界逐条固化(8000 步 / 7 小时 / 50 压力 /
  65 静息心率 / 40 HRV)
- 均值按窗口计算而非逐日;只取最近 14 天
- 无睡眠记录的日子不被当作 0 拉低均值
- metric 名走白名单,SQL 注入串不生效
- 建议按 high/medium/low 排序

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-08-23 12:34:18 +08:00