fix(ux): surface hidden notices, bound report waits, add root boundaries
Independent Staging Quality Gate / validate (push) Successful in 12m8s
Independent Staging Quality Gate / publish (push) Successful in 14m29s

Framework-level UX fixes found while auditing staging (BUG-216..220).

- chat: route 44 previously discarded composer notices to sonner with
  dedupe, so recovery, cancel and archive feedback is actually visible
  (BUG-216)
- chat: anchor stream auto-scroll to bottom proximity and add a
  jump-to-latest control, so reading history is no longer interrupted
  on every token (BUG-218)
- reports: replace the silent 120s poll cutoff with an explicit
  timed-out state, an 8m budget, stepped backoff and an elapsed
  counter (BUG-217)
- reports: pause polling while the tab is hidden, via a shared hook
- app: add root error, global-error and not-found boundaries (BUG-219)
- admin: add antd SSR style extraction and the React 19 render adapter,
  and move admin-only css out of the global stylesheet (BUG-220)
- membership: run bootstrap fetches concurrently and pause payment
  polling while hidden
- build: configure optimizePackageImports

Verified on top of 2d370f2e: tsc, eslint, next build, and the related
frontend contract suites.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Jesse_Chen
2026-08-17 12:52:24 +08:00
parent 2d370f2e9d
commit 9c296f1e3f
23 changed files with 1144 additions and 88 deletions
+70
View File
@@ -3618,3 +3618,73 @@
- 防复发:被多处断言复用的测试辅助函数必须有自己的回归,不得以副本形式散落在各测试文件。以字符串匹配近似 CSS 语义时,必须按规则解析并覆盖同名选择器的全部声明;`indexOf` 式首个匹配不可用于可能重复出现的选择器。
- 相关记录:BUG-214
- 修复版本:本地未提交候选
## BUG-216 | 咨询中断、取消、归档等 44 条提示文案计算后从未显示
- 状态:resolved(本地修复,待提交与发布)
- 首次发现:2026-08-17
- 最近更新:2026-08-17
- 影响面:`/` 主对话页的断线恢复、取消回答、模型下线切换、会话重命名与删除失败等全部提示通路。
- 用户现象:网络中断、刷新后恢复、点击停止、删除会话失败时,界面只有一个转圈或静默无反应,用户无法知道回答仍在后台生成、是否已取消、失败原因是什么。
- 根因:`frontend/src/app/page.tsx` 将提示状态声明为 `const [, setComposerNotice] = useState("")`,解构时丢弃了状态值,JSX 中也没有任何渲染点;44 处 `setComposerNotice(...)` 调用的文案全部写入一个永不读取的 state。恢复轮询逻辑本身正确,缺的只是出口。
- 修复:新增 `frontend/src/lib/chat-notice.ts`,把提示按语义分派到既有 sonner `toast.success` / `toast.error` / `toast`;根布局早已挂载 `<Toaster />`,无需改动。以固定 `id` 复用同一条 toast,避免 1750ms 恢复轮询把同一句话堆成几十条;空字符串走 dismiss 而不弹空 toast。44 处调用点与全部中文文案逐字未改。
- 验证:新增 `frontend/tests/chat-notice-and-scroll-contract.test.ts` 锁定状态不再被丢弃、提示进入 toast、空串不弹窗、轮询防刷屏与分级语义;`tsc --noEmit``eslint` 清洁;与改动文件相关的 43 个测试文件共 386 条断言全绿。
- 防复发:禁止以 `const [, setX]` 形式声明用户可见文案状态;任何面向用户的提示必须有可断言的渲染出口,合同测试需覆盖“文案确实可达 UI”而不仅是“文案存在”。
- 相关记录:BUG-211
- 修复版本:本地未提交候选
## BUG-217 | 个人报告页轮询 120 秒后静默停止,界面仍显示“生成完成后页面会自动显示”
- 状态:resolved(本地修复,待提交与发布)
- 首次发现:2026-08-17
- 最近更新:2026-08-17
- 影响面:`/reports/[reportId]` 报告详情页的生成等待态。
- 用户现象:报告生成超过两分钟后,页面永远停在“报告正在生成中,请稍候…”的转圈上,即使报告已经在后台完成也不会刷新;页面同时声称“生成完成后页面会自动显示”,与实际行为矛盾。
- 根因:`personal-report-page.tsx``MAX_POLLS = 40` × `POLL_INTERVAL_MS = 3000` 限制轮询次数,达到上限后 effect 直接 `return` 停止轮询,但 `state.phase` 仍保持 `"generating"`,UI 继续渲染等待分支,没有任何超时态或失败态承接。
- 修复:在 `ReportLoadState` 中新增客户端专用的 `timed-out` 相位(`classifyReportEnvelope` 不产出该值,服务端语义未变)。轮询改为 8 分钟墙钟预算配阶梯退避(首分钟 3s,之后 6s/10s/15s,总请求数由 160 降到约 48)。预算耗尽后进入 `timed-out` 屏:说明生成仍在后台继续,提供“继续等待”重置时钟并立即重取,保留“返回报告中心”。等待中与超时后均显示“已等待 X 分 Y 秒”。
- 验证:新增 `frontend/tests/report-polling-contract.test.ts`,除文本合同外还对导出的 `pollIntervalForElapsed` / `formatWaitedDuration` 做真实单元断言(退避边界、单调性、请求数上限);既有 `personal-report-*` 测试全部通过。
- 防复发:任何有次数或时间上限的轮询,达到上限时必须切换到显式终态并给出用户可执行的下一步;等待文案承诺“自动刷新”时,必须由测试保证该承诺在整个等待窗口内成立。
- 相关记录:BUG-217 无前序同类记录
- 修复版本:本地未提交候选
## BUG-218 | 流式回答期间强制滚到底部,用户无法向上翻阅历史
- 状态:resolved(本地修复,待提交与发布)
- 首次发现:2026-08-17
- 最近更新:2026-08-17
- 影响面:`/` 主对话页的会话滚动容器。
- 用户现象:长回答生成过程中向上滚动查看此前内容,会被立即拽回底部,无法停留;也没有任何“回到最新”的入口。
- 根因:`page.tsx` 的自动滚动 effect 依赖数组包含 `activeStreamingText`,每个流式 token 都会触发一次无条件 `scrollTo(scrollHeight)`,未判断用户当前是否已在底部附近。
- 修复:新增 `frontend/src/hooks/use-conversation-scroll-anchor.ts`,以 rAF 合并的 passive 滚动监听维护锚定状态:用户向上越过约 96px 阈值即解除锚定,回到阈值内自动恢复;仅在锚定时执行自动滚动。切换会话与用户自己发送消息仍强制滚到底部。解除锚定时显示“跳到最新”按钮(真实 `<button>``aria-label`、可见焦点环、44×44 触控区),点击后滚到底并恢复锚定。沿用既有 `prefers-reduced-motion` 处理。
- 验证:同 BUG-216 的合同测试覆盖锚定守卫、有意跳转与无障碍跳转控件;`tsc``eslint` 清洁。浏览器内的视觉位置未经人工目视确认。
- 防复发:聊天类自动滚动必须做底部锚定判断,禁止把流式文本直接作为无条件滚动的依赖项。
- 相关记录:BUG-218 无前序同类记录
- 修复版本:本地未提交候选
## BUG-219 | 应用根级缺少错误与 404 边界,渲染崩溃时落到 Next.js 默认页
- 状态:resolved(本地修复,待提交与发布)
- 首次发现:2026-08-17
- 最近更新:2026-08-17
- 影响面:`/``/membership``/login``/admin/*` 等除 `/reports/[reportId]` 外的全部路由,以及所有未知 URL。
- 用户现象:主对话页等发生渲染异常时,用户看到的是 Next.js 默认错误界面,无中文说明、无重试入口、无返回路径;访问不存在的地址得到默认 404,与产品界面割裂。
- 根因:`src/app/` 下从未创建 `error.tsx``global-error.tsx``not-found.tsx`,全应用唯一的错误边界位于 `src/app/reports/[reportId]/`
- 修复:新增三个根级边界。`error.tsx` 为客户端边界,提供“重试”调用 `reset()` 与返回入口,并以克制方式展示 `error.digest` 供用户报障引用,不暴露堆栈。`global-error.tsx` 自带 `<html lang="zh-CN">``<body>`,零依赖并使用内联样式配 `var(--color-*, 字面回退)`,因为它替换根布局时 `globals.css` 不可达。`not-found.tsx` 为服务端组件,文案兼容 `login/page.tsx` 在未识别域名下主动 `notFound()` 的既有行为。
- 验证:新增 `frontend/tests/root-error-boundaries-contract.test.ts` 6 条断言,覆盖文件存在、客户端指令、`global-error` 自带文档骨架、`role="alert"`、标题层级与中文文案;`tsc``eslint` 清洁。未做浏览器目视验证。
- 防复发:新增顶层路由段时必须同步确认错误与未找到边界覆盖;`global-error` 不得依赖根布局引入的全局样式。
- 相关记录:BUG-219 无前序同类记录
- 修复版本:本地未提交候选
## BUG-220 | 管理端 antd 缺少 SSR 样式提取与 React 19 适配,首屏闪烁无样式内容
- 状态:resolved(本地修复,待提交与发布)
- 首次发现:2026-08-17
- 最近更新:2026-08-17
- 影响面:`/admin/**` 全部 18 个后台页面;以及所有用户端路由的首屏 CSS 体积。
- 用户现象:进入后台页面时先闪现一屏无样式内容再套上 antd 样式;同时聊天、登录、会员等用户端页面也要下载解析只有后台会用到的 antd 覆盖样式。
- 根因:其一,admin 是 `"use client"` 子树并使用 antd v5 CSS-in-JS,但项目从未接入 `AntdRegistry` / `@ant-design/cssinjs` 服务端样式提取,也未应用 antd v5 + React 19 的官方渲染适配,服务端输出 0 字节 antd 样式。其二,`globals.css` 由根布局引入,却在尾部包含 37 行 `.admin-app-shell .ant-*` 后台专用规则。
- 修复:新增 `frontend/src/components/admin/admin-antd-registry.tsx`,在 admin 子树内完成按请求的样式提取(`createCache` + `extractStyle` + `useServerInsertedHTML`,输出 `data-rc-order="prepend"` 保证服务端样式先于客户端注入),并用 antd 5.29.3 公开导出的 `unstableSetRender` 完成 React 19 适配,不新增 registry / patch 包。仅显式声明已随 antd 安装的 `@ant-design/cssinjs@^1.24.0`(版本未变、锁文件仅增 1 行)。后台 37 行样式移入 `frontend/src/app/admin/admin.css` 由 admin 布局引入;移除与 antd reset 重复的 refine reset。另在 `next.config.ts` 增加 `experimental.optimizePackageImports`,既有 `outputFileTracingIncludes` 等设置逐项保留。
- 验证:`npx next build` 退出码 0;构建产物显示用户端 CSS chunk 184,968 字节且不含任何后台规则,后台规则独立成 5,358 字节 chunk 且仅被 `.next/server/app/admin/` 下 18 个 client-reference-manifest 引用。以 `react-dom/server``ServerInsertedHTMLContext` 实测 registry 服务端输出 90,132 字节含主题色 `#85432f``<style id="antd-cssinjs">`(修复前为 0)。`npm ci --dry-run` 报告锁文件同步。standalone 产物仍包含 Python skill 资产。未能以真实管理员会话做端到端 HTTP 验证(需 Postgres 与登录态)。
- 防复发:引入 CSS-in-JS UI 库时必须同时接入 SSR 样式提取;路由段专用样式不得写入根布局引入的全局样式表。
- 相关记录:BUG-213
- 修复版本:本地未提交候选