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
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
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
ericwyuan
6ad87115ab
fix: 返回键失效的根因 + 同步页重做 + 连接池会永久阻塞线程
...
返回键
- 根因是 Framework7 的页面过渡由动画事件驱动:push 时把 allowPageChange
置 false,等动画报告结束再恢复。这个报告不来,路由就永久卡住,之后每次
导航都被静默丢弃,back() 还会把上一页重建一份而不是弹出。
实测对照:navigate({animate:false}) 前后状态完全正确,带动画则必卡。
因此关掉页面过渡动画——导航同步完成,处处正确。动效改由内容承担
(卡片入场、hero 揭示、顶部进度条),这个取舍里正确性优先。
- Screen 的返回改为显式 handler,先清掉残留过渡状态再 back(),
不依赖路由自己的闸门。注意只清视图上的 router-transition 类:
页面自身的 page-previous 是 F7 判断「回到哪一页」的依据,
一并清掉会导致重建出一个重复的页面(中途踩过这个坑)。
同步页
- .btn 系列样式原本只定义在 pages/Pages.css,而那个文件只被一个没有路由的
遗留页面引用,所以真实页面上按钮全都退化成 Framework7 的默认样式——
就是你看到的三条链接。样式移进每个界面都会加载的 Screen.css。
- 主次分明:一个填充主按钮 + 两个带副标题的次按钮;补上「同步会取哪些数据」
说明,页面不再是一大片空白。
- 进度条显示当前阶段(每日数据 2026-08-01 / 运动详情 12/174 / 身体成分…),
原来只有「0 / 730 天」,几分钟里完全看不出在做什么。
后端
- MariaDB 连接池:_mariadb_release 用的是阻塞 put(),而队列 maxsize=10,
_mariadb_acquire 在池空时又会新建连接。并发超过 10 之后,归还的线程会
永久停在 put() 上,请求就此挂死。改为 put_nowait,多出来的连接直接关闭。
- 进程重启会带走同步线程却留下 status=syncing 的行,界面上是一个永远不动
的进度条,还拒绝开始新同步。启动时清理。
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com >
2026-08-24 08:41:48 +08:00
ericwyuan
67a7c4b87e
fix(ui): 自动同步开关不可点、返回键热区过小、路由卡死
...
开关
- 我的 .toggle 类名和 Framework7 自带的 .toggle 组件撞了,元素被压成 0×0;
另外 F7 全局给 input[type=checkbox] 加了 display:none,胜过我的 opacity:0,
所以那个开关根本点不到。改名 set-switch 并显式恢复 input 的 display。
- 设置项改为乐观更新:先动控件,再发请求,失败回滚并报错。
经隧道一个来回约半秒,开关在那半秒里纹丝不动会被当成坏了,
用户再按一次,两个写入就打架了。
返回键
- 去掉导航栏毛玻璃背景时也把尺寸一起去掉了,链接缩成 44×16,
手机上很难点中,按下也没有反馈。恢复 44px 高的热区并加按下态。
路由卡死
- F7 启动时给根节点加 framework7-initializing 来抑制动画,靠下一帧移除。
这一帧在后台标签页或被节流的 webview 里不会到来,于是该类一直挂着,
它的 transition-duration: 0ms !important 让过渡永远不发 transitionend,
路由等的就是这个事件——allowPageChange 置 false 后再也没恢复,
之后每一次导航(包括每一次返回)都被静默丢弃,只能刷新页面。
- 启动后主动清掉该类,并加一个看门狗:过渡类存活超过 1.6 秒即视为卡住,
清理残留状态并放行路由。
进度条改用 portal 挂到 document.body。它原本是 F7 根节点的第一个子元素,
就这一个多余的节点会干扰 F7 的初始化。
部署脚本改为原子替换 static:原先 rm -rf 后再解包,中间有一段时间
文件是缺的,正好撞上就是 502。
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com >
2026-08-24 07:32:35 +08:00
ericwyuan
8430ff335e
feat(ui): 今日页日期切换 + 卡片可点 + 路由切换进度条
...
需求 3.7 / 4.5 / 4.6 / 4.7
今日页
- 左右箭头切换日期,中间点开日历选任意一天,范围限定在已有数据内
- 没有记录的日期直接跳过,不会停在一个空白页上
- 一次读一年:往回翻一天不该产生一次请求,卡片的迷你曲线本来也需要前后几天
- 曲线窗口跟着所看的那天结束,卡片上的走势总是通向它上面那个数字
- 卡片改为从 lib/metrics.ts 渲染,点击进入指标详情
路由切换进度条
- 切换本身是瞬时的,用户等的是新页面的第一次请求,没有指示就像点了没反应
- 跑到 90% 停住等页面就位,不谎报完成
- 有最短显示时长,快速跳转时不会闪一下
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com >
2026-08-24 00:49:38 +08:00
ericwyuan
fd9b610810
[阶段10.1] Tab 改为 今日/健康/趋势/运动/设置
...
按指定结构调整导航:每日归入趋势、睡眠归入健康、同步归入设置,
它们是各自 Tab 的详情视图而非独立目的地。新增「运动」Tab。
运动页(新):
- 近 30 天运动时长/距离/消耗,以及今日强度分钟
- 项目分布:按累计时长排序,横条表示占比。只用一个色相 —— 每个项目
一种颜色会让人以为分类另有含义
- 强度分钟趋势图,附 WHO 每周 150 分钟的参考
- 记录 / 个人纪录 / 奖励三个分页,运动记录 174 条
fix(nav): 五个 Tab 全部加载了同一个页面
- browserHistory 开在每个 View 上,导致它们都去读浏览器地址栏(当时是
"/")而不是各自的 url,五个 Tab 渲染出五份「今日」
- 浏览器历史只绑定主 View,其余 Tab 用自己的 url 独立加载
fix(nav): 导航栏右侧对所有页面塞同一组图标
- 睡眠/同步/设置三个图标出现在每个页面上,其中两个在多数页面无意义。
改为按页面声明:趋势 → 每日,健康 → 睡眠,设置 → 同步
fix(viz): 图表标题在窄栏里竖排
- 标题与「看数据」按钮争抢宽度,短标题被压成竖排字符。
标题允许收缩省略,按钮不收缩
fix: 未映射的运动类型直接暴露 snake_case
- 补充登山/划船/椭圆机等映射;仍未覆盖的转为空格分词并首字母大写,
而不是把 Garmin 的原始 key 摆给用户看
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com >
2026-08-23 23:52:18 +08:00
ericwyuan
7ab150537d
[阶段10] 前端以 Framework7 重建,采用 iOS 原生形态
...
按要求废弃手写外壳,改用 Framework7 React(theme=ios)。参照 PeakWatch
的信息架构与卡片语言。
保留(这些是资产,不该重来):
- 数据层 services/api.ts、聚合 lib/aggregate.ts、参考区间 lib/ranges.ts
- 图表组件 Chart / Ring / Sparkline / BandBar / MetricCard / MetricStrip
- 经校验的配色令牌(色盲安全 + 对比度,浅深两档)
替换:
- 路由与外壳交给 F7:五个 Tab 各自独立导航栈,推入详情页不影响其他 Tab
- 页面转场、橡皮筋滚动、大标题折叠、半透明栏 —— 这些正是换框架的理由,
手写做不像
- 底部标签栏改用 F7 Toolbar,触控目标与安全区由框架处理
配色接入:
- 新增 f7theme.css 把我们的令牌映射到 F7 的 CSS 变量,让它的导航栏/
列表/面板与我们的图表同属一套设计,而不是两种视觉打架
- F7 的深色靠 .dark 类,我们的靠 data-theme,两者在 App 里同步切换
fix: 图标显示为原始名称(squ/hea/cale…)
- iconIos/iconMd 引用的是 framework7-icons 字体,没装就只会渲染出名字
其他:
- tsconfig moduleResolution 改为 bundler —— F7 用 exports 映射,
node 解析方式找不到它的类型
- 登录页不套 Tab 外壳,未登录时不该出现导航
桌面与手机都要好看:内容在宽屏收进 1100px 居中列并加密卡片列数,
窄屏走底部标签栏;两档都已实机核对。
bundle 190KB -> 399KB,是换取原生手感的代价。
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com >
2026-08-23 23:38:21 +08:00
ericwyuan
bfda1cd017
[阶段8] 新增每日数据模块;趋势改为全指标并列 + 周期聚合
...
每日数据(新页面 /daily):
- 按活动/能量/心率/压力/睡眠/血氧呼吸/训练七组,列出全部 40 项指标
- 日期选择器 + 前后一天翻页;"只显示有数据的指标"开关
- 附当天的运动记录明细
- 标题处显示当天记录到多少项,缺数据一目了然
趋势(重写):
- 15 组指标全部并列展示,不再一次只能看一个
- 标签可逐个隐藏/显示,选择存入 localStorage(每次刷新都重置的
选择算不上偏好);提供全选/全不选
- 两级筛选:范围(一月/一季/半年/一年/两年)× 周期(每天/每 7 天/
每月/每季度)。周期选项按范围过滤,避免出现"近一月按季度聚合"
- 所有图表共用同一份聚合结果,一行筛选器统摄全部图表
聚合口径(lib/aggregate.ts):
- 无论累计型还是速率型指标,一律折算为"周期内日均",这样 30 天的
月份和 31 天的月份不会仅因日历差 3%
- 周按最新一天往回切,而不是按自然周一 —— 否则开头会出现一个半空
的桶,看起来像低谷,其实只是窗口起点
- 累计型指标的统计标签写作"日均"而非"平均",读者不必猜口径
fix(viz): 聚合后柱形/面积图掩盖了变化
- 柱形与面积都以延展量编码大小,必须从 0 起;而一年的月均步数都在
9,590~12,701 之间,画出来几乎一样高,恰恰看不见要看的变化
- 聚合视图改用折线:折线编码位置而非延展量,非零轴是正当的。
改后 y 轴自动落在 9350~12750,走势清晰可读
fix(health): 运动记录按日期筛选时 500
- get_activities 复用了按 date 列过滤的子句,但 activities 表只有
start_time,报 "Unknown column 'date'"。此前唯一的调用方不传
日期,所以一直没暴露,每日数据页一传就炸
- 上界改用次日零点的开区间:SQLite 按字符串比较,而存储的分隔符
可能是 'T'(0x54) 也可能是空格(0x20),写成 "end 23:59:59" 会让
当天 18:30 的记录排在上界之后而被排除在自己那天之外
tests (+6, 共 305): 运动记录的单日范围、上下界闭合、仅起点/仅终点、
不传范围返回全部、端点级验证
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com >
2026-08-23 22:33:59 +08:00
ericwyuan
9e5e77755e
[阶段7.2] 隐藏 AI 模块;剩余页面迁移到设计令牌
...
AI 模块暂时下线(待重新设计),加 FEATURES 开关集中控制:
- 导航不显示"建议",路由也不注册 —— 只隐藏入口而保留可访问的
URL 等于没隐藏
- 设置页的模型清单区块一并隐藏,且不再请求 /analysis/models
- 页面与后端端点都保留,重新打开是一行改动
登录/同步/建议/设置四个页面此前仍在用旧样式(共 101 处硬编码颜色),
深色模式下全部失效。已改写为设计令牌。
fix(a11y): 主按钮白字对比度不足
- 白字在 --accent 上实测 4.42:1(浅)/ 3.64:1(深),均低于正文
所需的 4.5:1
- 新增 --accent-solid 专用于填充面(按钮、选中标签),取同一蓝色阶
更深的一步:5.39:1 / 6.63:1;--accent 继续用于文字、边框和标记
fix(viz): 一年档柱形图不可读
- 365 根柱子在 607px 宽度下每根仅 1.66px,既看不清也远低于
~24px 的悬停命中下限
- 超过 90 个点时柱形自动改为面积图,并在副标题里说明原因。
折线/面积的十字准星吸附最近点,本就不依赖逐标记命中区
其他:
- 统计数值精度按量级取(11,013.99 既是虚假精度,又长到把单位挤到
第二行)
- 设置页隐私说明更正:改用 OAuth 令牌后同步已不需要每次输密码
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com >
2026-08-23 22:05:28 +08:00
ericwyuan
ad88ec7e41
[阶段7] 前端重做:31 项指标全部露出,配色经色盲校验
...
原来仪表板只有 4 张卡片和 4 张图,31 项指标里绝大多数没有出口。
信息架构重组为 7 个页面(原 5 个):
- 今日 按活动 / 心率压力 / 睡眠呼吸三组展示 16 项指标
- 趋势 15 组指标可切换,5 档时间窗口(最长一年)
- 睡眠 新增。分期堆叠图 + 夜间血氧/呼吸/压力
- 成就 新增。奖励徽章按年份分组、个人纪录、运动记录
- 建议 / 同步 / 设置 保持
配色(依据 dataviz 规范,用校验器实测而非目测):
- 采用验证过的分类色板并按既定 slot 顺序取色 —— 顺序本身就是
色盲安全机制,不是审美选择,因此只取不循环
- 浅色 worst adjacent CVD ΔE 9.1 / 常视觉 22.9
深色 worst adjacent CVD ΔE 8.4 / 常视觉 19.8,两档全部通过
- 浅色表面下 aqua 2.74:1、yellow 2.11:1 低于 3:1,按规范提供
"relief":每张图都带表格视图,且图例始终与文字标签同现
- 深色不是自动反色,是同色相针对深色表面重新取阶并单独校验
- 状态色(good/warning/serious/critical)保留专用,绝不当作
第 N 个系列色;且始终图标 + 文字同现,不靠颜色单独表意
图表规范:
- 单一 y 轴,绝不双轴;量纲不同的指标拆成不同图
- 2px 线宽、4px 圆角柱端锚定基线、堆叠段间 2px 表面色间隙、
悬停标记 2px 表面色描边
- 两系列以上必有图例,单系列不加(标题已经点明)
- 折线 connectNulls,设备漏记的日子不把线打断
- 数值文字一律用文字色令牌,不染系列色
其他:
- 主题切换(自动/浅色/深色),选择写入 localStorage 并标在 <html>
- 导航改为顶部横向,移动端可横滑
- 表格、徽章、空状态等组件统一到设计令牌
验证方式: DOM 审计确认 2px 线宽、圆角为 A 4,4 弧、堆叠段
2px 间隙、图例数量与系列数匹配、两档主题令牌各自解析到校验过的色值。
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com >
2026-08-23 21:17:08 +08:00
ericwyuan
8616a13525
[阶段4.2] 前端对接 Flask API + 建议/分析/设置页面,移除废弃的 Node 后端
...
选型确认为 Python Flask 后,删除 server/ 整套 Node 实现,
根 package.json 改为只管理 client workspace,
npm run dev 同时拉起 Flask 与 React。
fix: 前端读取响应的方式与后端不符
- Flask 返回裸数组/对象,而前端读的是 response.data.data
(Node 那套 {success,data} 包装),登录后拿到 undefined 直接崩
- api.ts 重写为返回 response.data,并补齐全部端点的 TypeScript 类型
- 401 拦截器改为清除会话并跳转 /login,而不是留在空白页
fix: 同步页缺少 Garmin 密码输入
- 后端只存密码哈希、无法还原,/garmin/sync 要求请求体带明文密码,
原页面没有该输入框,点同步必然 400
- DataSync 增加密码字段,请求结束后立即清空,并说明为何每次都要输入
新增页面:
- Recommendations: 模型下拉切换(未配置密钥的模型置灰),
展示本次由哪个模型作答、是否发生了降级、分析了多少天数据;
按优先级配色,底部附免责声明
- Analysis: 6 个指标 × 4 个时间窗口,展示均值/中位数/极值/
前后半段差值,并绘制趋势图
- Settings: 模型清单与配置状态、数据隐私说明、退出登录
Dashboard:
- 修正响应结构,数据扁平化后再传给图表
(原先给 dataKey 传了函数,与 Chart 的 string 类型不符)
- 无数据时引导用户去同步,而不是显示一堆空图表
Chart:
- 按指标而非按行判断是否为空,某天缺某项指标不再让整张图判空
- 折线图 connectNulls,避免设备漏记的日子把线断成碎片
验证: 前端 tsc --noEmit 通过;后端 161 passed / 1 skipped;
实机启动 Flask 后 register/login/models/ai-recommendations 均正常,
未配置密钥时正确降级到规则引擎。
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com >
2026-08-23 12:43:52 +08:00
ericwyuan
3b2d0697f0
[阶段1.1-1.7] 实现完整的认证系统
...
后端实现:
- 创建 AuthService 包含密码加密、JWT 生成和验证
- 创建 authMiddleware 用于 API 路由保护
- 实现 auth 路由 (register, login, logout, /me)
前端实现:
- 创建 Login 页面 (登录/注册标签页)
- 创建 ProtectedRoute 组件用于路由保护
- 更新 App.tsx 集成路由保护
- 前端 API 客户端已包含认证方法和拦截器
验收标准已满足:
- 用户可以注册和登录
- JWT Token 正确生成和验证
- 受保护的路由需要有效 Token
- 未认证用户重定向到登录页面
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com >
2026-08-23 12:25:33 +08:00
ericwyuan
d73405decb
Initial commit: Set up Garmin Health Lab project structure
...
- Initialize monorepo with root workspace configuration
- Set up Express.js backend with TypeScript
- Set up React 18 frontend with TypeScript
- Create database schema with SQLite
- Implement project architecture and documentation
- Add development and deployment guidelines
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com >
2026-08-23 11:11:29 +08:00