docs: 文档/配置/测试同步 NAS :8124 生产现状,清除 Node.js 与甲骨文残留

背景:文档停在 Node.js 时代或甲骨文 8123 部署,与生产(NAS :8124 + Flask +
auth-hub + ai-gateway)严重脱节,曾导致凭旧记忆误判'无线上环境'。

- CLAUDE.md 重写:技术栈/结构/命令/部署事实/关键坑(F7 button、UTC 日期、
  429 退避以 DB 为准、迁移幂等、AI 生成耗时)
- docs/ARCHITECTURE.md 重写为 Flask 蓝图+services+可插拔数据层 + NAS 部署
- docs/DEVELOPMENT.md 重写为 Flask/CRA 开发指南 + push.sh 部署流程
- docs/REQUIREMENTS.md:部署条目改 NAS 8124;补 auth-hub/AI 教练/新修复
- docs/AUTH_HUB_INTEGRATION.md 新增(补 .env.example 悬空引用)
- README.md:技术栈/DB/auth-hub/API 清单/部署节修正
- backend/config.py 与 .env.example:AUTH_HUB_REDIRECT_URI 默认 8123→8124,
  MariaDB 注释 Oracle→NAS
- tests:GatewayCourtesy 并发测试对齐 MAX_CONCURRENT(AI_JOB_CONCURRENCY=2);
  conftest 禁用 create_app 后台队列线程,修整库测试 flaky(585 passed)
This commit is contained in:
ericwyuan
2026-09-02 19:34:32 +08:00
parent 68363957ec
commit 2d9a2be185
12 changed files with 609 additions and 496 deletions

3
.gitignore vendored
View File

@@ -28,6 +28,9 @@ logs/
# WorkBuddy tool state (not project code)
.workbuddy/
# Local dev tooling config (not project code)
.claude/
# --- Python / Flask backend ---
__pycache__/
*.py[cod]

192
CLAUDE.md
View File

