docs(home): record slow-network acceptance
Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
+3
-3
@@ -693,13 +693,13 @@ Agent 的 live 标记只有 `InlineSpinner` 一种。曾经并存的 canvas 小
|
||||
|
||||
校正面的所有等待复用行内等待:进入前的 hydration 在揭幕之前完成,进入后唯一的等待形态是时间线 live 行(含「正在准备下一个问题…」这一条独立 live 行)。区间交付卡只挂在最新那条采用旁白下面,不得留在更早的采集/区分题下。有未答的采集或选择题时卡仍在,「更像这个」置灰并写「先答完上面这道,再选时间」,不得整卡消失。卡上至多三列并排,相同性格句只写一次,点「更像这个」即采用该列分钟,按钮显示「正在采用…」或「已采用」。采用过程中整张卡留在原处,不得因 `busy` 卸掉。采用后前事核对结束走 `verified_idle`:一行收尾文案跟在卡片下面、与助手列对齐,没有 live 行、没有重载、没有采用状态条。卡片与右栏细则见 §11、§12。
|
||||
|
||||
首页只揭幕一次。揭幕前的加载屏分两阶段:先取账户、模型目录与会话列表,再并行取今日星语与校正入口摘要,并预热校正分包;全部就绪或 4 秒预算到期(`BOOTSTRAP_PREPARE_TIMEOUT_MS`)才揭幕。揭幕后不得再出现任何阻塞等待或组件级 spinner:今日星语未到显示静态文案「今天的星语还没写出来。」(不带 `aria-busy`),校正卡用无摘要文案,内容到达后静默替换。登录后的空首页只剩问候语、今日星语卡、生时校正卡和输入框,没有主题卡。切换到消息尚未缓存的会话时消息区留白并只给 `sr-only` 文案,不转圈;揭幕后按侧栏顺序后台预取最近 5 条会话(`SESSION_PREFETCH_COUNT`)让常见切换零等待。轨道环消失后不得再换一套动效继续等。
|
||||
首页只揭幕一次。揭幕前的加载屏先并行取账户 / 会话、模型目录和咨询状态,再等待 lookup、会话详情等依赖数据全部到齐;不设 8 秒硬超时,超过 `HOME_BOOTSTRAP_SLOW_MS = 20_000` 才进入现有全屏错误屏。错误屏的「重试」只取消旧请求、重载 provider 并局部重跑 bootstrap,不调用 `location.reload()`。揭幕后不得再出现任何阻塞等待或组件级 spinner:今日星语未到显示静态文案「今天的星语还没写出来。」(不带 `aria-busy`),校正卡用无摘要文案,内容到达后静默替换。登录后的空首页只剩问候语、今日星语卡、生时校正卡和输入框,没有主题卡。切换到消息尚未缓存的会话时消息区留白并只给 `sr-only` 文案,不转圈;揭幕后按侧栏顺序后台预取最近 5 条会话(`SESSION_PREFETCH_COUNT`)让常见切换零等待。轨道环消失后不得再换一套动效继续等。
|
||||
|
||||
### 首屏死屏兜底
|
||||
|
||||
首页揭幕前的轨道环是纯 CSS。客户端 bundle 没执行、关键 chunk 没加载或没解析成功时,8 秒的云端超时和 4 秒揭幕都不会发生,轨道会一直转。
|
||||
首页揭幕前的轨道环是纯 CSS。客户端 bundle 没执行、关键 chunk 没加载或没解析成功时,应用层 20 秒云端慢网错误屏不会发生,轨道会一直转,交给独立的首屏兜底脚本处理。
|
||||
|
||||
根 layout 里有一段与 bundle 无关的内联经典脚本。它只在同时满足这两条时换掉 `.app-loading-content`:页面上仍有 `.app-loading`,且 `<html data-hydrated>` 不是 `"1"`。时限是 13 秒,大于载入账户的 8 秒加上准备阶段的 4 秒,正常揭幕不会闪出这一屏。应用揭幕(`hydrated` 为真)后,现有首页装配 hook 写下 `data-hydrated="1"`。
|
||||
根 layout 里有一段与 bundle 无关的内联经典脚本。它只在同时满足这两条时换掉 `.app-loading-content`:页面上仍有 `.app-loading`,且 `<html data-hydrated>` 不是 `"1"`。时限是 25 秒,大于应用层 20 秒慢网阈值并留出余量,正常揭幕不会闪出这一屏。应用揭幕(`hydrated` 为真)后,现有首页装配 hook 写下 `data-hydrated="1"`。
|
||||
|
||||
出现时复用 `.app-loading-error` 的卡片,不新增第二套转圈、骨架或自动刷新。
|
||||
|
||||
|
||||
@@ -84,7 +84,6 @@ export function useBillingPanel(input: {
|
||||
readonly initialTab: BillingPaneTab;
|
||||
}) {
|
||||
const seedAccount = input.account;
|
||||
const [accountError, setAccountError] = useState("");
|
||||
const [paymentPackages, setPaymentPackages] = useState<MembershipProduct[]>([]);
|
||||
const [paymentEnabled, setPaymentEnabled] = useState(false);
|
||||
const [packagesError, setPackagesError] = useState("");
|
||||
@@ -160,7 +159,8 @@ export function useBillingPanel(input: {
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
void fetchPackages();
|
||||
const timer = window.setTimeout(() => void fetchPackages(), 0);
|
||||
return () => window.clearTimeout(timer);
|
||||
}, [fetchPackages]);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -294,7 +294,6 @@ export function useBillingPanel(input: {
|
||||
|
||||
return {
|
||||
account: toBillingAccount(seedAccount),
|
||||
accountError,
|
||||
activeSubscription,
|
||||
createPayment,
|
||||
currentProductCode,
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
* Home bootstrap failures are not one "the API is slow" bucket.
|
||||
*
|
||||
* `api-error` is the in-bundle account/session path (HTTP status or the 20s
|
||||
* cloud timeout). The other categories are what the inline dead-screen script
|
||||
* cloud slow-network timeout). The other categories are what the inline dead-screen script
|
||||
* can see before that bundle runs. Observation records only category, build
|
||||
* id, and duration — never a payload, exception string, or birth data.
|
||||
*/
|
||||
@@ -28,7 +28,7 @@ export type HomeBootstrapFailureInput = {
|
||||
resourceFromCache?: boolean;
|
||||
/** HTTP status from an account/session/model request the bundle actually made. */
|
||||
apiStatus?: number | null;
|
||||
/** Bundle-side cloud timeout (page.tsx 8000ms), not the dead-screen timer. */
|
||||
/** Bundle-side cloud slow-network timeout (page.tsx 20_000ms), not the dead-screen timer. */
|
||||
apiTimedOut?: boolean;
|
||||
};
|
||||
|
||||
|
||||
@@ -128,12 +128,11 @@ export function SessionListProvider({ children }: { children: ReactNode }) {
|
||||
const bootRef = useRef<SessionListBoot | null>(null);
|
||||
const controllerRef = useRef<AbortController | null>(null);
|
||||
const reloadRef = useRef<() => void>(() => undefined);
|
||||
const readyPackRef = useRef(createReadyGate());
|
||||
const [readyVersion, forceReadyVersion] = useState(0);
|
||||
const [readyPack, setReadyPack] = useState(createReadyGate);
|
||||
|
||||
useEffect(() => {
|
||||
let disposed = false;
|
||||
const load = (readyPack = readyPackRef.current) => {
|
||||
const load = (nextReadyPack: ReturnType<typeof createReadyGate>) => {
|
||||
controllerRef.current?.abort();
|
||||
const controller = new AbortController();
|
||||
controllerRef.current = controller;
|
||||
@@ -149,22 +148,21 @@ export function SessionListProvider({ children }: { children: ReactNode }) {
|
||||
setAccount(boot.account);
|
||||
setSignedOut(boot.signedOut);
|
||||
setSettled(true);
|
||||
readyPack.resolve();
|
||||
nextReadyPack.resolve();
|
||||
})
|
||||
.catch(() => {
|
||||
if (disposed || controller.signal.aborted || controllerRef.current !== controller) return;
|
||||
bootRef.current = { sessions: [], rawRows: [], cursor: null, account: null, signedOut: false };
|
||||
setSettled(true);
|
||||
readyPack.resolve();
|
||||
nextReadyPack.resolve();
|
||||
});
|
||||
};
|
||||
reloadRef.current = () => {
|
||||
const nextReadyPack = createReadyGate();
|
||||
readyPackRef.current = nextReadyPack;
|
||||
forceReadyVersion((version) => version + 1);
|
||||
setReadyPack(nextReadyPack);
|
||||
load(nextReadyPack);
|
||||
};
|
||||
load();
|
||||
load(readyPack);
|
||||
return () => {
|
||||
disposed = true;
|
||||
reloadRef.current = () => undefined;
|
||||
@@ -180,7 +178,7 @@ export function SessionListProvider({ children }: { children: ReactNode }) {
|
||||
|
||||
const boot = useCallback(() => bootRef.current, []);
|
||||
|
||||
void readyVersion;
|
||||
const ready = readyPack.promise;
|
||||
const value = useMemo<SessionListContextValue>(() => ({
|
||||
sessions,
|
||||
setSessions,
|
||||
@@ -190,7 +188,7 @@ export function SessionListProvider({ children }: { children: ReactNode }) {
|
||||
setAccount,
|
||||
signedOut,
|
||||
settled,
|
||||
ready: readyPackRef.current.promise,
|
||||
ready,
|
||||
boot,
|
||||
reload,
|
||||
registerShellControls,
|
||||
@@ -203,7 +201,7 @@ export function SessionListProvider({ children }: { children: ReactNode }) {
|
||||
sessionsCursor,
|
||||
settled,
|
||||
signedOut,
|
||||
readyVersion,
|
||||
ready,
|
||||
]);
|
||||
|
||||
return (
|
||||
|
||||
@@ -7,7 +7,6 @@ import { EpayConfigurationError, resolveEpayConfig } from "../src/lib/epay/confi
|
||||
import { assertConfiguredEpayUrl, assertPublicEpayGateway, assertPublicGatewayUrl, isPublicEpayAddress } from "../src/lib/epay/gateway-policy";
|
||||
|
||||
const root = new URL("../", import.meta.url);
|
||||
const readProjectFile = (path: string) => readFileSync(new URL(path, root), "utf8");
|
||||
const route = readFileSync(new URL("src/app/api/admin/epay-settings/route.ts", root), "utf8");
|
||||
const management = readFileSync(new URL("src/components/admin/payment-management.tsx", root), "utf8");
|
||||
const createRoute = readFileSync(new URL("src/app/api/payment/epay/create/route.ts", root), "utf8");
|
||||
|
||||
@@ -15,7 +15,7 @@ test("packages load on mount without a redundant account request", () => {
|
||||
// 原值: 挂载并发 fetchAccountData() 与 fetchPackages(),并锁定 Promise.allSettled。
|
||||
// 新值: 挂载只执行 fetchPackages(),账户余额来自首页 bootstrap;成功兑换/支付使用接口返回 credits 通过共享事件同步。
|
||||
// 原因: BUG-1022 / TASK-home-slow-network D5,删除无用的完整 /api/account 请求,避免重复数据库读取。
|
||||
assert.match(hookSource, /void fetchPackages\(\);/);
|
||||
assert.match(hookSource, /setTimeout\(\(\) => void fetchPackages\(\), 0\)/);
|
||||
assert.doesNotMatch(hookSource, /Promise\.allSettled\(\[fetchAccountData\(\), fetchPackages\(\)\]\)/);
|
||||
assert.doesNotMatch(hookSource, /fetchAccountData/);
|
||||
assert.doesNotMatch(hookSource, /fetch\("\/api\/account"/);
|
||||
|
||||
Reference in New Issue
Block a user