Compare commits
2 Commits
74f2721401
...
4331a07462
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4331a07462 | ||
|
|
9d6ebbe422 |
57
CLAUDE.md
57
CLAUDE.md
@@ -16,8 +16,8 @@
|
||||
**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`)。
|
||||
本地开发 SQLite(`backend/data/health.db`,默认);生产 MariaDB(甲骨文云主机
|
||||
本机,`garmin_health_lab` 库,连接配置见该机 `/opt/garmin-health-lab/backend/.env`)。
|
||||
- **认证**:**auth-hub SSO**(OAuth2/OIDC,`services/auth_hub_client.py`),
|
||||
本地邮箱密码注册/登录已移除;应用内 JWT 由后端签发。
|
||||
- **AI**:自建 ai-gateway(OpenAI 兼容,`https://ai.zichuan.xyz/v1`)为唯一上游
|
||||
@@ -57,7 +57,8 @@ GarminHealthLab/
|
||||
│ ├── 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)
|
||||
├── deploy/ # push_oracle.sh 现役;其余(S99garmin/start/stop/
|
||||
│ # push/deploy)是 NAS 时代脚本,已废弃保留参考
|
||||
├── docs/ # 架构 / 开发 / 需求文档
|
||||
├── PROGRESS.md # 进度与部署事实(排查必读)
|
||||
└── README.md # 项目说明 + API 文档
|
||||
@@ -83,24 +84,42 @@ npm run build # client/react-scripts build → client/build/
|
||||
cd backend && .venv/bin/python tests/smoke.py
|
||||
```
|
||||
|
||||
## 部署(生产 = NAS,端口 8124)
|
||||
## 部署(生产 = 甲骨文云主机,2026-09-12 起;此前是 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`。
|
||||
- **一键部署**:本地 `./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.11,root 经 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` 两个地址。
|
||||
- **位置**:`129.146.26.249` `/opt/garmin-health-lab`,**`ubuntu` 用户**跑
|
||||
gunicorn `127.0.0.1:5500`(2 workers / 4 threads / --timeout 300),
|
||||
systemd 单元 `garmin-health-lab.service`(`enabled`,随机器开机自启,
|
||||
`Restart=always`)。和这台机器上其它服务(ai-gateway / auth-hub / fam-edge)
|
||||
同一套约定:`/opt/<项目>` 下独立部署 + 独立 venv + Caddy 按子域名反代。
|
||||
- **公网**:`https://garmin.zichuan.xyz` → Caddy → `127.0.0.1:5500`。
|
||||
DNS(腾讯云 DNSPod,A 记录)与 TLS(Caddy 自动签发)都已配好。
|
||||
~~旧的 `http://129.146.26.249:8124`~~ 已下线(走 NAS frpc 转发,隧道已拆)。
|
||||
- **一键部署**:本地 `./deploy/push_oracle.sh`(tar 经 ssh key 认证同步
|
||||
backend + 清空重推 client/build + pip install + systemctl restart,
|
||||
**校验 main PID 变化 + health 200**)。前置:先 `npm run build`。
|
||||
`deploy/push.sh` / `start.sh` / `stop.sh` / `S99garmin.sh` / `deploy.sh`
|
||||
是 NAS 时代的脚本,已废弃保留仅供参考。
|
||||
- **DB**:甲骨文本机 MariaDB 10.3.39(`127.0.0.1:3306`),独立账号
|
||||
`garmin`(**注意**:`garmin@localhost` 与 `garmin@127.0.0.1` 是两个不同账号,
|
||||
必须同密码建两份,否则 TCP 连接用的是另一个密码),库 `garmin_health_lab`。
|
||||
和其它项目(`chat_relay`、`zhongyuan`)共用同一个 MariaDB 实例,各自独立库
|
||||
独立账号。**这台机器磁盘 96% 已满**,改动前留意剩余空间。
|
||||
- **AI 网关**:`AI_GATEWAY_BASE_URL` 现在是**本地回环** `http://127.0.0.1:5100/v1`
|
||||
(不再经 Caddy/公网 —— 网关和这个服务同机了)。
|
||||
- **auth-hub**:client `996aLPw4T5gl-rYZ`;回调只保留
|
||||
`https://garmin.zichuan.xyz/auth/callback` 一条(NAS/旧公网端口那几条已用
|
||||
`manage_clients remove-redirect-uri` 清掉)。改注册用
|
||||
`/opt/auth-hub` 下 `PYTHONPATH=/opt/auth-hub/src venv/bin/python -m
|
||||
auth_hub.manage_clients`。
|
||||
- **NAS 现状**:`garmin_health_lab` 库已 `DROP DATABASE`(备份在本地
|
||||
`~/Desktop/Work/backups/garmin_health_lab_nas_backup_20260912.sql.gz`,
|
||||
20 张表逐条精确计数核对过一致后才删的),`S99garmin.sh` 开机项已移除,
|
||||
frpc 配置里 `garmin-health` 转发段已删(`gitea`/`wordpress`/`fam-core`/
|
||||
`nexusai` 那几条没动——**Gitea 还在 NAS 上,这个仓库的 git remote 仍然指
|
||||
向它**,这次迁移只搬了应用和数据,没搬源码托管)。
|
||||
- **部署/排障前**:先读 `PROGRESS.md` 与 `deploy/` 脚本确认事实(曾经凭旧记忆
|
||||
断言"无线上环境"而误判)。服务以 root 运行:重启用 sudo,日志
|
||||
`logs/error.log`、`logs/access.log` 是 root 所有。
|
||||
断言"无线上环境"而误判,后来又把"生产在 NAS"当成默认事实——**两次都错在
|
||||
没有先查,部署位置是会变的**)。
|
||||
|
||||
## 关键约定与坑(写代码/改样式前看)
|
||||
|
||||
|
||||
50
PROGRESS.md
50
PROGRESS.md
@@ -10,13 +10,55 @@
|
||||
|
||||
- [x] Gunicorn 生产服务器配置(2 workers / 4 threads)
|
||||
|
||||
- [x] 部署到本地 NAS(`/volume1/web/garmin-health-lab`),端口 8124
|
||||
- [x] ~~部署到本地 NAS(`/volume1/web/garmin-health-lab`),端口 8124~~ 已下线,见下
|
||||
|
||||
- [x] frp 隧道配置,通过甲骨文公网 IP 外网访问(`http://129.146.26.249:8124`)
|
||||
- [x] ~~frp 隧道配置,通过甲骨文公网 IP 外网访问(`http://129.146.26.249:8124`)~~ 已拆除
|
||||
|
||||
- [x] 开机自启脚本(`deploy/S99garmin.sh`)
|
||||
- [x] ~~开机自启脚本(`deploy/S99garmin.sh`)~~ 已移除,脚本保留仅供参考
|
||||
|
||||
- [x] 甲骨文 iptables 放行 8124 端口
|
||||
- [x] ~~甲骨文 iptables 放行 8124 端口~~ 该端口不再使用
|
||||
|
||||
### 迁移到甲骨文云主机(2026-09-12)
|
||||
|
||||
NAS 的局域网 IP 因 DHCP 重启换过两次(`.64` → `.65` → 又变回 `.64`),且磁盘/
|
||||
网络都不如已经稳定跑着好几个服务的甲骨文机器可靠。整体搬迁,NAS 只保留 Gitea
|
||||
(这个仓库的源码托管,未动)。
|
||||
|
||||
- [x] 甲骨文本机新建 MariaDB 库 `garmin_health_lab` + 独立账号 `garmin`(同时建
|
||||
`garmin@localhost` 与 `garmin@127.0.0.1` 两个 host 变体、同密码——MariaDB 视
|
||||
为两个不同账号,只建一个会导致 TCP 连接失败)
|
||||
|
||||
- [x] NAS `mysqldump`(10.11 源库,处理了 `/*M!999999` 注释兼容 10.3 目标)→
|
||||
导入甲骨文 MariaDB 10.3.39,**20 张表逐条精确 `COUNT(*)` 比对完全一致**才继续
|
||||
|
||||
- [x] 应用部署到 `/opt/garmin-health-lab`,systemd 单元 `garmin-health-lab.service`
|
||||
(`ubuntu` 用户,非 root;和 ai-gateway/auth-hub 同一套约定)
|
||||
|
||||
- [x] DNS:腾讯云 DNSPod API 新增 `garmin.zichuan.xyz` A 记录;Caddy 反代 +
|
||||
自动签发 TLS,实测已生效
|
||||
|
||||
- [x] auth-hub 回调地址改为 `https://garmin.zichuan.xyz/auth/callback`,清掉
|
||||
NAS/旧公网端口那几条历史回调
|
||||
|
||||
- [x] AI 网关配置从公网域名改本地回环 `http://127.0.0.1:5100/v1`(网关和本服务
|
||||
现在同机),**触发真实生成验证通过**(`upstream: gemini`)
|
||||
|
||||
- [x] 迁移窗口期加了安全阀:甲骨文这边先 `AUTO_SYNC=false`,避免和 NAS 的调度
|
||||
器同时刷新 Garmin 令牌撞上按账号计算的 SSO 限流(见下方"同步逻辑修复"一节
|
||||
09-03 那次事故)。确认 NAS 已冻结、数据无缺口后才在甲骨文打开
|
||||
|
||||
- [x] 冻结 NAS(停 gunicorn)→ 补一次终态 dump 核对无数据差异 → 移除开机自启
|
||||
→ `DROP DATABASE garmin_health_lab`(备份在本地
|
||||
`~/Desktop/Work/backups/garmin_health_lab_nas_backup_20260912.sql.gz`)
|
||||
|
||||
- [x] NAS frpc 配置精确删除 `garmin-health` 转发段,`gitea`/`wordpress`/
|
||||
`fam-core`/`nexusai` 几条未动,改后逐条复检确认其它站点未受影响
|
||||
|
||||
- [x] 新部署脚本 `deploy/push_oracle.sh`(key 认证 + systemd,校验 PID 变化 +
|
||||
health 200),落地当场跑通一次真实部署
|
||||
|
||||
- [x] `CLAUDE.md` 部署章节整体重写;NAS 时代的 `deploy/` 脚本加了废弃说明保留
|
||||
参考,未删除
|
||||
|
||||
### 认证
|
||||
|
||||
|
||||
28
README.md
28
README.md
@@ -20,9 +20,9 @@
|
||||
|
||||
### 后端
|
||||
- Python 3.10+ + Flask(应用工厂)
|
||||
- Gunicorn(生产运行,NAS :8124)
|
||||
- 可插拔数据层:SQLite(本地开发)/ MariaDB(生产,NAS 10.11 库
|
||||
`garmin_health_lab`,经 PyMySQL/socket)
|
||||
- Gunicorn(生产运行,甲骨文云主机 :5500,Caddy 反代出 `garmin.zichuan.xyz`)
|
||||
- 可插拔数据层:SQLite(本地开发)/ MariaDB(生产,与应用同机的 10.3.39 库
|
||||
`garmin_health_lab`,经 PyMySQL/TCP)
|
||||
- JWT 鉴权;登录走 auth-hub 统一 SSO(OAuth2/OIDC,本地密码登录已移除)
|
||||
- garminconnect(Garmin API 集成)
|
||||
|
||||
@@ -93,8 +93,8 @@ CORS_ORIGIN=http://localhost:3000,http://localhost:5173
|
||||
数据层通过 `DB_TYPE` 环境变量切换后端,**业务代码无需改动**:
|
||||
|
||||
- **SQLite(默认,本地开发)**:零配置,由 `DATABASE_PATH` 指定文件位置。
|
||||
- **MariaDB(生产)**:运行于 NAS(192.168.50.64)本地 MariaDB 10.11,root 经
|
||||
socket `/run/mysqld/mysqld10.sock`(或 TCP `127.0.0.1:3306`)连接(PyMySQL),
|
||||
- **MariaDB(生产)**:运行于甲骨文云主机(`129.146.26.249`)本地 MariaDB
|
||||
10.3.39,独立账号 `garmin` 经 TCP `127.0.0.1:3306` 连接(PyMySQL),
|
||||
独立库 `garmin_health_lab`。完整配置见 `backend/.env.example`:
|
||||
|
||||
```env
|
||||
@@ -195,15 +195,17 @@ python tests/smoke.py
|
||||
> 轮询,趋势归因与 Copilot 走显式触发;任一模型失败时降级为规则引擎,
|
||||
> `meta.source` 会说明本次由谁作答。
|
||||
|
||||
## 🚀 部署(生产 = NAS)
|
||||
## 🚀 部署(生产 = 甲骨文云主机,2026-09-12 起)
|
||||
|
||||
- **位置**: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/` 脚本确认事实。
|
||||
- **位置**:`129.146.26.249` `/opt/garmin-health-lab`,`ubuntu` 用户跑
|
||||
gunicorn `127.0.0.1:5500`(2 workers / 4 threads / --timeout 300),
|
||||
systemd 单元 `garmin-health-lab.service`。
|
||||
- **公网**:`https://garmin.zichuan.xyz` → Caddy → `127.0.0.1:5500`。
|
||||
- **一键部署**:先 `npm run build`,再 `./deploy/push_oracle.sh`(ssh key 同步
|
||||
backend + 清空重推 `client/build` + pip install + systemctl restart +
|
||||
PID/health 双校验)。
|
||||
- **部署排障前**:先读 `PROGRESS.md` 与 `deploy/` 脚本确认事实——部署位置换过
|
||||
一次(NAS → 甲骨文),凭记忆断言曾经出过错。
|
||||
|
||||
## 🔐 安全说明
|
||||
|
||||
|
||||
@@ -8,14 +8,15 @@ 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 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 (used when DB_TYPE=mariadb) — production DB is co-located with the
|
||||
# app on the Oracle box (129.146.26.249, MariaDB 10.3.39), TCP 127.0.0.1:3306,
|
||||
# dedicated account (not root). MariaDB treats `user@localhost` and
|
||||
# `user@127.0.0.1` as two different accounts — if you create this user by
|
||||
# hand, create both host variants with the same password, or TCP connections
|
||||
# fail with a password that looks right but is not the one that account has.
|
||||
# MARIADB_HOST=127.0.0.1
|
||||
# MARIADB_PORT=3306
|
||||
# MARIADB_USER=root
|
||||
# MARIADB_USER=garmin
|
||||
# MARIADB_PASSWORD=your_production_mariadb_password
|
||||
# MARIADB_DATABASE=garmin_health_lab
|
||||
|
||||
@@ -38,9 +39,10 @@ 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). 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
|
||||
# registers https://garmin.zichuan.xyz/auth/callback — add/remove redirect
|
||||
# URIs on that client with /opt/auth-hub's manage_clients CLI, not by editing
|
||||
# auth-hub's own database directly.
|
||||
AUTH_HUB_REDIRECT_URI=http://127.0.0.1:5500/auth/callback
|
||||
|
||||
# --- CORS (comma-separated allowed front-end origins) ---
|
||||
# localhost stays in the production list on purpose: CORS is not an auth
|
||||
@@ -57,9 +59,12 @@ CORS_ORIGIN=http://localhost:3000,http://localhost:5173
|
||||
# absorbs single-vendor quota limits. Reached directly, bypassing any local
|
||||
# HTTP proxy. NOTE: its NVIDIA upstream is a large reasoning model — replies
|
||||
# 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://ai.zichuan.xyz/v1
|
||||
# Production (co-located with ai-gateway on the same Oracle box) uses the
|
||||
# loopback address — skips Caddy and the public hop entirely, and the token
|
||||
# never leaves localhost either way. Use the HTTPS domain instead only when
|
||||
# this app runs somewhere else: the token rides in an Authorization header
|
||||
# and must not cross the public internet in the clear.
|
||||
AI_GATEWAY_BASE_URL=http://127.0.0.1:5100/v1
|
||||
AI_GATEWAY_TOKEN=
|
||||
AI_GATEWAY_MODEL=ai-gateway-auto
|
||||
|
||||
|
||||
@@ -42,13 +42,13 @@ JWT_EXPIRY_DAYS = int(os.environ.get("JWT_EXPIRY_DAYS") or 7)
|
||||
# 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.
|
||||
# databases). Production always overrides this via backend/.env — the value
|
||||
# in use is the client registered for https://garmin.zichuan.xyz/auth/callback.
|
||||
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 ""
|
||||
# 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"
|
||||
# Production sets this via backend/.env to https://garmin.zichuan.xyz/auth/callback
|
||||
# — the fallback below is a dev-only placeholder, not a live production route.
|
||||
AUTH_HUB_REDIRECT_URI = os.environ.get("AUTH_HUB_REDIRECT_URI") or "http://127.0.0.1:5500/auth/callback"
|
||||
|
||||
# --- Static UI --------------------------------------------------------------
|
||||
# Directory holding the built React app. When set and populated, the Flask
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
#!/bin/sh
|
||||
# RETIRED 2026-09-12: production moved off the NAS to the Oracle box
|
||||
# (systemd unit garmin-health-lab.service there). Kept for reference only.
|
||||
# Garmin Health Lab — DSM boot script.
|
||||
# Mirrors the convention already used by S99frpc.sh on this NAS.
|
||||
APP=/volume1/web/garmin-health-lab
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
#!/bin/sh
|
||||
# RETIRED 2026-09-12: production moved off the NAS to the Oracle box
|
||||
# (systemd unit garmin-health-lab.service there). Kept for reference only.
|
||||
# Deploy the latest local code to NAS.
|
||||
# Usage: sh deploy/deploy.sh
|
||||
# Prerequisites: client has been built (`npm run build` in client/)
|
||||
|
||||
@@ -1,4 +1,13 @@
|
||||
#!/bin/sh
|
||||
# RETIRED 2026-09-12: production moved off the NAS to the Oracle box — see
|
||||
# deploy/push_oracle.sh. The NAS's garmin_health_lab database has been
|
||||
# DROPPED and its S99garmin.sh boot entry removed; this script now has
|
||||
# nothing live to deploy to. Kept for reference (the tar-over-ssh dance for
|
||||
# a password-only host may be useful again) — do not run it expecting it to
|
||||
# reach a running service.
|
||||
#
|
||||
# --- original header below ---
|
||||
#
|
||||
# Push this working tree to the NAS and restart it.
|
||||
#
|
||||
# The NAS only accepts password auth, so one ssh master connection is opened
|
||||
|
||||
65
deploy/push_oracle.sh
Executable file
65
deploy/push_oracle.sh
Executable file
@@ -0,0 +1,65 @@
|
||||
#!/bin/sh
|
||||
# Push this working tree to the Oracle box and restart it.
|
||||
#
|
||||
# Unlike deploy/push.sh (the retired NAS deployment), this target uses key
|
||||
# auth and systemd — matching the convention every other service on that box
|
||||
# already follows (ai-gateway, auth-hub, fam-edge, ...): code lives under
|
||||
# /opt/<name>, gunicorn is a systemd unit, Caddy reverse-proxies a subdomain
|
||||
# to a local port. Nothing here needs a password or sudo for the deploy
|
||||
# itself; only the one-time systemd/Caddy setup did, and that is already done.
|
||||
#
|
||||
# ./deploy/push_oracle.sh [user@host] [ssh-key]
|
||||
#
|
||||
# Never touches backend/.env, .venv or the database on the far side.
|
||||
set -e
|
||||
|
||||
HOST="${1:-ubuntu@129.146.26.249}"
|
||||
KEY="${2:-$HOME/.ssh/oracle_new}"
|
||||
APP=/opt/garmin-health-lab
|
||||
REPO="$(cd "$(dirname "$0")/.." && pwd)"
|
||||
|
||||
sh_() { ssh -i "$KEY" -o BatchMode=yes "$HOST" "$@"; }
|
||||
|
||||
if [ ! -f "$REPO/client/build/index.html" ]; then
|
||||
echo "client/build is missing — run 'npm run build' first" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# macOS bsdtar writes com.apple.provenance xattrs and ._ resource forks that
|
||||
# the Linux side cannot read; --no-xattrs plus COPYFILE_DISABLE strip them.
|
||||
TAR="tar czf - --no-xattrs"
|
||||
export COPYFILE_DISABLE=1
|
||||
|
||||
echo "==> backend"
|
||||
$TAR --exclude .venv --exclude .env --exclude __pycache__ \
|
||||
--exclude '*.db' --exclude tests --exclude .pytest_cache --exclude static \
|
||||
-C "$REPO/backend" . | sh_ "tar xzf - -C '$APP/backend'"
|
||||
|
||||
echo "==> static (cleared first, so stale JS chunks do not pile up)"
|
||||
sh_ "rm -rf '$APP/backend/static' && mkdir -p '$APP/backend/static'"
|
||||
$TAR -C "$REPO/client/build" . | sh_ "tar xzf - -C '$APP/backend/static'"
|
||||
|
||||
echo "==> pip install (in case requirements.txt changed)"
|
||||
sh_ "$APP/backend/.venv/bin/pip install -q -r $APP/backend/requirements.txt"
|
||||
|
||||
echo "==> restart"
|
||||
BEFORE=$(sh_ "systemctl show garmin-health-lab -p MainPID --value" || true)
|
||||
sh_ "sudo systemctl restart garmin-health-lab"
|
||||
sleep 2
|
||||
AFTER=$(sh_ "systemctl show garmin-health-lab -p MainPID --value" || true)
|
||||
if [ -z "$AFTER" ] || [ "$AFTER" = "0" ]; then
|
||||
echo "service failed to come back up — check journalctl -u garmin-health-lab" >&2
|
||||
exit 1
|
||||
fi
|
||||
if [ "$BEFORE" = "$AFTER" ]; then
|
||||
echo "the main PID did not change ($AFTER) — the old process may still be" >&2
|
||||
echo "serving and your changes are NOT live." >&2
|
||||
exit 1
|
||||
fi
|
||||
echo "==> restarted: $BEFORE -> $AFTER"
|
||||
|
||||
echo "==> health"
|
||||
sh_ "curl -sf -m 5 -o /dev/null -w 'local: %{http_code}\n' http://127.0.0.1:5500/api/health/status" \
|
||||
|| echo "local: unreachable"
|
||||
curl -sf -m 8 -o /dev/null -w "public (https://garmin.zichuan.xyz): %{http_code}\n" \
|
||||
https://garmin.zichuan.xyz/api/health/status || echo "public: unreachable"
|
||||
@@ -1,4 +1,6 @@
|
||||
#!/bin/sh
|
||||
# RETIRED 2026-09-12: production moved off the NAS to the Oracle box
|
||||
# (systemd unit garmin-health-lab.service there). Kept for reference only.
|
||||
# Start Garmin Health Lab. Safe to run repeatedly: an already-running
|
||||
# instance is stopped first. Intended for DSM Task Scheduler (boot-up).
|
||||
APP="$(cd "$(dirname "$0")/.." && pwd)"
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
#!/bin/sh
|
||||
# RETIRED 2026-09-12: production moved off the NAS to the Oracle box
|
||||
# (systemd unit garmin-health-lab.service there). Kept for reference only.
|
||||
APP="$(cd "$(dirname "$0")/.." && pwd)"
|
||||
GUNICORN="$APP/backend/.venv/bin/gunicorn"
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
# 项目架构说明
|
||||
|
||||
> 现状(2026-09-02):后端为 **Python/Flask 单体**,生产部署在 **NAS :8124**。
|
||||
> 现状(2026-09-12):后端为 **Python/Flask 单体**,生产部署在**甲骨文云主机**
|
||||
> (`garmin.zichuan.xyz`,此前是 NAS :8124,已整体迁移)。
|
||||
> 早期 Node.js/Express + `server/` 目录的后端已被整体重写为
|
||||
> `backend/`(Python),本文件描述的是当前实现。
|
||||
|
||||
@@ -17,7 +18,7 @@
|
||||
│ HTTP/JSON + JWT(Bearer);Copilot 用 SSE
|
||||
▼
|
||||
┌──────────────────────────────────────────────────────────┐
|
||||
│ Flask 单体(gunicorn 2w/4t,NAS :8124) │
|
||||
│ Flask 单体(gunicorn 2w/4t,甲骨文云主机 :5500) │
|
||||
│ ┌────────────────────────────────────────────────────┐ │
|
||||
│ │ 蓝图层 routes/ /api/auth /api/garmin /api/health │ │
|
||||
│ │ /api/analysis /api/settings │ │
|
||||
@@ -142,16 +143,20 @@ Copilot: POST /analysis/copilot 走 SSE(stream_chat,失败非流式回退)
|
||||
|
||||
- **Development 本地**:`npm run dev` = Flask 5000(`BACKEND_PORT` 固定,避开
|
||||
`PORT` 被 CRA 抢占)+ CRA dev server 3000(proxy → 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。
|
||||
- **Production**(甲骨文云主机,`129.146.26.249`):`/opt/garmin-health-lab`,
|
||||
`ubuntu` 用户跑 gunicorn `127.0.0.1:5500`(2w/4t,--timeout 300 —— AI 生成
|
||||
可长达分钟级),systemd 单元 `garmin-health-lab.service`(`enabled`,
|
||||
`Restart=always`);MariaDB 10.3.39 本机 TCP、独立账号 `garmin`,库
|
||||
`garmin_health_lab`。和这台机器上的 ai-gateway/auth-hub/fam-edge 同一套
|
||||
部署约定。此前(2026-09-01~12)部署在 NAS,已整体迁移并下线。
|
||||
- **公网**:Caddy 反代 `https://garmin.zichuan.xyz` → `127.0.0.1:5500`,
|
||||
TLS 自动签发。
|
||||
- **一键部署**:`deploy/push_oracle.sh`(ssh key 认证同步 backend、清空重推
|
||||
`client/build`、pip install、systemctl restart、**PID 变化 + health 200
|
||||
双校验**);前端先 `npm run build`。NAS 时代的 `deploy/push.sh` /
|
||||
`start.sh` / `stop.sh` / `S99garmin.sh` 已废弃,保留仅供参考。
|
||||
- **auth-hub**:回调只注册 `https://garmin.zichuan.xyz/auth/callback`
|
||||
一条(client `996aLPw4T5gl-rYZ`);CORS 白名单含该地址 + localhost。
|
||||
|
||||
## 关键设计约束
|
||||
|
||||
|
||||
@@ -12,12 +12,12 @@
|
||||
| `AUTH_HUB_CLIENT_SECRET` | 对应 secret(**只放 .env,不入库**) |
|
||||
| `AUTH_HUB_REDIRECT_URI` | 回调地址,必须与 auth-hub 侧注册值完全一致 |
|
||||
|
||||
## 生产 client(NAS :8124)
|
||||
## 生产 client(甲骨文云主机,`garmin.zichuan.xyz`;2026-09-12 前是 NAS :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`
|
||||
- `https://garmin.zichuan.xyz/auth/callback`(唯一注册的回调地址;NAS 时代
|
||||
的 LAN / frp 地址已用 `manage_clients remove-redirect-uri` 清掉)
|
||||
- 注意 auth-hub 的 dev 与 prod 是**两套独立数据库、各自注册 client**:
|
||||
在 dev auth-hub 上创建的 client 不能用于 prod,反之亦然。
|
||||
|
||||
|
||||
@@ -184,7 +184,8 @@ npm run build # 1. 先出前端产物
|
||||
入库 + 「只读本地」保证、AI 缓存/队列互斥、调度器、auth-hub 客户端。
|
||||
- 前端界面回归:本地起后端 + CRA,Chrome headless + CDP 截图比对
|
||||
(参见 `.workbuddy/memory/` 日志里的做法)。
|
||||
- 生产自测入口:NAS `curl http://127.0.0.1:8124/api/health/status`(200)。
|
||||
- 生产自测入口:甲骨文云主机 `curl http://127.0.0.1:5500/api/health/status`
|
||||
(200),或公网 `curl https://garmin.zichuan.xyz/api/health/status`。
|
||||
|
||||
## 环境变量速查(backend/.env)
|
||||
|
||||
@@ -198,7 +199,7 @@ npm run build # 1. 先出前端产物
|
||||
| `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 | 回调地址 |
|
||||
| `AUTH_HUB_REDIRECT_URI` | https://garmin.zichuan.xyz/auth/callback | 回调地址 |
|
||||
| `STATIC_DIR` | backend/static | 前端产物目录(生产 ./static) |
|
||||
| `AI_GATEWAY_BASE_URL` | https://ai.zichuan.xyz/v1 | OpenAI 兼容网关 |
|
||||
| `AI_GATEWAY_TOKEN` | — | 网关 Bearer token |
|
||||
|
||||
@@ -13,8 +13,8 @@
|
||||
| # | 需求 | 理解 | 状态 |
|
||||
|---|---|---|---|
|
||||
| 1.1 | 新建项目分析佳明海外账号健康数据 | Gitea 自建仓库,Web 应用 | ✅ |
|
||||
| 1.2 | 部署到 NAS(192.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.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 + 公网两地址;本地邮箱密码登录已移除 | ✅ |
|
||||
|
||||
Reference in New Issue
Block a user