Files
Jyotisha/docs/tasks/PROGRESS-cend-report-20260916.md
T
Jesse_ChenandClaude Opus 5 a1a78c7ebc
Independent Staging Quality Gate / validate (push) Canceled after 3m14s
Independent Staging Quality Gate / publish (push) Canceled after 0s
feat(ui): 报告中心改行式列表,阅读页并入外壳并把目录挪到右侧常驻
报告中心原来是卡片方阵,状态只靠三块底色区分;报告一多,扫读成本
按卡片数线性涨。现在一列一行:带色点的状态 chip、标题、创建时间 ·
深度 · 主题,操作靠右。失败原因从右边一小块挪进行内,能完整读到。

chip 里原来的 StatusIcon(generating 转圈、ready 打勾、failed 警告)
换成 5px 色点——原型如此,且列表是轮询不是演出。失败行不加「重新
生成」,顶栏已经有唯一的生成入口。

行内小字只写接口真给的东西。GET /api/reports 不返回节数和盘数,所以
不写「9 节 · 22 张盘」(VOICE.md 第 2 条),原型图上那行是 mock。

/reports/[reportId] 是最后一个脱离外壳的全屏路由,九个 phase 全部并进
SecondaryShell,根节点从 <main> 改成 <div>(外壳自己就是 main)。

任务书 E10「没有目录」已过期:目录在 cfcd369d 就存在。本轮把它从左栏
挪到正文右侧并定稿视觉,位置用 grid-column 显式指定而不是靠 DOM 次序,
这样窄屏抽屉仍能排在源码最前面,不会掉到全文末尾。860px 以下常驻栏
消失、折叠抽屉保留——删掉抽屉等于窄屏彻底失去章节定位。

挂外壳带出一个真实风险:window.print() 打的是整篇,而 .chat-app /
.chat-panel 是 height:100%;overflow:hidden,会把九节报告裁成一页。阅读
页因此多挂一条 media="print" 样式,把外壳既隐藏又解锁。放组件里而不是
globals.css:它只在阅读页挂载期存在,对话页的打印不受影响,也不用
:has() 去够祖先,更不越界到并行轮次的 CSS 区段。

纸面三个 token 一个没动,--report-accent 仍是 #85432f;目录栏用应用
调色板,因为它是 chrome 不是纸。report-chart-grid-rehype.ts 一行未动,
新增断言守住它仍然接线(BUG-616/617)。

tsc 0 错;lint 0 error / 118 warning(未增);3350→3355 条,0 条既有
断言被改,31 条无 Docker 失败与基线逐条一致;四个路由渲染标记不变,
/ 首屏 gzip −0.58%。

「20 张盘以上不重叠、滚动不卡」与「挂外壳后的打印真实输出」无真机做
不了,已写成环境缺口,清单在 docs/testing/cend-report-20260916.md。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0193vBv6w5MV2cifdTUu9H5P
2026-09-16 06:41:39 +00:00

