Commit Graph

12 Commits

Author SHA1 Message Date
ericwyuan
74f2721401 fix(deploy): S99garmin.sh 丢了可执行位,重启两天没人发现服务已经死了
09-06 07:08 NAS 重启(DHCP 顺带把它的局域网地址从 .64 换成了 .65),佳明健康
服务再也没起来——两天后才被发现。

根因:`deploy/S99garmin.sh` 本地就没有 +x(`-rw-r--r--`,另外三个部署脚本都是
`-rwxr-xr-x`)。DSM 的 rc.d 启动器碰到一个没有执行权限的软链接目标,既不执行
也不报错,静默跳过。push.sh 用 tar 同步 deploy/ 目录会原样带走权限位,所以自
09-01 起只要重新部署一次,就会把 NAS 上(原本可能是手工修过的)可执行权限
再次覆盖回不可执行——这颗雷从那天就埋下了,直到这次重启才被踩到。

- 本地补上 S99garmin.sh 的 +x
- push.sh 同步完 deploy/ 之后显式 chmod +x 三个会被开机脚本或部署流程直接
  执行的文件,并断言生效——以后这个位再丢,部署会报错退出,不会再悄悄失效
  到下次重启才现形
- push.sh 的默认目标 IP 也是这次连带发现的坑:硬编码的 192.168.50.64 已经
  证明会被 DHCP 换掉,改成当前地址 .65 并加注释——真正的解法是给 NAS 做
  DHCP 保留(MAC 90:09:d0:22:a6:33),不在这次改动范围内

现场同时处理:手动 chmod +x 后 sh start.sh 拉起服务;frpc 配置里的
localIP 硬编码着 192.168.50.64(同样的病),改成 127.0.0.1 后不再受局域网
IP 变化影响,公网已验证恢复。frpc 改动是 NAS 系统配置,不在这个仓库里,
旧文件备份在 NAS 的 /etc/frp/frpc.toml.bak-20260908。

遗留:auth-hub 注册的 LAN 回调地址还是 http://192.168.50.64:8124/auth/callback
(CLAUDE.md:99),局域网内直接用 .65 访问会在登录环节被拒;公网入口不受影响。
要不要把它也换成 DHCP 保留后的固定地址,留给用户决定。

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-08 07:27:22 +08:00
ericwyuan
317cb4996d fix(deploy): 构建前端后复制到 backend/static/,避免部署旧版
之前 deploy.sh 只打包了 backend/ 目录,但前端构建输出在
client/build/,从未被复制到 backend/static/,导致部署的
始终是旧版前端。现在在 build 后自动复制。
2026-09-01 19:00:42 +08:00
ericwyuan
984a06a098 fix(deploy): deploy/ 自己没跟着上机,改它等于没改
重启跑的是 NAS 上那份 stop.sh / start.sh,而 push.sh 只同步 backend/ 和
client/build/。结果就是:修好 stop.sh 的 pgrep、提交、部署、报成功,远端
照样跑着旧脚本——上一个提交正是这么被架空的。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-01 14:34:59 +08:00
ericwyuan
14b6bbecb4 fix(deploy): 群晖没有 pgrep,那三处「守卫」一直是空转
上一版给 push.sh 加的 pid 比对刚跑就露馅了:`sh: pgrep: command not found`,
于是 BEFORE 和 AFTER 都是空字符串,`[ -n "$BEFORE" ]` 直接跳过检查——我刚加
的安全网自己就是个摆设。

顺手查了一圈,stop.sh 和 start.sh 里同样的 pgrep 也一直在空转:
- stop.sh 的 `pgrep ... || exit 0` 每次都以 127 失败,永远匹配不到那个提前
  返回,所以每次停服都白等满 15 秒,也从没真正确认过残留 worker 已经没了。
- start.sh 的等待循环同理,白等 20 秒。

DSM 有 pkill 没有 pgrep,而且非特权的 ps 看不见 root 起的进程(服务是开机
以 root 启动的),两者任一都会让检查静默返回「没有」——这正是最危险的答案,
因为它长得和「已经停干净了」一模一样。

- push.sh 改用 `sudo ps -eo pid,args | grep`;并且 AFTER 为空时直接报错退出,
  不再把「看不见」当成「通过」
