diff --git a/docs/BUG_HISTORY.md b/docs/BUG_HISTORY.md index 92863c06..b935f482 100644 --- a/docs/BUG_HISTORY.md +++ b/docs/BUG_HISTORY.md @@ -14942,3 +14942,44 @@ - 防复发:真机复测后更新本条状态;若仍复现需真机录屏与 Safari 版本。 - 相关记录:TASK-report-english-edition-20260929 - 修复版本:分支 `codex/surface-feedback-20260929` + +## BUG-1111 | 首页常驻「上次那次校正还没完成,可以在历史对话里接着做。」 + +- 状态:resolved(代码 + 回归测试) +- 首次发现 / 最近更新:2026-09-30 / 2026-09-30 +- 影响面:`components/starter-home.tsx::rectificationEntryHint`。 +- 现象:有未完成的校正时首页入口下方一直挂着这句提示;产品要求去掉。 +- 修复:`rectificationEntryHint(rectificationForSelf)` 只保留「生时校正暂时只支持本人。」一句;未完成的校正仍在历史列表里。 +- 验证:`starter-home-polish-20260926.test.tsx`、`rectification-agentic-entry.test.ts`(断言三栏更新)、`surface-feedback-20260929.test.tsx`。 +- 相关记录:BUG-1041、TASK-starter-home-polish-20260926 D2 +- 修复版本:分支 `codex/surface-feedback-20260929` + +## BUG-1112 | 星盘类型面板里每格的说明文字被截掉下半截 / 显示不全 + +- 状态:resolved(代码 + 回归测试;iPhone 真机欠) +- 首次发现 / 最近更新:2026-09-30 / 2026-09-30 +- 影响面:`globals.css .chart-page-type-cell small`。 +- 根因:说明行 `white-space: nowrap; overflow: hidden`,行高沿用按钮的紧行高,iPhone 上中文字形下半被裁;「以月亮为第 1 宫」这类长说明只能省略。 +- 修复:说明行行高 1.4、最多两行(line-clamp),格子最小高 56 → 64px。 +- 验证:`chart-type-picker.test.tsx`(三栏)、`surface-feedback-20260929.test.tsx`。 +- 修复版本:分支 `codex/surface-feedback-20260929` + +## BUG-1113 | 星盘、星历加载时写「这一张盘还没拿到。」「这一天的星历还没拿到。」 + +- 状态:resolved(代码 + 回归测试 + 本机手机视口截图) +- 首次发现 / 最近更新:2026-09-30 / 2026-09-30 +- 影响面:新增 `components/orbit-waiting.tsx`;`secondary-page-shell.tsx`、`chart-page-skeleton.tsx`、`chart-dasha-tab.tsx`。 +- 现象:几秒内就会加载好的内容,先用一句「还没拿到」占位,读起来像出错。 +- 修复:统一用首页加载屏的轨道环(40px)作为次级页短等待动画:星盘空盘中央、星历 / 报告列表首屏、Chara 大运栏;原句只留给读屏器(`sr-only` + `role="status"`)。 +- 产品决定:此前 AGENTS §6「揭幕后不得再出现加载动画」只对首页;星盘页 Chara 栏等待改为轨道环属产品本次明确要求,记录在此。 +- 验证:`surface-feedback-20260929.test.tsx`;既有「还没拿到」断言因文本仍在读屏节点里而不变。 +- 修复版本:分支 `codex/surface-feedback-20260929` + +## BUG-1114 | 「那一刻的天空」按钮用的是各平台常见的星芒(AI)图标 + +- 状态:resolved(代码 + 回归测试) +- 首次发现 / 最近更新:2026-09-30 / 2026-09-30 +- 修复:改为自绘「小星座」图标(四颗星以细线相连),与点开后的群星汇聚一致;产品在三个方案中选定。 +- 验证:`surface-feedback-20260929.test.tsx`。 +- 相关记录:BUG-1102 +- 修复版本:分支 `codex/surface-feedback-20260929` diff --git a/frontend/DESIGN.md b/frontend/DESIGN.md index 41f58057..0c514564 100644 --- a/frontend/DESIGN.md +++ b/frontend/DESIGN.md @@ -563,6 +563,10 @@ one skeleton, none of them carrying the sidebar. more. A page's own reassurance copy (the chart page's cost-and-speed eyebrow, for one) stays in the body; it is content, not chrome. +### Short waits on secondary pages (2026-09-30, BUG-1113) + +The chart page, the ephemeris and the report list wait with one motion: the home loading screen's orbit ring at 40px (`OrbitWaiting`), centred in the empty chart grid or in the page. No sentence is shown; the old 「……还没拿到。」 copy is kept only as the screen-reader status. The chart's breathing empty grid stays (BUG-1016 exception). + ### Personal report centre - **Structure:** inside the app shell, not a page of its own. The name 「我的报告」 sits alone in the 46px header. The body opens with the **generate card** (`.report-center-create`, 2026-09-29, product sketch): a centred `--color-canvas` sheet, `--space-6` below the header (BUG-1103: it used to sit flush under it), with a file icon, the title 「完整本命报告」 (what you get), one line 「星盘、力量、大运、年运、瑜伽共 6 章,中英两版;生成后可离开,完成时下方自动出现。」 and the filled 「生成报告」 button (what it does) — title and button no longer say the same thing — the page's only generate entry; the header button was deleted rather than kept as a second one. Below it, 「过往的报告」 heads the **row list** of reports; with none yet, a single quiet line 「还没有个人报告。」 replaces the old second big empty card. The supporting paragraph (「有填报到分钟的出生时间即可生成……」) and the 「共 N 份 · 已完成 N 份」 overview line above it were removed on 2026-09-28 (TASK-self-edit-avatar-menu-20260928 S3, product: 「这里的提示去掉」); the minute requirement still lives in the 生成 button's hover title. @@ -1183,7 +1187,7 @@ D40 / D45 / D60 的表格上方,出生时间不是「已校正」时多一句 ### 位置一:星盘页顶栏按钮 -- 星盘页 `ok` 后立即预取 `/api/birth-sky?subject=<当前人物>` 并在屏外画好 PNG;**画好之后**顶栏右侧才出现一枚描边按钮「那一刻的天空」(` {open ? setWanted(false)} /> : null} ); } + +/** + * A small constellation: four stars joined by thin lines (2026-09-30, product: + * the sparkle icon is on every AI product). It says what the button opens — + * the stars of that moment drawing together — and is drawn here, not taken + * from an icon set. + */ +function ConstellationIcon() { + return ( + + ); +} diff --git a/frontend/src/components/chart-page/chart-dasha-tab.tsx b/frontend/src/components/chart-page/chart-dasha-tab.tsx index 82ee5570..47f7a8c9 100644 --- a/frontend/src/components/chart-page/chart-dasha-tab.tsx +++ b/frontend/src/components/chart-page/chart-dasha-tab.tsx @@ -1,4 +1,5 @@ import { chartLayerFailureCopy } from "@/lib/chart-view-failure"; +import { OrbitWaiting } from "@/components/orbit-waiting"; import type { ChartViewFailureStatus, ChartViewOk } from "@/lib/chart-view-contract"; import { CHARA_ZERO_YEAR_LABEL, CHARA_ZERO_YEAR_NOTE, CHART_VIEW_COPY } from "@/lib/chart-view-labels"; @@ -154,7 +155,9 @@ export function ChartDashaTab({ {charaPending || charaFailure ? (
-

