ericwyuan
|
db7f182030
|
fix(garmin): 退出账号会把邮箱和令牌一起永久删掉,SSO 账号无处补救
用户刚才绑定失败:`Request failed with status code 400`。查下来是
`delete_token()`("退出 Garmin 账号"背后的函数)的注释说"邮箱留在 user 表,
下次只需要密码",但代码只有 `DELETE FROM garmin_tokens`——而 `garmin_email`
唯一的落脚点就是这张表。`users.garmin_email` 是本地密码登录时代的遗留列,
`get_remembered_email` 里写得很清楚:auth-hub 接管之后,没有任何绑定路径再
往那张表写过东西。也就是说现在**所有账号**(auth-hub SSO)退出一次,等于
永久忘记邮箱——注释描述的"安全网"对这些账号从来没生效过。
- `delete_token` 删除前把 `garmin_tokens.garmin_email` 复制一份到
`users.garmin_email`,注释里承诺的行为终于是真的
- 顺带修了一条原有测试掩盖真相的问题:`test_disconnecting_drops_the_
current_binding_but_not_the_legacy_value` 预先在 users 表塞了一条 legacy
邮箱,退出后自然能读到——从来没测过 SSO 账号真正会遇到的情况(legacy 列
本来就是空的)。新增两条测试覆盖这个和"连续退出两次不会把邮箱也搞丢"
- 生产账号的邮箱已经从退出前的 dump 备份里手工恢复,不用重新绑定就能补上
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
|
2026-09-13 07:20:52 +08:00 |
|
ericwyuan
|
4331a07462
|
docs: docs/ 目录同步甲骨文部署事实(上一提交漏加)
上一个提交 git add 漏了 docs/,架构/开发/需求/auth-hub 集成文档里的 NAS
IP、8124 端口还是旧的,补上。
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
|
2026-09-12 23:53:26 +08:00 |
|
ericwyuan
|
9d6ebbe422
|
ops: 生产从 NAS 整体迁移到甲骨文云主机
NAS 局域网 IP 因重启被 DHCP 换过两次,磁盘/网络稳定性都不如已经跑着好几个
生产服务的甲骨文机器。整体搬迁:应用 + 数据库都搬走,NAS 只保留 Gitea(这个
仓库的源码托管,未动)。
## 迁移过程(已核对无损)
- MariaDB:NAS 导出(10.11 源库,处理了只有新版本才有的 `/*M!999999` 注释)
→ 导入甲骨文 MariaDB 10.3.39,**20 张表逐条精确 COUNT(*) 比对完全一致**
- 冻结 NAS(停服务)后又 dump 一次核对,确认期间零数据差异,才继续删库
- NAS `garmin_health_lab` 已 DROP DATABASE,备份在本地
`~/Desktop/Work/backups/garmin_health_lab_nas_backup_20260912.sql.gz`
- 应用部署到 `/opt/garmin-health-lab`,systemd 单元(`ubuntu` 用户,非
root),和这台机器上的 ai-gateway/auth-hub 同一套约定
- 公网:`https://garmin.zichuan.xyz`,DNS + Caddy 反代 + 自动 TLS,替代原来
`NAS frpc → 甲骨文:8124` 那条隧道(已从 NAS 的 frpc.toml 精确删除对应段,
其它转发未动,改完逐条复检过没打断)
- auth-hub 回调地址换成新域名,NAS/旧端口那几条历史回调已清掉
- AI 网关配置改本地回环(网关现在同机了),触发真实生成验证过
## 一个当场拦下来的风险
甲骨文部署完默认开着自动同步。迁移窗口期两边并行跑时,若两边的调度器同时去
刷新 Garmin 令牌,会撞上按账号计算的 SSO 限流(`GarminHealthLab` 仓库
2026-09-03 那次事故的根因,那次修复花了一整天)。确认账号级 auto_sync 设置
本来是关的、这次算侥幸没撞上——不是设计上的保险,所以迁移期间显式在甲骨文这边
加了 `AUTO_SYNC=false`,直接在运行进程里验证过生效,确认 NAS 已冻结、数据无
缺口后才打开。
## 文档 / 脚本同步
CLAUDE.md 明确写过"部署位置会变,排障前先查、不要凭记忆"——这次是第二次踩中
同一类问题(上次是"NAS 有没有生产环境"判断错),所以把 CLAUDE.md / PROGRESS.md
/ README.md / docs/* 里的部署事实全部更新,NAS 时代的 `deploy/` 脚本加废弃
说明保留参考、不删除,新增 `deploy/push_oracle.sh`(当场跑通一次真实部署)。
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
|
2026-09-12 23:53:15 +08:00 |
|
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
|
12f8928d88
|
docs(CLAUDE): 沉淀 Garmin 限流的真相——出在登录端点,且窗口内每试一次延长一次
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
2026-09-04 06:21:41 +08:00 |
|
ericwyuan
|
1c80b16325
|
fix(garmin): 重新绑定走的是同一个被封的登录接口,也得拦住
用户问:限流了,重新输账号密码验证码换个新令牌行不行。
不行,而且是最糟的一种试法。`garth.login()` 和 `refresh_oauth2()` 打的是
同一个 SSO 端点,流程还更重;限流按**账号**计(不是按 IP、按 UA),换设备
换网络都绕不开;而窗口内每次尝试都会把窗口往后推。
而这正是被卡住时第一个会去试的操作,代码里却只有 `_connect` 的刷新有闸门,
重新绑定那条路照发不误。
- start_login 在 sso 冷却窗口内直接拒绝,不建会话行、不碰网络
- 错误信息说清三件事:为什么现在不试、什么时候恢复、换设备没用
- 路由返 429(请求本身没毛病,是该晚点再来)并带 retryAfterSeconds
- 数据端点的 429 不参与拦截,force 可以推翻
前端补上 UI:报错文案早先承诺了「同步页选择强制重试」,但那个按钮不存在。
现在只在被冷却拒绝之后才出现,样式刻意做得不像第二个「开始同步」——它是给
估算失准时的出口,不是随手可点的第二选择。
顺带修一个正要被我引入的 bug:`onClick={syncHistory}` 会把 MouseEvent 当成
force 传进去,等于每次点开始同步都跳过冷却。
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
2026-09-04 06:20:05 +08:00 |
|
ericwyuan
|
fd5e3363fd
|
fix(garmin): 登录端点的 429 在封锁期内再试会延长封锁,唯独这一处要停手
实测记录:昨天为了验证令牌写回的修复,我发了一次 1 天同步,把 rate_limited_until
从 09-04T00:41 推到了 09-04T15:26——一次尝试,延长十五小时。
Garmin 的登录/换令牌端点和数据端点是两套规则:
- 数据端点的 429:本地冷却只是估算,过一会儿发个真实请求正是确认它有没有解除
的唯一办法,没解除也不吃亏
- 登录端点的 429:窗口内每次尝试都把窗口往后推。这也是这个账号一直卡着出不来
的原因——每次同步都去换一次令牌,每次都把封锁续上
所以:
- sync_status 加 rate_limit_source 列,记住 429 是哪个端点给的
- 只有 source=sso 且窗口未过时,才拒绝**刷新令牌**这一个动作
这个闸门刻意做得很窄,因为上一次的教训是「一刀切的闸门会让健康账号白白闲置」:
- 只拦刷新,不拦同步。库里的令牌只要还没过期,冷却期内照常同步
- 数据端点的 429 不参与判断——为了一次指标调用把账号锁一天,比问题本身更糟
- 用户可以推翻它:同步请求带 force 就先清掉冷却记录。那个截止时间是我们自己
猜的 24 小时,不是佳明说的,所以必须能被推翻
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
2026-09-04 06:13:52 +08:00 |
|
ericwyuan
|
682936b0b6
|
fix(garmin): 刷新出来的令牌从来没写回库,于是每次连接都重换一次
「又被限流了」的根因找到了,不是请求量,是令牌。
`_connect` 里 `refresh_oauth2()` 换来的新 OAuth2 令牌只活在进程内存里——
`save_token` 只在绑定账号时调用过一次。于是每次客户端缓存过期(15 分钟)、
每个 gunicorn worker、每次部署重启,都从库里读回**同一个已过期的令牌**,
然后再做一次真实 SSO 换令牌。而 SSO 端点是按账号限流最狠的那个,社区报告能
封 48 小时(garth #217、python-garminconnect #337)。我今天为了部署重启了
八次服务,每次都清掉缓存。
- `refresh_oauth2()` 成功后 `_persist_token()` 写回。拆出这个函数是因为它和
`save_token` 想要的正好相反:重新绑定要作废现有会话,持久化刷新结果必须
保住刚刚产出它的那个会话
- 写回时不带 garmin_email,否则 upsert 会把绑定邮箱刷成 NULL,数据同步页会
忘记绑的是哪个账号
- 刷新加进程内锁,并在拿到锁后重读一次库:另一个线程刚换过就直接用它的,
不再自己去换一次
- 五条测试盯住这个不变量,包括「冷缓存不该再换一次」(这条如果回归,就是同一
个 bug 再来一遍)
顺带把数据端点也节流了——那是另外一半问题,不是这次的病因,但一天历史要 9 次
调用,730 天全历史 6600 个请求全速打出去,不该指望佳明一直容忍:
- services/garmin_throttle.py:代理包住 client,所有调用(含以后新加的)都经
同一个收口,按间隔排队并计数
- 0.5s 是查过的:garmin-data-export 默认 0.15s、garmin-connect-scraper 默认
3s、官方合作方 API 100 次/分钟(0.6s)。依据写在文件顶部
- 单次同步 1200 个请求预算,跑满就干净收尾、下次接着跑(已存的天数本来就跳过)
- 运动详情每次最多补 40 条——新账号几百条,不限量就是一次性打光预算
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
2026-09-03 23:24:48 +08:00 |
|
ericwyuan
|
57c236ba16
|
fix: 连接阶段真实 429 以 rate_limited 状态呈现,而非泛化 error
sync_data 的 _connect 抛 RateLimited(真实 429,冷却已写入)原本落进
except Exception 返回 status=error,message 带 'RateLimited:' 前缀——前端
SettingsPage 只对 status=rate_limited 渲染'被限流'提示,导致放开本地拦截
后真实 429 的提示错位。
单独捕获 RateLimited:status=rate_limited + 干净 message + history 记录
rate_limited。补测试 test_a_real_429_at_connect_surfaces_as_rate_limited
(597 passed)
|
2026-09-03 07:18:14 +08:00 |
|
ericwyuan
|
e0e7b3cf53
|
fix: 本地限流估算不再拦截同步——一律真实请求 Garmin,仅真实 429 记录冷却并退避
此前 sync_data/start_sync/_connect/scheduler 四处会在请求前按本地
rate_limited_until 估算直接拒绝同步,用户看到'立即同步→被限流'实际是
本地拦截、零请求。若 Garmin 已恢复,陈旧估算会让账号一直闲置。
2026-09-02 起冷却只是信息不是闸门:
- start_sync / sync_data 开头移除本地拒绝,_connect 移除冷却提前抛错,
scheduler 不再跳过冷却中的账号
- 真实 429(refresh_oauth2 / 拉取中途)仍 _note_rate_limit 写 24h 冷却
并 stand down,中途退避分支保留用冷却算恢复时间
- 成功收尾 _clear_rate_limit 退休陈旧冷却,避免误导后续诊断
- 测试:blocked→仍会真实请求;stale cooldown→healthy connect 放行;
真实 429 mid-run 仍记 rate_limited;成功清除冷却(596 passed)
|
2026-09-03 07:06:12 +08:00 |
|
ericwyuan
|
7e8e376a6c
|
docs: sync_history 文档一致性——CLAUDE.md 沉淀 MariaDB trigger 保留字坑,ARCHITECTURE.md 表清单 20→21 张并补数据流
|
2026-09-02 20:45:34 +08:00 |
|
ericwyuan
|
5e6fc01f76
|
feat(sync-history): 新增同步结果查询(每次自动/手动/立即同步的记录)
后端:
- db.py SCHEMA 新增 sync_history 表(不可变,每次同步尝试一行) + 索引
- garmin.py: _log_sync_history() 在 sync_data 全部出口记录; trigger 区分
auto(调度器)/manual(同步页开始同步)/quick(设置页立即同步); start_sync
限流拦截分支同样留档; 写入失败只告警不影响同步
- scheduler.py 自动同步传 trigger="auto"; routes 新增 GET /garmin/sync-history
按时间倒序返回(默认 50 条,上限 200)
前端:
- api.ts 增加 SyncHistoryItem 类型 + getSyncHistory()
- 新页面 /sync-history/ 同步记录: 卡片列表, 状态(成功/失败/被限流)chip 配色,
时间本地化(今天/昨天/X月X日), 触发类型标签, 范围与耗时
- 同步页与设置页同步区块均加入口链接
|
2026-09-02 20:42:26 +08:00 |
|
ericwyuan
|
26ff853f0b
|
docs(CLAUDE): refill_backlog 1064 已修(617c832),移除已知问题并沉淀 MariaDB CAST TEXT 坑
|
2026-09-02 20:20:08 +08:00 |
|
ericwyuan
|
617c8325a6
|
fix(analysis): refill_backlog 活动查询去掉 CAST(... AS TEXT)
MariaDB 不支持 CAST 到 TEXT(仅 SQLite 支持),导致该语句在 prepare 阶段
直接 1064,activity 分支每次 idle 循环都报错刷屏——且无论 backlog 是否有
数据都会失败(语法错误先于执行)。activities.id 本身是 VARCHAR(64),
ai_jobs/ai_insights.subject 是 VARCHAR(96),两侧同类型,CAST 本就不必要。
改为直接比较 ai.subject = a.id / aj.subject = a.id,双后端(SQLite/MariaDB)
语义一致。
已在生产 MariaDB 上只读验证两段 SQL 均正常执行(daily-backlog=0,
activity-backlog=0),本地 analysis/ai/coach 测试全绿。
|
2026-09-02 20:18:40 +08:00 |
|
ericwyuan
|
224144915c
|
fix(sync): 限流拦截不再把'上次同步'刷成现在 + 设置页新增立即同步按钮
- _set_sync_status 支持保留旧 last_sync_time;两处限流拦截(start_sync /
sync_data)改为传旧值——被拒的同步零请求发生,不能把 UI 的'上次同步'
拨快成'刚刚',掩盖数据早已停更的事实
- SettingsPage 同步区块新增'立即同步'行:走 /garmin/sync-latest(最近2天,
秒级阻塞返回),成功/限流/未绑定三种状态都有明确反馈;限流中后端零请求
拦截并回传恢复倒计时文案
|
2026-09-02 20:14:11 +08:00 |
|
ericwyuan
|
2d9a2be185
|
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)
|
2026-09-02 19:34:32 +08:00 |
|
ericwyuan
|
68363957ec
|
fix(ui): Copilot 浮窗按钮被 F7 全局 width:100% 撑满,显式覆盖为 width:auto
Framework7 自带 button { width: 100% } 且打包后排在 Copilot 规则之后,把
面板内 ✕ 与发送按钮拉满父容器,input 被挤成 21px、✕ 跑到面板中部。
在 .copilot-close / .copilot-compose button 上显式 width:auto + flex 固定,
重建并同步 backend/static 产物(main.00d05e81.css)。
|
2026-09-02 19:34:24 +08:00 |
|
ericwyuan
|
bc91f328c9
|
docs: 记录 deploy.sh 修复前端构建复制
|
2026-09-01 19:00:58 +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
|
dd4105eb36
|
docs: 更新进度,记录每日/运动详情持续后台排队功能
|
2026-09-01 18:15:59 +08:00 |
|
ericwyuan
|
7fd4106b0a
|
feat(ai): 每日和运动详情在后台持续排队生成,不限量
- 移除 prefetch_insights 中 daily 的 LIMIT 30 和 activity 的 LIMIT 50
- 新增 refill_backlog(),在队列空闲时自动查找未排队的 daily/activity 项
- 新增 jobs.set_refiller() 机制,worker 空闲时调用 refiller 补充队列
- 队列空闲时每 5 个轮询周期触发一次 refill,持续填充 backlog
|
2026-09-01 18:15:26 +08:00 |
|
ericwyuan
|
05f55e695b
|
feat(ai): 设置里加「AI 生成队列」,看得见后台在算什么
队列本来是完全不可见的:页面上一句「排队生成中」说不出自己是下一个、第二十
个,还是已经放弃了——网关挂掉的时候,「还在生成」和「永远不会好」长得一模
一样。今天排查就是这么排的。
- GET /analysis/insight/queue 返回队列(running 在前,其次按优先级和年龄,
和 worker 实际取任务的顺序一致)、已生成的解读、scope 名到中文标签的映射
(前端不必再抄一份),以及消费者的限流配置
- POST /analysis/insight/queue/retry:手动把「已放弃」的重新排队,不等冷却。
自动重试要等冷却是为了不去捶一个正在抽风的上游;人按下重试是他自己判断值得
再试一次
- 页面在 设置 → AI 生成队列。插队的任务标「插队」——这是整个界面最想让人看见
的一件事:为什么是它排在最前面
- 「已生成」单独列:队列空了意味着「没有待办」,不是「什么都没生成过」,
没有这一节这两件事在界面上没法区分
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
2026-09-01 16:02:05 +08:00 |
|
ericwyuan
|
a2d6a5c57f
|
fix(ai): 队列会把共用的网关打到 502,加并发上限和间隔
排查生产上一直生成不出来,发现网关在返 502。上去看:机器好好的、systemd
说 active、5100 端口在监听——但它是 `gunicorn -w 1 --threads 4`,全部并发
就四个,而且 fam-edge 和摄像头项目也在用同一个。
我们这边一个请求占一个线程 2~5 分钟,NVIDIA 链重试起来最坏十七分钟(它自己
README 已知问题 #3)。而我写的 worker 是跑完一个立刻拉下一个,同步后还有八
个 scope 排队——等于拿满线程不撒手。这个 502 大概率是我打出来的,而且顺带
把另外两个项目也打下线了。
- 并发按整个部署计算,不是每个 gunicorn worker 一个:claim 前先数全局
running(两个 worker 各跑「一个」就是两个并发)
- 每跑完一个任务停 20 秒,不只是空闲时才停
- 两个都可用环境变量调,注释里写清楚调大的代价是什么
补齐的历史数据晚二十分钟到没有任何人受影响;网关不响应是三个项目一起受影响。
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
2026-09-01 15:46:42 +08:00 |
|
ericwyuan
|
2488956f36
|
fix(ai): 折叠态只显示一句话,那句话得说清是哪个指标
精简版的卡片正文就是第一条 highlight,标题不渲染。于是趋势页折叠起来是
「395 天内由 4376.0分 到 4905.0分(改善)」——什么的 4376 分?身体成分、
运动详情同样。
每个 builder 的第一条 detail 现在都自带主语,并加了不变量测试:detail 必须
包含它自己的 title(纯汇总性标题除外)。
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
2026-09-01 15:41:28 +08:00 |
|
ericwyuan
|
6dd070ec9b
|
fix(ai): subject 里塞了行数,每轮询一次就新建一个任务
生产上 trends 队列里积了 36 个任务,subject 是 2026-09-01:1033、:1039、
:1044……一路涨。这台账号当时正在补历史,get_summary 的行数每隔几分钟就变,
而我把 len(rows) 写进了 subject——subject 同时是缓存键和任务队列的键,一变
就是一条全新的任务,轮询几次就刷出十几条。
subject 该回答的是「这条解读是关于什么的」,不是「当时有多少行数据」。
数据变化本来就由 fingerprint 负责。
- trends 的 subject 改成快照日期;sleep 用配置的窗口常量而不是实际夜数
(缺一晚也不该换键);challenges 用固定键
- 加了不变量测试:补一天历史数据后 subject 不许变;任何 subject 段都不许
长得像行数
顺带加一层兜底 jobs.supersede():单实例 scope 只该有一个在跑的 subject,
队列里同 kind 的其它 pending 任务是关于已经不存在的快照的,跑完也没人看。
per_item 的 daily / activity 不受影响——它们本来就一天一条、一次运动一条。
兜底不是机制,机制是 subject 稳定;它存在只是因为这次 subject 不稳定,而
36 条任务堆在那里之前没人发现。
顺带按要求把 AiPanel 改成默认精简:只显示标题、来源和一句话结论,点「展开
详细」才出要点/建议/依据,可再收起——和今日晨报卡片一致。这些面板压在本来
就很密的图表页上面,全部默认展开会把真正的数据一次性挤到屏幕外。
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
2026-09-01 15:33:49 +08:00 |
|
ericwyuan
|
241ae0d6a3
|
feat(ai): 每个数据页面都有 AI 解读,靠一条带优先级的生产者/消费者队列
原来只有今日页有晨报、指标详情页有归因,其余页面一片空白。现在除设置外
的 10 个页面都有:健康、睡眠、运动、趋势、每日、身体成分、成绩预测、
身体年龄、挑战赛、运动详情。
不是给每个页面写一套,而是一个通用管线:
- services/scopes.py:一个页面一个 context builder,返回同一个信封。
context["highlights"] 是已经算好的白话事实——模型负责解读它们,模型不
可用时规则引擎原样渲染。两者引用同一批数字,所以降级读起来不像换了个 App。
没数据的页面返回 None,宁可不出卡片,也不让模型对着空表格发挥。
- coach.scope_messages / parse_scope_insight:一套提示词吃所有页面,页面
的差异全在 context 里,加页面 = 加一个 builder。
- 前端 <AiPanel scope="…">:一个组件渲染所有页面,轮询逻辑抽成
lib/insight.ts 的 usePolledInsight,晨报卡也改用它。
## 队列
一次生成 40 秒到 4.5 分钟,所以什么都不能在请求里生成。页面只负责入队,
worker 负责消费(services/jobs.py)。
优先级才是用队列而不是后台线程的理由:同步完成后 prefetch 把所有页面按
背景优先级排进去,可能要跑半小时;而用户一打开某个页面,那个页面的任务
立刻提到队首、下一个就跑。你在看什么,队列就在算什么。
队列放在数据库而不是内存里,因为 gunicorn 有两个 worker:任务带 holder
声明后回读确认,和 scheduler.py 抢 tick 是同一套做法。id 由
user+kind+subject 推导,所以每几秒一次的轮询是幂等的入队,不会每几秒堆一
个任务。
## 网关中断时踩到的两个坑(当场修了)
写完正好赶上 oracle 那台机器不通,于是看到:
- 三次失败后任务被永久标 failed,网关恢复了也不会重试——一次瞬时中断就把
那个页面的解读判了死刑,直到它的数据碰巧变化。加了冷却期,过期后重置
尝试次数再排一次。
- 队列已经放弃了,页面还在 pending 转圈,要转满 8 分钟才停。meta.pending
现在跟着队列状态走,并把失败原因带给卡片。
顺带把 BAND_SOURCES 从 routes/settings.py 下沉到 services/insights.py:
教练要拿它做参照,而 services 不该反向依赖 routes。
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
2026-09-01 15:17:17 +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
|
570d1590eb
|
fix(router): 深链接全被塞进「今日」,点底部标签页显示的是别人的内容
打开 …/sleep/ 之后,底部点「今日」出来的是睡眠,点「健康」出来的健康从来
没去过地址栏说的地方。
原因:browserHistory 只开在主视图(今日)上,而 Framework7 在启动时也会用
它去消费地址栏——它只认识这一个视图。于是不管哪个标签页真正拥有那个路径,
睡眠都被压进了**今日**的栈,还顺手伪造了一份两条记录的 history。
- View 加 browserHistoryOnLoad={false}。注意不是 browserHistoryInitialMatch
——那个只决定两条记录里渲染哪一条,伪造的栈照样留着(读 router-class.js
的 getInitialUrl 才看明白)。之后的导航追踪不受影响。
- routes.ts 加 tabForPath():冷启动的 URL 归哪个标签页。只有深链接查这张表,
应用内点击照旧——从今日卡片点进睡眠,仍然留在今日。
- App.tsx 的 useDeepLink 负责派发初始地址,并把目标页压在该标签页自己的根
之上,这样返回箭头回到的是健康,而不是空栈。
派发要延后一帧再做、并在 unmount 时取消:StrictMode 会把 <View> 挂两次,
第一次挂载的 F7 视图会被销毁重建,在它上面导航的结果全丢——症状是一个睡眠
页孤零零留在 DOM 里,router 却坚称自己在健康。另外比较路径要先归一化斜杠,
每个页面都注册了带斜杠和不带斜杠两种写法,直接比会把趋势压在趋势上面。
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
2026-09-01 14:31:34 +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
|
c57c930949
|
feat(ai): AI 教练 —— 晨间简报、运动处方、趋势归因与 Copilot
数值全部在服务端算好再交给模型,模型只做解读。让模型从 CSV 里自己推
z 分数,它算错的次数足以让简报引用图表反驳它的数字。
- services/insights.py:z 分数(28 天个人基线,且**排除当天**——用一个
值参与算出来的均值去衡量它自己,会把真实离群点摊平)、13 个月趋势斜率
(按序数日期最小二乘,手表放充电器上一周不会压缩 x 轴)、近 7 天活动量
对比。
- services/coach.py:三套提示词 + 回复解析,每套都配一个规则引擎版本。
网关一次生成要几分钟,上游被限流时给一个朴素的答案,好过给一张空卡片。
- services/ai.py:多轮 chat()、SSE stream()、complete()/stream_chat(),
以及 extract_json()——上游是推理模型,可见输出以思维链开头,所以从末尾
倒着找最后一个配平的 JSON(字符串感知,扛得住引号里的 } 和转义引号)。
- 接口 briefing / trend-insight / copilot(SSE),缓存表 ai_insights。
- 前端:今日页晨报卡(后台生成 + 轮询升级)、全局 Copilot 浮窗、指标详情
页归因面板。features.ai 打开。
实测(对着自建 ai-gateway):晨报一次 273 秒,缓存命中 18 毫秒——所以简报
绝不能同步阻塞首屏。网关的流式通道比阻塞通道更不可靠:同一条提示词流式
139 秒后返回「所有模型均不可用」,阻塞则成功,因此 stream_chat() 在流式零
输出时对同一模型退回非流式重试。Copilot 实测 TTFB 9ms、全程 40 秒。
顺带修两处:refresh 原来只跳过缓存读、不删行,导致「重新生成」后的轮询读
到旧行、看到 cached 就停了,用户一直盯着他刚要求替换掉的那段字;基线零方差
时原来返回 z=0.0,把「和每一条观测都不同」标成「完全正常」,改为 z=null。
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
2026-09-01 13:57:35 +08:00 |
|
ericwyuan
|
a746327560
|
fix(ui): 奖励卡片被 Framework7 的 .badge 压成灰色小药丸
.badge 在三处定义:Screen.css 的奖励卡、Settings.css 的状态药丸、以及
Framework7 自带的消息计数小圆点。F7 的样式表在打包顺序里排最后,于是
它赢了——每张奖励卡都被强制成 display:inline-flex、固定 20px 高、灰底
白字的圆点,名字和日期硬塞在里面。Screen.css 精心写的卡片样式基本全被
覆盖。65 个奖励堆在一起,看着自然不成样子。
* 奖励卡改名 .award/.award-grid/.award-name/.award-meta/.award-count,
彻底避开 F7 的全局类名。
* Settings.css 里的 .badge/.badge.ok/.badge.off 没有任何地方引用,是
死规则,唯一作用就是参与撞车——删掉。
* 顺手让卡片等高、日期用等宽数字对齐、×N 做成右侧小标签。
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
2026-09-01 09:34:53 +08:00 |
|
ericwyuan
|
92ea33b800
|
feat(sync): 全部历史真的是全部,不再截断在两年
730 天是个凭空写死的上限。账号有七年数据的人选「全部历史」,拿到的是
最近两年,而且没有任何提示说剩下的被丢掉了。
* 全部历史现在一直回溯到账号最早的数据:连续 EMPTY_RUN_STOP(120) 天
完全没有内容就停,所以既不会截断,也不会去问手表存在之前的年份。
MAX_HISTORY_DAYS(3650) 只是兜底,可用环境变量覆盖。
* 已经存过的日期跳过(最近 3 天除外,它们还在写入中)。这让多年的
回填变成可续传的:撞上限流停下来,冷却过后再点一次就从断点继续,
而不是每次都从今天重新爬。
* 选择器补上 3 年 / 5 年。
* 前端把每个范围的实际代价写出来,并说明中断可续。
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
2026-09-01 09:30:03 +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
|
9503fca370
|
feat(auth): 接入 auth-hub 统一登录,网页登录与 Garmin 同步彻底分离
网页身份改由 auth-hub 做 OAuth2 + PKCE 单点登录,本地邮箱/密码登录与注册整条链路删除
(routes/auth.py、auth.py 的密码哈希、config.py 的 ALLOW_REGISTRATION)。Garmin 账号绑定/
同步保持完全独立、可选:routes/garmin.py 不再直接查 users 表,Garmin 邮箱回退统一走新增
的 services/garmin.py::get_remembered_email()(优先读 garmin_tokens 当前绑定,兼容早期账号
落在 users.garmin_email 的历史值),彻底把「你是谁」和「你绑没绑 Garmin」两件事拆开。
- db.py: users 表新增 auth_hub_sub/auth_hub_username,MIGRATIONS 补上这两列(此前遗漏导致
已存在的生产 MariaDB 表永远不会自动加列);同时把历史遗留的 garmin_email/
garmin_password_hash NOT NULL 约束在线迁移为可空,因为新账号不再在注册时收集这些字段。
- routes/auth.py: 修掉 /callback 路由重复拼接 /api/auth 前缀导致 404 的 bug。
- client: LoginPage 去掉本地登录/注册标签页,只保留 auth-hub 统一登录;登录成功/失败后都
用 history.replaceState 清理地址栏,修掉 Framework7 browserHistory 读取
/auth/callback?code=... 导致「找不到页面」的问题。
- 新增 test_auth_hub_client.py 锁定 find_or_create_user 按 auth_hub_sub 幂等——生产上曾经因为
这个函数在没有该测试保护时被测试触发,误建过一个空账号,靠手工核对 health_data 计数才发现。
- 生产 auth-hub 侧另行为该项目注册了正式 client(未随本次提交变更,凭证只存在服务器 .env)。
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
|
2026-08-31 23:12:17 +08:00 |
|
ericwyuan
|
7e51223eb9
|
fix(rate-limit): _is_rate_limited 识别 RetryError 的 429 形态,登录 429 真正触发自动退避
生产实测:登录时 garth 把 429 折进 RetryError('too many 429 error responses'),无 status_code、无 'rate limit' 字样,旧检测漏判 → 走 else 分支存原始报错、不调 _note_rate_limit,冷却永远不落库,用户可反复撞 Garmin。\n\n- garmin.py: _is_rate_limited 沿 __cause__ 链找 429 响应,并接受文本中的 '429' 标记。\n- garmin_auth.py: 退避文案 6h 改为 24h。\n- 生产:已补挂 24h 冷却(2026-08-30 10:00 北京),RetryError 形状验证识别为 429。
|
2026-08-29 10:00:30 +08:00 |
|
ericwyuan
|
30ae431ed9
|
fix(rate-limit): 退避 24h 且守卫以 DB 为准,终结限流死循环
根因:Garmin 限流窗口 >6h,旧 6h 退避每到期就再撞一次 429,永久循环无法退出;且 _note_rate_limit 的 DB 写入被 except:pass 静默吞掉,worker 内存揣着未来冷却时间、DB 停在旧值,守卫被陈旧内存卡死(用户冷却已过仍被拦)。\n\n- RATE_LIMIT_BACKOFF 6h->24h:一次 429 静默一天,真正超过 Garmin 窗口。\n- rate_limited_until() 改为 DB 为准(内存仅在无行时兜底):杜绝陈旧内存永久拦截。\n- _note_rate_limit 失败改为 logging 输出,不再静默吞错。\n- 生产已把冷却设到 2026-08-30 09:45 北京时间。
|
2026-08-29 09:45:49 +08:00 |
|
ericwyuan
|
ae126f90d4
|
fix(rate-limit): 退避延长至 6h 且登录路径同样遵守,打破 Garmin 限流死循环
根因:迁移后自动同步+旧 oauth2 刷新轰炸把 Garmin 账号撞进限流黑洞;退避原仅 60min 且登录路径完全不检查,导致每小时退避到期又撞一次 429、登录也直接连 Garmin 被 429,窗口永远退不出。
- garmin.py: RATE_LIMIT_BACKOFF 60min->6h;_note_rate_limit 改为固定 now+6h(不再短延期)。
- garmin_auth.py: _run_login 入口先查 rate_limited_until 拦截(不撞 Garmin),遇 429 也调 _note_rate_limit 延长退避。
- 已在生产把 sync_status.rate_limited_until 设到 now+6h,强制 Garmin 安静以让窗口真正关闭。
|
2026-08-28 17:18:37 +08:00 |
|
ericwyuan
|
bec45414a7
|
feat(garmin): 增加「退出 Garmin 账号」功能,删除已保存令牌以便重新登录
backend: services/garmin.py 新增 delete_token(),删除 garmin_tokens 行并 forget_client 丢弃缓存会话;routes/garmin.py 新增 POST /api/garmin/disconnect(require_auth),返回 ok + 提示文案。
frontend: api.ts 增加 disconnectGarmin();SyncPage 增加「退出 Garmin 账号」按钮(带二次确认弹窗);DataSync.css 增加危险色样式。
设计:仅删除 OAuth 令牌,保留 users.garmin_email,下次重登只需密码;已同步的健康数据不受影响。
|
2026-08-28 15:13:32 +08:00 |
|
ericwyuan
|
11869c6a8d
|
fix(sync): 限流退避持久化到 DB,调度器与手动同步均尊重退避期
db.py: 修复迁移非幂等——gunicorn 双 worker 并发 init_db 时后到者遇 Duplicate column 会让整个服务起不来,改为吞掉 duplicate column 错误。garmin.py/scheduler.py: 退避期写入 sync_status.rate_limited_until,自动同步与手动同步在退避期内跳过,不再反复撞击 Garmin 把限流窗口越撞越深。
|
2026-08-28 14:54:10 +08:00 |
|
ericwyuan
|
a4bedc263f
|
fix(sync): 限流检测漏掉了它真实的样子
上一版加了 429 识别,但点同步按钮报的还是 JSONDecodeError——因为检测
只看 `e.response.status_code` 和异常消息,而真实到达我们手里的异常两样
都不满足:garth 对着 429 调 .json(),抛出的 JSONDecodeError 里状态码已经
丢了,消息是无用的 "Expecting value: line 1 column 1"。
响应体「Rate limited」唯一幸存的地方是异常的 `doc` 属性,改为从那里取。
同时改正上一版的测试:它用 ValueError("Rate limited") 构造,那个形态现实中
根本不会出现,所以测试通过、检测却漏掉了每一个真实的限流。现在用真的
json.JSONDecodeError 构造,并先断言「光看消息不够」。
实测同步现在返回:
RateLimited: Garmin 暂时限制了请求频率…令牌本身没有失效
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
|
2026-08-28 14:27:15 +08:00 |
|
ericwyuan
|
042c4d52be
|
fix(sync): 被 Garmin 限流时说人话,并停止把限流越撞越深
服务器上的数据停在 8-25,而同步状态是 error、卡在「连接 Garmin」,
记录的原因是 `JSONDecodeError: Expecting value: line 1 column 1`。
真相:Garmin 返回的是 HTTP 429,响应体是纯文本 "Rate limited"(12 字节),
garth 把它交给 json.loads,于是限流被伪装成了解析错误。令牌本身好好的,
oauth1 有效期到 2027-08-23,账号也没问题。
- 新增 RateLimited 异常并识别 429(按状态码或响应体),同步状态里显示
「Garmin 暂时限制了请求频率…令牌本身没有失效」,不再是一句解析报错。
- 命中限流后整个进程对该账号退避 30 分钟。重试正是把限流撞得更深的原因,
原来失败后每小时还接着试。
- 只在 oauth2 令牌确实过期时才 refresh_oauth2()。原先每次 _connect 都无条件
刷新一次,白白消耗配额——正是这个把账号一步步推到了 429。
排查过程中我一度误判:先以为是数据库迁移把令牌 base64 包了一层,
改了解码逻辑反而把能用的令牌弄坏(garth.loads 本来就要 base64 形式),
已还原。真正的定位靠拦截 requests.Session 打印出原始响应。
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
|
2026-08-28 14:20:43 +08:00 |
|
ericwyuan
|
ac99c1342d
|
docs: 迁移收尾——部署信息同步到 Oracle 新机 129.146.26.249
- backend/.env.example: AI 网关地址 129.146.203.203 → 129.146.26.249;
MariaDB 注释段更新为生产实际(garmin 专用账号 @ 127.0.0.1:3306,socket 已不用)
- README: 生产数据层/数据库说明改为 Oracle 新机本地 MariaDB 10.3
- docs/REQUIREMENTS: 部署目标 NAS → Oracle 新机;公网方式 frp 隧道 → gunicorn 直绑 8123
- docs/ARCHITECTURE: 部署架构改为真实生产栈(Flask+gunicorn+MariaDB+SPA)
|
2026-08-25 23:59:47 +08:00 |
|
ericwyuan
|
e7c336b9df
|
fix(db): 数据库晚一步启动就会让服务永久挂掉
今天 11:33 站点整个不可用。不是代码的问题,是启动顺序:
NAS 上 MariaDB 在 11:34 才起来,而应用 11:33 就尝试连接,
init_db() 抛异常 → gunicorn 报 Worker failed to boot → master 退出。
一分钟后数据库好了,但已经没有进程在跑,没人会去重试——
站点就一直躺到有人手工重启为止。
init_db 改为在 DB_INIT_RETRY_SECONDS(默认 120 秒)内重试等待数据库,
超时仍然如实抛错,不会假装启动成功。NAS 重启时应用和数据库一起起来,
这个竞争是常态而不是意外。
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
|
2026-08-25 14:23:07 +08:00 |
|
ericwyuan
|
d2d6143ed6
|
fix(ui): 切换日期后数字不更新,卡片显示的是前一天的数据
useCountUp 只靠 requestAnimationFrame 推进,没有兜底。帧不来的时候
(后台标签页、被节流的 webview)setDisplay 一次都不会调用,组件继续
渲染上一个值——往前翻一天,标题的日期变了,但每一个数字还停在前一天。
不是动画没播,是把另一天的数据当成这一天理直气壮地显示出来。
实测(造的数据):08-24 → 08-23 → 08-22 三天,三张卡片全都显示 8,826,
而真实值是 8,826 / 10,232 / 10,715。
- 动画结束时间点加一个 setTimeout 兜底,保证最终值一定落地
- document.hidden 时直接跳过动画
- from 引用在收敛时同步更新,避免动画被打断后从错误的起点继续
动画是装饰,数字不是。
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
|
2026-08-24 09:33:22 +08:00 |
|
ericwyuan
|
564aaef6c1
|
fix(auth): 登录页不停刷新、无法输入
未登录时五个 Tab 视图会同时挂载各自的 Screen,每个都判定「没登录」,
于是每个都对**当前**视图发一次 navigate('/login/', {reloadAll: true})。
每次 reloadAll 又让其他几个重新挂载,再各发一次——登录表单被持续拆掉重建,
根本打不出字。关掉页面过渡动画后这个循环变紧,症状才明显起来。
判断登录与否是外壳的职责,不是每个页面各自抢着跳转:
- App 持有会话状态:没有会话就只渲染一个 /login/ 视图,连 Tab 栏都不出;
有会话才渲染五个 Tab。
- Screen 不再做任何跳转。
- setSession / clearSession 派发 ghl:auth 事件。storage 事件只在**其他**
标签页触发,同标签页的登录登出需要自己的信号。
- 登录成功后不再手动 navigate:外壳会换掉整个视图,从一个正要被卸载的
视图里发起路由是在和它抢。
实测:登出后登录页只有 1 个视图、无 Tab 栏,输入的内容 2.5 秒后仍在,
且输入框还是同一个 DOM 节点(没有重挂);恢复会话后自动切回 5 个 Tab。
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
|
2026-08-24 09:08:02 +08:00 |
|