@@ -1,88 +1,138 @@
# Garmin Health Lab - 项目指南
# Garmin Health Lab 项目指南
## 项目简介
佳明Garmin健康数据分析平台同步 Garmin Connect 数据,做多维度健康分析、
可视化与 AI 解读(晨报 / 趋势归因 / Copilot
Garmin Health Lab 是一个完整的健康数据分析平台,用于:
- 获取和同步佳明Garmin设备数据
- 进行多维度的健康数据分析
- 提供个性化的健康建议
- 可视化健康趋势
> **阅读顺序**:先读本文件(协作约定与现状速览)→ [PROGRESS.md](PROGRESS.md)
> (功能与部署进度,**排查前必读**)→ [README.md](README.md)API 与使用)→
> [docs/](docs/)(架构 / 开发 / 需求)。判断"部署在哪里、跑没跑、什么版本"时
> 一律以 `PROGRESS.md` 和 `deploy/` 脚本为准,**不要凭记忆断言**。
## 技术栈
## 技术栈现状2026-09
- **端**: React 18 + TypeScript + Recharts
- **后端**: Node.js + Express + TypeScript
- **数据库**: SQLite3
- **API 集成**: Garmin Connect API
- **端**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`
## 快速开始
## 目录结构
### 安装
```bash
npm install
```
### 配置
```bash
cp server/.env.example server/.env
# 编辑 server/.env 填入 Garmin 凭证
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
npm run dev
```
- 前端: http://localhost:3000
- 后端: http://localhost:5000
## 项目结构
- `client/` - React 前端应用
- `server/` - Node.js 后端服务
- `docs/` - 文档
详见 [ARCHITECTURE.md](docs/ARCHITECTURE.md) 和 [DEVELOPMENT.md](docs/DEVELOPMENT.md)
## 核心功能
1. **仪表板** - 健康数据概览和最近数据展示
2. **数据同步** - 从 Garmin 同步最新健康数据
3. **数据分析** - 趋势分析和数据可视化
4. **健康建议** - 基于数据的个性化建议
5. **设置** - 用户配置和偏好设置
## 关键特性
- ✅ Garmin API 集成
- ✅ 多维度数据分析(步数、心率、睡眠等)
- ✅ 实时数据同步
- ✅ 交互式数据可视化
- ✅ JWT 认证安全
- ✅ 本地数据存储
## 开发指南
- 详见 [DEVELOPMENT.md](docs/DEVELOPMENT.md)
- API 文档见 README.md
## 常用命令
```bash
npm run dev # 开发模式
npm run build # 构建项目
npm run typecheck # 类型检查
npm start # 生产模式
# 后端依赖(首次):在 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
- [ ] 实现 Garmin OAuth 认证
- [ ] 完成 Garmin API 数据获取
- [ ] 实现仪表板可视化
- [ ] 添加数据分析算法
- [ ] 部署和优化
- **位置**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 所有。
## 联系方式
## 关键约定与坑(写代码/改样式前看)
项目维护: ericwyuan.g@gmail.com
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. **同步限流**Garmin 账号 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`
- 已知独立 bugNAS `logs/stdout.log` 里 `refill_backlog ... SQL syntax near
'TEXT)) AND NOT EXISTS'` 反复刷——某条 AI 回填 SQL 与 NAS MariaDB 语法对
不上,尚未修复。

View File

@@ -42,7 +42,7 @@
- [x] 身体年龄计算(`services/fitness_age.py`
- [x] 运动详情与全天曲线同步(`services/extras.py`
- [x] 运动详情与全天曲线同步(`services/garmin_extras.py`
- [x] 可插拔数据层(`db.py`,支持 SQLite ↔ MariaDB 切换)
@@ -109,7 +109,7 @@
- [x] 前端:今日页 AI 晨报卡片(后台生成 + 轮询升级)、全局 Copilot 浮窗、
指标详情页 AI 归因面板;`features.ts``ai` 开关已打开
- [x] 接入自建 ai-gateway`https://oracle.zichuan.xyz/ai/v1`),实测走通
- [x] 接入自建 ai-gateway`https://ai.zichuan.xyz/v1`),实测走通
- [x] 生产者/消费者队列(`services/jobs.py`):优先级队列 + 多 worker 互斥锁
@@ -122,6 +122,16 @@
> 流式 139 秒后返回「所有模型均不可用」,阻塞则成功,因此 `stream_chat()`
> 在流式无输出时会对同一模型退回非流式重试。
### Copilot 浮窗 UI 修复与文档同步2026-09-02
- [x] Copilot 浮窗 UI 修复Framework7 全局 `button { width: 100% }` 把面板内
✕ / 发送按钮拉满父容器,挤坏 flex 布局(输入框缩到 21px、✕ 跑到面板中间)。
`Copilot.css` 显式 `width: auto` 覆盖,重构产物已部署到 NAS :8124 验证。
- [x] 文档与代码对齐生产现状CLAUDE.md / README.md / docs/\* 全部更新为
Python/Flask + NAS :8124 + auth-hub + ai-gateway清除 Node.js 时代与
甲骨文 8123 的过时描述);`config.py` / `.env.example` 默认值同步。
## 待办
### 功能完善
@@ -135,7 +145,9 @@
- [ ] 多用户支持完善
### 运维
- [x] `deploy.sh` 自动复制前端构建到 `backend/static/`,避免部署旧版
- [ ] 监控与告警
- [ ] 日志轮转与清理
@@ -146,9 +158,11 @@
### 文档
- [x] ARCHITECTURE.md 需更新(当前仍为 Node.js 架构描述
- [x] ARCHITECTURE.md 已重写为 Python/Flask 架构 + NAS :8124 部署2026-09-02
- [x] DEVELOPMENT.md 需更新(当前仍为 Node.js 开发指南
- [x] DEVELOPMENT.md 已重写为 Flask/CRA 开发指南2026-09-02
- [x] REQUIREMENTS.md 更新
- [x] REQUIREMENTS.md 更新部署位置与新增需求2026-09-02
- [x] CLAUDE.md / README.md 同步为最新技术栈与部署现状2026-09-02

View File

@@ -13,20 +13,21 @@
## 🛠 技术栈
### 前端
- React 18 + TypeScript
- React 18 + TypeScriptCRA / react-scripts 构建)
- Framework7 9`framework7-react`iOS 风格 UI
- Recharts (数据可视化)
- Tailwind CSS (样式)
- Axios (API 请求)
- Axios (API 请求JWT 拦截器)
### 后端
- Python 3.10+ + Flask
- Gunicorn生产运行
- 可插拔数据层SQLite本地开发/ MariaDB生产Oracle 云服务器本地 MariaDB 10.3,经 PyMySQL
- JWT 鉴权 + scrypt 密码哈希
- garminconnect可选Garmin API 集成
- Python 3.10+ + Flask(应用工厂)
- Gunicorn生产运行NAS :8124
- 可插拔数据层SQLite本地开发/ MariaDB生产NAS 10.11 库
`garmin_health_lab`,经 PyMySQL/socket
- JWT 鉴权;登录走 auth-hub 统一 SSOOAuth2/OIDC本地密码登录已移除
- garminconnectGarmin API 集成)
> 注:原 Node/TypeScript 后端保留在 `server/`(仅 service 逻辑骨架);
> 当前可运行实现为 `backend/` 下的 Python/Flask 单体服务
> 早期 Node.js/TypeScript 后端`server/`)已整体重写为 `backend/` 下的
> Python/Flask 单体,目录已删除
## 📁 项目结构
@@ -39,13 +40,13 @@ GarminHealthLab/
│ ├── wsgi.py # Gunicorn 入口
│ ├── config.py # 配置(读 .env
│ ├── db.py # 可插拔数据层 (SQLite / MariaDB)
│ ├── auth.py # scrypt + JWT + require_auth
│ ├── auth.py # JWT 签发/校验 + require_auth
│ ├── services/ # 业务逻辑 (health / analysis / garmin)
│ ├── routes/ # 蓝图 (auth / garmin / health / analysis)
│ ├── tests/smoke.py # 冒烟测试
│ ├── requirements.txt
│ └── .env.example
├── server/ # 原 Node/TS 后端(仅 service 骨架,未接入路由)
├── server/ # (已删除)原 Node/TS 后端,已重写为 backend/
├── docs/ # 文档
├── package.json # 工作空间根配置
└── README.md
@@ -85,22 +86,23 @@ JWT_SECRET=your_jwt_secret_here # 生产务必更换
CORS_ORIGIN=http://localhost:3000,http://localhost:5173
```
> 原 `server/.env` 的 Node 配置已弃用,请改用 `backend/.env`。
> 早期 Node 后端的配置方式已弃用,一律使用 `backend/.env`。
### 数据库SQLite / MariaDB 可插拔
数据层通过 `DB_TYPE` 环境变量切换后端,**业务代码无需改动**
- **SQLite默认本地开发**:零配置,由 `DATABASE_PATH` 指定文件位置。
- **MariaDB生产**:运行于 Oracle 云服务器129.146.26.249)本地 MariaDB 10.3专用账号 `garmin``127.0.0.1:3306` 连接PyMySQL
生产使用独立库 `garmin_health_lab`(与 `sentinel_home_ai` 隔离)。完整配置见 `backend/.env.example`
- **MariaDB生产**:运行于 NAS192.168.50.64)本地 MariaDB 10.11root 经
socket `/run/mysqld/mysqld10.sock`(或 TCP `127.0.0.1:3306`连接PyMySQL
独立库 `garmin_health_lab`。完整配置见 `backend/.env.example`
```env
DB_TYPE=mariadb
MARIADB_SOCKET=/run/mysqld/mysqld10.sock
MARIADB_HOST=127.0.0.1
MARIADB_PORT=3306
MARIADB_USER=garmin
MARIADB_USER=root
MARIADB_PASSWORD=your_production_mariadb_password
MARIADB_DATABASE=garmin_health_lab
```
@@ -117,14 +119,18 @@ python app.py
# 或生产方式gunicorn wsgi:app -b 0.0.0.0:5000
```
前端(Vite/React端口 3000 或 5173
前端ReactCRA 开发服务器proxy 到后端 5000
```bash
npm run dev
npm install # 在仓库根目录执行workspaces 装 client
npm run dev # 同时起后端 5000 + 前端 3000或分别 dev:backend / dev:client
```
- 前端: http://localhost:3000
- 后端 API: http://localhost:5000/api
> 登录走 auth-hub SSO点登录会跳到统一认证页回跳后后端签 JWT。
> 数据库首次启动自动建表(`db.init_db()`,幂等)。
### 冒烟测试
```bash
@@ -134,15 +140,25 @@ python tests/smoke.py
## 📚 API 文档
### 认证
- `POST /api/auth/register` - 用户注册email, garminEmail, garminPassword
- `POST /api/auth/login` - 用户登录email, password
- `POST /api/auth/logout` - 用户登出
### 认证auth-hub 统一 SSO
- `POST /api/auth/auth-hub/start` - 发起 SSO 登录(返回 auth-hub 授权 URL
- `GET /api/auth/callback?code=…` - auth-hub 回调(浏览器跳转,签发 JWT
- `POST /api/auth/refresh` - 刷新 Token
- `POST /api/auth/logout` - 登出
> 本地邮箱/密码注册登录已移除;账号体系由 auth-hub 统一提供
> (见 [AUTH_HUB_INTEGRATION.md](docs/AUTH_HUB_INTEGRATION.md))。
> Garmin 账号的绑定/解绑走 `/api/garmin/*`(见下),与 Web 登录相互独立。
### Garmin 数据同步
- `POST /api/garmin/login` - 发起 Garmin OAuth 授权(需验证码时 `POST /api/garmin/mfa` 提交)
- `GET /api/garmin/auth-status` / `GET /api/garmin/login-status` - 授权状态
- `DELETE /api/garmin/login` / `POST /api/garmin/disconnect` - 退出/解绑 Garmin 账号
- `POST /api/garmin/sync` - 同步 Garmin 数据
- `GET /api/garmin/status` - 获取同步状态
- `POST /api/garmin/sync-latest` - 仅同步最新数据
- `POST /api/garmin/sync-details` - 补齐运动详情/全天曲线(后台队列)
- `GET /api/garmin/status` / `GET /api/garmin/auto-sync` - 同步状态
- `GET /api/garmin/activities/<activity_id>/detail` - 单次运动详情
### 健康数据
- `GET /api/health/summary` - 获取健康摘要
@@ -150,6 +166,10 @@ python tests/smoke.py
- `GET /api/health/heart-rate` - 获取心率数据
- `GET /api/health/sleep` - 获取睡眠数据
- `GET /api/health/activities` - 获取运动数据
- `GET /api/health/fitness-age` - 身体年龄
- `GET /api/health/series` - 全天曲线(心率/压力/身体电量/呼吸/血氧)
- `GET /api/health/body-composition` / `blood-pressure` / `race-predictions`
- `GET /api/health/badges` / `personal-records` / `challenges` / `devices`
### 分析与建议
- `GET /api/analysis/trends` - 获取数据趋势
@@ -169,20 +189,31 @@ python tests/smoke.py
- 请求体 `{question, history?, date?, model?}`
- 事件序列 `start``delta`* → `done`,失败时为 `error`
> AI 相关接口全部经由自建 **ai-gateway**OpenAI 兼容,见 `AI_GATEWAY_BASE_URL`)。
> AI 相关接口全部经由自建 **ai-gateway**OpenAI 兼容,`https://ai.zichuan.xyz/v1`
> 见 `AI_GATEWAY_BASE_URL`)。
> 该网关的主上游是大型推理模型,一次生成实测需 2~5 分钟,因此简报走后台生成 +
> 轮询,趋势归因与 Copilot 走显式触发;任一模型失败时降级为规则引擎,
> `meta.source` 会说明本次由谁作答。
## 🚀 部署(生产 = NAS
- **位置**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`
- **公网**NAS frpc → `http://129.146.26.249:8124`
- **一键部署**:先 `npm run build`,再 `./deploy/push.sh`(同步 backend +
deploy + 清空重推 `client/build` + sudo 重启 + pid/health 双校验)。
- **部署排障前**:先读 `PROGRESS.md``deploy/` 脚本确认事实。
## 🔐 安全说明
- Garmin 账户密码使用加密存储
- 所有 API 请求需要 JWT 认证
- 敏感数据不在前端存储
- Garmin 账户密码不入库(令牌授权,约一年有效)
- 所有 API 请求需要 JWT 认证auth-hub SSO 签发)
- 敏感数据不在前端存储`backend/.env` 含密钥,不提交 git
## 📝 开发指南
详见 [DEVELOPMENT.md](./docs/DEVELOPMENT.md)
详见 [DEVELOPMENT.md](./docs/DEVELOPMENT.md) 与 [ARCHITECTURE.md](./docs/ARCHITECTURE.md)
## 📄 许可证

View File

@@ -8,12 +8,14 @@ DB_TYPE=sqlite
# SQLite file (used when DB_TYPE=sqlite)
DATABASE_PATH=./data/health.db
# MariaDB (used when DB_TYPE=mariadb) — production DB on the Oracle server
# (129.146.26.249, local MariaDB 10.3). Dedicated account over TCP 127.0.0.1;
# MARIADB_SOCKET is only needed if TCP auth is disabled for the app user.
# MariaDB (used when DB_TYPE=mariadb) — production DB on the NAS
# (192.168.50.64, MariaDB 10.11). Connection is over the socket
# /run/mysqld/mysqld10.sock (or TCP 127.0.0.1:3306) as root; the socket path
# only matters when TCP auth is disabled for the app user.
# MARIADB_SOCKET=/run/mysqld/mysqld10.sock
# MARIADB_HOST=127.0.0.1
# MARIADB_PORT=3306
# MARIADB_USER=garmin
# MARIADB_USER=root
# MARIADB_PASSWORD=your_production_mariadb_password
# MARIADB_DATABASE=garmin_health_lab
@@ -35,8 +37,10 @@ AUTH_HUB_BASE_URL=http://129.146.26.249:5300
AUTH_HUB_CLIENT_ID=your_client_id
AUTH_HUB_CLIENT_SECRET=your_client_secret
#
# Callback URL (must exactly match what's registered in auth-hub)
AUTH_HUB_REDIRECT_URI=http://129.146.26.249:8123/auth/callback
# Callback URL (must exactly match what's registered in auth-hub). Production
# registers both the public frp address (129.146.26.249:8124) and the LAN
# address (192.168.50.64:8124).
AUTH_HUB_REDIRECT_URI=http://129.146.26.249:8124/auth/callback
# --- CORS (comma-separated allowed front-end origins) ---
# localhost stays in the production list on purpose: CORS is not an auth
@@ -55,7 +59,7 @@ CORS_ORIGIN=http://localhost:3000,http://localhost:5173
# can take 2-3 minutes, so set AI_TIMEOUT_SECONDS accordingly.
# HTTPS (Caddy, strips the /ai prefix) rather than http://…:5100 — the token
# rides in an Authorization header and should not cross the internet in clear.
AI_GATEWAY_BASE_URL=https://oracle.zichuan.xyz/ai/v1
AI_GATEWAY_BASE_URL=https://ai.zichuan.xyz/v1
AI_GATEWAY_TOKEN=
AI_GATEWAY_MODEL=ai-gateway-auto
@@ -91,14 +95,9 @@ AI_MAX_TOKENS=1024
AI_COACH_MAX_TOKENS=4000
# --- AI coach job queue ---
# The gateway runs `gunicorn -w 1 --threads 4` and is shared with fam-edge and
# the camera project: four concurrent requests for everyone, while one of ours
# holds a thread for 2-5 minutes. So this consumer runs one job at a time
# across the whole deployment (not one per Gunicorn worker) and waits between
# jobs. Raising either of these makes the backfill finish sooner at the cost of
# the shared box — a 502 there is a 502 for the other two projects as well.
AI_JOB_CONCURRENCY=1
AI_JOB_GAP_SECONDS=20
# Three projects share the gateway, so going too high causes 502s.
AI_JOB_CONCURRENCY=2
AI_JOB_GAP_SECONDS=5
# Set AI_JOBS=false to stop consuming entirely (screens then show the computed
# figures with no model reading).
AI_JOBS=true

View File

@@ -41,9 +41,14 @@ JWT_EXPIRY_DAYS = int(os.environ.get("JWT_EXPIRY_DAYS") or 7)
# fallback on purpose — unlike the issuer URL and client id, it must never be
# hardcoded in source; put it in backend/.env (gitignored) instead.
AUTH_HUB_BASE_URL = os.environ.get("AUTH_HUB_BASE_URL") or "http://129.146.26.249:5300"
# Dev fallback client id (auth-hub keeps dev and prod clients in separate
# databases). Production always overrides this via backend/.env — the value in
# use on the NAS is the registered client for the :8124 callbacks.
AUTH_HUB_CLIENT_ID = os.environ.get("AUTH_HUB_CLIENT_ID") or "0asGO0FdX_XYOk6O"
AUTH_HUB_CLIENT_SECRET = os.environ.get("AUTH_HUB_CLIENT_SECRET") or ""
AUTH_HUB_REDIRECT_URI = os.environ.get("AUTH_HUB_REDIRECT_URI") or "http://129.146.26.249:8123/auth/callback"
# Production callback goes through the NAS frp tunnel to the public address
# (129.146.26.249:8124); the LAN callback 192.168.50.64:8124 is registered too.
AUTH_HUB_REDIRECT_URI = os.environ.get("AUTH_HUB_REDIRECT_URI") or "http://129.146.26.249:8124/auth/callback"
# --- Static UI --------------------------------------------------------------
# Directory holding the built React app. When set and populated, the Flask

View File

@@ -52,6 +52,19 @@ def _isolate_ai_env(monkeypatch):
monkeypatch.delenv(var, raising=False)
@pytest.fixture(autouse=True)
def _no_ai_jobs_background_thread(monkeypatch):
"""create_app() starts a daemon queue-consumer thread that outlives the
test that launched it and races the *next* test for jobs on that test's
fresh database (with whatever _runner the previous stub left behind) —
which made queue tests flaky. Tests drive the queue themselves through
jobs.run_once(), so the thread is disabled here.
"""
from services import jobs
monkeypatch.setattr(jobs, "ENABLED", False)
@pytest.fixture(autouse=True)
def _clear_garmin_client_cache():
"""Drop cached Garmin sessions between tests.

View File

@@ -1076,15 +1076,20 @@ class TestHighlightsReadAlone:
class TestGatewayCourtesy:
"""The gateway runs one worker with four threads and is shared with two
other projects. This consumer must not be able to saturate it."""
"""The gateway is shared with two other projects, so this consumer must
not saturate it. The cap is MAX_CONCURRENT (AI_JOB_CONCURRENCY; default 2
since 2026-09-01) and is counted across the whole deployment, not per
Gunicorn worker."""
def test_only_one_job_runs_at_a_time_across_the_deployment(self, db, user):
jobs.enqueue(user["id"], "health", "a")
jobs.enqueue(user["id"], "sleep", "b")
assert jobs._claim_next() is not None
assert jobs._claim_next() is None, \
"a second Gunicorn worker must not start a second gateway call"
def test_concurrency_is_capped_at_MAX_CONCURRENT(self, db, user):
"""More than the cap may queue, but only MAX_CONCURRENT run at once."""
limit = jobs.MAX_CONCURRENT
for i in range(limit + 2):
jobs.enqueue(user["id"], "health", f"job-{i}")
claimed = [jobs._claim_next() for _ in range(limit + 1)]
assert sum(1 for c in claimed if c is not None) == limit
assert claimed[limit] is None, \
"a second claim past MAX_CONCURRENT must not start another gateway call"
def test_a_finished_job_frees_the_slot(self, db, user):
jobs.enqueue(user["id"], "health", "a")

View File

@@ -1,167 +1,160 @@
# 项目架构说明
> 现状2026-09-02后端为 **Python/Flask 单体**,生产部署在 **NAS :8124**。
> 早期 Node.js/Express + `server/` 目录的后端已被整体重写为
> `backend/`Python本文件描述的是当前实现。
## 整体架构
```
┌─────────────────────────────────────────────────────┐
│ 浏览器 (React 前端)
┌────────────┬──────────────┬──────────┬──────────┐
│ 仪表板 │ 数据同步 │ 数据分析 │ 设置
└────────────┴──────────────┴──────────┴──────────┘
────────────────────────────────────────────────────
│ HTTP/REST
┌──────────────────────────────────────────────────────┐
│ Node.js/Express 后端服务器 │
┌──────────────────────────────────────────────┐
│ API 路由层 │
│ │ ├─ /auth - 用户认证
│ │ ├─ /garmin - Garmin 数据同步 │
│ ├─ /health - 健康数据查询 │
│ │ └─ /analysis - 数据分析
└──────────────────────────────────────────────┘
┌──────────────────────────────────────────────┐
│ │ 服务层
│ ├─ GarminService - Garmin API 集成 │
│ │ ├─ HealthService - 健康数据业务逻辑
│ │ ├─ AuthService - 认证授权
│ └─ AnalysisService - 数据分析 │
└──────────────────────────────────────────────┘
└──────────────────────┬───────────────────────────────┘
┌──────────────────────────────┐
│ SQLite 数据库
│ ├─ users
│ ├─ health_data │
│ ├─ activities
└─ sync_status
└──────────────────────────────┘
┌──────────────────────────────┐
│ Garmin Cloud API
│ ├─ 用户认证
│ ├─ 数据获取 │
│ └─ 数据同步 │
└──────────────────────────────┘
┌──────────────────────────────────────────────────────────
浏览器 (React SPA)
Framework7 9 (iOS 主题) + Recharts单端口同源部署
┌──────┬──────┬──────┬──────┬──────────────┐
│ 今日 │ 健康 │ 趋势 │ 运动 │ 设置 / 更多… │
└──────────────────────────────────────┘
└───────────────┬──────────────────────────────────────────┘
│ HTTP/JSON + JWTBearerCopilot 用 SSE
┌──────────────────────────────────────────────────────────┐
Flask 单体gunicorn 2w/4tNAS :8124
┌────────────────────────────────────────────────────┐
│ │ 蓝图层 routes/ /api/auth /api/garmin /api/health │ │
│ │ /api/analysis /api/settings │
├────────────────────────────────────────────────────┤
│ │ 服务层 services/ 业务逻辑(无 Flask 依赖) │ │
│ auth_hub_client garmin garmin_auth garmin_extras │
│ scheduler(定时同步) ai/coach/insights/jobs(AI队列) │
│ │ health analysis fitness_age settings scopes │ │
├────────────────────────────────────────────────────┤
│ │ 横切auth.py(JWT+require_auth) config.py(配置) │ │
│ │ db.py(可插拔数据层+幂等迁移) │ │
└────────────────────────────────────────────────────┘
───────┬──────────────────────────────┬──────────────────┘
│ │
┌──────────────────┐ ┌─────────────────────────────┐
│ SQLite (开发) │ │ MariaDB (生产, NAS 10.11) │
│ data/health.db │ │ garmin_health_lab 库
│ DB_TYPE=sqlite │ root@socket mysqld10.sock
└──────────────────┘ └─────────────────────────────┘
│ 外部系统(经 services/ 调用)│
▼ ▼
┌──────────────────────────────────────────────────────────┐
auth-hub SSO Garmin Cloud API ai-gateway │
129.146.26.249: (OAuth, 账号级限流 (https://ai.zichuan
│ 5300 OIDC 429 → 退避24h) .xyz/v1, NVIDIA推理)
└──────────────────────────────────────────────────────────┘
```
## 核心模块
- 单端口部署Flask 既出 API 又托管 React 构建产物(`STATIC_DIR`),前端 API
请求为**同源 `/api/*`**`client/.env.production` 写死 `REACT_APP_API_URL=/api`)。
- SPA 路由兜底404 handler 对非 `/api/` 路径回退 `index.html`,页面刷新不 404。
### 1. 前端 (Client)
- **框架**: React 18 + TypeScript
- **路由**: React Router
- **UI 组件**: 自定义 + CSS
- **数据可视化**: Recharts
- **API 通信**: Axios
## 后端分层
**主要页面**:
- Dashboard (仪表板)
- DataSync (数据同步)
- Analysis (数据分析)
- Recommendations (健康建议)
- Settings (设置)
### routes/ — 蓝图(只做参数校验与序列化,逻辑在 services
### 2. 后端 (Server)
- **框架**: Express.js
- **语言**: TypeScript
- **数据库**: SQLite3
- **认证**: JWT
| 蓝图 | 前缀 | 主要端点 |
|---|---|---|
| `auth` | `/api/auth` | SSO 登录 `POST /auth-hub/start``GET /callback``POST /logout``/refresh` |
| `garmin` | `/api/garmin` | `POST /sync``/sync-latest``/sync-details``GET /auth-status``/login-status``/status``/auto-sync``/activities/<id>/detail``POST /login``/mfa``/disconnect``DELETE /login` |
| `health` | `/api/health` | `summary` `steps` `heart-rate` `sleep` `activities` `fitness-age` `badges` `personal-records` `body-composition` `blood-pressure` `race-predictions` `series` `challenges` `devices` |
| `analysis` | `/api/analysis` | `trends` `recommendations` `models` `ai-recommendations` `briefing` `trend-insight` `copilot`(SSE) `insight` `insight/queue` |
| `settings` | `/api/settings` | `GET/PUT ""` `GET /options` `GET /rating-basis` |
**主要服务**:
- **AuthService**: 用户认证和授权
- **GarminService**: Garmin API 集成和数据获取
- **HealthService**: 健康数据管理
- **AnalysisService**: 数据分析和建议生成
另有 `GET /api/health/status`健康检查app.py 直接定义)。
### 3. 数据库 (SQLite)
**表结构**:
- `users`: 用户信息和 Garmin 凭证
- `health_data`: 每日健康数据汇总
- `activities`: 运动活动记录
- `sync_status`: 数据同步状态追踪
### services/ — 领域逻辑
- **garmin 同步族**`garmin.py`(核心同步 + 429 限流退避,`rate_limited_until`
持久化到 `sync_status`,以 DB 为准防多 worker 内存不一致)、`garmin_auth.py`
OAuth 登录 + MFA 验证码会话,跨 worker 经 `garmin_mfa_sessions` 表交接)、
`garmin_extras.py`(身体成分/血压/血氧/全天曲线/设备/徽章/挑战等扩展数据)。
- **调度与队列**`scheduler.py`自动同步DB `job_locks` 抢占,多 worker 只
跑一次)、`jobs.py`AI 生产者/消费者队列,同表互斥,含 `refill_backlog`)。
- **AI 教练**`insights.py` 特征工程z 分数 / 趋势斜率 / 活动量对比)→
`coach.py` 三套提示词 + 规则引擎兜底 → `ai.py` 多模型链 + 缓存
`ai_insights`(数据指纹失效)。
- 其余:`analysis.py` / `health.py` / `fitness_age.py`(身体年龄推算)/
`settings.py`(设置吸附校验)/ `auth_hub_client.py`OIDC 客户端)/
`scopes.py`
### 认证链路auth-hub SSO
```
用户点「登录」→ POST /api/auth/auth-hub/start → 302 auth-hub 登录页
→ auth-hub 回调 GET /api/auth/callback?code=… → exchange_code_for_token
→ get_userinfo → find_or_create_user → 后端签 JWT7 天)→ 前端存 localStorage
```
- 本地邮箱/密码注册登录**已移除**;每个账号来自 auth-hub。
- **Garmin 账号授权是独立流程**(与 Web 登录分开):`/api/garmin/login`
起 OAuth 流程,需要验证码时 `/api/garmin/mfa` 提交,令牌存
`garmin_tokens`(约一年有效,密码不入库)。
- 所有数据路由经 `require_auth` 校验 JWT。
### 数据层db.py 可插拔)
- `DB_TYPE=sqlite`(开发默认,`backend/data/health.db`)↔ `DB_TYPE=mariadb`
(生产 NAS`config.py` 读取,业务代码不分叉。
- 20 张表:`users health_data activities sync_status garmin_tokens badges
personal_records job_locks garmin_mfa_sessions user_settings
activity_details body_composition blood_pressure race_predictions
daily_series challenges devices ai_recommendations ai_jobs ai_insights`。
- `init_db()` 幂等(`CREATE TABLE IF NOT EXISTS` + 按列检测的 `ALTER`
多 gunicorn worker 并发调用安全。`id` 为 VARCHAR(64),外键指向 `users`。
## 数据流
### 1. Garmin 数据同步流程
### 1. Garmin 同步(手动 + 自动)
```
用户点击"同步"
POST /api/garmin/sync
GarminService.syncData()
获取 Garmin 授权 Token
调用 Garmin API 获取数据
转换数据格式
存储到本地 SQLite
更新 sync_status
返回同步结果
用户/调度器触发
→ 检查 sync_status.rate_limited_until429 冷却 24hDB 为准)
→ garmin.py 分批拉取health_data 每日指标、activities、daily_series 等)
→ 库内 upsert → 更新 sync_status → prefetch_insightsAI 洞察预取入队)
```
### 2. 数据分析流程
- 同步频率与历史范围history_days读用户设置`0` 表示全部历史 = 730 天上限。
- 全天曲线每天 5 个请求14 天内同步顺带拉取,更长的历史走「补齐详细数据」
后台任务(`POST /api/garmin/sync-details` 触发,队列消费)。
### 2. AI 晨报 / Copilot / 趋势归因
```
GET /api/health/summary (日期范围)
从数据库查询健康数据
计算统计指标
生成趋势分析
返回分析结果
前端绘制图表
AI 作业入队(ai_jobs优先级队列) → jobs.py worker 单飞消费(DB 互斥)
→ services/ai.py 调 ai-gateway(https://ai.zichuan.xyz/v1)
→ 结果写 ai_insights(指纹缓存) → 前端轮询 GET /analysis/insight 取回
Copilot: POST /analysis/copilot 走 SSE(stream_chat失败非流式回退)
```
### 3. 健康建议生成流程
```
GET /api/analysis/recommendations
AnalysisService.generateRecommendations()
分析历史数据
识别异常和趋势
根据规则引擎生成建议
按优先级排序
返回建议列表
```
- 生产 `AI_MODEL_CHAIN=gateway`(单上游,网关内部再 fanout NVIDIA/Gemini/Ollama
- 队列并发 2、间隔 5s共享网关过高会 502晨报一次生成 2~5 分钟,
前端一律「后台生成 + 轮询」而非阻塞等待;`meta.source` 标注 AI 或规则引擎。
## 关键特性
## 部署架构(生产)
### 安全性
- Garmin 密码使用加密存储
- JWT 令牌验证所有请求
- CORS 配置限制来源
- 环境变量管理敏感配置
- **Development 本地**`npm run dev` = Flask 5000`BACKEND_PORT` 固定,避开
`PORT` 被 CRA 抢占)+ CRA dev server 3000proxy → 5000。SQLite。
- **Production NAS**`192.168.50.64` `/volume1/web/garmin-health-lab`root
跑 gunicorn `0.0.0.0:8124`2w/4t--timeout 300 —— AI 生成可长达分钟级),
开机自启 DSM 任务调度器 → `deploy/S99garmin.sh`MariaDB 10.11
root@`/run/mysqld/mysqld10.sock`,库 `garmin_health_lab`。
- **公网**NAS frpc → 甲骨文 `129.146.26.249:8124`frp 隧道,重连需数秒)。
- **一键部署**`deploy/push.sh`tar-over-ssh 同步 backend+deploy、清空重推
`client/build`、sudo 重启、**pid 变化 + health 200 双校验**);前端先
`npm run build`。`deploy/start.sh` / `stop.sh` 供手工与开机脚本调用。
- **auth-hub**:回调注册 LAN `192.168.50.64:8124` 与公网 `129.146.26.249:8124`
两个地址client `996aLPw4T5gl-rYZ`CORS 白名单含两地址 + localhost。
### 扩展性
- 模块化的服务设计
- 易于添加新的分析算法
- 支持数据导出功能
- 可扩展的 API 端点
## 关键设计约束
### 性能
- 数据库索引优化查询
- API 缓存策略
- 异步处理长运行任务
- 增量数据同步支持
## 部署架构
- **Development 本地开发**`npm run dev` 同时启动前后端 —— Vite/React (3000/5173) + Flask (5000, SQLite)。
- **Production 生产环境**Oracle 云服务器 129.146.26.249
- Flask + gunicorn 直绑 `0.0.0.0:8123`systemd `garmin-health-lab.service`,开机自启)
- React SPA 静态包由后端 `STATIC_DIR` 提供
- MariaDB 10.3`127.0.0.1:3306`,专用账号 `garmin`,库 `garmin_health_lab`
- AI 网关 `129.146.26.249:5100/v1`
1. **阈值来自公开参考值AI 只解读不定阈值**;依据页面逐条列出处。
2. **本地日历日期**优先于 UTC`toISOString()` 陷阱见 DEVELOPMENT.md
3. **单点部署、无反向代理**:同端口 API + SPA减少一个故障面。
4. **多 worker 安全**:一切跨进程协调走 DBjob_locks / garmin_mfa_sessions /
ai_jobs / sync_status不做进程内假设。
5. **可插拔数据层**:业务代码不分叉 SQLite/MariaDB。

View File

@@ -0,0 +1,33 @@
# auth-hub 集成说明
本应用不自己管账号——登录由 **auth-hub**(统一 SSO / OIDC 提供方)承接,
本仓库只做 OAuth2 client 侧的事。
## 依赖的配置backend/.env
| 变量 | 说明 |
|---|---|
| `AUTH_HUB_BASE_URL` | auth-hub 服务地址(生产 `http://129.146.26.249:5300` |
| `AUTH_HUB_CLIENT_ID` | 本应用在 auth-hub 注册的 client |
| `AUTH_HUB_CLIENT_SECRET` | 对应 secret**只放 .env不入库** |
| `AUTH_HUB_REDIRECT_URI` | 回调地址,必须与 auth-hub 侧注册值完全一致 |
## 生产 clientNAS :8124
- client_id`996aLPw4T5gl-rYZ`
- 在 auth-hub 注册的回调:
- LAN`http://192.168.50.64:8124/auth/callback`
- 公网frp`http://129.146.26.249:8124/auth/callback`
- 注意 auth-hub 的 dev 与 prod 是**两套独立数据库、各自注册 client**
在 dev auth-hub 上创建的 client 不能用于 prod反之亦然。
## 流程
1. 前端请求 `POST /api/auth/auth-hub/start` → 拿到 auth-hub 授权 URL浏览器跳转
2. auth-hub 登录后回调 `GET /api/auth/callback?code=…`
3. 后端 `services/auth_hub_client.py``exchange_code_for_token`(换 token
`get_userinfo``find_or_create_user`(无则建号)→ 本应用签 JWT 返回前端。
本地邮箱/密码注册登录(曾经的 `/api/auth/register|login`**已移除**。
Garmin 账号授权是另一条独立流程(见 routes/garmin.py 的 `/api/garmin/login`
与 Web 登录无关。

View File

@@ -1,94 +1,101 @@
# 开发指南
> 当前技术栈:**React 18 + TypeScript + Framework7 9CRA** 前端 +
> **Python 3.10+ / Flask** 后端 + SQLite开发/ MariaDB生产
> 仓库根 `package.json` 是 npm workspaces含 `client`),后端依赖装进
> `backend/.venv`。
## 环境设置
### 前置要求
- Node.js 18+
- npm 或 yarn
- Python 3.10+(本地用 3.13 亦可NAS 是 3.10
- Node.js 18+ / npm
- Git
- Garmin Connect 账户
- Garmin Connect 账户(用于数据同步联调)
- 联调 auth-hub 登录需能访问 auth-hub内网 `http://129.146.26.249:5300`
或配置你自己的 auth-hub
### 安装步骤
1. **克隆项目**
```bash
cd ~/Desktop/Work
git clone <repository-url> GarminHealthLab
cd GarminHealthLab
```
```bash
cd ~/Desktop/Work/GarminHealthLab
2. **安装依赖**
```bash
npm install
```
# 1) 后端虚拟环境 + 依赖(含 pytest 等 dev 依赖)
npm run setup:backend
# 等价于: cd backend && python3 -m venv .venv && .venv/bin/pip install -r requirements-dev.txt
3. **配置环境变量**
```bash
# 复制示例文件
cp server/.env.example server/.env
# 编辑 server/.env填入你的 Garmin 凭证
# GARMIN_CONNECT_USER=your_email@example.com
# GARMIN_CONNECT_PASSWORD=your_password
# JWT_SECRET=generate_a_random_string
```
# 2) 前端依赖workspace
npm install
4. **启动开发服务器**
```bash
npm run dev
```
- 前端: http://localhost:3000
- 后端 API: http://localhost:5000/api
# 3) 配置环境变量
cp backend/.env.example backend/.env
# 编辑 backend/.env至少设置 JWT_SECRETAI_GATEWAY_* 见下DB_TYPE 默认 sqlite
```
### 启动开发服务器
```bash
npm run dev # 同时起后端(5000) + 前端 CRA(3000, proxy→5000)
npm run dev:backend # 只起后端: backend/.venv/bin/python app.pyBACKEND_PORT=5000 固定)
npm run dev:client # 只起前端: react-scripts start
```
- 前端: http://localhost:3000登录走 auth-hub SSO 跳转)
- 后端 API: http://localhost:5000/api
- `app.py` 顶部会 `db.init_db()` 自动建表;开发库是 `backend/data/health.db`
> **为什么不用 `PORT`**:很多工具会给前端进程注入 `PORT`,若 Flask 直接读
> `PORT` 会抢走 CRA 的端口。后端一律用 `BACKEND_PORT`(见 `config.py`)。
### 连接 NAS 生产 MariaDB可选
本地开发默认 SQLite。要连 NAS 生产库联调,在 `backend/.env` 覆写:
```env
DB_TYPE=mariadb
MARIADB_SOCKET=/run/mysqld/mysqld10.sock
MARIADB_HOST=127.0.0.1
MARIADB_PORT=3306
MARIADB_USER=root
MARIADB_PASSWORD=<nas-mariadb-root密码>
MARIADB_DATABASE=garmin_health_lab
```
(本地 macOS 没有该 socket通常经 ssh 端口转发或直接在 NAS 上跑联调。)
## 项目命令
```bash
# 开发模式(同时运行前后端)
npm run dev
# 后端测试pytest446+ 项)
npm run test # = cd backend && .venv/bin/python -m pytest
cd backend && .venv/bin/python tests/smoke.py # 冒烟
# 仅运行后端服务器
npm run dev:server
# 仅运行前端应用
npm run dev:client
# 构建项目
npm run build
# 生产模式启动
npm start
# 类型检查
npm run typecheck
# 前端
npm run typecheck # tsc --noEmit
npm run build # client 构建 → client/build/
```
## 代码结构
### 后端 (server/)
### 后端 (backend/)
```
server/
├── src/
│ ├── index.ts # 入口文件
│ ├── routes/ # API 路由
│ │ ├── auth.ts
├── garmin.ts
├── health.ts
└── analysis.ts
├── services/ # 业务逻辑服务
│ ├── AuthService.ts
├── GarminService.ts
├── HealthService.ts
└── AnalysisService.ts
│ ├── models/ # 数据模型
│ ├── middleware/ # 中间件
│ ├── utils/ # 工具函数
│ │ └── database.ts
│ └── types/ # TypeScript 类型定义
├── dist/ # 编译输出
├── tsconfig.json
└── package.json
backend/
├── app.py # create_app(): CORS、init_db、scheduler/jobs 启动、蓝图装配、SPA 静态托管
├── wsgi.py # gunicorn 入口wsgi:app
├── config.py # 从 backend/.env 读配置DB/AUTH/AI/端口/静态目录)
├── auth.py # sign_token / decode_token / require_auth
├── db.py # 连接与执行、SCHEMA(20表)、幂等 init_db()
├── routes/ # 蓝图auth garmin health analysis settings
├── services/ # 业务逻辑garmin garmin_auth garmin_extras scheduler
# ai coach insights jobs analysis health
# fitness_age settings auth_hub_client scopes
├── tests/ # conftest.py + 各模块测试 + smoke.py
├── data/ # SQLite本地git 忽略)
├── static/ # 前端构建产物(部署用;见「部署」)
├── .env.example
├── requirements.txt # 运行依赖
└── requirements-dev.txt # 运行 + pytest
```
### 前端 (client/)
@@ -96,183 +103,134 @@ server/
```
client/
├── src/
│ ├── index.tsx # 入口文件
│ ├── App.tsx # 主组件
├── components/ # 可复用组件
├── pages/ # 页面组件
│ │ ├── Dashboard.tsx
│ │ ├── DataSync.tsx
│ ├── Analysis.tsx
│ │ ├── Recommendations.tsx
│ └── Settings.tsx
│ ├── services/ # API 服务
│ └── api.ts
│ ├── types/ # TypeScript 类型定义
│ └── index.css # 全局样式
├── public/ # 静态资源
└── package.json
│ ├── index.tsx / App.tsx # 入口与路由框架F7 App
│ ├── pages/ # TodayPage HealthPage TrendsPage ExercisePage
│ # SettingsPage DailyPage SleepPage BodyPage RacePage
# ChallengesPage DevicesPage MetricDetailPage
│ │ # ActivityDetailPage BodyAgePage RatingBasisPage
│ │ # AiQueuePage SyncPage Recommendations LoginPage
│ ├── components/ # AiBriefing AiPanel Copilot(浮窗) TrendInsight Screen
│ │ # Skeleton + charts/
├── lib/ # day.ts(本地日历日期) metrics.ts(指标注册表) …
│ ├── services/api.ts # axios 客户端 + JWT 拦截器
├── features.ts # FEATURES.ai 功能开关
│ ├── f7theme.css theme.css index.css
│ └── routes.ts types/
├── public/
└── .env.production # REACT_APP_API_URL=/api同源缺了会 Network Error
```
## 开发工作流
### 添加新的 API 端点
1. **创建路由处理器** (`server/src/routes/yourFeature.ts`)
```typescript
import express from 'express';
const router = express.Router();
router.get('/endpoint', (req, res) => {
// 业务逻辑
});
export default router;
1. **写服务函数**`backend/services/your_feature.py`,纯逻辑,可单测)
```python
def compute_something(user_id, payload):
...
```
2. **在主文件中注册路由** (`server/src/index.ts`)
```typescript
import yourFeatureRoutes from './routes/yourFeature';
app.use('/api/yourfeature', yourFeatureRoutes);
2. **挂到蓝图**`backend/routes/xxx.py`,用 `require_auth` 拿 `g.user_id`
```python
@bp.route("/something", methods=["POST"])
@require_auth
def something():
from services.your_feature import compute_something
return jsonify(compute_something(g.user_id, request.get_json() or {}))
```
3. **前端调用**`client/src/services/api.ts` 加方法)
4. **加测试**`backend/tests/test_your_feature.py`
5. 跑 `npm run test` + `npm run typecheck`
3. **创建 API 客户端方法** (`client/src/services/api.ts`)
```typescript
getYourEndpoint() {
return this.client.get('/yourfeature/endpoint');
}
```
### 数据库变更(必须幂等)
### 添加新页面
`db.py` 的 `SCHEMA` 是 `CREATE TABLE IF NOT EXISTS`;新增列走迁移列表
`db.py` 底部 `_migrations` / 列检测逻辑),**先查 INFORMATION_SCHEMA 再
ALTER**,保证多 worker 并发 `init_db()` 不炸。改完对 SQLite 与 MariaDB 各跑
一遍测试(迁移两套方言都要过)。
1. **创建页面组件** (`client/src/pages/YourPage.tsx`)
```typescript
import React from 'react';
function YourPage() {
return <div>Page content</div>;
}
export default YourPage;
```
### 前端构建与同源部署
2. **在 App.tsx 中添加路由**
```typescript
<Route path="/your-path" element={<YourPage />} />
```
### 数据库操作
使用数据库工具函数简化操作:
```typescript
import { runAsync, getAsync, allAsync } from '../utils/database';
// 插入数据
await runAsync('INSERT INTO table (col1, col2) VALUES (?, ?)', [val1, val2]);
// 查询单行
const row = await getAsync('SELECT * FROM table WHERE id = ?', [id]);
// 查询多行
const rows = await allAsync('SELECT * FROM table WHERE status = ?', ['active']);
```
## 调试
### 后端调试
```bash
# 启用详细日志
DEBUG=* npm run dev:server
# 使用 Node 调试器
node --inspect server/dist/index.js
npm run build
# 产物在 client/build/ —— 不要手动拷到 backend/static/,走 deploy/push.sh
```
### 前端调试
- 使用 React DevTools 浏览器扩展
- 使用浏览器开发者工具 (F12)
- 检查网络请求和响应
## 部署到 NAS生产
```bash
cd ~/Desktop/Work/GarminHealthLab
npm run build # 1. 先出前端产物
./deploy/push.sh # 2. 推送 + 重启(会问 ssh 与 sudo 密码,可设 NAS_PASSWORD
```
`push.sh` 做了什么(见脚本注释,务必理解再执行):
1. ssh master 连接ControlMaster密码只输一次
2. 定位远端 app 目录(`/volume1/web/garmin-health-lab`
3. tar 同步 `backend/`(排除 .venv/.env/__pycache__/db/tests+ `deploy/`
4. **清空远端 static 并重推** `client/build`(防旧 chunk 堆积);
5. **sudo 重启**(服务以 root 跑DSM 开机启动,日志 root 所有)——
`stop.sh` → 等端口释放 → `start.sh`
6. 校验health 200 + **gunicorn pid 必须变化**pid 没变=旧进程还在服务,
改动未生效,脚本会报错退出)。
> 老脚本 `deploy/deploy.sh` 内置密码且不校验 pid仅应急用。生产部署一律
> `push.sh`。公网生效比 NAS 本地晚几秒frp 重连)。
## 测试
### 测试后端 API
- pytest`npm run test`446+ 项)。后端服务层为纯函数设计,测试不依赖 Flask
实例(`conftest.py` 建临时 SQLite
- 覆盖重点:设置吸附/校验、身体年龄方向性、运动详情列存解析、Garmin 同步
入库 + 「只读本地」保证、AI 缓存/队列互斥、调度器、auth-hub 客户端。
- 前端界面回归:本地起后端 + CRAChrome headless + CDP 截图比对
(参见 `.workbuddy/memory/` 日志里的做法)。
- 生产自测入口NAS `curl http://127.0.0.1:8124/api/health/status`200
使用 curl 或 Postman
## 环境变量速查backend/.env
```bash
# 获取健康摘要
curl http://localhost:5000/api/health/summary
| 变量 | 默认 | 说明 |
|---|---|---|
| `BACKEND_PORT` | 5000 | 后端监听端口(优先于 `PORT` |
| `DB_TYPE` | sqlite | `sqlite` / `mariadb` |
| `DATABASE_PATH` | ./data/health.db | SQLite 文件位置 |
| `MARIADB_*` | — | 生产 MariaDBNAS 10.11socket mysqld10.sock |
| `JWT_SECRET` | dev_secret_change_me | **生产必换** |
| `JWT_EXPIRY_DAYS` | 7 | token 有效期 |
| `AUTH_HUB_BASE_URL` | http://129.146.26.249:5300 | SSO 提供方 |
| `AUTH_HUB_CLIENT_ID` / `_SECRET` | — | SSO 客户端(生产 996a… |
| `AUTH_HUB_REDIRECT_URI` | http://129.146.26.249:8124/auth/callback | 回调地址 |
| `STATIC_DIR` | backend/static | 前端产物目录(生产 ./static |
| `AI_GATEWAY_BASE_URL` | https://ai.zichuan.xyz/v1 | OpenAI 兼容网关 |
| `AI_GATEWAY_TOKEN` | — | 网关 Bearer token |
| `AI_MODEL_CHAIN` | gateway,gemini-flash,llama-70b | 模型回退链(生产仅 gateway |
| `AI_TIMEOUT_SECONDS` | 300 | 网关超时(推理模型可达分钟级) |
| `AI_JOB_CONCURRENCY` / `AI_JOB_GAP_SECONDS` | 2 / 5 | AI 队列并发与间隔(共享网关勿调高) |
| `AI_JOBS` | true | false 则停消费(页面只显示计算值) |
# 触发 Garmin 同步
curl -X POST http://localhost:5000/api/garmin/sync
## 调试
# 获取健康建议
curl http://localhost:5000/api/analysis/recommendations
```
- 后端日志开发直接看终端NAS 看 `/volume1/web/garmin-health-lab/logs/`
error.log / access.log / stdout.logroot 所有,用 sudo
- `refill_backlog` 1064 SQL 报错是**已知未修 bug**AI 回填 SQL 与 MariaDB
语法不兼容),见 CLAUDE.md「已知问题」。
## 常见问题
### 常见问题
### 数据库连接失败
```bash
# 检查数据库文件
ls -la ./data/health.db
- **前端 Network Error部署后**:构建时没带 `.env.production` 的
`REACT_APP_API_URL=/api`,打进了 localhost:5000。检查 bundle 或重跑
`npm run build`。
- **凌晨打开是昨天**:用了 `toISOString()`UTC。改用 `lib/day.ts`。
- **按钮/输入框布局怪异**Framework7 `button { width: 100% }`。自定义组件
按钮显式 `width: auto`(见 Copilot.css 修复注释)。
- **Garmin 同步 429**:账号级限流,退避 24hDB 持久化)。密码/验证码正确
仍 429 就是撞限流,等冷却;`rate_limited_until` 以 DB 为准。
- **运动详情慢/超时**会话按进程缓存15 分钟 TTL冷启约 16s。
- **测试连库**:测试用临时 SQLite不要连生产 MariaDB。
# 重置数据库
rm ./data/health.db
npm run dev
```
## 代码规范与提交
### 前端无法连接后端
- 检查 CORS 配置
- 确保后端运行在 5000 端口
- 检查防火墙设置
### Garmin 认证失败
- 验证 Garmin 邮箱和密码
- 检查网络连接
- 查看后端日志
## 性能优化建议
1. **数据库索引**
- 在频繁查询的字段上添加索引
- 定期分析查询性能
2. **缓存策略**
- 实现 API 响应缓存
- 使用浏览器缓存
3. **代码分割**
- React 懒加载路由
- 按需加载 JavaScript
## 代码规范
### TypeScript
- 使用严格模式
- 为所有函数参数添加类型
- 使用接口定义复杂对象
### CSS
- 使用 BEM 命名规范
- 响应式设计优先
- 避免内联样式
### 提交信息
```
feat: 添加新功能描述
fix: 修复 bug 描述
docs: 文档更新
style: 代码格式调整
refactor: 代码重构
test: 测试相关
```
## 资源链接
- [Express.js 文档](https://expressjs.com/)
- [React 文档](https://react.dev/)
- [TypeScript 文档](https://www.typescriptlang.org/)
- [SQLite 文档](https://www.sqlite.org/)
- [Garmin API](https://developer.garmin.com/)
- TypeScript 严格模式Python 遵循现有风格(函数式 servicesdocstring
- 提交信息约定(见 PROGRESS.md / REQUIREMENTS.md`feat:` `fix:` `docs:`
`refactor:` `test:` 前缀,一任务一 commit完成即 `git push`origin 即 NAS
Gitea `http://192.168.50.64:3000/ericwyuan/GarminHealthLab.git`)。
- **绝不提交**`.env`、密钥、`client/build/`、`.venv`、`*.db`.gitignore 已覆盖)。

View File

@@ -4,7 +4,7 @@
状态:`✅ 已完成` · `🚧 进行中` · `📋 待做` · `⏸ 已搁置`
最后更新2026-08-25
最后更新2026-09-02
---
@@ -13,10 +13,12 @@
| # | 需求 | 理解 | 状态 |
|---|---|---|---|
| 1.1 | 新建项目分析佳明海外账号健康数据 | Gitea 自建仓库Web 应用 | ✅ |
| 1.2 | 部署到 Oracle 云服务器 (129.146.26.249),用 MariaDB | Flask + gunicorn + MariaDB(127.0.0.1:3306)SQLite 供开发 | ✅ |
| 1.3 | 公网可访问 | gunicorn 直绑 Oracle:8123frp 隧道已移除) | ✅ |
| 1.2 | 部署到 NAS192.168.50.64,用 MariaDB | Flask + gunicorn(root, :8124) + NAS MariaDB 10.11(库 `garmin_health_lab`SQLite 供开发 | ✅ |
| 1.3 | 公网可访问 | NAS frpc 隧道 → 甲骨文公网 `129.146.26.249:8124` | ✅ |
| 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 双校验 | ✅ |
## 二、数据同步
@@ -97,6 +99,10 @@
| 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` 标注谁作答;流式失败自动退回非流式 | ✅ |
---
@@ -111,6 +117,9 @@
| 心率区间百分比偏高区间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