Files
GarminHealthLab/CLAUDE.md

155 lines
10 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 — 项目指南
佳明Garmin健康数据分析平台同步 Garmin Connect 数据,做多维度健康分析、
可视化与 AI 解读(晨报 / 趋势归因 / Copilot
> **阅读顺序**:先读本文件(协作约定与现状速览)→ [PROGRESS.md](PROGRESS.md)
> (功能与部署进度,**排查前必读**)→ [README.md](README.md)API 与使用)→
> [docs/](docs/)(架构 / 开发 / 需求)。判断"部署在哪里、跑没跑、什么版本"时
> 一律以 `PROGRESS.md` 和 `deploy/` 脚本为准,**不要凭记忆断言**。
## 技术栈现状2026-09
- **后端**Python 3.10+ / Flask应用工厂Gunicorn 生产运行。原 Node/Express
后端早已重写为 Python仓库中**不存在 `server/` 目录**。
- **前端**React 18 + TypeScript用 **CRAreact-scripts**构建UI 组件库为
**Framework7 9**`framework7-react`iOS 主题)+ Recharts 图表。根目录
`package.json` 是 npm workspaces`client`)。
- **数据库**:可插拔数据层(`backend/db.py``DB_TYPE` 切换——
本地开发 SQLite`backend/data/health.db`,默认);生产 NAS MariaDB
`garmin_health_lab` 库,连接配置见 NAS 上 `backend/.env`)。
- **认证****auth-hub SSO**OAuth2/OIDC`services/auth_hub_client.py`
本地邮箱密码注册/登录已移除;应用内 JWT 由后端签发。
- **AI**:自建 ai-gatewayOpenAI 兼容,`https://ai.zichuan.xyz/v1`)为唯一上游
NVIDIA 推理模型,单次生成可达 2~5 分钟);生产 `AI_MODEL_CHAIN=gateway`
## 目录结构
```
GarminHealthLab/
├── backend/ # Flask 后端(当前唯一后端)
│ ├── app.py # 应用工厂:装配蓝图 + 静态 UI + scheduler/jobs
│ ├── wsgi.py # Gunicorn 入口
│ ├── config.py # 集中配置(读 backend/.env
│ ├── auth.py # JWT 签发/校验 + require_auth
│ ├── db.py # 可插拔数据层SQLite ↔ MariaDB+ 幂等迁移
│ ├── routes/ # API 蓝图blueprint
│ │ ├── auth.py # /api/auth auth-hub SSO: callback / logout / refresh / auth-hub/start
│ │ ├── garmin.py # /api/garmin 绑定/同步/MFA/退出/活动详情/同步详情
│ │ ├── health.py # /api/health 摘要/睡眠/心率/运动/身体成分/血氧/设备/挑战等
│ │ ├── analysis.py # /api/analysis 趋势/建议/briefing/trend-insight/copilot/insight
│ │ └── settings.py # /api/settings 个人资料/单位/同步频率/评分依据
│ ├── services/ # 业务逻辑(纯函数/无 Flask 依赖,可单测)
│ │ ├── garmin.py # Garmin API 同步核心(限流退避 24h 写 DB
│ │ ├── garmin_auth.py # Garmin OAuth 登录 + MFA 会话
│ │ ├── garmin_extras.py # 身体成分/血压/血氧/设备/徽章等扩展数据
│ │ ├── scheduler.py # 后台自动同步DB 锁,多 worker 只跑一次)
│ │ ├── ai.py / coach.py / insights.py / jobs.py # AI 教练 + 生产者/消费者队列
│ │ ├── analysis.py / health.py / fitness_age.py / settings.py
│ │ └── auth_hub_client.py / scopes.py
│ ├── data/ # SQLite本地开发git 忽略)
│ ├── static/ # 前端构建产物(生产由 Flask 同端口提供)
│ ├── tests/ # pytest 测试446+ 项)
│ └── .env.example # 环境变量样例(真实 .env 不提交)
├── client/ # React 前端CRA + Framework7
│ ├── src/pages/ # 今日/健康/趋势/运动/设置/每日/睡眠/指标详情等页面
│ ├── src/components/ # AI 晨报 / Copilot 浮窗 / 趋势归因 / Screen 骨架
│ ├── src/lib/ # day.ts本地日期/ metrics.ts指标注册表
│ ├── src/features.ts # 功能开关FEATURES.ai
│ └── .env.production # REACT_APP_API_URL=/api同源防 Network Error
├── deploy/ # NAS 部署脚本族S99garmin/start/stop/push/deploy
├── docs/ # 架构 / 开发 / 需求文档
├── PROGRESS.md # 进度与部署事实(排查必读)
└── README.md # 项目说明 + API 文档
```
## 常用命令
```bash
# 后端依赖(首次):在 backend/ 下建 .venv 并装 requirements-dev.txt
npm run setup:backend
# 同时起前后端开发backend:5000 由 BACKEND_PORT 固定client CRA 代理到 5000
npm run dev
# 单独起后端 / 前端 / 测试 / 类型检查 / 构建
npm run dev:backend
npm run dev:client
npm run test # backend/.venv/bin/python -m pytest446+ 项)
npm run typecheck
npm run build # client/react-scripts build → client/build/
# 冒烟测试(后端单测入口)
cd backend && .venv/bin/python tests/smoke.py
```
## 部署(生产 = NAS端口 8124
- **位置**NAS `192.168.50.64` `/volume1/web/garmin-health-lab`root 用户跑
gunicorn `0.0.0.0:8124`2 workers / 4 threads / --timeout 300开机自启走
DSM 任务调度器执行 `deploy/S99garmin.sh`
- **一键部署**:本地 `./deploy/push.sh`tar 经 ssh 同步 backend + deploy +
清空重推 client/build + sudo 重启,**校验 gunicorn pid 变化 + health 200**)。
前置:先 `npm run build`
- **公网**NAS frpc → 甲骨文 `http://129.146.26.249:8124`frp 重连需几秒)。
- **DB**NAS MariaDB 10.11root 经 socket `/run/mysqld/mysqld10.sock`(或
TCP 127.0.0.1:3306`garmin_health_lab`。10.11 dump 含 `/*M!999999`
注释、旧客户端会报错,且须 `--default-character-set=utf8mb4` 防中文丢失。
- **auth-hub**client `996aLPw4T5gl-rYZ`;回调注册了 LAN
`http://192.168.50.64:8124/auth/callback` 与公网
`http://129.146.26.249:8124/auth/callback` 两个地址。
- **部署/排障前**:先读 `PROGRESS.md``deploy/` 脚本确认事实(曾经凭旧记忆
断言"无线上环境"而误判)。服务以 root 运行:重启用 sudo日志
`logs/error.log``logs/access.log` 是 root 所有。
## 关键约定与坑(写代码/改样式前看)
1. **日期一律本地日历日期**,别用 `toISOString()`UTCUTC+8 每天前 8 小时
查的是昨天)。前端统一走 `client/src/lib/day.ts`
2. **阈值全部来自公开参考值AI 只解读不定阈值**(设计原则,勿破)。
3. **Framework7 全局 `button { width: 100% }`** 会把任何 `<button>` 拉满父容器
——自定义组件里的按钮要显式 `width: auto`Copilot 浮窗 ✕/发送按钮曾因此
错位,见 `Copilot.css` 注释。F7 `.navbar .left/.right` 的 frosted pill 也
需在 `f7theme.css` 覆盖。
4. **同步限流**:真正把账号打进 429 的是 **SSO 换令牌端点**,不是数据端点。
`refresh_oauth2()` 刷出来的新令牌**必须写回 DB**`_persist_token`)——不写回
的话每次客户端缓存过期15 分钟)、每个 gunicorn worker、每次部署重启都会
从库里读回同一个过期令牌再换一次,而这个端点按账号限流、能封几小时。
2026-09-03 排查一整天的根因就是这个。
**窗口内每次尝试都会延长封锁**(实测:一次同步把截止时间从 00:41 推到 15:26
所以 `sync_status.rate_limit_source` 区分 `sso` / `data`:只有 sso 冷却会拒绝
动作,且只拒绝「刷新令牌」和「重新绑定」这两个真的会打登录端点的操作——库里
令牌没过期就照常同步。**重新输账号密码换新令牌解决不了**`garth.login()`
同一个端点、更重的流程,限流按账号计,换设备换网络都绕不开。用户可用 force
推翻我们自己猜的 24 小时。
数据端点另有节流:所有调用经 `services/garmin_throttle.py` 的代理,默认间隔
0.5s + 单次预算 1200 次(依据见该文件顶部)。
429 退避 24h`rate_limited_until`**DB 为准**(多 worker 内存不一致会卡死
守卫),且只作**信息**不作闸门;`_is_rate_limited` 沿异常 `__cause__` 链识别
RetryError 里的 429。
5. **AI 生成慢**:网关上游推理模型单次 2~5 分钟,晨报走后台生成 + 轮询,
接口超时/流式不可靠见 `services/ai.py` 的非流式回退;生产队列并发
`AI_JOB_CONCURRENCY=2`(共享网关,太高会把别家打成 502
6. **迁移幂等**`db.py``init_db()` 会被多 worker 并发调用,所有
`ALTER/CREATE` 必须幂等(`CREATE TABLE IF NOT EXISTS` + 先查列再加列)。
7. **不要提交**`.env`、构建产物 `client/build`、SQLite 数据、`.workbuddy/`
`.claude/`(已在 .gitignore。改代码后同步 `backend/static/` 用 deploy 流程。
8. **一任务一 commit**,完成即推送 NAS Giteaorigin 就是 Gitea
## 测试
- pytest446+ 项)覆盖:设置吸附/校验、身体年龄、运动详情列存解析、Garmin
同步入库与只读本地保证、AI 缓存/队列、调度器等。改后端先跑
`npm run test`(或 `cd backend && .venv/bin/python -m pytest`)。
- 前端无单测框架,用 Chrome headless + CDP 截图做界面回归(见每日日志)。
## 下一步 / 已知问题
-`PROGRESS.md` 的待办区与 `docs/REQUIREMENTS.md`
- 双后端 SQL 注意:**MariaDB 的 CAST 不支持 TEXT 目标类型**SQLite 支持)——写跨库
查询不要用 `CAST(x AS TEXT)`,两侧都是字符串时直接比较即可(曾导致 refill_backlog
活动分支在 NAS 上 1064 刷屏commit 617c832 已修)。
- 双后端 SQL 注意:**MariaDB 的 `trigger` 是保留字SQLite 却允许它做列名**——本地
pytest 全绿也不代表生产可用(首次部署 `sync_history` 即 1064 起服务失败)。跨库建表
列名避免 `trigger`,用 `trigger_kind` 之类替代commit 5e6fc01