fix(synastry): scroll the report from the full-width outer container

The narrow column was the only scrollport, so the wheel did nothing on the side blanks.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
jesse-ux
2026-10-21 17:53:08 +08:00
co-authored by Cursor
parent ff6b228dd6
commit 229df727c9
7 changed files with 37124 additions and 37070 deletions
+4
View File
@@ -1,5 +1,9 @@
# 印度占星 Skill 更新日志
## 2026-10-21 — 合盘报告两侧空白也能滚动
- 合盘报告改由整宽的外层滚动,中间一栏只负责居中排版。指针停在两侧空白处时,整份报告一起动。打印仍按整页展开。Skill 版本不变(BUG-1404)。
## 2026-10-21 — 合盘时间线写出起止,删掉的模型改用默认模型
- 关系时间线每段标题写出起止年月。第一段从本月开始时,起点写「现在」或 Now。
+37033 -37018
View File
File diff suppressed because it is too large Load Diff
@@ -1,47 +1,52 @@
# 进度:婚恋合盘 staging 修复(T3–T6 与红线 3)
- 任务书:`docs/tasks/TASK-synastry-staging-fix-20261021.md`
- 基线:`origin/staging` `998d07baf`(T1、T2 已在这条上,BUG-1399)
- 分支:`codex/synastry-staging-fix-20261021`
- 工作树:`.worktrees/synastry-staging-fix-20261021`
- 记录:BUG-1400~1403。重复的第二段 BUG-1397 已删。
- 未推 staging,未改 `.gitea/`。真机未做。
## 做了什么
- 时间线标题用 `timelineHeading`:中文「2027 年 9 月 – 2029 年 3 月 阶段名」,英文「Sep 2027 – Mar 2029 Settling」。只有第一段、且起点年月等于本地今天所在月时,起点写「现在」/「Now」。
- 英文报告恢复收起的分析逻辑,和时间线依据同一套 `<details class="synastry-why">`。英文摘要是 Analysis,依据行是 Chart / So。正文来自英文那一次生成。
- 普通咨询钉住的模型版本解析不到时,改用当前默认模型,并把 `model_id` 写回会话(版本号仍由原触发器钉)。回退 id 与原 id 相同时先清空再写回,否则触发器留着旧版本。没有默认模型时仍是 503「模型暂不可用」。请求若仍带已删 id,不回 409;若改指另一个仍在线的模型,仍是 409「会话模型已经变化」。对话里没有新加提示。合盘追问和生时校正的解析没有改。打开页面时原有的「此前选择的模型已下线,已切换为默认模型。」没有动。
- 选人页在开关结果回来之前只渲染标题和副标题。开关连不上时说「合盘服务暂时连不上,请稍后再试」。档案失败仍是「星盘档案暂时读不出来」。
- 红线 3:`frontend/tests/database-synastry-full-create.test.ts` 用真的自托管客户端走 `createSynastryFullReport`,到预留成功、任务入队。模型目录打桩,产品和计费走真库。没有手写 `Query` 桩。
## 断言变更
| 测试 | 原值 | 新值 | 原因 |
| --- | --- | --- | --- |
| `synastry-full-report.test.ts` 选人页初始状态 | `[serviceClosed, setServiceClosed] = useState(false)` | `useState<"pending" \| "open" \| "closed" \| "unreachable">("pending")` | BUG-1403。开关未返回时不再画出选人区 |
| 同上,关闭与读档案的顺序 | `setServiceClosed(true)` 在 `fetchAccount()` 之前 | `setAccess("closed")` 在 `fetchAccount()` 之前 | 状态从布尔改成四态,关闭文案不变 |
## 测试
| 检查 | 结果 |
| --- | --- |
| `tsc --noEmit` | 0 错 |
| `npm run lint` | 0 error,122 warning,与基线相同 |
| 定向:`consult-session-model-fallback.test.ts`、`synastry-full-report.test.ts` | 19 通过,0 失败,0 cancelled |
| 红线 3 单独跑 | 通过。本机 Docker 29.8.0。返回 202,报告 `generating`,任务 `queued`,预留 `reserved`,虚构账户点数 100 变为 81 |
| `npm run test:db` | 90 条,通过 74,失败 16,cancelled 0。新建的创建测试在全量里是通过的(ok 73)。16 条失败都是本机 Windows 环境:`G:\G:\` 路径被拼两次、WSL 里没有 bash、迁移脚本因此起不来。没有合盘业务断言失败 |
| 非库测试(`database-*` 以外的 `tests/*.test.ts(x)`,串行) | 5018 条,通过 4969,失败 49,cancelled 0。失败是同一类环境问题:`path.join` 吃了 file URL 的 pathname、tsx 解析 `tailwind-merge` 报错、WSL、shell/Docker。没有本轮新增测试的失败。一次把库测试和全量绑在一起、并发 2 的 `npm test` 跑到约 3327 条时进程退出,没有汇总行,不把它当成结果 |
| `npm run build` | `/` 为 Static(路由表 `○ /`)。`index.html` 引用的 `/_next/static` js/css 逐个 gzip -9:JS 576,261,CSS 70,491,合计 646,752。对照已记录的 643,725,差 +0.47%,在 ±2% 内。测量脚本没有留在仓库里,这个合计是按 `index.html` 引用的静态文件重算的 |
隐私扫描 `tests/test_repo_privacy_markers.py` 没有跑:这个工作树和主检出都没有可用的 `.venv`。文档里没有真实个人出生资料。
## 真机复测(产品负责人,执行方未做,不得写成已通过)
部署到 staging 之后:
1. 婚恋合盘选两个人,点「开始合盘」,应进入等待页,而不是「报告生成暂时不可用」。
2. 等待页走完,报告揭幕。关系时间线每一段能看出起止年月;若第一段从本月起,起点是「现在」。
3. 切到 English。各节和时间线都有收起的 Analysis,点开能看到 Chart / So。
4. 从报告末尾选一节进对话,发出问题,能按这一节问下去。
5. 另:后台删掉某个非默认的已发布模型后,钉着它的旧咨询对话还能继续,并且改用当前默认模型。没有默认模型时才是「模型暂不可用」。
# 进度:婚恋合盘 staging 修复(T3–T6 已部署;T8 滚动已做)
- 任务书:`docs/tasks/TASK-synastry-staging-fix-20261021.md`
- 基线:`origin/staging` `998d07baf`(T1、T2 已在这条上,BUG-1399)
- 分支:`codex/synastry-staging-fix-20261021`
- 工作树:`.worktrees/synastry-staging-fix-20261021`
- 记录:BUG-1400~1403。重复的第二段 BUG-1397 已删。
- 未推 staging,未改 `.gitea/`。真机未做。
## 做了什么
- 时间线标题用 `timelineHeading`:中文「2027 年 9 月 – 2029 年 3 月 阶段名」,英文「Sep 2027 – Mar 2029 Settling」。只有第一段、且起点年月等于本地今天所在月时,起点写「现在」/「Now」。
- 英文报告恢复收起的分析逻辑,和时间线依据同一套 `<details class="synastry-why">`。英文摘要是 Analysis,依据行是 Chart / So。正文来自英文那一次生成。
- 普通咨询钉住的模型版本解析不到时,改用当前默认模型,并把 `model_id` 写回会话(版本号仍由原触发器钉)。回退 id 与原 id 相同时先清空再写回,否则触发器留着旧版本。没有默认模型时仍是 503「模型暂不可用」。请求若仍带已删 id,不回 409;若改指另一个仍在线的模型,仍是 409「会话模型已经变化」。对话里没有新加提示。合盘追问和生时校正的解析没有改。打开页面时原有的「此前选择的模型已下线,已切换为默认模型。」没有动。
- 选人页在开关结果回来之前只渲染标题和副标题。开关连不上时说「合盘服务暂时连不上,请稍后再试」。档案失败仍是「星盘档案暂时读不出来」。
- 红线 3:`frontend/tests/database-synastry-full-create.test.ts` 用真的自托管客户端走 `createSynastryFullReport`,到预留成功、任务入队。模型目录打桩,产品和计费走真库。没有手写 `Query` 桩。
## 断言变更
| 测试 | 原值 | 新值 | 原因 |
| --- | --- | --- | --- |
| `synastry-full-report.test.ts` 选人页初始状态 | `[serviceClosed, setServiceClosed] = useState(false)` | `useState<"pending" \| "open" \| "closed" \| "unreachable">("pending")` | BUG-1403。开关未返回时不再画出选人区 |
| 同上,关闭与读档案的顺序 | `setServiceClosed(true)` 在 `fetchAccount()` 之前 | `setAccess("closed")` 在 `fetchAccount()` 之前 | 状态从布尔改成四态,关闭文案不变 |
## 测试
| 检查 | 结果 |
| --- | --- |
| `tsc --noEmit` | 0 错 |
| `npm run lint` | 0 error,122 warning,与基线相同 |
| 定向:`consult-session-model-fallback.test.ts`、`synastry-full-report.test.ts` | 19 通过,0 失败,0 cancelled |
| 红线 3 单独跑 | 通过。本机 Docker 29.8.0。返回 202,报告 `generating`,任务 `queued`,预留 `reserved`,虚构账户点数 100 变为 81 |
| `npm run test:db` | 90 条,通过 74,失败 16,cancelled 0。新建的创建测试在全量里是通过的(ok 73)。16 条失败都是本机 Windows 环境:`G:\G:\` 路径被拼两次、WSL 里没有 bash、迁移脚本因此起不来。没有合盘业务断言失败 |
| 非库测试(`database-*` 以外的 `tests/*.test.ts(x)`,串行) | 5018 条,通过 4969,失败 49,cancelled 0。失败是同一类环境问题:`path.join` 吃了 file URL 的 pathname、tsx 解析 `tailwind-merge` 报错、WSL、shell/Docker。没有本轮新增测试的失败。一次把库测试和全量绑在一起、并发 2 的 `npm test` 跑到约 3327 条时进程退出,没有汇总行,不把它当成结果 |
| `npm run build` | `/` 为 Static(路由表 `○ /`)。`index.html` 引用的 `/_next/static` js/css 逐个 gzip -9:JS 576,261,CSS 70,491,合计 646,752。对照已记录的 643,725,差 +0.47%,在 ±2% 内。测量脚本没有留在仓库里,这个合计是按 `index.html` 引用的静态文件重算的 |
隐私扫描 `tests/test_repo_privacy_markers.py` 没有跑:这个工作树和主检出都没有可用的 `.venv`。文档里没有真实个人出生资料。
## 真机复测(产品负责人,执行方未做,不得写成已通过)
部署到 staging 之后:
1. 婚恋合盘选两个人,点「开始合盘」,应进入等待页,而不是「报告生成暂时不可用」。
2. 等待页走完,报告揭幕。关系时间线每一段能看出起止年月;若第一段从本月起,起点是「现在」。
3. 切到 English。各节和时间线都有收起的 Analysis,点开能看到 Chart / So。
4. 从报告末尾选一节进对话,发出问题,能按这一节问下去。
5. 另:后台删掉某个非默认的已发布模型后,钉着它的旧咨询对话还能继续,并且改用当前默认模型。没有默认模型时才是「模型暂不可用」。
## T8 报告滚动(BUG-1404)
- 合盘报告外层改为整宽的 `.synastry-report-scroll`(`width: 100%`、`height: 100%`、`overflow-y: auto`)。`.synastry-report` 只居中排版,不再自带 `height: 100%` 和 `overflow`。打印时外层 `height: auto`、`overflow: visible`。
- 真机(执行方未做,不得写成已通过):桌面打开一份合盘报告,把指针放在正文栏两侧的空白上滚动,整份报告应跟着动。窄屏和打印另看一眼,长报告不要被裁成一页。
+1 -1
View File
@@ -2,7 +2,7 @@
## 婚恋合盘报告(2026-10-19)
选人页标题下是副标题「看这段感情往后的走向,和两人之间的助力与考验。」两个人并排,中间一个 ×;窄屏改成上下排,× 不显示。各自是自定义列表,最多二十人,列表可滚动。资料不全的人灰掉,不能选;已经在另一边的人不能再选。列表最底一行是「+ 新增星盘」,点开一张卡片:称呼、性别(男 / 女)、出生日期、出生时间、出生地,右下角「取消」和「保存并选中」。满二十人时这一行变灰,并提示去星盘档案删人。「开始合盘」居中,点数说明在按钮正下方。之前的合盘只列人名和日期。生成中是轨道和四步(算两张完整命盘、整理两人之间的证据、写报告、检查报告),句子是「正在写 甲 × 乙 的合盘报告…」。报告页回到「‹ 婚恋合盘」,标题下是日期和「已保存,再次打开不扣点」,语言是「中文 / English」。九节编号,分析逻辑默认收起。英文报告同样收起分析逻辑(Analysis,依据行是 Chart 和 So),时间线每段标题写出起止年月;第一段从本月起时,起点写「现在」或 Now。英文准备时同一套轨道,写「正在写英文版…」,揭幕后整份替换。报告末尾「对哪一段想再问清楚?」,下面是八枚胶囊(`.synastry-report-ask`),不含共同练习。点一枚只新建对话并预填问题,不自动发送。对话里人物选择器锁住,显示「甲 × 乙」。合盘删除后输入框占位是「这份合盘已删除」,发送停用。
选人页标题下是副标题「看这段感情往后的走向,和两人之间的助力与考验。」两个人并排,中间一个 ×;窄屏改成上下排,× 不显示。各自是自定义列表,最多二十人,列表可滚动。资料不全的人灰掉,不能选;已经在另一边的人不能再选。列表最底一行是「+ 新增星盘」,点开一张卡片:称呼、性别(男 / 女)、出生日期、出生时间、出生地,右下角「取消」和「保存并选中」。满二十人时这一行变灰,并提示去星盘档案删人。「开始合盘」居中,点数说明在按钮正下方。之前的合盘只列人名和日期。生成中是轨道和四步(算两张完整命盘、整理两人之间的证据、写报告、检查报告),句子是「正在写 甲 × 乙 的合盘报告…」。报告页回到「‹ 婚恋合盘」,标题下是日期和「已保存,再次打开不扣点」,语言是「中文 / English」。九节编号,分析逻辑默认收起。英文报告同样收起分析逻辑(Analysis,依据行是 Chart 和 So),时间线每段标题写出起止年月;第一段从本月起时,起点写「现在」或 Now。英文准备时同一套轨道,写「正在写英文版…」,揭幕后整份替换。报告末尾「对哪一段想再问清楚?」,下面是八枚胶囊(`.synastry-report-ask`),不含共同练习。点一枚只新建对话并预填问题,不自动发送。对话里人物选择器锁住,显示「甲 × 乙」。合盘删除后输入框占位是「这份合盘已删除」,发送停用。报告在整宽的外层滚动,两侧空白也能滚;中间一栏只负责居中排版。
## 生时校正从对话进入、做完回到原对话(2026-10-07,BUG-1247~1251,在途)
+12 -3
View File
@@ -4756,17 +4756,25 @@ button.synastry-dd-add { border-radius: 0 0 var(--radius-md) var(--radius-md); }
}
.synastry-steps li.done { color: var(--color-ink-tertiary); text-decoration: line-through; }
.synastry-report {
width: min(720px, 100%);
.synastry-report-scroll {
width: 100%;
height: 100%;
min-height: 0;
overflow-x: hidden;
overflow-y: auto;
overscroll-behavior: contain;
-webkit-overflow-scrolling: touch;
touch-action: pan-y;
scrollbar-gutter: stable;
}
.synastry-report {
width: min(720px, 100%);
margin-inline: auto;
box-sizing: border-box;
display: flex;
flex-direction: column;
gap: 22px;
padding: var(--space-6);
overflow: auto;
overflow-wrap: anywhere;
}
.synastry-report-back { color: var(--color-ink-secondary); font-size: 14px; text-decoration: none; }
@@ -5384,6 +5392,7 @@ button.synastry-dd-add { border-radius: 0 0 var(--radius-md) var(--radius-md); }
.personal-report-print-always { display: block !important; }
.personal-report-appendix-details:not([open]) > .personal-report-print-always { display: block !important; }
.personal-report-reader { width: auto !important; height: auto !important; min-height: 0 !important; overflow: visible !important; background: #fff !important; }
.synastry-report-scroll { width: auto !important; height: auto !important; min-height: 0 !important; overflow: visible !important; }
.personal-report-reader-body { width: auto !important; padding: 0 !important; }
.personal-report-toc { display: none !important; }
.personal-report-md-layout { display: block !important; }
@@ -556,7 +556,9 @@ export function PersonalReportPage({ reportId, initialLanguage = "zh" }: { repor
<>
<style>{REPORT_SHELL_PRINT_CSS}</style>
<SecondaryHeader title="婚恋合盘" />
<SynastryReportView reportId={state.reportId} snapshot={state.synastry} createdAt={state.createdAt} initialLanguage={language} />
<div className="synastry-report-scroll">
<SynastryReportView reportId={state.reportId} snapshot={state.synastry} createdAt={state.createdAt} initialLanguage={language} />
</div>
</>
);
}
@@ -555,3 +555,22 @@ test("a public pair dossier fills every section without birth keys", { timeout:
}
rmSync(output, { force: true });
});
test("the synastry report scrolls in a full-width outer container", () => {
const css = readFileSync(new URL("../src/app/globals.css", import.meta.url), "utf8");
const page = readFileSync(new URL("../src/components/personal-report/personal-report-page.tsx", import.meta.url), "utf8");
const scroll = css.slice(css.indexOf(".synastry-report-scroll {"), css.indexOf(".synastry-report {"));
const column = css.slice(css.indexOf(".synastry-report {"), css.indexOf(".synastry-report-back"));
assert.match(scroll, /width:\s*100%/);
assert.match(scroll, /height:\s*100%/);
assert.match(scroll, /overflow-y:\s*auto/);
assert.match(scroll, /overflow-x:\s*hidden/);
assert.match(column, /width:\s*min\(720px, 100%\)/);
assert.match(column, /margin-inline:\s*auto/);
assert.doesNotMatch(column, /height:\s*100%/);
assert.doesNotMatch(column, /overflow:\s*auto/);
assert.match(page, /<div className="synastry-report-scroll">\s*<SynastryReportView /);
const printAt = css.indexOf(".synastry-report-scroll { width: auto !important;");
assert.ok(printAt > css.indexOf("@media print"));
assert.match(css.slice(printAt, printAt + 180), /overflow: visible !important/);
});