- stop.sh / start.sh 改用 `ps -eo args | grep -q`

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-01 14:34:27 +08:00
ericwyuan
f6687d2b5d fix(deploy): push.sh 报「部署成功」,但跑的还是旧代码
服务是开机时以 root 起的(DSM 任务计划 → S99garmin.sh),所以 logs/
error.log 和 access.log 归 root。以 ericwyuan 重启会立刻失败——gunicorn
连自己的错误日志都打不开;而 stop.sh 同样杀不掉 root 进程,于是旧 master
继续占着 :8124,start.sh 的健康检查看到 200,高高兴兴报了「started」。

这次 AI 教练部署就撞上了:文件推上去了,接口却一直 404,查了半天才发现
在跑的是几小时前那个进程。

- 重启改走 sudo,和服务实际的运行身份一致(密码问一次,或走 NAS_PASSWORD)
- 健康检查加一条:只有 200 不算数——那正是旧 master 会给的答案。比对重启
  前后 gunicorn 的 pid,没变就报错退出

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-01 14:11:17 +08:00
ericwyuan
526ece7d14 refactor(sync): 手动同步与自动同步彻底分开
「历史范围」原本放在设置页,却只对同步页的一个按钮起作用;而同步页最
显眼的主按钮「同步最新数据」写死 2 天,根本不看这个设置。选了「全部
历史」再点主按钮,表现就是应用无视你 —— 这正是反复出现的「只同步下来
两天」。

现在两条链路各管各的:

* 自动同步:只在设置页配置(开关 + 频率),窗口固定 SYNC_DAYS,不再
  读 history_days。措辞也改成「拉取最近几天」,不再暗示会补历史。
* 手动同步:范围就在同步页当场选,紧挨着用它的按钮,并标出每个范围的
  实际代价(自上次同步 / 7 天 / … / 全部历史约 730 天、20-40 分钟)。
  两个按钮合成一个「开始同步」,写死 2 天的那个删掉。

history_days 保留为「上次手动选的范围」,只有同步页读它;默认值改成
-1(自上次同步),对日常使用是正确的起点。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-01 09:20:52 +08:00
ericwyuan
4d55fb4776 chore(deploy): 别把 macOS 的 xattr 塞进 tar,群晖读不懂只会刷警告
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-01 09:09:21 +08:00
ericwyuan
1c48435b9d chore(deploy): NAS 的 ssh 端口是 2222
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-01 09:05:35 +08:00
ericwyuan
41b7ae82e4 fix(sync): 「全部历史」真的拉全部历史,自动同步不再每次静默失败
四个独立的 bug 叠在一起,表现为「只同步两天、没有进度」:

* 前端 `...(days ? { days } : {})` 把 days=0 当成未传。「全部历史」
  存的就是 0,请求体里根本没有 days,后端退回 7 天默认值。
* scheduler 用 `s[0]` 读 query_one 返回的 dict,抛 KeyError 后被
  per-account 的 except 吞掉。只要用户存过一次设置,每 30 分钟的
  自动同步就一次都没成功过——库里那 2 天全是手动点出来的。
* 增量同步查 `health_daily`(表其实叫 health_data),后台线程直接
  死掉,状态永远卡在 syncing,进度条不动。
* UI 完全不看 /sync 的返回值,rate_limited 时按钮点了没反应;轮询
  结束时又把 rate_limited 归进 else 分支报「同步完成」。

顺带:
* 日循环遇到 429 立即退避并保留已拉到的天数,而不是当成「跳过一天」
  继续往下捶 700 天——这正是之前限流死循环的来源之一。
* 定时循环显式传 SYNC_DAYS。历史范围按 UI 文案只描述手动全量同步,
  让半小时一次的 tick 重拉 730 天必然把限流撞得更深。
* 短同步逐天上报进度(原来每 5 天一次,7 天的同步全程停在 0)。
* /sync 路由重复解析 body,空 body 会 None.get 崩。
* 4 个 StubGarth 缺 configure(),7 个测试在此之前一直是红的。