131 lines
16 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# PROGRESS · R6 报告中心 + 报告阅读(2026-09-16
任务书:`docs/tasks/TASK-cend-surfaces-claude-alignment-20260916.md`**R6**T6.1 / T6.2 + 顺带把 `/reports/[reportId]` 挂进 `SecondaryShell`)。
- 基线 commit`origin/staging` = `50ce02c8`R4 外壳已合入)
- 分支:`codex/cend-report-20260916`
- 执行方:Claude subagent(直接执行模式)
- **未 push**,按任务要求只提交到本地分支
- BUG 编号:**未占号**。本轮是产品改造,实现中没有发现新的真实缺陷;`docs/BUG_HISTORY.md` 当前最大号 BUG-738 不变
---
## 一、开工基线数字(均在本 worktree 实测)
| 项 | 基线值 |
|---|---|
| `./node_modules/.bin/tsc --noEmit` | 0 错 |
| `npm run lint` | **0 error**118 warning |
| `npm test` | tests **3350** / pass 3304 / **fail 31** / skipped 15 |
| `npm run build` 渲染标记 | `/` **○ Static**、`/chart` ○、`/ephemeris` ○、`/reports` ƒ、`/reports/[reportId]` ƒ |
| `/` 首屏 gzip`.next/server/app/index.html` | 4,109 B(复测 4,110 B |
| globals CSS chunk gzip | 36,334 Braw 242,608 B |
31 条失败全部是**无 Docker** 的既有失败(数据库套件 / 部署套件),与任务书「环境缺口」一致。基线清单存为 `base.txt``npm test``^not ok` 行去掉序号后排序)。
---
## 二、逐条验收对照
### T6.1 · 报告中心改行式列表(D12)
| 验收标准 | 结论 | 证据 |
|---|---|---|
| `.report-center-list` 从卡片网格改成单列行式列表 | **通过** | `.report-center-list` 改为单列 `display: grid` + `gap: 1px`,靠 `--color-border` 底色透出一条发丝线(不是两条相邻边框);`.report-center-card*` 全部改名为 `.report-center-row*``git grep report-center-card``src/` `tests/``globals.css` 均 0 命中 |
| 每行:状态 chip(带色点)+ 标题 + 元信息 + 右侧操作 | **通过** | `.report-center-status::before` 加 5px `currentColor` 圆点;`.report-center-row``minmax(0, 1fr) auto` 两列;元信息在 `.report-center-row-meta` |
| 四种状态都有对应行样式 | **三种通过,第四种是接口缺口**(见 §四) | `is-ready` / `is-generating` / `is-failed` 三个 chip 变体保留并各有行内表现;「生成中的分节进度」**没有服务端投影**:`GET /api/reports``frontend/src/app/api/reports/route.ts``REPORT_LIST_COLUMNS`)不返回节数,只在 `status === "failed"` 时才去查 `personal_report_sections`。按 `VOICE.md` 第 2 条「数字只来自服务端投影,不口算、不编」,前端不得自造「第 4 节,共 9 节」。原型图上那行是 mock 文案 |
| `role="status"``generating` 行上保留 | **通过** | `role={report.status === "generating" ? "status" : undefined}` 原样保留在状态 chip 上 |
| 失败行仍显示 `failureSummary ?? failureCode` | **通过** | 从右栏移到行内正文(meta 行下方),表达式一字未改:`report.failureSummary ?? report.failureCode ?? "生成失败"` |
| 导出错误 `report-center-export-error``role="alert"` 保留 | **通过** | 原样 |
| 空态与读取失败态不变 | **通过** | `report-center-empty` / `report-center-alert` / 未登录 `report-center-message` 三个分支的 JSX 与文案一字未动 |
补充设计决定(写进 `frontend/DESIGN.md`):
- 状态 chip 里原来的 `StatusIcon`generating 用 `InlineSpinner`、ready 用 `Check`、failed 用 `TriangleAlert`)**删除**,改为 CSS 色点。理由有二:原型图就是一枚色点;列表是轮询不是演出,chip 里再转一个圈与「揭幕后不得出现 spinner」的口径不合。`InlineSpinner` 仍用于首次读取态与导出按钮内(两者都是真实在等)。
- 失败行右侧**不加**「重新生成」按钮。顶栏已经有唯一的生成入口,再加一个就是第二个入口(用户偏好:多余入口宁可删除也不修)。
- ready 行的按钮次序改为「导出报告(.md)」在前、「查看报告」在后,让主操作(outline)落在行尾靠右——这是行式列表里视线终点的位置。测试 `ready reports expose Markdown export only in the ready branch` 按分支切片取文本,不依赖次序,未受影响。
### T6.2 · 报告阅读页常驻目录(D11+ 挂进 `SecondaryShell`
**先说一条与任务书事故实证不一致的事实:E10 写的「整篇直下,没有目录、没有章节定位」在 `50ce02c8` 上已经不成立。** 目录在 `cfcd369d`(长文 Markdown 那一轮)就已经存在:`personal-report-markdown-view.tsx``ReportToc``buildLongformOutline``##`/`###` 标题生成,用 `IntersectionObserver` 高亮当前节,`@media print` 下已隐藏。任务书是按更早的状态写的。所以本轮 T6.2 的实际工作是**把它做成原型图那条右侧常驻栏,并把阅读页并进外壳**,不是从零加目录。
| 验收标准 | 结论 | 证据 |
|---|---|---|
| 在 `.personal-report-reader` 右侧加一条常驻目录 | **通过(改为右侧 + 视觉定稿)** | `.personal-report-md-layout``minmax(11rem,15rem) minmax(0,1fr)`(目录在左)改为 `minmax(0,1fr) minmax(11rem,15rem)`;位置用 `grid-column: 1 / 2` 显式指定而不是靠 DOM 次序,这样窄屏抽屉仍然能排在源码最前面(否则抽屉会掉到全文末尾)。目录去掉外框,改成原型图的安静竖栏:当前项 2px `--color-action` 左条 + `--color-canvas-muted` 底 |
| 从 Markdown 的 `##` 标题生成 | **通过(且保留 `###` 子项)** | `buildLongformOutline` 原有实现,未改;`###` 子项是既有能力,本轮不删弱 |
| 滚动时高亮当前节 | **通过** | `ReportToc``IntersectionObserver``rootMargin: "-20% 0px -70% 0px"`)原样,`aria-current="location"` + `.is-current` |
| 目录项点击滚动到对应节 | **通过** | `href="#{id}"` + `.personal-report-md-article h2, h3 { scroll-margin-top: 80px }`(既有) |
| 窄屏(< 860px)隐藏目录 | **通过** | `@media (max-width: 860px)``.personal-report-toc-desktop { display: none }`**常驻栏消失**。同一断点下 `.personal-report-toc-drawer`(折叠的「目录」`<details>`)仍然显示——那是既有的、默认收起的另一种形态,不是常驻栏;删掉它等于窄屏彻底失去章节定位,属于功能倒退,所以保留并给它补回栏消失后没人画的那个盒子(边框 + 圆角 + `--color-canvas` |
| `@page` 规则保留 | **通过** | `<style media="print">{"@page { size: A4; margin: 13mm 12mm 14mm; }"}</style>` 一字未改,既有断言仍然精确匹配 |
| `@media print` 下目录隐藏 | **通过** | `globals.css``.personal-report-toc { display: none !important; }` 原样(两处,均为既有) |
| 纸面 token 一个不改 | **通过** | `--report-paper` / `--report-rule` / `--report-accent` 三处定义(:root、两处 dark、print 覆盖)全部未动;`--report-accent` 仍是 `#85432f`。目录栏用的是 `--color-action` / `--color-canvas-muted` / `--color-ink*`——它是应用 chrome,不是纸面,新增断言 `assert.doesNotMatch(toc + current, /--report-(?:paper\|rule\|accent)/)` 守住这条 |
| 不触碰 `report-chart-grid-rehype.ts` | **通过** | `git diff --stat` 里没有这个文件;新增断言 `assert.match(viewSource, /rehypePlugins=\{\[reportChartGrid\]\}/)` 守住它仍然接线 |
| `/reports/[reportId]` 挂进 `SecondaryShell` | **通过** | 九个 phaseloading/generating、timed-out、unauthorized、not-found、failed、invalid\|network-error、legacy-unavailable、兜底、markdown-ready**全部**包进 `<SecondaryShell title="个人报告">`,根节点从 `<main>` 改成 `<div>`(外壳自己就是 `<main className="chat-app">`,不能再套一层 main)。新增断言:`doesNotMatch(/<main className="personal-report-(?:state\|reader)"/)`,且 `SecondaryShell` 出现次数 == phase 根节点次数 |
**滚动所有权。** `.personal-report-reader` 仍然是自己的滚动容器(`height: 100%; overflow-y: auto`),现在坐在 `.chat-panel``minmax(0, 1fr)` 行里,同时被 `.secondary-panel > :not(.chat-header)` 覆盖到——两条规则给的是同一件事,既有断言 `report centre and ready reader scroll inside the chat shell lock` 无需改动即通过。目录栏的 `top: 72px` 相对的是这个滚动容器(避开报告自己那条 54px 的 sticky 操作条),`max-height``calc(100vh - 96px)` 调到 `calc(100vh - 150px)`,因为外壳那 46px 顶栏在滚动容器之外。`max-height` 不在 `viewport-unit-fallback-contract` 的管辖范围(该测试只管 `height`),也没有引入 `dvh`,红线 8 不受影响。
**打印行为(挂外壳带来的真实风险,已处理)。** `printPersonalReport()` 走的是 `window.print()`,打的是整个文档。挂进外壳后,侧栏和 46px 顶栏会跟着上纸;更要命的是 `.chat-app` / `.chat-panel``height: 100%; overflow: hidden`,会把九节报告**裁成一页**。处理方式:阅读页多挂一条 `<style media="print">{REPORT_SHELL_PRINT_CSS}</style>`,把 `.chat-app` / `.chat-panel` / `[data-slot='sidebar-inset']` 解成 `display: block; height: auto; overflow: visible`,并隐藏 `[data-slot='sidebar']` / `sidebar-rail` / `sidebar-scrim` / `.chat-header`
放在组件里而不是 `globals.css`,有三个理由:(1) 这条样式只在阅读页挂载期间存在,对话页自己的打印行为一点不受影响;(2) 它要选的是 `.chat-app` 这类**祖先**,在 `globals.css` 里没法用 `.personal-report-*` 前缀限定,只能靠 `:has()`,那是更脆的写法;(3) 任务约束要求本轮只动 `.report-center-*` / `.personal-report-*` 两段 CSS,不得越界到 R5/R7/R8 的地盘——放组件里就完全不越界。新增断言覆盖了这条样式的存在与内容。
**没有做、也不打算顺手做的:** `ReportActions`(返回报告中心 / 打印 / 导出)保持原样留在阅读页正文顶部的 sticky 条里,没有提到外壳顶栏。任务书 T6.2 没有要求,原型图顶栏里也确实有一个返回入口(列表 → 详情的返回是合理的上下文导航,和 R4 删掉的「返回对话」不是一回事)。提上去会连带动 `.personal-report-actions` / `.personal-report-back` 的既有 CSS 与 `touch-target` / `theme-contract` 两条断言,超出本轮范围。建议留作后续单。
---
## 三、改了哪些既有断言
**一条都没有改。** 基线 3350 条全部保持原样,31 条无 Docker 失败逐条一致。
新增 5 条测试(不是修改):
| 文件 | 测试名 | 守什么 |
|---|---|---|
| `frontend/tests/personal-report-entry.test.ts` | `report centre is a single-column row list with a dotted status chip` | `.report-center-card` 不得复活;列表是单列;行是两列;chip 有色点;三个状态变体都在 |
| `frontend/tests/personal-report-entry.test.ts` | `every report state keeps the affordance its row carried before` | `role="status"` / 失败原因 / `role="alert"` / 空态 / 读取失败态;行元信息不得出现「张盘」「节,共」这类前端口算 |
| `frontend/tests/personal-report-view.test.ts` | `the reader renders inside the app shell, in every phase` | 每个 phase 恰好包一层外壳;不得有裸 `<main>` |
| `frontend/tests/personal-report-view.test.ts` | `printing a report inside the shell drops the chrome and its height lock` | `@page` 不变;外壳打印样式既隐藏又解锁;目录不上纸 |
| `frontend/tests/personal-report-view.test.ts` | `the report TOC is a persistent right-hand rail that leaves the paper palette alone` | 目录在右(`grid-column: 2`)、sticky`--report-*` 三个 token 不进 chrome`--report-accent: #85432f` 仍在;`reportChartGrid` 仍接线(BUG-616/617 |
测试总数 3350 → **3355**
---
## 四、收工数字
| 项 | 基线 | 收工 | 判定 |
|---|---|---|---|
| `tsc --noEmit` | 0 错 | **0 错** | 通过 |
| `npm run lint` | 0 error / 118 warning | **0 error / 118 warning** | 通过(warning 未增,也未顺手修范围外的) |
| `npm test` | 3350 / 3304 / 31 / 15 | **3355 / 3309 / 31 / 15** | 通过;`diff base.txt after.txt` 输出为空(逐条一致) |
| `/` 渲染标记 | ○ Static | **○ Static** | 通过 |
| `/chart` / `/ephemeris` | ○ / ○ | **○ / ○** | 通过(未触碰) |
| `/reports` / `/reports/[reportId]` | ƒ / ƒ | **ƒ / ƒ** | 通过(`force-dynamic` 维持) |
| `/` 首屏 gzip | 4,109 B | **4,085 B** | 0.58%,在 ±2% 内 |
| globals CSS chunk gzip | 36,334 B | **36,447 B** | +113 B(+0.31%)。来源:行式列表与目录栏的新规则,减去删掉的卡片规则 |
> 量 CSS chunk 时踩过一个坑,记下来:`.next/static/chunks/` 里会留上一轮构建的 css 文件,对着通配符求和会把两轮的都算进去,得到两次「完全一样」的假数字。chunk 名是内容哈希(基线 `1vtco9m60b6ps.css` / 收工 `24xydqkp8fxdk.css`),要按名字对,不要按目录求和。
---
## 五、是否触发让步顺序
**没有触发。** 任务书让步顺序第 4 条是「若从 Markdown 生成标题锚点与现有 rehype 链冲突 → 退到只做『回到顶部』」。实际情况是锚点与 rehype 链**本来就共存**`buildLongformOutline` 只按行扫 `^#{2,3}` 生成 id 并交给 `markdownComponents``h2`/`h3` 渲染,`reportChartGrid` 只动 `h4``D\d+` / `Moon Chart`)与其后的图盘块,两者作用的标题层级不重叠。`report-chart-grid-rehype.ts` 一行未动。
---
## 六、环境缺口(不得记作通过)
1. **20 张盘以上的报告加目录后仍不重叠、滚动不卡** —— **做不了,缺口**。本会话无登录态、无 Chrome、无真实报告数据,`npm test` 只能证明 `reportChartGrid` 仍然接线、grid CSS 未被改动,**证明不了真机渲染**。这是任务书红线 9(BUG-616/617 不得回退)的配套实测,必须由产品负责人在真机上走一遍。已写进 `docs/testing/` 清单(见下)。
2. **打印 / 导出 PDF 在挂外壳后的真实输出** —— **做不了,缺口**。新加的 `REPORT_SHELL_PRINT_CSS` 是本轮改动里唯一无法自动验证的部分:测试只能断言这条样式存在且内容正确,浏览器实际怎么分页得真打一次。这是本轮**风险最高**的一项。
3. **窄屏(< 860px)目录抽屉的展开、焦点与滚动** —— 做不了,缺口。
4. **侧栏在阅读页移动端抽屉的手势与焦点** —— 与 R4 同一条缺口,未解除。
### 给产品负责人的真机清单(建议并入 `docs/testing/`
1. 打开一份**九节、20 张盘以上**的完整报告。逐屏滚到底:每张北印盘各占一格,**没有两张叠在同一位置**(BUG-616);连续滚动不掉帧、不卡顿(BUG-617)。
2. 同一份报告,右侧目录栏:滚动时高亮跟着走;点任一节直接跳到该节标题,标题不被顶部操作条盖住。
3. 同一份报告点「打印」→ 另存为 PDF:**侧栏和顶部窄栏不出现在纸上**;报告**不是只印出一页**,九节完整分页;目录栏不上纸;纸面底色与正文颜色是浅色(打印锁定浅色调色板)。
4. 把浏览器窗口拉窄到 860px 以内:右侧目录栏消失,正文上方出现一个收起的「目录」;展开可用。
5. 报告中心:至少同时存在一份**生成中**和一份**已完成**的报告,确认行式列表里状态色点一眼可辨,生成中那行能被读屏软件播报。
6. 制造一次失败报告(或用历史失败记录),确认失败原因写在行内、能完整读到。