Files
Jyotisha/docs/tasks/TASK-sidebar-unify-20260916.md
T

139 lines
14 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.
# TASK · 侧栏统一成一个组件 + 次级页共享外壳 + 跳转不再整页刷新(2026-09-16
分支:`codex/sidebar-unify-20260916`。纯前端,不动数据库、不动 Skill、不动 Python。
## 基线 commit
`origin/staging` = `cfb41daf`feat(rectification): 出卡加精度门槛,补经历改成系统点名)。本单涉及的文件自 `317e9f18` 起没有变化,进度记录里按开工时的 `git rev-parse origin/staging` 重新写一次。
## 事故实证(行号按符号定位,会漂)
产品负责人在 staging 真机上的三条观察,逐条对到代码:
1. **首页与三个次级页的侧栏长得不一样。**
- `/``AppSidebar``frontend/src/components/app-sidebar.tsx`),会话行是 `SidebarSessionRow``sidebar-session-row.tsx``.session-row > .session-main` + 菜单按钮 + 副标题),页脚是 `.profile-trigger`56px、带 chevron、打开账户菜单)。
- `/chart``/ephemeris``/reports``/reports/[reportId]``AppNavRail``app-nav-rail.tsx`),会话行在 `renderRow` 里是 `<Link className="session-row nav-rail-row">` 直接套 `.session-title`**没有 `.session-main` 这一层**:丢了 `padding: var(--space-2)``min-height: 44px`、当前项 2px 左标记(`globals.css``.session-main[data-active="true"]::before`),而 `.session-row``grid-template-columns: minmax(0, 1fr) 44px` 还给不存在的菜单按钮留着一列 44px 空位。页脚是 `.nav-rail-identity`(44px、flex、文字「N 点」)。
- 两个组件共用一份 `globals.css`,所以「四个导航项」本身一致(`.new-chat` 强调色、`.report-nav-button` 文字色,`DESIGN.md` §Scarce 第 1 条写明这是设计);分叉全在会话行与页脚。
- `SidebarProvider``ui/sidebar.tsx``defaultOpen = true`)不存 cookie / localStorage,折叠状态只活在内存里。
2. **每次进次级页都重新读会话列表。**
- `app-sidebar.tsx``leaveChat()``window.location.assign(path)`:首页 → 次级页是**整页刷新**,React 树与内存全部清零。
- 三个次级页各自在组件内部渲染 `SecondaryShell``chart-page-view.tsx``ephemeris-page.tsx``personal-report-center.tsx``personal-report-page.tsx` 共 8 处调用),每个 `SecondaryShell` 挂一份 `SidebarProvider + AppNavRail``app/` 下没有把这四个路由包起来的 layout,所以次级页之间即便是客户端跳转,外壳也整个重挂,`useNavRail()``hooks/use-nav-rail.ts`)的 effect 重新 `GET /api/sessions?limit=40` + `GET /api/account`,且无任何缓存。
3. **从次级页回「新建对话」等很久。**
- `AppNavRail` 里「新建对话」是 `<Link href="/">`,客户端跳转本身没问题;慢在 `Home()` 的启动链每次都从零跑:`page.tsx``loadCloudData()``Promise.all(账户, 模型目录, 会话列表)``fetchActiveConsultationStatus``resolveLookupBootstrap` → 当前会话 `fetchSessionDetail``setBootstrapPhase("prepare")` → 「准备」阶段并行拉 `/api/rectification/cases/entry-summary``POST /api/daily-starlanguage``bootstrapRevealDelayMs``lib/home-bootstrap.ts`,上限 `BOOTSTRAP_PREPARE_TIMEOUT_MS = 4000`)→ `setHydrated(true)` 揭幕。串行至少 4 次往返。
- 2026-09-16 从验收机测 staging:接口单次 TTFB 0.630.90 s`/` 首屏脚本 23 个 gzip 604 KB`/chart` 17 个 gzip 283 KB,从次级页回 `/` 要新下载 8 个共 gzip 339 KB`<Link>` 默认会预取,但未在浏览器里证实)。
## 根因
- 样式分叉:`TASK-cend-surfaces-claude-alignment-20260916` D9 决定次级页侧栏只读,T4.1 为了不把 `Home()` 的会话管理层上提而**另写了第二个组件**。D9 只说「不带操作」,没说「长得不一样」;分叉是实现手段的副作用,不是设计。
- 重复读取:没有共享 layout、没有缓存、首页出去是整页刷新,三件事叠加。
- 回首页慢:启动链串行且不复用侧栏已经拿到的账户与会话列表;「准备」阶段两个请求每次现拉。
## 决策记录(产品负责人 2026-09-16 拍板)
- **D1|侧栏只保留一个组件。** `AppSidebar` 收编 `AppNavRail`:会话操作回调与账户菜单改成**可选**,不传就渲染成只读(行是纯链接、页脚是去 `/` 的链接)。`app-nav-rail.tsx` 删除。D9「次级页不带写操作」**维持**,只是实现方式从「第二个组件」改成「同一组件的只读模式」。
- **D2|四个次级路由共享一个外壳实例。** 用路由组 `app/(secondary)/` 的 layout 挂 `SidebarProvider + AppSidebar`,页面只保留 46px 顶栏与正文。路由之间跳转不重挂侧栏、不重拉列表。
- **D3|首页进次级页改客户端跳转。** `leaveChat()``window.location.assign` 换成 `<Link>`(保留 `persistLoginSessionReturn()`)。`/login` 维持硬跳转。
- **D4|会话列表与账户在内存里缓存,先用旧数据再后台刷新。** 缓存是模块级、按账户 id 键、同一标签页内有效、过期时间 60 s;`/` 上的会话写操作(新建 / 重命名 / 删除 / 归档 / 收藏)成功后写穿缓存或使其失效。不落 localStorage。
- **D5|折叠状态跨页记住。** 存 cookieshadcn 的 `sidebar_state` 惯例)或 localStorage 二选一,进度记录写明选哪个;移动端抽屉状态不记。
- **D6|本单不动 `Home()` 的启动链。** 回首页的等待另开一单(`TASK-home-bootstrap-reuse`),前提是本单先落地让内存缓存能活过跳转。本单只要求「新建对话」链接保持 `<Link>` 且预取不被关掉。
## 硬红线
1. `./node_modules/.bin/tsc --noEmit` 0 错;`npm run lint` **0 error**
2. 测试总数不得低于开工时 `origin/staging` 的实测;改既有断言必须写「原值 / 新值 / 原因」三栏。`tests/sidebar-contract.test.ts`43 条)与 `tests/sidebar-state.test.ts`(5 条)是侧栏 CSS 与状态合同,不得删条。
3. `next build``/` 保持 `○ Static``/chart``/ephemeris` 的渲染标记改前改后不变;`/reports``/reports/[reportId]` 维持 `force-dynamic`。路由组不得改变任何 URL。四个路由的标记逐个列进进度记录。
4. `/` 首屏 gzip ±2%(口径沿用 `PROGRESS-cend-ui-r1-20260916.md``.next/static` 下 css chunk gzip 合计,另附 `/` HTML 引用的 js chunk gzip 合计)。次级页体积改前改后列出。
5. `frontend/src/app/page.tsx` 不得增长;`Home()``useState` / `useRef` 数不得增长(`tests/home-shell-growth-contract.test.ts`)。本单对 `page.tsx` 的改动只允许:传给 `AppSidebar` 的 props 变化、写操作后调用缓存失效。
6. 次级页上的侧栏**不得**发起任何写操作接口(D9 维持)。只读模式下 `AppSidebar` 不得 import `page.tsx` 的任何 hook。
7. 揭幕后不得出现 spinner / 骨架 /「正在加载」。缓存未命中时侧栏显示现有静态空态文案(`sidebar-empty`),不是骨架。
8. 不改 `useConversationScrollAnchor``ChatComposer`;不动 BUG-698 的 `@supports (height: 1dvh)` 写法;不动报告盘面 grid。
9. 改 UI 的同一提交内更新 `frontend/DESIGN.md`(「The rail is read-only」一节改写成「只读模式」,删掉 `.nav-rail-*` 的描述)。
10. 不顺手升级依赖、不顺手修不在本单里的 warning。
## 任务分解
### T1 · `AppSidebar` 收编 `AppNavRail`D1
- `AppSidebarProps` 拆成两组:`sessions / activeSessionId / account / pathname`(两边都要)与 `controls?: { sessionControls, onNewChat, onSelectSession, onAccountMenuOpenChange, accountTriggerRef, onOpenProfile … }`(只有 `/` 传)。`controls` 缺省时:
- 四个导航项:新建对话是 `<Link href="/">`,三个页面项是 `<Link>` 并按 `pathname``data-active`(沿用 `report-nav-button` 现有选中态)。
- 会话行:仍渲染 `SidebarSessionRow` 的**同一套标记**`.session-row > .session-main`),但 `.session-main``<Link href={sessionHref("", id)}>`,不渲染菜单按钮;`grid-template-columns` 在只读模式下不留 44px 空列(加 `data-readonly` 属性切换,不新造类名)。
- 页脚:同一 `.profile-trigger` 标记,渲染成 `<Link href="/">`,不带 chevron、不带菜单。
- 删除 `app-nav-rail.tsx``globals.css``.nav-rail-row` / `.nav-rail-identity` 段。
- `use-nav-rail.ts` 改名为 `use-sidebar-data.ts`(或等价),加 D4 的缓存(见 T3)。
**验收标准**
- `grep -rn "AppNavRail\|nav-rail" frontend/src` 为 0。
- `/``/chart` 的侧栏 DOM 结构除了「菜单按钮 / chevron / 账户菜单」三处之外逐节点相同;进度记录贴两页 `document.querySelector('[data-sidebar="sidebar"]').outerHTML` 的 diff(脱敏会话标题)。
- 只读模式下 `document.querySelectorAll('[data-sidebar="sidebar"] button')` 只剩折叠触发器。
- `sidebar-contract.test.ts` 全绿,不删条;新增断言:只读模式会话行有 `.session-main`、无 `.session-menu-trigger`、无 44px 空列。
### T2 · 路由组 layout 承载外壳(D2)
- 新建 `app/(secondary)/layout.tsx`client component):`SidebarProvider + AppSidebar(只读) + SidebarInset``children` 是页面。
-`app/chart``app/ephemeris``app/reports` 移进 `app/(secondary)/``page.tsx` 内容与 `metadata``dynamic` 声明原样保留。
- `SecondaryShell` 拆成只剩顶栏的 `SecondaryHeader({ title, note, actions })`8 处调用改名;`SidebarTrigger` 仍在顶栏里(provider 由 layout 提供)。
- `../site-styles` 的 import 移到 layout 一处。
**验收标准**
- 四个 URL 不变;`next build` 路由表四个标记与开工时一致(红线 3)。
-`/chart``/ephemeris``/reports` 之间跳转,Network 面板里**没有** `/api/sessions``/api/account` 请求(缓存未过期时);这条做不了自动化就写进 `docs/testing/` 清单,并用 `tests/` 里的渲染测试证明 layout 只挂一次(记录 `useSidebarData` 的 effect 调用次数)。
- `/reports/[reportId]` 的打印样式(`media="print"` 隐藏侧栏,见 `DESIGN.md` 「Print inside the shell」)在 layout 化之后仍生效,进度记录写明验证方式。
### T3 · 列表与账户缓存 + 首页写穿(D4)
- 模块级 `Map<accountId, { sessions, account, fetchedAt }>``useSidebarData()` 先同步读缓存渲染,再后台刷新;60 s 内不重拉。
- `/``useSessionManagement` 的新建 / 重命名 / 删除 / 归档 / 收藏成功回调里调用 `invalidateSidebarCache()`(或直接写穿)。这是本单唯一允许碰 `page.tsx` / `use-session-management.ts` 的地方。
- 401 时清缓存并按现有 `signedOut` 分支降级。
**验收标准**
- 单测:命中缓存时不发 fetch;过期后发一次;写操作后下一次进次级页拿到的是新标题 / 新顺序(用 `vi.useFakeTimers` 推时间)。
-`/` 重命名一个会话 → 进 `/chart`,侧栏立刻显示新标题(真机清单)。
### T4 · 首页进次级页改客户端跳转(D3)
- `app-sidebar.tsx``leaveChat()` 删除,三个页面项改 `<Link>``onClick` 里保留 `persistLoginSessionReturn()` 与移动端关抽屉。
- `onOpenReports` 这个已经没人用的 prop(当前解构成 `_onOpenReports`)一起删。
**验收标准**
- 点星盘 / 星历 / 我的报告不再整页刷新(Network 里没有 document 请求;真机清单)。
- `/login` 仍是硬跳转(`tests/chat-session-url.test.ts` 补一条 `persistLoginSessionReturn` 在跳转前被调用的断言)。
### T5 · 折叠状态持久化(D5)
- `SidebarProvider` 读写 cookie `sidebar_state`(或 localStorage,二选一写明),`defaultOpen` 由 layout / `page.tsx` 从存储读;SSR 首帧与客户端一致,不得闪一下。
- `tests/sidebar-state.test.ts` 补两条:收起后重挂仍收起;移动端不受影响。
**验收标准**
- `/` 收起侧栏 → 进 `/chart`,仍收起;反向同理。
### T6 · 文档与清单
- `frontend/DESIGN.md`:侧栏一节改成「一个组件、两种模式」,写明只读模式少哪三样。
- `docs/testing/sidebar-unify-20260916.md`:真机清单(跳转无 document 请求、跨页无 `/api/sessions`、重命名后立刻可见、折叠状态跨页、打印隐藏侧栏、移动端抽屉)。
- `docs/BUG_HISTORY.md`:BUG-744 侧栏两套标记结构导致次级页会话行 / 页脚样式分叉;BUG-745 首页进次级页整页刷新 + 次级页外壳逐页重挂导致每次跳转重拉会话列表;BUG-746 折叠状态不持久。三条都要有针对性回归测试才能写 `resolved`
- `CHANGELOG.md` 一条。
## 让步顺序
1. T5 折叠持久化如果 SSR 首帧对不齐(闪一下),可以退到只在客户端 `useEffect` 后同步,进度记录写明并保留 `docs/testing/` 条目。
2. T3 的写穿如果撞 `page.tsx` 增长门禁,退到「写操作后只失效、不写穿」。
3. T2 若 `/reports/[reportId]` 的打印样式在 layout 化后失效且一时修不好,先让 `[reportId]` 留在路由组外、保留一份自己的外壳,其余三页照做,并在进度记录标 `blocked`
4. 不得让步的:T1(一个组件)、T4(不整页刷新)、红线 5 / 6。
## 开工前置命令
```bash
git -C /workspace/Jyotisha status -sb | head -1
git -C /workspace/Jyotisha fetch origin --prune
git -C /workspace/Jyotisha worktree add -b codex/sidebar-unify-20260916 .worktrees/sidebar-unify-20260916 origin/staging
cd /workspace/Jyotisha/.worktrees/sidebar-unify-20260916/frontend && npm ci
./node_modules/.bin/tsc --noEmit && npm run lint && npm test 2>&1 | tail -5 # 记基线:测试总数、fail 清单
npm run build 2>&1 | grep -E "○|ƒ|λ" | grep -E "^\s*[○ƒλ] /(chart|ephemeris|reports|$)" # 记四个路由标记
```
同一时间若有别的单在改 `frontend/src/app/page.tsx``globals.css`(当前状态板上:`TASK-home-state-lowering-batch2-20260916` 待领取),本单**后于**它合入;本单对这两个文件的改动都很小,rebase 即可。
## BUG 编号起点
开工时核对 `docs/BUG_HISTORY.md` 最大号;本单写作时最大号为 **BUG-743**,本单从 **BUG-744** 起。