Files
GarminHealthLab/CLAUDE.md

10 KiB
Raw Blame History

Garmin Health Lab — 项目指南

佳明Garmin健康数据分析平台同步 Garmin Connect 数据,做多维度健康分析、 可视化与 AI 解读(晨报 / 趋势归因 / Copilot

阅读顺序:先读本文件(协作约定与现状速览)→ PROGRESS.md (功能与部署进度,排查前必读)→ README.mdAPI 与使用)→ docs/(架构 / 开发 / 需求)。判断"部署在哪里、跑没跑、什么版本"时 一律以 PROGRESS.mddeploy/ 脚本为准,不要凭记忆断言

技术栈现状2026-09

  • 后端Python 3.10+ / Flask应用工厂Gunicorn 生产运行。原 Node/Express 后端早已重写为 Python仓库中不存在 server/ 目录
  • 前端React 18 + TypeScript用 **CRAreact-scripts**构建UI 组件库为 Framework7 9framework7-reactiOS 主题)+ Recharts 图表。根目录 package.json 是 npm workspacesclient)。
  • 数据库:可插拔数据层(backend/db.pyDB_TYPE 切换—— 本地开发 SQLitebackend/data/health.db,默认);生产 NAS MariaDB garmin_health_lab 库,连接配置见 NAS 上 backend/.env)。
  • 认证auth-hub SSOOAuth2/OIDCservices/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 文档

常用命令

# 后端依赖(首次):在 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-labroot 用户跑 gunicorn 0.0.0.0:81242 workers / 4 threads / --timeout 300开机自启走 DSM 任务调度器执行 deploy/S99garmin.sh
  • 一键部署:本地 ./deploy/push.shtar 经 ssh 同步 backend + deploy + 清空重推 client/build + sudo 重启,校验 gunicorn pid 变化 + health 200)。 前置:先 npm run build
  • 公网NAS frpc → 甲骨文 http://129.146.26.249:8124frp 重连需几秒)。
  • DBNAS MariaDB 10.11root 经 socket /run/mysqld/mysqld10.sock(或 TCP 127.0.0.1:3306garmin_health_lab。10.11 dump 含 /*M!999999 注释、旧客户端会报错,且须 --default-character-set=utf8mb4 防中文丢失。
  • auth-hubclient 996aLPw4T5gl-rYZ;回调注册了 LAN http://192.168.50.64:8124/auth/callback 与公网 http://129.146.26.249:8124/auth/callback 两个地址。
  • 部署/排障前:先读 PROGRESS.mddeploy/ 脚本确认事实(曾经凭旧记忆 断言"无线上环境"而误判)。服务以 root 运行:重启用 sudo日志 logs/error.loglogs/access.log 是 root 所有。

关键约定与坑(写代码/改样式前看)

  1. 日期一律本地日历日期,别用 toISOString()UTCUTC+8 每天前 8 小时 查的是昨天)。前端统一走 client/src/lib/day.ts
  2. 阈值全部来自公开参考值AI 只解读不定阈值(设计原则,勿破)。
  3. Framework7 全局 button { width: 100% } 会把任何 <button> 拉满父容器 ——自定义组件里的按钮要显式 width: autoCopilot 浮窗 ✕/发送按钮曾因此 错位,见 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 退避 24hrate_limited_untilDB 为准(多 worker 内存不一致会卡死 守卫),且只作信息不作闸门;_is_rate_limited 沿异常 __cause__ 链识别 RetryError 里的 429。
  5. AI 生成慢:网关上游推理模型单次 2~5 分钟,晨报走后台生成 + 轮询, 接口超时/流式不可靠见 services/ai.py 的非流式回退;生产队列并发 AI_JOB_CONCURRENCY=2(共享网关,太高会把别家打成 502
  6. 迁移幂等db.pyinit_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