{charaPending ? CHART_VIEW_COPY.waitingLayer : chartLayerFailureCopy(charaFailure!)}

+ {charaPending + ? + :

{chartLayerFailureCopy(charaFailure!)}

}
) : ( diff --git a/frontend/src/components/chart-page/chart-page-skeleton.tsx b/frontend/src/components/chart-page/chart-page-skeleton.tsx index 6eeafc5a..e9b6ee13 100644 --- a/frontend/src/components/chart-page/chart-page-skeleton.tsx +++ b/frontend/src/components/chart-page/chart-page-skeleton.tsx @@ -1,5 +1,6 @@ import { EMPTY_NORTH_INDIAN_CHART, VedicChartSvg } from "@/components/personal-report/vedic-chart-svg"; import { EMPTY_WESTERN_WHEEL, WesternWheelSvg } from "./western-wheel-svg"; +import { OrbitWaiting } from "@/components/orbit-waiting"; /** Only chart positions opt in; the shared page shell remains static. */ export function ChartPageSkeleton({ copy, western = false }: { copy: string; western?: boolean }) { @@ -10,9 +11,10 @@ export function ChartPageSkeleton({ copy, western = false }: { copy: string; wes {western ? : } - {/* Over the empty drawing, not under it: the finished chart has no line - below it any more (BUG-1101), so a line here would make it jump. */} -

{copy}

+ {/* Over the empty drawing, not under it, so the waiting stage and the + finished one are the same height (BUG-1101). An orbit, not a sentence + (2026-09-30); the sentence is for screen readers. */} + ); diff --git a/frontend/src/components/orbit-waiting.tsx b/frontend/src/components/orbit-waiting.tsx new file mode 100644 index 00000000..ec621804 --- /dev/null +++ b/frontend/src/components/orbit-waiting.tsx @@ -0,0 +1,17 @@ +/** + * The one short-wait animation for secondary pages (2026-09-30, product: while + * a chart or a day's ephemeris loads, show motion instead of 「……还没拿到。」). + * It is the home loading screen's orbit ring at a smaller size, so the app has + * one waiting motif. The sentence stays for screen readers only. + */ +export function OrbitWaiting({ label, className = "" }: Readonly<{ label: string; className?: string }>) { + return ( +
+
+ ); +} diff --git a/frontend/src/components/secondary-page-shell.tsx b/frontend/src/components/secondary-page-shell.tsx index 9da7a2bd..5a7cc552 100644 --- a/frontend/src/components/secondary-page-shell.tsx +++ b/frontend/src/components/secondary-page-shell.tsx @@ -3,6 +3,7 @@ import type { ReactNode } from "react"; import { CurrentSubjectSwitcher } from "@/components/current-subject-switcher"; +import { OrbitWaiting } from "@/components/orbit-waiting"; import { SecondaryHeader } from "@/components/secondary-header"; export type SecondaryPageShellProps = { @@ -40,7 +41,7 @@ export function SecondaryPageShell({ actions={actions} />
- {waiting ?

{waiting}

: children} + {waiting ? : children}
); diff --git a/frontend/src/components/starter-home.tsx b/frontend/src/components/starter-home.tsx index 324d28da..0bb1853b 100644 --- a/frontend/src/components/starter-home.tsx +++ b/frontend/src/components/starter-home.tsx @@ -45,16 +45,11 @@ export function StarterGreeting({ starterGreeting, dailyTrend = null, dailyTrend } /* The line under the pills only appears when there is something to do about - it. First runs and finished cases say nothing: the pill already names the - action. The branch reads the entry summary itself, not the derived - start/restart label, and a non-self subject wins because the case summary - belongs to the account owner. */ -export function rectificationEntryHint( - rectificationForSelf: boolean, - summary: RectificationEntrySummary | null, -): string | null { + it: a non-self subject cannot start a rectification. A resumable case says + nothing here any more (2026-09-30, product: 「这个提示可以去掉」); the case + is still in the history list. */ +export function rectificationEntryHint(rectificationForSelf: boolean): string | null { if (!rectificationForSelf) return "生时校正暂时只支持本人。"; - if (summary?.hasResumableCase) return "上次那次校正还没完成,可以在历史对话里接着做。"; return null; } @@ -67,6 +62,7 @@ export type StarterEntriesProps = { readonly rectificationLoading: boolean; readonly rectificationMutationPending: boolean; readonly openRectificationFromHomepage: () => void; + /** Still passed by the page; the hint no longer reads it (2026-09-30). */ readonly rectificationEntrySummary: RectificationEntrySummary | null; readonly rectificationError: string; readonly rectificationSurfaceOpen: boolean; @@ -82,7 +78,6 @@ export function StarterEntries({ rectificationLoading, rectificationMutationPending, openRectificationFromHomepage, - rectificationEntrySummary, rectificationError, rectificationSurfaceOpen, rectificationErrorMessage, @@ -90,7 +85,7 @@ export function StarterEntries({ const subjectId = useCurrentSubject(); const rectificationForSelf = subjectId === "self"; const dailyLabel = natalMinuteAvailable ? "今日星语" : "每日运势"; - const rectificationHint = rectificationEntryHint(rectificationForSelf, rectificationEntrySummary); + const rectificationHint = rectificationEntryHint(rectificationForSelf); return (
diff --git a/frontend/tests/chart-type-picker.test.tsx b/frontend/tests/chart-type-picker.test.tsx index d8f4eaba..964a38e2 100644 --- a/frontend/tests/chart-type-picker.test.tsx +++ b/frontend/tests/chart-type-picker.test.tsx @@ -69,7 +69,9 @@ test("the strip is one sideways-scrolling line and the sheet is a three-column g assert.match(strip, /min-width:\s*0/); assert.match(cssDeclarations(".chart-page-type-grid", globalStyles), /grid-template-columns:\s*repeat\(3, minmax\(0, 1fr\)\)/); assert.match(cssDeclarations(".chart-page-type-menu", globalStyles), /width:\s*44px/); - assert.match(cssDeclarations(".chart-page-type-cell", globalStyles), /min-height:\s*56px/); + // 原值:min-height 56px;新值:64px;原因:2026-09-30 真机截图说明文字被截底,说明行改为最多两行、行高 1.4,格子加高以容纳 + assert.match(cssDeclarations(".chart-page-type-cell", globalStyles), /min-height:\s*64px/); + assert.match(cssDeclarations(".chart-page-type-cell small", globalStyles), /line-clamp:\s*2/); assert.doesNotMatch(globalStyles, /\.chart-page-varga-chips/); }); diff --git a/frontend/tests/rectification-agentic-entry.test.ts b/frontend/tests/rectification-agentic-entry.test.ts index 8978e749..3f58201c 100644 --- a/frontend/tests/rectification-agentic-entry.test.ts +++ b/frontend/tests/rectification-agentic-entry.test.ts @@ -986,8 +986,11 @@ test("homepage CTA is server-driven from the entry summary, not the session list // 「restart」分支对应的那句已删除,改锁 hint 读的是 summary 本身、不读会话列表 // 原因:TASK-starter-home-polish-20260926 D2 删掉「上次已经校正完…」一句;BUG-1041 查明 // `=== "restart"` 从未命中(解析器读错键名)。「CTA 与提示由服务端 entry summary 驱动」这条意图不变。 - assert.match(page, /rectificationEntryHint\(rectificationForSelf, rectificationEntrySummary\)/); - assert.match(page, /summary\?\.hasResumableCase/); + // 原值:首页小字 rectificationEntryHint(rectificationForSelf, rectificationEntrySummary) 读 summary?.hasResumableCase + // 新值:rectificationEntryHint(rectificationForSelf),不再读 summary(未完成校正的提示句已删) + // 原因:2026-09-30 产品「首页的这个提示可以去掉」;CTA 仍由 entry summary 驱动(下方 resolveRectificationEntryAction 断言不变) + assert.match(page, /rectificationEntryHint\(rectificationForSelf\)/); + assert.doesNotMatch(page, /上次那次校正还没完成/); assert.doesNotMatch(page, /rectificationCardAction === "restart"/); assert.match(entryLib, /开始新的生时校正/); assert.match(entryLib, /再次校正/); diff --git a/frontend/tests/starter-home-polish-20260926.test.tsx b/frontend/tests/starter-home-polish-20260926.test.tsx index 451d2ff1..18d3dc17 100644 --- a/frontend/tests/starter-home-polish-20260926.test.tsx +++ b/frontend/tests/starter-home-polish-20260926.test.tsx @@ -73,19 +73,21 @@ test("D2: the line under the pills only appears when the user has something to d assert.doesNotMatch(markup, REMOVED_HINTS); } - // An unfinished case: one sentence, the resumable one. + // 原值:未完成的校正显示一句「上次那次校正还没完成,可以在历史对话里接着做。」 + // 新值:未完成的校正也不显示任何提示 + // 原因:2026-09-30 产品「首页的这个提示可以去掉」;校正会话仍在历史列表里 const resumable = renderToStaticMarkup(); - assert.equal(hintOf(resumable), RESUMABLE_HINT); + assert.equal(hintOf(resumable), null); + assert.doesNotMatch(resumable, new RegExp(RESUMABLE_HINT)); assert.doesNotMatch(resumable, REMOVED_HINTS); }); test("D2: a non-self subject gets the self-only sentence, even with an unfinished case on record", async () => { - assert.equal(rectificationEntryHint(false, null), NON_SELF_HINT); - assert.equal(rectificationEntryHint(false, summaries.resumable), NON_SELF_HINT); - assert.equal(rectificationEntryHint(true, summaries.resumable), RESUMABLE_HINT); - assert.equal(rectificationEntryHint(true, summaries.rectified), null); - assert.equal(rectificationEntryHint(true, summaries.firstRun), null); - assert.equal(rectificationEntryHint(true, null), null); + // 原值:rectificationEntryHint(rectificationForSelf, summary),本人 + 未完成时返回 RESUMABLE_HINT + // 新值:rectificationEntryHint(rectificationForSelf),本人一律无提示;非本人仍是「生时校正暂时只支持本人。」 + // 原因:2026-09-30 产品删除未完成校正提示,入参 summary 不再使用 + assert.equal(rectificationEntryHint(false), NON_SELF_HINT); + assert.equal(rectificationEntryHint(true), null); // Rendered through the real subject store, not just the helper. const harness = createClientLifecycleHarness(); @@ -194,5 +196,6 @@ test("BUG-1041: the homepage parser reads what the entry-summary route actually const resumable = entrySummaryFromResponse(resumableWire); assert.equal(resumable.hasResumableCase, true); assert.equal(resumable.latestResumable?.lastActivityAt, "2026-09-26T00:00:00.000Z"); - assert.equal(rectificationEntryHint(true, resumable), RESUMABLE_HINT); + // 原值:rectificationEntryHint(true, resumable) === RESUMABLE_HINT;新值:解析仍读出 hasResumableCase,但首页不再据此出提示;原因同上 + assert.equal(rectificationEntryHint(true), null); }); diff --git a/frontend/tests/surface-feedback-20260929.test.tsx b/frontend/tests/surface-feedback-20260929.test.tsx index d6da0201..c3025078 100644 --- a/frontend/tests/surface-feedback-20260929.test.tsx +++ b/frontend/tests/surface-feedback-20260929.test.tsx @@ -130,3 +130,34 @@ test("switching editions keeps the reader on the same chapter (BUG-1108)", () => assert.match(view, /drawn=\{drawnThrough !== null && chapter <= drawnThrough\}/); assert.match(view, /useState\(section\.eager \|\| drawn\)/); }); + +// Product feedback 2026-09-30 01:00. +test("the home no longer says an unfinished rectification is waiting in history", async () => { + const { rectificationEntryHint } = await import("../src/components/starter-home"); + assert.equal(rectificationEntryHint(true), null); + assert.doesNotMatch(read("../src/components/starter-home.tsx"), /上次那次校正还没完成/); +}); + +test("chart-type cells show the whole meaning line (two lines, CJK-safe line height)", () => { + const css = read("../src/app/globals.css"); + const small = css.slice(css.indexOf(".chart-page-type-cell small {"), css.indexOf("}", css.indexOf(".chart-page-type-cell small {"))); + assert.match(small, /line-height: 1\.4/); + assert.match(small, /-webkit-line-clamp: 2/); + assert.doesNotMatch(small, /white-space: nowrap/); +}); + +test("chart, ephemeris and report waits show the orbit, the sentence only for screen readers", () => { + const orbit = read("../src/components/orbit-waiting.tsx"); + assert.match(orbit, /\{label\}<\/span>/); + assert.match(orbit, /app-loading-orbit/); + assert.match(read("../src/components/secondary-page-shell.tsx"), //); + assert.match(read("../src/components/chart-page/chart-page-skeleton.tsx"), //); + assert.doesNotMatch(read("../src/components/chart-page/chart-page-skeleton.tsx"), /

{ + const entry = read("../src/components/birth-sky/birth-sky-entry.tsx"); + assert.doesNotMatch(entry, /Sparkles/); + assert.match(entry, //); + assert.equal((entry.match(/