Files
Jyotisha/docs/tasks/PROGRESS-secondary-page-entry-20260918.md
T

66 lines
5.6 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 · 次级页进入时的等待态与抖动(2026-09-18)
分支:`codex/secondary-page-entry-20260918`
基线:`origin/staging` @ `1061514f`(任务书写的 `41902067` 是写单时的 head;本 worktree 按开工指令跟当前 `origin/staging`)
方案:产品 2026-09-18 选定方案一。方案 B 未做。
## BUG-967 调查:Next 16.3.1 的 mismatch fallback 为什么没开火
对照 `next@16.3.1` 源码(GitHub tag `v16.3.1`),不是猜的。
### Next 自己会在什么时候整页跳
`packages/next/src/client/components/router-reducer/fetch-server-response.ts`:
1. RSC 响应 `Content-Type` 不是 `text/x-component`(export 模式下还接受 `text/plain`),或 `!res.ok`,或没有 body → `doMpaNavigation`。
2. 解出 Flight 之后:`(res.headers.get(NEXT_NAV_DEPLOYMENT_ID_HEADER) ?? flightResponse.b) !== getNavigationBuildId()` → 同样 MPA。
3. `app-router.tsx` 看到 `pushRef.mpaNavigation` 才 `location.assign` / `replace`,并 `throw unresolvedThenable` 卡住当前树。
另有 `nav-failure-handler.ts` 的 `handleHardNavError`:导航过程中未捕获错误则 `window.location.href = window.next.__pendingUrl`。**整份文件包在 `process.env.__NEXT_APP_NAV_FAIL_HANDLING` 里**,standalone 默认不打开。
既有 `StaleClientRecovery` 只在 `window.error` / `unhandledrejection` 文本像 chunk 失败、且 `sessionStorage` 还没记过一次时 `reload()`。router reducer 如果把 rejection 吃掉,它看不见。
### 为什么这次三条都不触发
1. **预取缓存让检查根本不跑。** 侧栏三项是 `<Link>`。标签页在部署前已经打开对话页时,Next 已经把 `/chart` `/ephemeris` `/reports` 的 Flight 预取进内存。点击走这段缓存,不再 `fetchServerResponse`。构建 id 比对、非 2xx、非 Flight Content-Type,全部不执行。
2. **自托管没有 Vercel 那种 `?dpl=` 分流。** `next.config.ts` 写了 `deploymentId: process.env.NEXT_DEPLOYMENT_ID`,静态资源带 `?dpl=`。在 Vercel 上这个 query 把请求打到对应部署,旧部署不在了就是 404,于是走条件 1。本仓 Caddy(`deploy/Caddyfile.staging` / `Caddyfile.production.selfhosted`)对 `/_next/static` **没有任何特殊 404 改写**,只 `reverse_proxy web:3000`。主机上只有当前镜像。旧 tab 就算真的去 fetch `/chart`,新服务器仍对这条路径返回 **200 + 新的 Flight**。条件 1 的 `!res.ok` 不成立。BUG-204 当年靠 `deploymentId` 修好的是 Vercel 形状,不是这台 VPS。
3. **错误回退开关是关的。** 旧 chunk 404 变成 `ChunkLoadError` 时,默认 standalone 不会把它升级成 MPA。点击看起来像没反应。`app-router.tsx` 若已经丢出 `unresolvedThenable` 或 `startTransition` 一直 pending,同一页上的账户按钮(本就不是 Link)也会一起没反应——这与「对话还能发」不矛盾:对话代码在内存里,路由切换被卡住。
Caddy 不是元凶:它没有把 404 变成 200 HTML。问题是 **200 的新 Flight** 加上 **根本不再 fetch 的预取缓存**。
### 自愈怎么做(才没有废掉正常客户端导航)
- 客户端 commit:构建期 `NEXT_PUBLIC_GIT_COMMIT`(Dockerfile 里与 `NEXT_DEPLOYMENT_ID` 同值写入,不改 workflow)。
- 服务端 commit:已有的 `GET /api/health` → `.deployment.gitCommit`(运行时 `GITHUB_SHA`)。
- `StaleBuildGuard` 在挂载和 `visibilitychange → visible` 时拉一次 health。不轮询、不定时 reload。
- 导航前读这份快照。不一致:`window.location.assign`。一致:原样走 `Link` / `router.push`。health 失败或任一侧是空/`unknown`:不强制跳。
## BUG-966
三页改为 `SecondaryPageShell`。`.secondary-page` 是 chat-panel 第二行(`minmax(0,1fr)`),等待与正文同一格子;等待句居中。`SecondaryHeader reserveNote` 让出生行晚到时标题行不塌。模块级缓存 + 侧栏 `pointerenter`/`pointerdown` 预取。报告列表首屏去掉 `InlineSpinner`「正在读取报告…」,改静态句「报告列表还没拿到。」刷新按钮上的 spinner 仍是用户点的动作,DESIGN 允许。
未做方案 B(延迟揭幕)。
## 测试
| 命令 | 结果 |
| --- | --- |
| `npx tsc --noEmit` | 0 错 |
| `npm run lint` | 0 error(既有 120 warning,本轮文件未新增) |
| 定向:`secondary-page-entry` / `chart-page-view` / `ephemeris-page` / `sidebar-contract` / `stale-client-recovery` / `personal-report-entry` / `chat-navigation-a11y-contract` / `class-name-definition-contract` | 114 / 114 |
| `personal-report-view.test.ts` | 通过(详情页仍用 `SecondaryHeader`) |
| `next build` | compile + tsc 过;收集 `/api/daily-starlanguage` 时 Windows `EPERM` 无法 symlink skill runtime。环境缺口,与本轮无关。`/` Static 与 gzip 未在本机量到。 |
无 Chrome:高度真机清单在 `docs/testing/secondary-page-entry-20260918.md`。无登录态:缓存第二次进入与跨部署自愈的浏览器步骤同样写在那里。
改过的既有断言:
| 测试 | 原值 | 新值 | 原因 |
| --- | --- | --- | --- |
| `sidebar-contract` 只读行 / 页脚 | `<Link className="session-main">`、`<Link className="profile-trigger" href="/">` | `AppLink`,class 与 href 不变 | BUG-967 跨部署自愈;只读行仍是链接、页脚仍去 `/` |
## 偏离
- 任务书写 `app/(secondary)/layout.tsx`,当前树是 `app/(app)/layout.tsx`(会话列表单源之后)。外壳挂在这里,没有把路由组改回去。
- 报告详情 `/reports/[reportId]` 仍用 `SecondaryHeader` 各阶段一份,不进三页进入抖动的范围;loading.tsx 里的 spinner 本轮不动。