新增 deploy/push.sh:NAS 只认密码,脚本开一个 ssh 复用连接,密码只
输一次,后面推送 / 重启 / 健康检查全走它。不碰 .env、.venv 和数据库。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-01 08:11:24 +08:00
ericwyuan
15fba8c25c feat: NAS 部署 + auth-hub 认证 + 同步逻辑修复
部署:
- 后端 Python/Flask,数据层可插拔 SQLite/MariaDB
- NAS 部署路径 /volume1/web/garmin-health-lab,端口 8124
- Gunicorn 生产服务器 (2 workers / 4 threads)
- 开机自启脚本 deploy/S99garmin.sh
- frp 隧道甲骨文 8124 → NAS 8124,外网访问
- 前端构建产物纳入版本管理 (backend/static/)

认证:
- auth-hub OAuth2/OIDC 统一登录接入
- 新建 NAS 专用 client,注册内外网回调地址
- 前端 LoginPage 支持回调路由 /auth/callback
- 数据同步页增加 Garmin 邮箱输入框

同步逻辑修复:
- 自动同步调度器读取用户 history_days 设置,不再固定 2 天
- 前端 0(全部历史)不再被 || 吞掉,改为 ?? 处理
- 后端路由和 sync_data 中 0 不再被当成 falsy 回退默认值
- sync_data 和调度器中 0 → 730 天(全部历史=最大范围)
- 已同步天数显示数据库实际总天数 (totalDays)
- 历史范围新增「自上次同步」增量选项 (days=-1)
- 后端 settings.py HISTORY 添加 -1 值

项目文档:
- 创建 PROGRESS.md 跟踪项目进度
2026-09-01 07:39:09 +08:00
ericwyuan
283d530f82 fix(deploy): 启动脚本等端口真正释放,并确认服务可用后才报成功
上一次部署里旧 worker 没随 master 退出,一直占着 8123。新 master 启动、
打印 started、然后什么都不服务——日志每一行看起来都正常,站点却是挂的。

- stop.sh 等进程真正退出,超时后 -9
- start.sh 停完等端口释放,起来后 curl 健康检查通过才算成功,否则返回非零

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-08-24 04:23:05 +08:00
ericwyuan
6de7562cd8 [阶段5.1] 开机自启 + 接入真实 garminconnect,修复同步的三处 API 误用
fix(garmin): 同步代码调用的 garminconnect API 全是错的

装上库核对签名后发现(garminconnect 0.2.8),原代码从未被真正
执行过,三处调用都不成立:

1. get_activities(date) —— 该方法签名是 (start, limit),收的是
   分页下标和条数,不是日期。传日期等于在问"第 2026-08-23 条运动",
   而且少传一个参数必然 TypeError。
   改为 get_activities_by_date(start, end),整个窗口一次取回,
   顺带把原来"每天一次调用"减为一次。

2. 睡眠数据不在 get_user_summary 里,是独立的 get_sleep_data(cdate);
   原代码从 summary 里读 sleep 字段,结果会把每一晚都记成无睡眠数据。
   HRV 同理,走 get_hrv_data(cdate)。

3. 字段名不符:实际是 totalSteps / totalKilocalories /
   averageStressLevel,原代码用的是 steps / calories.total /
   stress.average。

其他改进:
- 运动记录改用 Garmin 自己的 activityId 作主键,重复同步同一窗口
  不再产生重复行(原来每次同步都会把同一条运动再插一遍)
- 某天无数据时返回全 None,不再写入空行让读端点再过滤掉
- 原来 bare except 吞掉每天的异常,全部失败也报 success;
  现在全窗口失败会如实返回 error 并记录原因
- 同步天数、是否走 Garmin 中国区改为环境变量可配

tests/test_garmin_sync.py (23 通过):
- 用 StubClient 模拟真实 0.2.8 的接口形状,无需库、凭证或网络
- 回归用例覆盖上述三处误用:活动必须按日期区间一次取回、
  睡眠与 HRV 必须走各自端点
- 重复同步不产生重复的天和重复的运动记录
- 单天失败跳过、全窗口失败报错、登录失败如实上报

部署:
- NAS 安装 garminconnect(pydantic-core 有 cp38 x86_64 轮子,
  无需 gcc)
- 新增 deploy/S99garmin.sh 开机自启脚本,与 NAS 上既有的
  S99frpc.sh 同一套惯例;以 root 启动但降权到 ericwyuan 运行,
  因为服务不需要特权而 .env 里有数据库和 API 凭证
- PID 文件放应用目录而非 /var/run(非特权用户写不了)

NAS 真机全量测试: 240 passed

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-08-23 19:05:19 +08:00