Files
GarminHealthLab/docs/REQUIREMENTS.md
ericwyuan 4331a07462 docs: docs/ 目录同步甲骨文部署事实(上一提交漏加)
上一个提交 git add 漏了 docs/,架构/开发/需求/auth-hub 集成文档里的 NAS
IP、8124 端口还是旧的,补上。

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-12 23:53:26 +08:00

153 lines
11 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Garmin Health Lab — 需求文档
记录所有已提出的需求、理解与进度。**每完成一条:更新本文件的状态 → commit → push。**
状态:`✅ 已完成` · `🚧 进行中` · `📋 待做` · `⏸ 已搁置`
最后更新2026-09-02
---
## 一、基础平台
| # | 需求 | 理解 | 状态 |
|---|---|---|---|
| 1.1 | 新建项目分析佳明海外账号健康数据 | Gitea 自建仓库Web 应用 | ✅ |
| 1.2 | 部署到甲骨文云主机,用 MariaDB | Flask + gunicorn(ubuntu, systemd, :5500) + 本机 MariaDB 10.3.39(库 `garmin_health_lab`SQLite 供开发2026-09-12 从 NAS 迁移 | ✅ |
| 1.3 | 公网可访问 | Caddy 反代 `https://garmin.zichuan.xyz`(自动 TLS与本机 ai-gateway/auth-hub 同一套约定 | ✅ |
| 1.4 | 单元测试 / 界面自测 / 性能测试 | 446 项 pytest界面用浏览器实测接口逐个计时 | ✅ |
| 1.5 | 一任务一 commit完成即推送 | 已成为固定流程 | ✅ |
| 1.6 | auth-hub 统一登录SSO | OAuth2/OIDC回调注册 LAN + 公网两地址;本地邮箱密码登录已移除 | ✅ |
| 1.7 | 一键部署脚本 | `deploy/push.sh`tar-over-ssh 同步 + 清空重推前端 + sudo 重启 + pid/health 双校验 | ✅ |
## 二、数据同步
| # | 需求 | 理解 | 状态 |
|---|---|---|---|
| 2.1 | 同步佳明所有数据,不只是运动 | 31 项每日指标 + 运动 + 徽章 + 个人纪录 | ✅ |
| 2.2 | 验证码在前端输入(人不在电脑前) | Web MFA后台线程阻塞在 `prompt_mfa`,验证码经数据库跨 worker 交接 | ✅ |
| 2.3 | 同步改为用户手动触发,不自动跑 | 同步是界面动作 | ✅ |
| 2.4 | 每小时后台同步最新数据 | `job_locks` 抢占,多 worker 只跑一次 | ✅ |
| 2.5 | 「同步最新数据」按钮 | 同步页首要按钮,等待即返回结果 | ✅ |
| 2.6 | 同步频率可设置 | 设置页选择,调度器按各账号自己的频率判断是否该同步 | ✅ |
| 2.7 | 同步历史范围可设置(多久以前 / 全部) | 设置页选择,同步页「同步历史」按此范围拉取 | ✅ |
| 2.8 | 同步界面简化设计 | 两个按钮 + 三格状态,去掉原来四段说明文字 | ✅ |
## 三、界面框架
| # | 需求 | 理解 | 状态 |
|---|---|---|---|
| 3.1 | 前端推倒重做,要求好看 | Framework7 React (iOS 主题),桌面与手机都要好看 | ✅ |
| 3.2 | Tab今日 / 健康 / 趋势 / 运动 / 设置 | 每日是趋势的子页 | ✅ |
| 3.3 | 首页改三圆环:步数 / 睡眠 / HRV | 并列三环,满环=进入参考区间 | ✅ |
| 3.4 | 交互流畅、有动画 | 数字滚动、入场揭示、`prefers-reduced-motion` | ✅ |
| 3.5 | 删掉各页右上角的半透明椭圆 | 导航栏右侧 `Link`(带 tooltip渲染出来的已移除 | ✅ |
| 3.6 | 睡眠入口不放右上角,改为点健康页睡眠卡片进入 | 入口跟着内容走;每日入口同样移进趋势页内容 | ✅ |
| 3.7 | 主界面切到子界面加 loading 动画 | 顶部进度条,跑到 90% 等页面就位;有最短显示时长,避免快速跳转时闪一下 | ✅ |
## 四、数据展示
| # | 需求 | 理解 | 状态 |
|---|---|---|---|
| 4.1 | 每日详情模块,看某天所有数据 | 每日页 | ✅ |
| 4.2 | 趋势模块7 天 / 月 / 季 / 年 周期 | 按周期取日均,桶长不同也可比 | ✅ |
| 4.3 | 趋势覆盖全部 15 组指标,可自选显示/隐藏 | 指标选择器 | ✅ |
| 4.4 | 指标选择器太丑,改成选择弹窗 | 改为 F7 Popup页面上只留一行「显示指标 n/15」 | ✅ |
| 4.5 | 所有卡片可点击进入详情 | `/metric/:id/`:大数值 + 参考区间 + 趋势图 + 统计 + 依据;新增 `lib/metrics.ts` 统一指标注册表 | ✅ |
| 4.6 | 今日页左右箭头切换前一天/后一天 | 无记录的日期自动跳过;到边界置灰 | ✅ |
| 4.7 | 今日页顶部日期选择控件,可看历史任一天 | 点中间日期开 F7 日历,范围限定在已有数据内 | ✅ |
| 4.8 | 点击每个运动看本次运动详情,数据全展示 | 概览/数据/分段/图表四个 Tab含心率区间条与时间/距离横轴切换 | ✅ |
| 4.9 | 健康页增加身体年龄 | 健康页卡片 + `/body-age/` 展示每一步推算过程与出处 | ✅ |
## 四之二、补齐未同步的数据2026-08-24
审计garminconnect 0.2.8 共 57 个 `get_*`,原先只用了 16 个。只读探测后确认
账号里真有数据、却从未入库的部分如下,全部已加入同步模块并配了界面。
| # | 数据 | 接口 | 存放 | 界面 | 状态 |
|---|---|---|---|---|---|
| 4.10 | 体重与身体成分 | `get_body_composition` | `body_composition` 表 | `/body/` 身体成分 | ✅ |
| 4.11 | 血压 | `get_blood_pressure` | `blood_pressure` 表 | `/body/` 内表格 | ✅ 接口通,账号暂无数据 |
| 4.12 | 跑步成绩预测 | `get_race_predictions` | `race_predictions` 表 | `/race/` 成绩预测 | ✅ |
| 4.13 | 爬坡分 | `get_hill_score` | `health_data.hill_score` | 指标详情 | ✅ |
| 4.14 | 饮水与出汗 | `get_hydration_data` | `health_data` 三列 | 指标详情 | ✅ |
| 4.15 | 全天曲线(心率/压力/身体电量/呼吸/血氧) | 五个日内接口 | `daily_series` 表 | 每日页「全天曲线」 | ✅ |
| 4.16 | 挑战赛 | `get_badge_challenges` 等四个 | `challenges` 表 | `/challenges/` | ✅ |
| 4.17 | 设备 | `get_devices` | `devices` 表 | 设置 → 已配对设备 | ✅ |
**探测为空、未做界面**`get_max_metrics`VO₂max 已从训练状态取到)、
`get_goals``get_inprogress_virtual_challenges`
**判定为重复**`get_stats`/`get_steps_data`/`get_floors`/`get_stress_data`
日聚合接口,数据已在 `health_data``get_activities`(分页);
`get_device_settings`/`get_gear_defaults` 等配置类接口。
同步开销:日内曲线每天五个请求,因此 14 天以内的同步顺带拉取,更长的历史
交给「补齐详细数据」后台任务,否则一年的同步会多出约 1800 个请求。
## 五、设置
| # | 需求 | 理解 | 状态 |
|---|---|---|---|
| 5.1 | 设置项太少:身高/体重/年龄/性别 | 设置页个人资料分组,改动即存,另显示 BMI 与年龄 | ✅ |
| 5.2 | 单位设置 | 公制 / 英制,已存储;各页按此显示待接入 | ✅ 设置完成,🚧 全局套用 |
| 5.3 | 评分依据要在设置说明中显示 | 设置 → 评分依据 `/rating-basis/`11 项区间逐条列出处;指标详情页也各带一份 | ✅ |
## 六、AI
| # | 需求 | 理解 | 状态 |
|---|---|---|---|
| 6.1 | 多个大上下文文本模型,可切换 | gateway / gemini-flash / llama-70b / nemotron-49b / mistral-large | ✅ |
| 6.2 | 暂时隐藏 AI 模块 | `FEATURES.ai = false` | ✅ |
| 6.3 | AI 用来解释,不用来定阈值 | 阈值全部来自公开参考值AI 只负责解读 | ✅ 已确认为设计原则 |
| 6.4 | AI 教练:晨报 / 趋势归因 / Copilot2026-09-01 | `services/insights` 特征工程 → `coach.py` 三套提示词 + 规则兜底 → ai-gateway晨报后台生成 + 轮询Copilot SSE | ✅ |
| 6.5 | AI 生成走后台队列,界面不阻塞 | `services/jobs.py` 生产者/消费者DB 互斥);每日/运动详情持续排队不限量(`refill_backlog` | ✅ |
| 6.6 | 队列状态可见、可重试 | 设置 → AI 生成队列页 + `/analysis/insight/queue/retry` | ✅ |
| 6.7 | AI 模型不可用时降级不报错 | 规则引擎兜底,`meta.source` 标注谁作答;流式失败自动退回非流式 | ✅ |
---
## 已修复的缺陷(实机验证发现)
| 现象 | 原因 | 处理 |
|---|---|---|
| 部署后整站 Network Error | 构建时没带 `REACT_APP_API_URL`,打包进了开发默认值 localhost:5000 | 加 `client/.env.production`,写死 `/api`,不再依赖构建时手输 |
| 凌晨打不开当天数据 | 各页用 `toISOString()` 取日期,那是 UTC。UTC+8 每天前 8 小时都在查昨天 | 新增 `lib/day.ts`,全部改用本地日历日期 |
| 右上角/返回键的半透明椭圆 | Framework7 9 给 `.navbar .left/.right` 加了 frosted pill | 在 `f7theme.css` 覆盖掉 |
| 运动详情四个 Tab 竖着排 | F7 把每个 `<button>` 渲染成整宽块元素 | `.metric-tab` 显式 `width: auto` |
| 心率区间百分比偏高区间1 显示 90%,手表是 52% | 分母用了「落在区间内的总时长」,应为整次运动时长 | 改用运动时长低于区间1 的时间不再被挤掉 |
| 打开运动详情超时 60s | 每个请求都重新认证 Garmin`_connect` 单次约 11 秒 | 按进程缓存已认证会话15 分钟 TTL冷启 16s → 热 7s → 命中缓存 0.8s |
| 主要收益显示 UNKNOWN | Garmin 用 UNKNOWN 表示「没有结论」 | 映射为中文UNKNOWN 直接不显示该区块 |
| Copilot 浮窗布局错乱(✕ 错位、输入框缩成 21px | Framework7 全局 `button { width: 100% }` 压过自定义类 | `.copilot-*` 按钮显式 `width: auto`2026-09-02 已修并部署 NAS :8124 |
| Garmin 429 限流反复触发(同步死循环) | 退避太短 + 守卫被多 worker 内存卡死 + RetryError 里的 429 未识别 | 退避 24h + `rate_limited_until` 以 DB 为准 + 沿异常 `__cause__` 链识别 429 |
| 文档与实现脱节导致误判部署位置 | 文档停在 Node.js / 甲骨文 8123 时代 | docs/* 与 CLAUDE.md/README 已按 NAS :8124 + Flask 现状重写2026-09-02 |
## 测试与性能2026-08-24
### 单元测试446 项
新增 122 项覆盖这一轮的新代码——设置校验与吸附、身体年龄的方向性与边界、
运动详情的列存解析与抽稀、同步入库与「只读本地」的保证。
### 性能:接口逐个计时(经 frp 公网)
| 接口 | 优化前 | 优化后 |
|---|---|---|
| 今日页首屏 | 394 KB / 2.03s | **51.5 KB / 0.82s** |
| 趋势 365 天 | 394 KB / 2.03s | 320 KB / 2.77s(仅在用户主动选 1 年时) |
| 运动列表 | 38 KB / 1.05s | 38 KB / 0.82s |
| 打开运动详情 | 超时 60s | **0.8s**(本地读) |
两处改动:
1. **摘要不再下发空值。** 一天 40 项指标里大部分是这块表没有的传感器,
全按 null 发出去占了整整两成体积。
2. **今日页首屏只取 60 天**,往前翻越界时再按需加载 180 天。原先一次取一年
是为了「翻页不发请求」,代价是首屏 394 KB——这个交易在手机上不划算。
基线往返约 440msNAS → Oracle → 客户端),所以请求**个数**比单个大小更值得省。
## 设计原则(已达成一致)
1. **AI 不定阈值。** 让模型现编「正常范围」会得到一个不可复现、无法追溯、却带着医学口吻的数字。所有参考区间来自公开来源WHO、AASM、Garmin 官方分级、人群常模AI 只做解读。
2. **依据必须可见。** 任何把数字判为「偏低 / 正常 / 优秀」的区间都是在下判断,出处在 设置 → 评分依据 里逐条列出。
3. **身体年龄是估算。** garminconnect 0.2.8 没有 Fitness Age 接口Garmin 的模型也不公开。本地按 VO₂max 常模推算并展示每一步中间值,明确标注不是医学评估。
4. **缺数据就说缺数据。** 指标没有值时显示「—」并说明原因,不用 0 冒充。