文件
hyb-oa/.trellis/spec/hyb-oa/frontend-react-vite-design.md
T
wangsli 8e17e8dfa1 补齐企微历史验收模拟接口隔离(09-06-release-gate-integrity / 09-17-company-loan-principal-repayment)
N3 第三轮候选 98bab397:浏览器 371 通过、1 失败;报销付款与 PWA 升级通过。
桌面企微历史测试整页导航后等待标题超时,快照退回登录页。
确认默认桌面 context 漏掉已有的 Service Worker 隔离;原运行没有请求来源证据,历史唯一根因未证实。
补文件级隔离,验证原导航实际获得该 fixture 的 refresh/profile 响应:200、非 Worker、requestId 对应。
原三项用例、登录/会话、桌面只读、手机布局及截图保持。受管定向 3 项通过,无失败/跳过/重试,环境和清理通过。
应用、资金规则、迁移、门禁范围、预算和全局并发未改。前三轮失败回执保留。
第四轮完整候选结果以新回执为准;独立代码复核及真实还款待验。
仅本地提交,不推送、不部署,不访问生产或真实 OCR。
2026-10-09 09:22:10 +08:00

167 KiB

Frontend React / Vite / Design

Convention: Invoice Detail Selection Has One URL State Source

  • InvoicePoolPage reads its selected invoice from useSearchParams().get("id"). Upload success, list detail selection and return-to-list update that parameter through the router's functional setter, preserving other parameters and the existing replace semantics.
  • Do not mirror the ID into useState and synchronize both directions with effects depending on both values. An upload changes one side first; opposing effects then exchange old values, repeatedly unmounting the detail and making manual correction unusable. A visible OCR banner alone does not prove a usable detail.
  • Keep OCR polling, form edits and invoice permissions unchanged. The management correction case uses a finance identity with invoice.manage; ordinary employee reimbursement uploads remain separate coverage.
  • The retained invoice-ocr-flow.spec.ts correction case must execute in the managed mock/jobs profile: missing fields reject premature availability, correcting them makes the invoice available without reload or forced click, and returning to the list then reopening preserves the result and selected URL. A conditional skip cannot serve as this regression's evidence.

Receipt Verification Dialog

  • Show document nature separately from extracted transaction status. A booked official debit receipt may use the actual booking date and checked-original declaration; do not ask the operator to invent a successful bank status. Show that basis to the reviewer and keep save/review separate from settlement. Non-final receipts or contradictory directions have no manual override.

  • Old evidence needing local document inspection exposes an explicit finance action in the existing dialog; retain history and explain the need to recheck. Reset document-proof choices along with corrections on refresh. Verify the actual desktop/mobile proof entry, required original check, applicable reviewer, final settlement and non-final refusal through the rendered dialogs.

  • When readiness is unmet and an active receipt has a system/key/snapshot failure, the batch banner must direct prerequisite repair, not an unavailable verification dialog. This is display only: a retained failed attachment must not block other sufficient evidence. Refresh stays read-only; controlled reupload retains history.

  • Reuse the receipt card and existing Dialog/Input/Button patterns. No separate receipt center. Show original and proposed summaries; account fields remain masked. Original-file preview keeps its existing authorized path.

  • Unchanged association submits page/item only. Corrections submit changed fields only; never round-trip account masks as bank accounts. Unedited fields are merged against the server's version-bound original evidence, not a client snapshot.

  • Detail query keys include actor/batch/attachment and forward AbortSignal. Disable background form refetch; explicit refresh clears selections, corrections and reason. Identity or batch switches close the dialog, and unmounted mutation callbacks must not refresh another identity's screen.

  • Backend canReview is an eligibility projection, not authorization. Hide review actions when false; actual review still checks role, permission and all participants. Save/review never call payment execution or upload.

Source Of Truth

  • Reminder cooldown hints render server UTC timestamps in Asia/Shanghai with an explicit Beijing-time label and second precision. Reuse formatDisplayDateTime's opt-in includeSeconds; existing callers retain minute precision. Both registered reason and legacy details paths reject malformed/non-finite timestamps without reflecting arbitrary values. Cover a UTC-to-Beijing cross-year case.

Frontend visual and interaction design uses the installed frontend-design skill as the primary design authority. docs/产品设计规则.md remains a product experience and business-boundary fact source, not a visual, brand or aesthetic rulebook. For PC/mobile/WeCom pages, add Playwright or visual verification when reasonable.

This spec keeps hyb-oa-specific React/Vite, shared-component, business-boundary and verification contracts. Do not copy broad aesthetic guidance from frontend-design into this file; use the skill directly for design judgment.

Convention: Reimbursement Payment Handoff Consistency

  • Reimbursement desktop shells and payment detail opt into detail-workspace-grid--payment: a 320px aside at lg and above, single column below lg. Both fill the same outer page width; do not add a reimbursement-only max-w-7xl or centered inner wrapper. Verify both workspace left/right boundaries, reimbursement hero alignment and 320px asides at 1280/1440/1920, plus narrow-screen overflow. Equal aside widths alone do not prove equal page widths; smaller viewports can hide a max-width defect. Leave unrelated detail grids unchanged.

  • BusinessDetailActionPanel({ inlineActions: true }) puts the primary action followed by the More trigger in a wrapping, content-width row. The default layout stays unchanged. Both detail pages use the existing DropdownMenu; menu items are portalled outside the card, so test via page-level menu roles. Keep disabled states, permission conditions, confirmations and mobile payment actions intact. Do not pass known-empty action components as children: a React element that renders null is still a nonempty slot and otherwise creates a phantom divider.

  • Linked reimbursement invoices use a desktop table with number preview, seller, date and right-aligned amount; narrow rows stack without duplicating IDs or preview controls. Keep OCR warnings, unlinked correction cards, attachment delete confirmation and evidence anchors. Test five synthetic invoices, long names/numbers, empty attachments, preview calls, narrow layout and the existing approval/detail consumers.

  • A compact payee-account reveal control stays beside the masked value as a 44px Eye button with title and aria-label 查看完整账号. Preserve role/permission/status/account gates and the reason/audit dialog; icon clicks must not immediately request plaintext. Layout measurements wait for fonts and compare aligned centers; round subpixel box heights to hundredths before checking the 44px minimum (Chromium can return 43.99994 for a CSS 44px target).

  • resolveReimbursementPaymentHandoff({ status, deletedAt, progress }) consumes the server's payment_batch_created, payment_batch_review and payment_and_receipt stages. It is a display projection, not a second payment eligibility or write state machine.

  • Only an explicit current creation stage followed by pending review/payment can advertise creation, and the caller still requires payment.batch.manage. Missing stages, unknown status or contradictory facts never produce a guessed creation action. Cancelled and failed batches use the existing stage explanation, not automatic recreation.

  • Read-only detail states (including reviewing, approved and paid) override global focus caching locally (staleTime: 0, refetch on mount and visibility return). Do not enable whole-app focus refetch or overwrite draft/rejected editors for this fix. A query error must not keep a stale actionable payment panel.

  • PC uses asideContext for current progress after actionPanel, with lg:static on the action card. Payment detail and reimbursement approval share this order; comments remain readable without horizontal overflow. Mobile retains its own safe-area action contract.

  • Test absent batch, review, export, receipt, paid, failed/cancelled, missing progress and no batch-read permission. Test create/review/export with real isolated roles, then return from batch to reimbursement. Include visibility return and refresh; a payment-only test does not prove the source detail is correct.

  • Wrong: approved => create batch, or !targetHref => no batch. Correct: stage-derived next step and one server-authorized destination, including a conservative unknown state.

  • Reimbursement desktop payment links use content width (justify-self-start) and the existing 44px button height, not grid stretching across the aside. The creation label is 创建批次; it navigates to the existing pending view and never creates a batch itself. Keep mobile touch sizing and the generic action-panel layout unchanged. Verify actual desktop bounds and that navigation sends no batch-creation POST.

  • When PaymentBatchesPage retains its lg:hidden mobile action bar, its desktop action card must keep hidden lg:block as well as lg:static; changing stickiness must not remove responsive visibility. The adjacent progress stays visible. Cover the narrow PC route in addition to the dedicated mobile route, including receipt-upload action switching.

Convention: Portal Query Eligibility Is Not Finance Display Eligibility

  • MobilePortalPage loads authenticated GET /portal/home independently of canUseFinance. That flag gates the finance card, not businessHandoffs; the server still filters each item by role, permissions, participation, department and maker-checker.
  • Test employee and CEO separately on PC and mobile, starting in a fresh browser context directly at the destination. Assert the mobile request actually occurs, the authorized link appears, processing removes it after reread, and failures show retry rather than an empty list.
  • A finance mobile mock plus an employee desktop test is not a role-by-device matrix. Real isolated authentication/HTTP/browser checks must exercise employee returned work and CEO independent review without granting either a finance role.
  • Portal response caches are employee-scoped: PC uses ["portal-home", me?.id], mobile uses ["mobile", "portal", "home", me?.id], and both queries require an existing identity. Keep these prefixes so existing invalidation callers still match; do not use previous-key placeholder data. staleTime: 0 causes a refetch but does not isolate identities, and clearing PWA Cache Storage does not clear the singleton React Query cache.
  • Identity keys also require cancellation: both portal query functions consume their signal and pass it through fetchPortalHome(signal?: AbortSignal) to apiRequestData. Otherwise an old request's late 401 can refresh under the new session, return the new employee's payload and cache it under the old employee's key. Validate actual transport cancellation plus A→B→A with the final A response held, not only A→B or successful late responses. Same-identity 401 refresh must still succeed; do not disable authentication refresh globally to make the test pass.
  • Validate account changes within one browser context and document, using the UI logout/login flow rather than clearing caches, reloading or creating a new context. After the second identity is visible, old handoffs must remain absent while its response is held, after an error, through retry and after the old identity's delayed request completes. Assert that a request was actually held so a fast empty result cannot hide stale rendering; cover PC and mobile. Synthetic visibility events must reach the window listener (bubbles: true when dispatched on document). Real-auth fixtures must resolve profile fields from the authenticated ID; request.employee is not the full /me profile.
  • A scoped portal fix does not prove every application query or same-account permission change has correct cache lifecycle. Keep unrelated authentication/cache work separate, and preserve the distinction between synthetic API browser checks, real isolated authentication/HTTP and production validation.

Scenario: Actionable Validation And Selectable Payment Channels

1. Scope / Trigger

  • New validation feedback, disabled selections, or payment creation controls.

2. Signatures

  • resolveErrorMessage(ApiError) prefers the explicit domain-scoped reason map.
  • PaymentReadiness is { ready, blockers: [{ reasonCode, entityId?, entityCode? }] }.
  • Candidate query identity includes payment channel; combination query identity includes selected IDs and channel. Never reuse another selection's readiness.

3. Contracts

  • Unknown VALID_001 uses actionable safe fallback, with a bounded diagnostic request ID only when present. Do not render raw message/details, IDs or paths.
  • Keep reason registration, client map and fixed concrete-text tests together. Structured invoice-count rendering remains narrowed and must tolerate malformed fields without exposing payloads.
  • Blocker text stays beside its row/action; use persistent text, role=alert or aria-live, and aria-describedby on disabled actions. Toast supplements, never replaces, the persistent explanation. Normal rows add no success noise.
  • The payment channel selector is available before any selection, otherwise bank-blocked rows make other supported channels unreachable.
  • A not-ready unselected row cannot be selected; a previously selected row that becomes invalid can still be deselected. Pending writes may disable edits.
  • Pending/error/not-ready combination state disables creation, and the mutation handler checks the same result. Server create rechecks independently.
  • Persisted payment failure summaries are not UI copy. Use the failure projection in paymentBatchDisplay.ts, associating risk reasons by batch ID and item ID; historical/unknown failure text and execution-risk details use safe fallback. Do not announce complete success when the execution result is failed.
  • Only authorized maintainers may receive maintenance links; other roles receive recovery-owner text. Do not instruct users to falsify their actual bank details.

4. Validation & Error Matrix

  • Missing company/bank/card or unsupported bank -> exact local row explanation.
  • Cross-company combination -> action-local explanation and disabled button.
  • Concurrent create refusal -> remain on the page with action-local safe text.
  • Unknown reason -> safe fallback, optional request ID, no empty identifier label.

5. Good/Base/Bad Cases

  • Good: switch to an existing non-bank channel before selection; deselect a row whose readiness changed without losing the rest of the selection.
  • Base: normal bank fixture remains selectable and submits for independent review.
  • Bad: hide the channel selector until a selectable bank row is chosen, or disable an already-selected invalid checkbox permanently.

6. Tests Required

  • Concrete message fixtures, unknown reasons with/without request IDs, and malformed structured invoice details; no broad message regex substitutions.
  • Local synthetic Playwright covers omissions, channel limits, combination block, concurrent failure, keyboard selection, accessible descriptions, channel-before- selection and deselection after invalidation. No real payment data is required.

7. Wrong vs Correct

  • Wrong: disabled={!candidateReady} for every checkbox.
  • Correct: block only new invalid selections while preserving deselection, and share the reason mapping between row, combination and create errors.

Scenario: PC Shell And Detail Breakpoint Coordination

1. Scope / Trigger

  • Trigger: changing the desktop sidebar, app-shell content offset, topbar drawer entry, detail two-column grid or sticky detail action panel.
  • Purpose: prevent the shell and detail workspace from expanding at the same narrow breakpoint and leaving the business main column unusably small.

2. Signatures

  • Fixed sidebar: .app-shell-sidebar uses xl:flex.
  • Main content offset: .app-shell-main uses xl:ml-72 outside mobile routes.
  • Topbar drawer trigger: aria-label="打开菜单" uses xl:hidden.
  • Detail workspace and action panel remain lg:grid-cols-* and lg:sticky unless a later task changes the complete contract.

3. Contracts

  • Sidebar visibility, main-content offset and drawer-trigger visibility switch at one shared breakpoint; never change only one of the three.
  • Evaluate detail widths after subtracting the fixed sidebar and page padding, not as a fraction of the full viewport.
  • At the narrowest viewport where the fixed sidebar appears, the main detail column is at least 560px, the action column is at least 280px, and the document has no horizontal overflow beyond a 2px rendering tolerance.
  • Business identifiers and normal company-time date strings use DOM line-count or geometry assertions; position: sticky alone is not proof that the layout is usable.
  • A Playwright spec writes screenshots only to its own current output directory. Never reuse an archived Trellis task's evidence directory as a live test target.
  • When comparing alignment inside an animated ancestor (for example .page-scaffold's page-reveal), read all relevant getBoundingClientRect() values in one synchronous browser evaluate/evaluateAll callback. Sequential boundingBox() calls, or calls grouped with Promise.all, can observe different animation frames. Keep the original alignment assertions and pixel tolerances; do not hide the race with sleeps, retries or disabled page animations.

4. Validation & Error Matrix

  • Fixed sidebar visible while drawer trigger is also visible -> breakpoint contract failure.
  • Main content has the sidebar offset while the sidebar is hidden -> breakpoint contract failure.
  • Two columns exist but either column is below its minimum width -> layout failure, even if sticky CSS passes.
  • documentElement.scrollWidth > clientWidth + 2 -> horizontal-overflow failure.
  • A routine test run modifies screenshots under another task's evidence directory -> output-contract failure; move the writer before regenerating any evidence.

5. Good/Base/Bad Cases

  • Good: 1100px uses the topbar drawer and keeps the detail grid; 1280px shows the fixed sidebar and still meets both column-width thresholds.
  • Base: mobile routes continue to use their existing mobile shell and sticky bottom actions.
  • Bad: move the detail grid from xl to lg while leaving the 288px app sidebar and content offset at lg, then validate only position: sticky.

6. Tests Required

  • A source-structure contract asserts the three shell classes use xl, while the shared detail grid and action panel retain lg.
  • A focused Playwright regression at 1280px measures both detail columns, horizontal overflow and representative identifier/date line counts using realistic-length fixtures.
  • At least one synthetic workflow detail and one asset detail cover the shared layout; existing mobile regression remains green.
  • After screenshot-producing tests, assert archived task evidence remains clean in git status; current-suite screenshots may be retained only under that suite's own output directory.

7. Wrong vs Correct

Wrong

<aside className="lg:flex lg:flex-col" />
<main className="lg:ml-72" />
<button className="lg:hidden" aria-label="打开菜单" />
.detail-workspace-grid { @apply lg:grid-cols-[minmax(0,1.45fr),minmax(280px,0.72fr)]; }

Correct

<aside className="xl:flex xl:flex-col" />
<main className="xl:ml-72" />
<button className="xl:hidden" aria-label="打开菜单" />
.detail-workspace-grid { @apply lg:grid-cols-[minmax(0,1.45fr),minmax(280px,0.72fr)]; }

Rules

  • Use existing React/Vite/Tailwind patterns and shared components before adding page-local UI systems.
  • When frontend-design and older project visual rules conflict, follow frontend-design for visual direction while preserving security, privacy, Trellis, deployment and business data boundaries.
  • Default product framing is a small-team Chinese workbench for domestic tech startups with fewer than 10 people. User-facing copy should be natural Chinese; avoid English product words such as Portal, Dashboard and Workflow unless they are internal test fixtures or technical identifiers.
  • PC pages should keep dense but orderly OA layouts: clear header, filters, table/list, status, details and predictable actions.
  • Mobile pages must reuse the existing unified mobile shell, card/list rows, safe-area and sticky action patterns unless the current task intentionally redesigns them with frontend-design.
  • Do not add decorative landing-page styling to operational OA pages.
  • The local Vite /api proxy must preserve the browser-facing Host when running Playwright against 127.0.0.1:5173; backend auth refresh validates Origin/Referer against the request host when APP_ORIGIN is not set. Rewriting Host to 127.0.0.1:3000 makes full-page reloads lose the session with AUTH_005.
  • Run cd client && npm run typecheck && npm run build for frontend changes.
  • Any task that changes user-visible frontend copy must also run node scripts/qa/check-ui-design-rules.mjs --strict before commit and again before deployment. Treat the public bundle copy scan as a post-deploy witness, not the first place to discover forbidden user-facing terms. For reimbursement/invoice flows, regression assertions cover all four internal terms: 绑定整单, 不绑定明细, 发票池, and OCR.

Scenario: Playwright API Mocks Against A Production PWA Build

1. Scope / Trigger

  • Trigger: an E2E spec uses page.route or browserContext.route to mock authentication or business APIs and may run against client/dist, Vite preview or another production-built PWA runner.
  • Purpose: keep the route fixture as the only network fact source after full navigation, reload or repeated page bootstrap.

2. Signatures

  • Spec-level context contract: test.use({ serviceWorkers: "block" }).
  • Production-build witness: single worker, zero retries, same business assertions and full-navigation sequence as the original spec.

3. Contracts

  • A spec that depends on Playwright route mocks must block Service Workers when it can run a PWA build. Do not rely on Service Worker installation or takeover timing to keep requests visible to page.route.
  • Cover both default { page } fixtures and manually created contexts. A mobile browser.newContext({ serviceWorkers: "block" }) does not protect the default desktop fixture; register full-navigation refresh/profile response witnesses before navigating and verify their synthetic source when diagnosing session loss.
  • Vite dev success is not equivalent evidence: the generated PWA worker is a production-build behavior and may be absent in dev.
  • Blocking the worker is a fixture boundary, not an authentication bypass. The spec must retain session hint, refresh, /api/me, full navigation and business assertions.
  • Tests whose purpose is offline/PWA behavior may allow Service Workers, but they must not also treat page-level API mocks as a complete network witness.

4. Validation & Error Matrix

  • Dev passes, production build stalls during session bootstrap -> inspect the Service Worker policy before changing authentication code.
  • serviceWorkers=allow and a mocked API request becomes incomplete after a later navigation -> fixture/harness candidate; prove causality with the single-variable blocked control.
  • Blocked control still reproduces a real refresh failure -> do not classify as fixture-only; continue with loopback real-backend session evidence.
  • A fix deletes navigation/session assertions or adds retry -> invalid fix.

5. Good/Base/Bad Cases

  • Good: a mock-heavy ledger spec blocks Service Workers and passes the same repeated full-navigation sequence against the production build.
  • Base: a focused PWA offline spec allows the worker and uses a network setup designed for worker-owned requests.
  • Bad: accept Vite dev green as proof that a production-built PWA runner has no session problem, or modify AuthContext to accommodate a route-fixture race.

6. Tests Required

  • Reproduce once against the production build with single worker and zero retries; record only safe request path/status and cookie presence.
  • Add the blocked-worker control without changing navigation or assertions, then rerun the historically failing titles and the complete target spec.
  • Run an independent frontend bootstrap refresh test and a loopback backend refresh/session regression before declaring the issue fixture-only.

7. Wrong vs Correct

Wrong

import { test } from "@playwright/test";

test.beforeEach(async ({ page }) => {
  await page.route("**/api/**", mockApi);
});

Correct

import { test } from "@playwright/test";

test.use({ serviceWorkers: "block" });

test.beforeEach(async ({ page }) => {
  await page.route("**/api/**", mockApi);
});

Scenario: Production PWA Cross-Version Upgrade Witness

1. Scope / Trigger

  • Trigger: a deployment, PWA change or cache-related incident needs evidence that an existing browser profile can move from an old production build to a new build.
  • Purpose: verify the real cross-version path; a fresh context that opens only the new build is not upgrade evidence.

2. Signatures

  • Standalone runner: bash scripts/qa/run-pwa-cross-version-upgrade.sh.
  • Full-suite owner: bash scripts/qa/run-web-gates.sh full-local; both runners use scripts/qa/pwa-cross-version-fixture.sh to prepare the same temporary A/B inputs. Missing inputs remain a collection error, never a skip or implicit exclusion.
  • Build inputs: two distinct 12-character synthetic VITE_BUILD_SHA values, written to temporary outDirs rather than client/dist.
  • Every Vite production-build subprocess sets NODE_ENV=production explicitly, including A/B fixtures and the managed client build. The application server may remain in NODE_ENV=test; inheriting it into Vite can emit import.meta.env.DEV=true and suppress real Service Worker registration. Keep the application registration guard and upgrade assertions intact.
  • Screenshot artifacts must not overwrite registered historical evidence. The internal-readiness and finance-wave4 specs use QA_VISUAL_OUTPUT_DIR as a root and create unique per-spec directories with mkdtempSync; standalone defaults also use fresh ignored directories, never the historical 07-18 directory. These are output-path changes, not authentication or assertion changes. Both discovery and full-browser runners supply the current report's visual root.
  • Full-local's global environment is runner-owned and natively online without public egress. Its separate unmocked fresh-context login probe requires a visible form, zero refresh requests and independently recorded native online samples. A visible form after refresh 401 is not equivalent evidence. Do not instrument AuthContext or override navigator.onLine for this check; explicit offline business contexts remain legitimate. Environment probes are not additional business cases, and invalid environment observations cannot be promoted into a product failure inventory.
  • Browser contract: serviceWorkers: "allow", one dedicated random-port loopback HTTP origin and one BrowserContext; the spec declares serial execution. The standalone runner keeps one worker; full-local retains its configured file-level concurrency and explicitly passes zero retries even under CI. Do not serialize the whole suite without evidence of interference and separate approval.

3. Contracts

  • Build A must first prove its document SHA, active Service Worker controller, Workbox precache and at least one cached A-only hashed JS/CSS asset. If that precondition is missing, stop as PAUSED; do not switch to B.
  • Synthetic APIs must be served by a loopback HTTP harness. Do not use page.route or context.route, because a real Service Worker owns worker-originated requests.
  • Switch the same server/origin from build A to B, close the A page while retaining the BrowserContext, then revisit the same route. Do not change port, hostname or profile.
  • After switching to B, test code must not reload, clear cookie/cache, unregister a worker, create another BrowserContext or emulate hard refresh. Application-owned update behavior is the subject under test.
  • Wait for B's document version with a locator attribute assertion within the existing convergence deadline. A raw page.evaluate inside expect.poll can throw when the application's expected navigation destroys the old execution context. Keep the version expectation, deadline and all joint witnesses; do not hide arbitrary errors or add test-owned navigation.
  • Prove B with a joint witness: controller change or worker version change, controller equals the active registration, browser-fetched no-store sw.js fingerprint matches build B, document SHA is B and a B-only hashed asset loaded successfully. The constant /sw.js URL alone is not version evidence.
  • Assert the business route and synthetic session survive, old-only hashed assets leave Cache Storage, and there are no login fallbacks, JS/CSS 404s, chunk/preload errors, page errors or reload loops.
  • If the first B witness fails, freeze the safe ledger and stop. Do not add a second navigation or reload to turn the first failure into an intermittent result.

4. Validation & Error Matrix

  • Build-command capture tests must prove both A/B subprocesses receive NODE_ENV=production, even when the parent is a test server. A static command check does not replace the real same-profile browser upgrade witness.
  • A build artifacts are not distinct -> PAUSED; the harness cannot prove an upgrade.
  • A controller or precache is absent -> PAUSED; B would be a fresh-install observation.
  • A is established and B does not converge without a prohibited action -> PRODUCT_FINDING; open a separate PWA product task before changing runtime code.
  • Same-profile B witness converges with the complete joint evidence and all negative assertions -> PASS.
  • Fresh B context passes while the same-profile witness fails -> still PRODUCT_FINDING; fresh install does not override upgrade failure.

5. Good/Base/Bad Cases

  • Good: A controls and caches the page, the same context revisits after an in-place B switch, and auto-update converges to B with zero forced actions.
  • Base: a fresh B context is used only as an explicitly labelled harness sanity check and does not satisfy acceptance.
  • Bad: start preview on B in a new browser profile, observe the latest shell and report that old users will upgrade correctly.
  • Bad: B initially remains on A, then the test calls page.reload() once more and reports the recovered state as PASS.

6. Tests Required

  • Run bash scripts/qa/run-pwa-cross-version-upgrade.sh; it must use temporary A/B production builds, loopback-only networking, single worker and zero retries. Full-local must supply the same inputs before collection and include this spec in its all-non-manual suite; its file-level worker count is not reduced for PWA.
  • A ledger assertions: document SHA A, active controller, fetched A worker fingerprint match, Workbox precache and cached A-only asset count greater than zero.
  • B ledger assertions: controller change, active B controller, fetched B worker fingerprint match, document SHA B, B-only loaded asset count greater than zero and A-only cache count zero.
  • Negative assertions: zero login fallback, static 404, page/chunk/preload error and prohibited B-stage test actions; route and synthetic refresh/session remain valid.
  • Run cd client && npm run typecheck && npm run build plus the current Trellis task/process/governance/sensitive gates.

7. Wrong vs Correct

Wrong

const context = await browser.newContext();
await context.newPage().then((page) => page.goto(buildBUrl));
// Latest build loaded, therefore upgrade passes.

Correct

// One context and one origin: establish A controller/precache first.
await pageA.close();
switchSameOriginToBuildB();
const pageB = await context.newPage();
await pageB.goto(`${origin}/portal`);
// Observe application-owned convergence; do not reload or clear browser state.

Scenario: UTC Date-Time Display In The Company Time Zone

1. Scope / Trigger

  • Trigger: a frontend page displays an API timestamp such as attachment upload, approval action, payment or audit time. API timestamps remain ISO 8601 UTC; the company-facing OA displays date-times in Asia/Shanghai.

2. Signatures

  • Shared formatter: formatDisplayDateTime(value: string | null | undefined): string.
  • Date-only values continue to use formatDisplayDate; do not turn business dates such as reimbursement or invoice dates into timezone-shifted instants.

3. Contracts

  • Date-time pages reuse client/src/lib/displayFormat.ts; do not add a page-local formatter for the same contract.
  • Intl.DateTimeFormat must set timeZone: "Asia/Shanghai" explicitly so the result does not depend on the browser, workstation or CI timezone.
  • Database storage and API ISO strings remain unchanged.

4. Validation & Error Matrix

  • Missing value -> -.
  • Invalid date-time -> the shared display fallback; never throw during render.
  • Valid UTC value -> Shanghai calendar date and clock time.
  • Browser outside China -> the same Shanghai result as a browser in China.

5. Good/Base/Bad Cases

  • Good: 2026-05-27T09:10:00.000Z displays as 2026年5月27日 17:10.
  • Base: a null attachment or approval time displays -.
  • Bad: value.slice(0, 19).replace("T", " "), which presents UTC clock fields as if they were local time.

6. Tests Required

  • Focused Playwright uses synthetic UTC data and a browser timezoneId outside Shanghai, then asserts the exact Shanghai attachment/approval time.
  • Run cd client && npm run typecheck && npm run build.

7. Wrong vs Correct

Wrong

const displayed = value.slice(0, 19).replace("T", " ");

Correct

const displayed = formatDisplayDateTime(value);

Scenario: Shared RMB Display

1. Scope / Trigger

  • Trigger: a reimbursement, invoice, fee-control or payment UI displays a CNY amount received as a string, number or nullable field.

2. Signatures

  • Shared formatter: formatMoney(value: string | number | null | undefined): string in client/src/lib/displayFormat.ts.

3. Contracts

  • Reuse the shared formatter across PC and mobile consumers; do not add page-local Intl.NumberFormat helpers for the same amount.
  • Valid amounts display the CNY symbol, thousands separators and exactly two decimal places. Missing, empty or non-finite values display -; they must not become ¥0.00.

4. Validation & Error Matrix

  • 1860 / "1860" -> ¥1,860.00.
  • "550.5" -> ¥550.50.
  • null, "", invalid text or a non-finite number -> -.

5. Good/Base/Bad Cases

  • Good: a PC detail total, mobile detail total and invoice summary all use formatMoney.
  • Base: a real zero amount displays ¥0.00.
  • Bad: use string interpolation such as `¥${amount}` or coerce a missing amount with Number(value ?? 0).

6. Tests Required

  • Focused frontend regression covers integer, fractional and thousands-separated amounts on PC and mobile.
  • Run cd client && npm run typecheck && npm run build.

7. Wrong vs Correct

Wrong

<span>¥{detail.totalAmount ?? "-"}</span>

Correct

<span>{formatMoney(detail.totalAmount)}</span>

Convention: Frontend Design Token Sources

What: Runtime visual tokens use the sources that are actually consumed by the frontend build. CSS custom properties live in client/src/design/theme.css, which is imported by client/src/index.css; Tailwind theme values live in client/tailwind.config.js.

Contracts:

  • Do not maintain a parallel TypeScript token object when no component, build plugin, generator or synchronization test consumes it.
  • Before adding, replacing or deleting a token source, search direct imports, type-only imports, import.meta.glob, generators, copy/sync scripts and build configuration.
  • Historical Issue CSV and archived Trellis references describe their original delivery state; they are not current runtime consumers and must not be rewritten merely because a source file is later removed.
  • If typed tokens become necessary later, introduce them with explicit runtime consumers or a generated single-source contract plus a drift regression test; do not manually duplicate CSS values.

Why: Unconsumed token mirrors drift from the CSS that actually renders the product, creating a misleading second fact source and unnecessary maintenance.

Tests Required:

  • Exact consumer search for the changed token module or symbol.
  • cd client && npm run typecheck
  • cd client && npm run build

Convention: Mature Enterprise Page Baseline

What: From 05-30-unified-page-design-review, hyb-oa PC/mobile pages use Feishu approval/workbench mental models plus Ant Design / Ant Design Pro page patterns as the mature enterprise baseline. This is a product/interaction reference, not a copy of Feishu or Ant Design branding.

Global contracts:

  • Interaction innovation is off by default for OA, approval, finance, contract, asset and management surfaces. Prefer established Feishu / Ant Design style patterns for page layout, action placement, button naming, tabs, filters, detail panels, confirmations and more menus. Project-specific differences should simplify or hide low-frequency work for small teams, not invent new controls or unusual action flows.
  • Page titles default to the route name only, such as 工作台, 待我审批, 报销, 合同, 采购, 资产, 个人设置 and 通知中心. Page subtitles are off by default and appear only for exception, empty, permission-denied, Enterprise WeChat/OA boundary, external-system boundary or irreversible-action explanations.
  • Normal workflows should not be explained in default-visible copy. Do not add subtitles, card descriptions, helper rows or notice bars that restate obvious actions such as "待我审批是处理工作", "查看通知消息", "先核对再处理" or "去处理要做的事".
  • Header primary actions are reserved for creation or a real page-level primary action: 新建报销, 新建采购, 新增合同, 新增资产, 新建项目. Workbench, pending-task, notification and read-only record pages must not force a header button for visual symmetry.
  • PC page header actions follow a strict mature enterprise rule: the title right side may have zero buttons, one page-level primary action, or one primary action plus one 更多. Do not place 筛选, 导出, 刷新, 显示测试数据, category managers, export tools or other list-level tools as persistent header-right buttons. Put those in the list toolbar, filter panel, explicit management panel, row action or detail action area.
  • Button labels use common enterprise verbs: 处理, 去处理, 查看详情, 查看全部, 查看进度, 继续编辑, 新建, 新增, 提交, 保存, 取消, 撤回, 作废, 归档, 更多. Do not use slogan-like or invented labels such as 处理最急的一件.
  • Sidebar entries are role-aware common work entries, not an all-permissions module catalog. Direct entries may include 工作台, 待我审批, 报销, 采购, 合同, 项目, 资产, 付款, 应付账款, 票据, 团队, 请假记录 and 管理 when the current role has real use. Low-frequency, frozen, staged or troubleshooting routes stay in 更多 or the 管理 landing page.
  • List pages use a right-side operation column or card tail. Default row actions are 查看详情, 处理, 继续编辑 or 查看进度; low-frequency or risky actions such as export, batch, void, download, payment and sensitive attachment handling move to 更多, clearly named permissioned actions, management entries or the detail page.
  • Business ledger pages use the same shape: page title and optional creation action, visible common filters, table/card list and right-side operation column. Do not use a generic 台账工具 entry; expose low-frequency abilities through concrete names such as 导出, 统计, 筛选, 导入, 新建方式, 分类管理, 批量处理 or 更多.
  • Business ledger first screens are single-column list pages. Do not use a default left-list/right-detail workspace for procurement, contracts, projects, assets, payments, AP or invoices. Open details through a detail route, drawer or explicit detail/edit area after the user chooses a row; the initial list page must not auto-select the first item just to fill a second column.
  • Ledger status summaries use tabs, segmented controls or compact chips with counts. Do not place four or five large status cards above ordinary business lists, and do not put explanatory sentences inside status cards.
  • Ledger filters fit the first PC row around search, status and scope when those capabilities exist. Supplier, legal entity, cost center, date ranges, contract type, sensitivity, export and test-data toggles belong in 筛选, 更多, clearly named role-only actions or a tab-specific advanced area; default-visible filters should not occupy multiple rows.
  • List toolbars may show one or two high-frequency controls beside common filters. Low-frequency navigation, export, expiry lists, trial-data toggles and troubleshooting actions should collapse into 更多 or advanced filters instead of forming a second row of persistent buttons.
  • Business list columns show object identity, counterpart/person, amount/date, state and operation. 下一步 is not a default list column or right-side help panel. Use short state labels such as 待补材料 or 待你处理; longer guidance belongs in detail pages, exception blocks or confirmations.
  • PC detail pages use an Ant Design Pro style detail workspace: top summary, main facts/details/evidence, right-side state and action panel, and bottom history. Do not build PC details as a free stack of equal-weight large cards.
  • Workflow approval details use one action bar, not vertical action cards: 通过 is the primary action, 退回 and 驳回 are secondary actions, and 转审 defaults to 更多. Actions that need a reason open a reason input and confirmation after click. Confirmation must include the current object summary and consequences.
  • Mobile pages use top status/focus content, sectioned single-column details or card lists, and one bottom safe-area primary action. Secondary, destructive, approval, payment and transfer actions go into 更多 or a confirmation panel; do not squeeze multiple risky actions into the bottom bar.
  • Normal empty sections should disappear when they have no useful content. Empty states only explain current filter/status absence and one real next step.

Page map:

Surface Template First-screen content Primary action rule
工作台 工作台总览 Compact counts, common entries, real reminders No forced header action. Use 去处理, 新建报销, 查看全部 inside relevant sections; do not show helper sentences for ordinary items.
待我审批 任务处理列表 待处理 / 已处理 tabs, count, minimal filters, task list Row action is 处理; 已处理记录 is a tab, not a separate header button.
审批详情 详情工作区 Summary, facts/evidence, right summary/action panel, history One action bar: primary 通过, secondary 退回/驳回, 转审 in 更多.
报销 / 采购 / 合同 / 项目 / 资产 / 付款 / 应付账款 / 票据 业务台账列表 One-row common filters plus table/card list Header creation only when the route owns creation; risky tools use 更多, clearly named role-only actions or detail pages; no default left-list/right-detail split.
Business details 详情工作区 Summary, facts, evidence, relations, history Right-side state/action panel on PC; bottom primary action on mobile.
管理 管理配置页 Grouped landing: 人员组织, 审批流程, 基础资料, 系统设置, 审计排障 Management subpages use search/filter, table and right operation column.
个人设置 管理配置页 variant 个人资料, 账号安全, 通知偏好, 企业微信绑定 No employee dossier, permission matrix or org maintenance in the default profile path.
通知中心 业务台账列表 variant Event inbox for state changes, reminders, failures and sync results Notification click deep-links to business detail, workflow task or failure explanation; never duplicates 待我审批.
请假记录 / 考勤 业务台账列表 or record view Enterprise WeChat-owned records, mirror status, exceptions and audit Ordinary users do not see OA daily leave/punch approval creation; OA emergency entry only for authorized emergency/admin scope.
Mobile routes Mobile expression of the same template Top status/focus, card list or sectioned detail One bottom safe-area primary action; secondary actions in 更多 or confirmation.

Scenario: Unified Reimbursement Object Workbench

1. Scope / Trigger

  • Trigger: changing PC/mobile reimbursement edit, read-only or approval detail, or changing workflow task routing for a reimbursement entity.
  • Purpose: keep one interactive business-object structure while allowing different desktop and mobile layout skeletons and different injected capabilities.

2. Signatures

  • Business routes: /reimbursements/:reimbursementId and /mobile/reimbursements/:reimbursementId.
  • Approval routes: /workflow/tasks/:taskId and /mobile/approvals/:taskId.
  • Dispatchers: WorkflowTaskRoutePage and MobileWorkflowTaskRoutePage fetch one authorized WorkflowTaskDetail, then select the reimbursement adapter or the generic non-reimbursement detail.
  • Pure layout shells: ReimbursementDesktopWorkspaceShell and ReimbursementMobileWorkspaceShell.
  • Shared model/capability types: ReimbursementWorkspaceModel, ReimbursementWorkspaceSlots and ReimbursementEvidenceCapabilities.
  • Evidence renderers: desktop expense table, mobile expense cards, independent invoice/attachment groups, and ReimbursementReturnReferencePicker inside the expanded return action.
  • Structural preflight: node scripts/qa/check-ui-design-rules.mjs --strict.

3. Contracts

  • Approval links keep the task URL. A dispatcher recognizes entityType=reimbursement and renders the reimbursement approval adapter directly; it never mounts the generic detail first and never requires a 查看报销单 detour for core evidence.
  • A task dispatcher owns the single task-detail query and passes the same result plus controlled refetch to the selected renderer. The reimbursement adapter may fetch current reimbursement detail but must not fetch the same task detail again.
  • Desktop and mobile shells share the same model, section order, ReimbursementItemEvidenceList and capability contract. They may use different DOM/layout skeletons: desktop main evidence plus one sticky command panel; mobile focus cards plus a bottom safe-area action surface.
  • Shells compose ready data and slots only. They contain no useQuery, useMutation, API/fetch call, role/permission inference, workflow/reimbursement state-machine decision, mode === ... comparison or isMobile business branch. check-ui-design-rules.mjs --strict enforces this boundary and rejects a dormant reimbursement branch in the generic approval pages.
  • The shared evidence component receives a minimal projection. Desktop renders one expense table; mobile renders expense cards from the same item projection. Invoices and supplementary attachments render after expenses as separate material groups rather than nested item cards. The table/card shows only invoice count or short status; current item-to-invoice ownership remains in the projection and server contract.
  • Open/edit/link/unlink behavior exists only through callbacks; the component never infers capabilities from roles. Approval and read-only expense rows have zero business-action controls. Edit injects one item-edit entry, with delete inside that edit flow, plus material maintenance/submission capabilities.
  • A material file name or file row is the single per-file open control. Do not add adjacent 查看原件 or 查看附件 buttons. Unlinked or blocking invoice recovery remains available in the material group, and an optional 查看完整材料 stays a section-level secondary action.
  • Item/invoice return references are not persistent evidence capabilities. Only the expanded return editor mounts ReimbursementReturnReferencePicker; abnormal candidates appear first, normal candidates require 展开全部, and selecting a candidate appends an editable reference to the return comment.
  • Submission-time fee-standard and budget facts come from the workflow context snapshot. Current item/invoice grouping, amount evidence and attachments come from the reimbursement detail. Approval pages do not call reimbursement precheck or present current budget data as a submission snapshot.
  • Reimbursement approval actions fail closed until workflow snapshot parsing and current reimbursement evidence are ready. Historical missing budget snapshots are explained; malformed snapshots disable all decision paths.
  • The normal desktop reimbursement decision panel stays quiet: do not repeat the current node, workflow status, approval role or a successful readiness message beside the actions. When approval is fail-closed, render the one accurate blocking reason immediately before the disabled action group and reuse that same reason projection in the action handler's secondary guard. A missing-budget notice remains independent and appears only for budgetCheckStatus=missing.
  • Mobile workflow caching uses an explicit persisted projection. Reimbursement items, invoices, attachments, amount evidence, material access data, workflow snapshot checks and shared component projections do not enter IndexedDB. Offline reimbursement approve/reject/return/draft is forbidden, while other workflow entity offline behavior remains unchanged.
  • The information order is compact header and reason, expense items, invoices, supplementary attachments, real issues/differences when present, then history/progress. Normal state creates no 0 条 check card. Issue copy identifies the object, reason, impact and next step rather than exposing internal counters alone.
  • Expense totals in the header, desktop table footer, mobile summary row and print view all display the server totalAmount. A shared workspace-model boundary may sum strictly parsed integer minor units only to classify integrity; that derived sum is never rendered or submitted. A mismatch or unparseable amount is a data-integrity failure, uses distinct non-color-only copy and fails reimbursement approval closed; normal records show no difference explanation.
  • Reimbursement workflow history answers approvers “who acted, when and with what comment”. Desktop uses the right aside: non-sticky decision panel, current processing progress, then full history; the main column receives no dormant timeline slot. Mobile keeps evidence, action explanation, current progress and history in one column with necessary safe-area actions.
  • ProcessingProgress remains separate from workflow history and may include future stages. An authorized batch link may be promoted into the action panel; pass that exact suppressedTargetHref to progress so it appears only once. Do not remove historical facts as duplicates or mistake an absent authorized link for an absent batch.
  • MobileStickyActionBar and MOBILE_STICKY_ACTION_PAGE_CLASS are one contract. Add the page class only when mobileActions exists; omitting it lets page-scaffold transform capture the fixed bar, while adding it without a bar leaves false bottom padding.
  • /reimbursements/:id/print remains a purpose-built print/PDF view. It is not a fourth interactive workbench and receives no workbench mutation.

4. Validation & Error Matrix

  • Authorized task resolves to reimbursement -> dispatcher renders the approval adapter from the one task-detail result.
  • Task resolves to leave/attendance/purchase/travel -> generic detail remains active, including its existing mobile offline approve/reject draft behavior.
  • Current reimbursement detail, critical check or snapshot parsing is absent/invalid -> all reimbursement decision entry points are disabled and the action handler rejects a direct call with natural Chinese recovery copy.
  • Mobile reimbursement approval is offline -> no evidence is loaded from IndexedDB and no approval draft is allowed; an explained disabled state remains visible.
  • Mobile edit/approval has a bottom action -> fixed bar is visible at the viewport bottom and final evidence remains reachable above it. No bottom action -> no fixed-action page padding.
  • Current or historical assigned reviewers may read the authorized current evidence; unrelated employees remain forbidden. Material endpoints continue their own permission, governance, audit and short-lived WeCom grant checks.
  • Supplementary attachment browser tests locate the intended file within approval-attachments by its accessible 查看补充附件:<filename> control, then activate that row's 删除附件. Do not depend on the retired .detail-evidence-card reimbursement layout or restore old product markup for a test. Preserve object/consequence copy, zero DELETE requests on cancellation or fewer than two reason characters, and exactly one target-specific DELETE carrying a valid reason; a mock browser pass does not prove persisted or production deletion.

5. Good/Base/Bad Cases

  • Good: edit, read-only and approval share one item/material projection; desktop uses an expense table, mobile uses cards, and only adapter-provided capabilities differ. The approver reads evidence and acts from one task URL.
  • Base: a completed task shows the latest authorized read-only evidence with a clear time-semantics note while history and submission snapshots remain historical.
  • Bad: keep a dormant reimbursement branch in WorkflowTaskDetailPage/MobileApprovalsPage, query task detail in both dispatcher and adapter, or put if (mode === "approval") around API calls in a shell.
  • Bad: nest full invoice cards and persistent file/reference buttons under every expense item, cache evidence/context data offline, show normal 0 条 summaries, render two copies of the reimbursement header, or leave a mobile fixed action captured by an animated ancestor.

6. Tests Required

  • UI strict/self-test proves shell and legacy-branch rules are active and reports zero findings.
  • Source/structure tests prove both task dispatchers contain one fetchWorkflowTaskDetail call, the approval adapter contains none, and both shells share the model/slots/capability contract and section order.
  • PC/mobile E2E covers edit/read-only/approval, desktop table/mobile cards, zero expense-row actions in approval/read-only, single file open controls, return-only progressive references, no-issue suppression, fail-closed, item-local invoice recovery, submit-confirm failure recovery, task deep links/back behavior and mobile fixed-action geometry.
  • IndexedDB tests inspect the stored record and reject reimbursement item/invoice/attachment/context/material fields plus synthetic sentinel values. Non-reimbursement mobile offline draft create/sync/conflict remains green.
  • Run cd client && npm run typecheck && npm run build, focused workflow/reimbursement Playwright, route/flow gates, Trellis governance and git diff --check.

7. Wrong vs Correct

Wrong

export function ReimbursementWorkspaceShell({ mode, taskId }) {
  const task = useQuery({ queryFn: () => fetchWorkflowTaskDetail(taskId) });
  return mode === "approval" ? <ApprovalContent task={task.data} /> : <EditContent />;
}

Correct

const taskQuery = useQuery({
  queryKey: ["workflow", "task", taskId],
  queryFn: () => fetchWorkflowTaskDetail(taskId),
});

return taskQuery.data.workflowInstance?.entityType === "reimbursement"
  ? <ReimbursementApprovalAdapter detail={taskQuery.data} refetchTask={taskQuery.refetch} />
  : <WorkflowTaskDetailPage detail={taskQuery.data} refetchTask={taskQuery.refetch} />;

Implementation order for design refactors:

  1. A-class frequent pages: workbench, pending tasks, workflow task detail, personal settings and notification center.
  2. Business ledgers and details: reimbursement, procurement, contracts, projects, assets, payments, AP and invoices.
  3. Management and boundary pages: management landing/subpages, leave records, attendance, Enterprise WeChat archive and mobile equivalents.

Tests Required:

  • Documentation-only updates: python3 ./.trellis/scripts/task.py validate <task>, node scripts/qa/check-trellis-governance.mjs and git diff --check.
  • Frontend implementation updates: cd client && npm run typecheck, cd client && npm run build, plus focused Playwright or screenshot coverage for changed A-class, detail, sidebar, action-bar or mobile surfaces.

Frozen / Boundary Items

Frozen modules and Enterprise WeChat-owned HR/attendance entries must be labeled or hidden according to the current Trellis task; do not silently re-enable them.

  • Use the shared WorkspaceNotice pattern for trial boundaries, safety notes, and post-trial capabilities instead of page-local warning blocks.
  • Trial-facing PC lists must hide obvious TEST/SMOKE records by default when a page-level toggle exists; the toggle may reveal them for troubleshooting.

Scenario: Business Flow Tests And Frozen Modules

1. Scope / Trigger

  • Trigger: Playwright or QA shell tests need to create workflow data while some product modules are frozen for launch.
  • Applies to client/tests/e2e/**, scripts/qa/run-business-flow-gates.sh, and frontend route/permission assertions.

2. Signatures

  • QA command: BUSINESS_FLOW_GROUPS=<groups> bash scripts/qa/run-business-flow-gates.sh local.
  • Frozen-module API expectation: non-admin launch roles may receive 403 for frozen module list/create/submit endpoints.
  • Enabled workflow test data sources should use active entities such as leave, reimbursement or procurement unless the task explicitly reopens the frozen module.

3. Contracts

  • Do not use a frozen module as fixture setup for an unrelated enabled feature. Example: mobile approval offline sync should not create a travel request when travel permissions are intentionally frozen.
  • Permission direct-access tests must assert the frozen contract directly, such as employee GET /api/travel-requests?... returning 403.
  • UI route smoke may still verify that a frozen page displays a boundary state without entering a management or write-capable mode.
  • Do not loosen roles or seed permissions merely to make old E2E data setup pass.

4. Validation & Error Matrix

  • Frozen role calls frozen module create/submit/list endpoint -> 403 or the documented feature-gate status.
  • Enabled workflow feature needs pending task data -> create data through an enabled workflow entity.
  • E2E fails with PERM_001 while setting up unrelated feature data -> update the test data source before changing app permissions.

5. Good/Base/Bad Cases

  • Good: mobile approval offline test creates a submitted leave request, then verifies pending-task deep link, offline queue, sync success and conflict.
  • Base: travel-specific tests are skipped or admin-scoped while travel remains frozen.
  • Bad: grant employee travel.create only so a mobile approval test can build a fixture.

6. Tests Required

  • Focused E2E group that previously failed.
  • Full affected business-flow group after the focused fix.
  • cd client && npm run typecheck && npm run build when TypeScript test code changes.

7. Wrong vs Correct

Wrong

await createSubmittedTravelRequestViaApi({ user: EMPLOYEE });

Correct

await createSubmittedLeaveViaApi({ user: EMPLOYEE });

Convention: PC Operation List Density

What: PC list/workflow pages that combine status metrics, tabs/filters, range controls, export and table/list content should reuse the shared operation classes in client/src/index.css:

  • ops-status-strip for compact status metrics.
  • ops-command-bar, ops-command-bar__row, ops-command-bar__tabs, ops-command-bar__actions and ops-filter-label for tabs, filters and secondary actions.
  • ops-table-focus for the primary desktop table container.
  • ops-inline-alert for temporary mode hints such as batch operation notices.
  • PrimaryTable already applies ops-table-focus by default; page code may add responsive visibility classes such as hidden md:block, but should not recreate a separate table frame.
  • Finance/admin status summaries should prefer the shared finance-status-strip and finance-metric-card classes unless the task introduces a new reusable metric component.

Why: This keeps high-frequency PC pages dense and predictable without recreating page-local Tailwind command bars. It also lets frontend-design change the visual treatment once while preserving component contracts.

Example:

<StatStrip columns={4} className="ops-status-strip">
  <MetricCard title="待办总数" value={summary.pendingTotal} />
</StatStrip>

<FilterBar className="ops-command-bar !p-3 md:!p-3.5">
  <div className="ops-command-bar__row">
    <Tabs value={status}>
      <TabsList className="ops-command-bar__tabs !h-auto !justify-start !bg-slate-50">
        <TabsTrigger value="pending">待处理</TabsTrigger>
      </TabsList>
    </Tabs>
    <div className="ops-command-bar__actions">...</div>
  </div>
</FilterBar>

<PrimaryTable className="hidden md:block">...</PrimaryTable>

Tests Required:

  • cd client && npm run typecheck
  • cd client && npm run build
  • Focused Playwright screenshot coverage when changing login, Portal, primary PC list pages or command-bar/table styling.

Convention: Page Template Taxonomy

What: hyb-oa frontend pages use six layout templates. New or redesigned pages must pick one primary template before implementation:

  • 工作台总览: Portal and mobile home. Compact summary, today's actionable items, conditional progress/blocker/reminder sections.
  • 任务处理列表: workflow task queues. Pending summary, task list, processed records and advanced filters.
  • 业务台账列表: reimbursement, contracts, projects, payments, AP and invoices. Page-header primary action, command bar, list/table body and folded ledger tools.
  • 主从工作台: only for true task execution after an explicit user selection, such as an inventory-check task that has been opened. It is not the default first screen for procurement, assets, payments, AP or invoices.
  • 详情工作区: reimbursement detail, workflow task detail, project detail and future contract detail. Overview, facts/evidence, right-side summary/command panel and history.
  • 管理配置页: people, organization, master data, approval rules, workflow definitions, feature flags and audit logs. Grouped settings, searchable tables and confirmed destructive/configuration actions.

Contracts:

  • A page has one primary template. Local sections may reuse shared components, but the page must not mix ledger, configuration, detail and dashboard patterns without an explicit Trellis task.
  • Mobile is the responsive expression of these templates: top status/focus, card list or sectioned detail, bottom primary action. Do not create a PC shell inside mobile routes.
  • B-class business pages should usually choose 业务台账列表. Procurement, assets, payments, AP and invoices must start as a list and open details only through an explicit row action, ?id= detail mode, drawer or detail route. Use 主从工作台 only for explicit execution tasks, and never auto-select the first item merely to fill a detail column.
  • A-class task pages must not drift into configuration surfaces. C-class configuration pages must not appear as ordinary employee task pages.

Tests Required:

  • cd client && npm run typecheck
  • cd client && npm run build
  • Focused Playwright or screenshot checks for changed A-class, mobile or shared template surfaces.

Convention: Business Template Components

What: Operational pages should reuse thin template components for layout and interaction coordinates, while keeping business fields, permission checks, state matrices and actions explicit in each page.

Contracts:

  • Use template components for repeated page structure instead of page-local table cards, custom command bars or copied empty states. For business ledger lists, prefer BusinessListToolbar, BusinessTable, BusinessTableFrame, BusinessMobileList and shared empty states from client/src/components/patterns/BusinessListTemplate.tsx.
  • For PC business detail pages, prefer the thin components in client/src/components/patterns/BusinessDetailTemplate.tsx: BusinessDetailWorkspace, BusinessDetailMain, BusinessDetailAside, BusinessDetailActionPanel, BusinessDetailFactGrid and BusinessDetailFact. They define the Ant Design Pro style coordinates only: main facts/evidence/history on the left, status/actions/compact summary on the right. Keep the page's business fields, permission checks, state conditions and API calls explicit in the page component.
  • For business edit/create pages, prefer the thin components in client/src/components/patterns/BusinessEditTemplate.tsx: BusinessEditTemplate, BusinessEditWorkspace, BusinessEditMain, BusinessEditAside, BusinessEditSection, BusinessEditFormGrid, BusinessEditValidationSummary and BusinessEditActionBar. They standardize title/back/status/action, sectioned form layout, validation summary and bottom save/submit coordinates only; fields, permissions, mutations and dirty-state decisions stay explicit in the page.
  • BusinessDetailActionPanel is the default PC detail operation surface. Use one primary action for save/create when the detail owns editing; use a horizontal action bar for approval actions; move low-frequency actions into 更多 or a clearly named grouped section. Do not add a second page-local 操作 card or a separate 摘要 card under the same right column.
  • Do not introduce or preserve a generic 台账工具 button on redesigned business pages. Move each ability to a concrete enterprise action coordinate: 新建 / 新建方式, 筛选, 导出, 统计, 批量处理, row 更多, detail action panel or an explicitly named management entry such as 资产分类.
  • Do not create a configuration-driven page generator for reimbursement, procurement, contracts, projects, assets, payments, AP or invoices. These pages have different permissions, state transitions, attachment rules and financial actions; hiding those differences in schema config increases maintenance risk for the solo-developer workflow.
  • PC ledger pages should render filters and table body as separate surfaces: command bar first, table/list after it. Do not place the primary table inside another card just to add a local title such as 台账列表, 项目列表, 批次列表 or 票据列表.
  • Mobile may keep card rows, but the card fields and row actions should mirror the PC table columns and operation column.
  • Details, edits and approval pages should follow the same template philosophy: shared shell and action placement, explicit business content. Implement them incrementally through focused Trellis tasks.

Why: Template components enforce mature Ant Design / Ant Design Pro style layout consistency without turning business pages into a brittle generator.

Convention: Global Shell And Page Header Baseline

What: PC page redesigns should preserve the shared Wave0-2 shell and header baseline instead of recreating page-local wrappers:

  • AppShellLayout owns the PC shell background and content offset through app-shell and app-shell-main.
  • Sidebar and NavLinks own role-aware task navigation. The mental model is a task-first primary list plus collapsed 更多; enabled OA-owned business ledgers such as contracts, procurement, assets and projects should not be hidden in 更多 by default when the current role has access. Management configuration is represented by one 管理 entry instead of many sidebar rows.
  • Topbar is a compact command surface: page title, group badge, user context, global search and notifications. Do not expand it back into a large hero.
  • PageHeader and WorkspaceHeader should use page-header-shell, page-header-shell__eyebrow, page-header-shell__title, page-header-shell__subtitle and page-header-shell__actions.
  • Auth guidance and step hints may use ux-command-note, ux-step-list and ux-step-item when they explain the next action without adding page-local warning cards.

Why: The shell/header baseline gives Wave3-5 pages a stable visual entry point and prevents each route from inventing a different title/action layout.

Navigation Rules:

  • Sidebar entries should be role-aware and task-first for domestic tech startups under 10 people; do not expose all permission-allowed modules as a default module catalog.
  • Do show enabled, permission-allowed day-to-day business ledgers directly in the primary sidebar, including 合同, 采购, 资产 and 项目 when relevant. 更多 is for low-frequency, frozen, staged or occasional entries, not a place to bury common work.
  • Approvers see 待我审批; ordinary employees should not see 待办 or 审批待办 in the sidebar by default.
  • PC leave navigation uses 请假记录 unless the current task explicitly opens OA emergency leave submission.
  • HR/attendance pages must encode WeCom ownership in the UI surface: employee leave and mobile leave are record-first/read-only, and OA emergency forms are hidden unless leave.manage; attendance correction forms are hidden unless attendance.manage. Labels must say OA 应急 / 提交应急审批 / 提交应急修正 instead of generic 提交.
  • Title assertions should use an exact heading role within the intended panel; broad getByText("票据详情") can also match transient loading copy. When a page repeats the same title in the page header and a content section, Playwright assertions should target the page header (h1 or the header test id) rather than getByRole("heading", { name }), which becomes ambiguous.
  • 个人设置 stays in the topbar account menu. Notifications stay in the topbar bell and notification center.
  • Finance labels should use work names such as 付款, 应付账款 and 票据; internal platform words like AP 运营 and 功能开关 should be hidden behind management wording or renamed for users. ERP 集成 is not a hyb-oa product entry.
  • 更多 is a lightweight collapsed row for low-frequency, frozen, staged or occasional entries; do not add a sidebar explanation card, subtitle or onboarding copy around it.
  • Playwright navigation helpers should use exact names or anchored regex for short sidebar labels such as 管理, otherwise fuzzy role-name matching can click longer labels such as 预算管理.

Example:

<WorkspaceHeader
  eyebrow="审批"
  title="审批中心"
  subtitle="先看待处理,再进入详情核对依据。"
/>

Tests Required:

  • cd client && npm run typecheck
  • cd client && npm run build
  • Focused Playwright coverage when changing AppShellLayout, Sidebar, Topbar, NavLinks, PageHeader, WorkspaceHeader, login, Portal or other A-class pages.

Convention: Action Placement Baseline

What: Page actions, filters, ledger tools and destructive commands use one system-wide coordinate model. New and redesigned pages should not invent page-local action placement.

Contracts:

  • PC page primary action lives in the page header action area (WorkspaceHeader / PageHeader actions). Keep one primary action per page surface and at most two visible secondary actions.
  • List filters live in the first command bar below the page header. Search, status, scope and other common filters stay on the left; ledger tools such as export, batch, statistics, templates and advanced filters stay on the right or inside 筛选, 更多 or concrete role-only action groups.
  • Row-level actions live at the right edge of a table row or at the tail of a card. Prefer 查看详情, 查看处理 and 继续编辑 over broad verbs that hide the result.
  • Detail pages keep facts and evidence in the main content. High-risk actions such as approve, reject, return, pay, publish, delete and configuration switching live in the right command panel or a clearly separated detail-action-stack, with confirmation that includes the current object summary.
  • Form save/submit actions live at the form bottom on PC. Long forms may add a right-side summary command panel, but the same action should not be repeated in unrelated header, row and footer locations.
  • Mobile pages use top status/focus content plus a bottom safe-area action surface for the primary action. Do not copy PC command bars or wide tables into mobile routes.
  • Batch actions only appear after selection, as a contextual command bar or bottom sticky bar.
  • Destructive, financial, approval and configuration-changing actions must be visually and structurally separated from normal navigation/edit actions.

Why: hyb-oa targets small teams where one person may switch roles many times per day. Stable action coordinates reduce relearning and help a solo developer avoid page-by-page exceptions.

Tests Required:

  • cd client && npm run typecheck
  • cd client && npm run build
  • Focused Playwright or screenshot checks when changing shared page headers, command bars, task lists, detail action panels or mobile bottom actions.

Convention: State-Driven Actions

What: Business actions are derived from explicit object state and current actor context. Do not scatter page-local button conditions across route components.

Contracts:

  • Before redesigning a page with mutable business records, define or reference a state matrix. The matrix must cover object state, actor role, primary action, secondary actions, forbidden actions, reason text, risk level and confirmation requirement.
  • Common lifecycle states include draft, submitted, in approval, returned or rejected, approved, pending execution or payment, completed or paid, cancelled, abnormal and archived. Use the business module's real names when they differ.
  • Draft states prioritize edit, submit and delete draft. Do not show approval or payment actions there.
  • In-approval states prioritize progress, withdraw and supplement-material actions. Approve, reject and return actions belong to the current user's workflow task detail, not broad ledger rows.
  • Returned or rejected states prioritize correction and resubmission.
  • Approved-but-not-paid states route payment work to finance or cashier views; ordinary employee views should explain the next step instead of exposing executable payment controls.
  • Payment actions such as mark paid, register payment, cancel payment, batch payment and execute payment live only in payment, finance-processing, payment-batch detail or an explicitly current finance task panel. Reimbursement, procurement, contract and project ledgers/details show payment status, receipt status, 查看付款, 补回单 or a payment-record link instead of executable payment controls.
  • Paid, completed, archived and cancelled states are read-mostly. Print, export, attachment download and history belong in 更多 or audit paths when allowed.
  • Abnormal states must explain source, impact and next step. A red status label alone is insufficient.
  • Destructive, financial, approval and configuration-changing actions require confirmation that shows the current object summary and writes audit evidence when the backend supports it.
  • The frontend may hide disabled actions or show reason text, but the backend remains the source of truth for permission and state validation.
  • Use destructive lifecycle verbs consistently: 删除 only for drafts, non-effective temporary records or wrong materials that have not entered a business process; 撤回 for initiator withdrawal during allowed workflow states; 取消 when the business will not continue; 作废 when a numbered, signed, paid, published or effective object becomes invalid; 归档 when a completed object is locked for retention. Submitted business records should not be physically deleted from ordinary UI paths.

Why: State-driven actions prevent pages from showing buttons that the user cannot complete and keep reimbursement, procurement, contracts, assets and payments consistent as the system grows.

Tests Required:

  • cd client && npm run typecheck
  • cd client && npm run build
  • Focused Playwright coverage for changed state/action behavior, especially draft, rejected/returned, in-approval, approved, paid/completed, cancelled and abnormal states.

Convention: Role-Scoped Data Surfaces

What: The backend is the source of truth for permissions and data scope. The frontend shapes the authorized capabilities into the current task role instead of exposing every allowed tool at once.

Contracts:

  • Ordinary employee views default to 我的, 与我相关, 我发起 and 需要我补充. Do not show company-wide ledgers, batch tools, export, audit, configuration, internal IDs or other people's sensitive attachments by default.
  • Initiator views emphasize current status, next step, supplement-material needs, approval/payment progress and whether edit or withdraw is allowed.
  • Approver and processor views emphasize 待我审批 and the current node task. Show approve, reject, return or transfer actions only when the server-backed task assignment says the current user can act.
  • Approval actions such as approve, reject, return and transfer execute only in 待我审批, workflow task detail, mobile approval detail or an explicitly current task-processing panel. Business ledger rows and ordinary business details use 查看处理, 查看进度, 补材料 or a link into the workflow task; they must not duplicate approval action buttons.
  • Finance and cashier views may expose authorized reimbursement, payment, invoice, AP, receipt and exception operations. Full-scope, export, batch, delegated creation and statistics tools belong in concrete actions such as 导出, 统计, 批量处理, 新建方式, 更多 or role-only panels.
  • Owner and project-lead views emphasize team/project scope, blockers, risks and approvals. They should not drift into system configuration unless the route is a management route.
  • Admin views live under 管理 and cover people, organization, workflow, feature flags, integrations, audit and troubleshooting. Do not mix these controls into ordinary employee paths.
  • Read-only or audit views can show authorized evidence but must not show mutable edit, approval, payment, publish or disable actions.
  • Do not add a global role or identity switcher. Multi-role users do not choose 员工 / 财务 / 管理员 before using the system. Routes infer the most relevant task view by default.
  • Multi-role users may switch scope inside a specific authorized ledger, but the default scope follows the page purpose: reimbursement starts with mine, workflow tasks start with my tasks, ledgers start with relevant or recent records, management starts with management scope. Use route-local scope controls such as 我相关 / 我负责 / 财务处理 / 全范围.
  • Use explicit controls such as 我的 / 团队 / 全范围 only when the server grants those scopes. Hiding controls in the frontend is not a security boundary.
  • Unauthorized actions should usually be hidden. When explanation is useful, show short natural Chinese reasons such as 当前节点不是你处理, 已支付单据不可编辑 or 仅财务可导出.

Why: hyb-oa users often wear several hats. Role-scoped surfaces keep one person's broad authority from turning every page into an admin console.

Tests Required:

  • cd client && npm run typecheck
  • cd client && npm run build
  • Role-focused Playwright coverage when changing visibility for employee, approver, finance/owner or admin paths.

Convention: User-Facing Identifiers And Copy Semantics

What: Ordinary product surfaces use business identifiers and natural Chinese. Internal identifiers are auxiliary evidence for audit and troubleshooting, not primary user-facing content.

Contracts:

  • Use business identifiers, names or summaries as primary labels: reimbursement number, purchase request number, contract number, asset number, payment batch number and project name.
  • Do not show UUID, taskId, entityId, permission code, role code, internal state code, raw payload, full local path, API path, database field name, OCR provider, debug trace, Secret or token on ordinary employee, approver, finance or mobile paths.
  • Audit logs, admin troubleshooting, integration failure details, printed evidence and export handoff files may show necessary internal IDs when authorized. Place them in low-priority areas such as 技术信息, 审计信息 or 排障信息; never use them as the page title, card title, button label or first ordinary list column.
  • Show one primary identifier for the same object in one surface. Avoid placing business number, UUID, taskId and entityId together unless the route is an audit/troubleshooting route.
  • Translate technical enum values and platform words into natural Chinese: PENDING_APPROVAL becomes 审批中; REJECTED becomes 已驳回 or 被退回待补正; AP Operations becomes 应付账款; Feature Flag becomes management wording.
  • Time copy should answer the business question. Prefer 已等待多久, 接收于, 提交于, 支付于 and 归档于 over generic 更新时间 when the user needs to decide what to do.
  • Errors and abnormal states must explain source, impact and next step in natural Chinese. Do not surface only HTTP codes, backend enum names or red badges.
  • Notifications and mobile cards use the shortest useful business identifier or summary. Do not pack long technical IDs into small surfaces.

Why: Internal implementation language makes the product feel complex and unfamiliar. Business identifiers and natural Chinese keep small-team workflows legible.

Tests Required:

  • cd client && npm run typecheck
  • cd client && npm run build
  • UI tests or screenshot review for changed list/detail/notification/mobile surfaces to confirm internal IDs and technical enum names are not visible.

Convention: Ledger List Defaults

What: Business list and ledger pages default to a light, task-oriented list. Advanced ledger capabilities remain available but must not dominate the first screen.

Contracts:

  • Keep 3 to 5 high-frequency filters visible by default. Prioritize search, status, scope and date/time; add business-specific filters such as project, person or type only when they are genuinely frequent.
  • The default view must match the route's main job: 我的报销, 待我审批, 合同, 资产 and similar task nouns. Do not default ordinary users into company-wide management views.
  • Rows and cards must answer what the object is, current state, why it relates to the current user and the next action.
  • Place common filters on the left side of the command bar below the page header. Put export, batch, statistics, templates, full-scope, advanced filters, delegated creation and low-frequency module links in 筛选, concrete right-side actions, 更多 or role-only panels.
  • Batch actions appear only after selection, as a contextual command bar or bottom sticky bar. High-risk batch actions such as approve, pay, delete or publish require summary confirmation and result details.
  • Export is not a primary page action. Explain export scope and sensitive content when relevant; finance, contract, asset and invoice exports must stay permissioned and auditable.
  • PC ledgers may use pagination, sorting and density controls. Default columns should be decision-supporting fields only; low-frequency fields belong in details, column configuration or advanced filters.
  • Empty states are status-specific. 没有草稿, 没有被驳回, 没有待处理 and 没有异常 should not all share one generic explanatory panel.
  • Mobile routes do not copy PC ledger tools. Use search, status filtering, card lists and one primary action; leave complex analysis, export and broad-scope management to PC.
  • Contract, procurement, project and finance ledgers use a list-to-detail boundary. The list is for discovery, filtering, status reading and navigation. High-risk or sensitive actions such as contract download, archive, payment, export, batch processing, void/cancel and sensitive attachment viewing belong in detail workspaces, detail command panels, concrete role-only actions or 更多, with permission, audit and confirmation.
  • Default ledger scope follows the current task role: ordinary employees see 我发起 / 我相关, owners and project leads see 我负责 / 项目相关, finance sees 待复核 / 待付款 / 异常, and admins may enter or switch to 全范围. Full scope, export and batch commands stay in concrete role-only actions or panels, not ordinary first screens.
  • Contract ledgers in V1 are registration, archive, due/renewal reminder and relation surfaces for project, procurement and payment context. Do not design /contracts-registry as e-signature, legal-review, clause-library, version-collaboration or full CLM software. If contract approval is needed, link to the OA approval center or a separate future task instead of adding an approval workspace inside the contract ledger.
  • Procurement ledgers in V1 are request, approval, attachment, light acceptance/archive and relation surfaces for project, contract and payment/AP context. Do not design /purchase-requests as purchase-order lifecycle, inventory, three-way matching, supplier-lifecycle or tendering software. Approved procurement means the request was approved; it does not imply paid, received into inventory or converted into a fixed asset.
  • Project pages in V1 are light project ledgers for status, owner/member, milestone/task, risk, delivery-record and related reimbursement, procurement, contract and payment context. Do not design /projects as full project management software with Gantt charts, complex dependencies, timesheets, resource scheduling, OKR, portfolio management or large collaboration suites. Project pages summarize and route; source records stay in their business modules.
  • Finance ledgers in V1 are payment-preparation, payment-batch, receipt-audit, AP-status, invoice/OCR-status and agency-accounting handover summaries. Do not design payment, AP or invoice pages as online banking, bank-statement ingestion, general-ledger, voucher-generation, auto-reconciliation, tax filing or full accounting software. Real payment stays in offline banking or existing finance/accounting workflows; OA records status, evidence and handover summaries. hyb-oa does not connect to ERP now and does not plan ERP integration later.
  • Payment, AP and invoice pages default to task/exception visibility: 付款 shows review, payment-marking, receipt and exception attention before batch creation; 应付账款 shows pending payment, exceptions, risks and differences before export; 票据 shows OCR status, failed invoices and bindable invoices before broad-scope handover tools. Batch creation, export, full-scope views and agency-accounting handover controls live in concrete role-only actions, 更多 or selected-context panels, never as the first-screen primary action.
  • Payment actions require the finance/payment context: amount, payee or target object, source records, offline payment explanation, evidence completeness, receipt requirement and correction boundary. Do not place 标记已付款, 取消付款, 批量付款 or 执行付款 directly in reimbursement, procurement, contract or project ledgers/details.
  • Cross-module relation sections are summary-and-route surfaces. Project, procurement, contract, reimbursement, asset and payment pages may show related business number/name, status, amount or quantity, owner, evidence completeness and next-step summary, then link to the owning detail/task/finance page. Do not duplicate the owning module's approval actions, payment actions, invoice/OCR processing, contract-body download, sensitive attachment actions, state machine or batch tools inside another module's relation card.

Why: The product's main complexity complaint is too many visible operations. Light default ledgers keep daily work simple while preserving advanced finance and management workflows.

Tests Required:

  • cd client && npm run typecheck
  • cd client && npm run build
  • Focused Playwright or screenshot checks when changing list filters, command bars, advanced action placement, batch selection behavior, empty states or mobile list cards.

Convention: Asset Registry Pages

What: Asset pages render FA fixed assets as a lightweight responsibility ledger inside the unified asset registry, not as a full accounting fixed-asset subsystem.

Contracts:

  • The default /assets view answers where the asset is, who uses it, who is responsible for it, what state it is in and whether it needs inventory or handover. Do not lead with depreciation, accounting subjects or external finance sync.
  • Register physical items that must be returned, transferred, inventoried or protected, such as computers, monitors, disks, equipment and security media. Do not create FA cards for ordinary consumables.
  • Every FA card needs an asset number. Physical assets that can carry labels should expose QR-label status for scan, inventory, assignment and return workflows. Assets that cannot be labeled must still expose serial number, custodian and location as the lookup fallback.
  • Inventory cadence is lightweight: quarterly routine checks by default, event checks for departure, transfer, repair and retirement, and monthly custodian plus location confirmation for high-risk physical assets such as backup disks, security media and critical equipment.
  • Assignment, return and transfer are daily flow actions that admins may record with audit evidence. Disposal actions such as retirement, loss, sale and gift require owner confirmation, a reason and consequence summary. If compensation, money or responsibility is disputed, the asset UI records pending/confirmed outcomes only and must not complete the dispute in one click.
  • Keep acquisition source, business number, owner/custodian, location, status, assignment, return, transfer, repair, retirement, inventory result and handover evidence visible in the business flow.
  • Procurement or reimbursement records may expose an asset-candidate signal, but they must not create an FA asset card automatically after approval, submission or payment. The asset registry UI may guide admins from candidates to 新建资产卡片, but the card is created only after manual verification of delivery, quantity, owner/custodian, consumable status, returns and gifts.
  • Move depreciation, residual value, accounting subject and monthly depreciation voucher concepts out of the OA primary UI unless a later Trellis task explicitly scopes accounting-ledger work.
  • Keep classification maintenance, raw metadata and advanced filters behind management, collapsed or advanced surfaces so they do not dominate the asset list/detail first screen. Do not show ERP sync state or ERP asset numbers in the asset UI.
  • Mobile asset lookup should prefer QR scan and fall back to asset number or serial number input. Do not require heavy photo inventory unless a later task explicitly scopes it.
  • Asset list/detail UI may show last inventory, next inventory and overdue hints. Do not turn cadence into a complex scheduling/configuration console in the default asset registry view.
  • Disposal commands must be visually separated from assignment/return/transfer commands and use confirmation that shows asset number, name, owner, current custodian, location, reason and effect on asset status.
  • Electronic assets (EA-DOC, EA-SYS, EA-REPO, EA-BKP) are metadata and responsibility indexes. Show owner, custodian/admin, permission-recovery requirement, backup or restore-verification date, sensitivity level and archive-location summary. Do not display or store source packages, secrets, tokens, full configs, real full paths or backup contents.
  • EA-DOC list/detail views show document name, type, hash or verification summary, sensitivity level, owner, related project/contract/payment and archive-location summary. Contract text, bank statements, certificate originals, customer materials, sensitive quotes, payroll/social-security data and personal identity materials must not appear as ordinary asset-page content; route business originals through their module attachment permissions or offline controlled archive.

Tests Required:

  • Asset page screenshots or E2E assertions should verify the first screen is asset-list/detail/task oriented, with classification and integration tooling demoted.
  • Mobile asset scan tests should cover lookup, summary and one allowed action; mobile must not expose classification maintenance or full metadata editing. Include QR scan or manual asset-number/serial-number fallback when this surface changes.
  • Electronic asset UI changes should assert that sensitive source/config/backup content and full paths are not visible in ordinary list/detail/mobile views.
  • EA-DOC UI changes should assert that high-sensitive originals are not rendered in ordinary asset list/detail/mobile views.

Convention: Forms, Drafts And Validation

What: Long business forms support interruption and recovery. Save draft and submit are distinct actions with different consequences.

Contracts:

  • Long forms should support drafts by default when the business object requires multiple fields, line items or materials, including reimbursements, procurement, contract drafting, asset intake and payment supplement flows.
  • 保存草稿 stores progress only. It must not trigger approval, payment, publishing or external synchronization. 提交 enters the business workflow.
  • Draft save may allow incomplete data, but it must preserve entered fields, line items, attachment references and the recoverable object state.
  • Before submit, run business validation and show a summary confirmation with the object summary, amount or quantity, important contract/asset/payment facts, attachment or invoice completeness, and next node or consequence.
  • Material upload is type-first. Let the user choose or enter the material type before upload, then apply type-specific copy, validation, permissions, audit and display placement. Do not use one generic attachment uploader for PDF invoices, ordinary attachments, contract files, payment receipts, acceptance materials, asset materials and electronic-document indexes.
  • Upload labels must describe the result, such as 上传 PDF 发票, 补充付款回单, 上传合同文件, 补充验收材料 or 登记资产材料.
  • Validation errors must point to the field, line item, invoice, attachment, contract material or asset section that needs work. A top-level 提交失败 message alone is insufficient.
  • Separate blocking errors from warnings. Blocking errors prevent submit; warnings may allow users to confirm and continue when the business rule permits it.
  • Server-side validation failures must preserve user input and show repair guidance in natural Chinese.
  • Warn before navigation when the form has unsaved changes, including leaving the route, closing a modal, switching record or returning to the list.
  • Autosave can be used as an enhancement, but the visible state must still make clear whether the object is draft, submitted or failed to save.
  • Mobile forms keep only necessary fields, material upload, status explanation and a bottom safe-area primary action. Do not move admin configuration, complex configuration matrices, bulk import or workflow design into mobile forms.
  • Mobile validation messages should appear near the field or material card, and bottom actions must not cover the final input.

Why: Small-team users often switch tasks mid-form. Drafts and precise validation prevent repeated re-entry and reduce support/debugging cost.

Tests Required:

  • cd client && npm run typecheck
  • cd client && npm run build
  • Focused Playwright coverage for draft save, submit validation, unsaved-change protection and server-validation failure paths when changing form behavior.

Scenario: Data-Router Unsaved-Change Protection

1. Scope / Trigger

  • Trigger: a React form can lose unsaved work through in-app navigation, browser back/forward, reload, closing its panel or switching records.

2. Signatures

  • Router: createBrowserRouter(...) rendered through RouterProvider.
  • Navigation blocker: React Router useBlocker(isDirty).
  • Reload/close guard: beforeunload while isDirty === true.
  • Shared UI contract: one unsaved-change dialog with continue-editing and discard actions.
  • Successful-save navigation: usePostSaveNavigation() opens one synchronous allow-next window around the canonical navigation action; its signature is (action: () => void) => void.

3. Contracts

  • Block navigation before the route or selected record changes; do not restore old UI state after navigation has already occurred.
  • blocker.reset() keeps the current route and form values. blocker.proceed() is called only after explicit discard confirmation.
  • Record switching and panel closing use the same pending-action contract as route navigation. Successful save or explicit discard clears dirty state.
  • When a successful server save immediately replaces a create route with its canonical detail, use the dedicated post-save navigation entry after a valid entity ID is available. Do not depend on React state cleanup completing before navigate(...), and do not expose a persistent guard-disable switch.
  • Failed, timed-out or invalid save responses never use the post-save bypass; they retain the form values and normal dirty protection.
  • Register beforeunload only while dirty and remove it during cleanup.

4. Validation & Error Matrix

  • Clean form navigation -> continue without a dialog.
  • Dirty in-app navigation or browser back -> block and show one dialog.
  • Continue editing -> URL, record selection and values remain unchanged.
  • Discard -> execute exactly one pending navigation/selection and clear dirty.
  • Successful create -> replace to canonical detail without an unsaved dialog, even when the clean-state update has not committed yet.
  • Save failure or missing entity ID -> remain on the form with dirty protection; never bypass navigation protection to hide the failure.
  • Reload/close while dirty -> browser-level unload warning remains active.

5. Good/Base/Bad Cases

  • Good: reimbursement, procurement, contract and asset forms reuse one guard while keeping their own save and dirty-field logic.
  • Good: an asset POST succeeds, then the page wraps one replace navigation in usePostSaveNavigation() before refreshing non-critical list caches.
  • Base: a short non-editable filter form does not register the guard.
  • Bad: listen to history after navigation, then push the previous URL back; the user can see state flicker or lose form data before confirmation.
  • Bad: call setSavedSignature(...) and immediately navigate while relying on React to commit the clean state before useBlocker evaluates.

6. Tests Required

  • Playwright covers link navigation, browser back, reload and record switch.
  • Assert cancel preserves URL and values; discard changes target once; save removes the warning.
  • A create-route regression covers successful canonical replace, absence of the unsaved dialog and browser history that does not reopen the submitted form.
  • Frontend typecheck and build after router or shared guard changes.

7. Wrong vs Correct

Wrong

window.addEventListener("popstate", () => navigate(previousUrl));
setSavedSignature(nextSignature);
navigate(canonicalDetailPath, { replace: true }); // dirty ref may still be true

Correct

const blocker = useBlocker(isDirty);
// reset to keep editing; proceed only after explicit discard.
const runAfterSuccessfulSave = usePostSaveNavigation();
// Call only after the API returned a valid entity ID.
runAfterSuccessfulSave(() => navigate(canonicalDetailPath, { replace: true }));
void refreshNonCriticalCaches();

Scenario: Business Ledger List-To-Detail Route Migration

1. Scope / Trigger

  • Trigger: an existing business ledger keeps a selected record inside the list route through ?id=, local selected-ID state or a list/detail split, and the current Trellis task migrates that record to an independent detail route.
  • Current examples are procurement and assets; the same contract applies to a later contract, payment or other ledger migration only when that task has an already-defined detail information structure.

2. Signatures

  • Canonical detail path: /<ledger>/:entityId, built through one shared path helper that trims the ID and encodes the path segment.
  • Optional creation path: /<ledger>/new; register and test it as a static route, never as a value of :entityId.
  • Safe return state: location.state.from = pathname + search, accepted only through a module-specific path allowlist.
  • Legacy entry: /<ledger>?id=<entityId> performs a replace redirect to the canonical detail and carries a normalized list source without id.

3. Contracts

  • The list route owns URL-backed filters, pagination, list queries, creation entry and row navigation. It must not request or render a selected entity's detail merely because legacy id is present.
  • The detail route owns the entity query, edit/actions/evidence/history and dirty-form registration. It must not request the complete ledger list.
  • Parse legacy ?id= before mounting list queries. Empty or whitespace IDs stay on the list; a non-empty ID redirects with replace so the old URL is not a durable history entry.
  • List filters use the URL as their single state source. Invalid enums, pages and unauthorized scopes fall back to safe defaults before API request construction; frontend scope controls never widen backend authorization.
  • Row navigation uses push and carries the current normalized list source. An explicit detail return uses the validated source with replace; browser back remains native and must not reopen the detail after an explicit return.
  • A create route that has no entity ID before submit uses its own page. On successful creation, replace it with the new detail while preserving the original list source so browser back cannot reopen a submitted form.
  • New detail routes copy the original ledger read guard exactly. A create route may use a narrower write permission, but must never reuse broad list-read permission as create authority. Server object scope remains authoritative.
  • Object-level 403 and 404 share one non-enumerating Chinese error state; do not reveal whether the object exists or expose a partial cached summary.
  • Every new application route must be classified in scripts/qa/route-manifest.json with executable behavior/visual evidence as required by the route's classification.

4. Validation & Error Matrix

  • Missing/blank legacy ID -> render the list and issue no detail request.
  • Valid legacy ID -> one replace to canonical detail; issue no list request before the redirect.
  • Invalid filter or unauthorized scope=all -> safe default / own-scope API query; never an all-scope request.
  • Direct detail refresh -> load the entity only and use the module list as the fallback return target.
  • Unsafe state.from (external, protocol-relative, hash, current page or cross-module path) -> reject it and use the module list fallback.
  • Dirty form + route/back/explicit-return navigation -> one shared unsaved dialog; cancel preserves URL and values, discard performs one navigation.
  • Static /new matched as :entityId or a route missing from the manifest -> acceptance failure.

5. Good/Base/Bad Cases

  • Good: a filtered procurement list opens a canonical detail, explicit return restores the filters, and a legacy bookmark reaches the same detail without first requesting the list.
  • Base: a direct asset detail has no trusted source and returns safely to the asset list; a mobile-preferred runtime may map it to an existing mobile fallback without adding mobile detail capability.
  • Bad: keep list and detail state synchronized through two effects, let /assets/new call /api/assets/new, or grant create access because the user can read the asset ledger.

6. Tests Required

  • Source/unit contract for path encoding, blank ID, filter normalization, unauthorized scope, legacy source normalization and return-source allowlist.
  • Focused Playwright for list/detail request isolation, direct deep link, legacy replace, filter return, static /new, create permission, 403/404, dirty cancel/discard and create-success history replacement.
  • Consumer coverage for project relations, global search, notifications, page metadata and mobile-runtime mapping when those entry points exist.
  • cd client && npm run typecheck && npm run build.
  • node scripts/qa/check-flow-coverage.mjs --json --fail-on-gaps and bash scripts/qa/run-local-candidate.sh list after adding routes.

7. Wrong vs Correct

Wrong

const [selectedId, setSelectedId] = useState(searchParams.get("id") ?? "");
useEffect(() => setSearchParams({ id: selectedId }, { replace: true }), [selectedId]);
<Route path="assets/:assetId" element={<AssetPage />} />
<Route path="assets/new" element={<AssetCreatePage />} />

Correct

const legacy = resolveAssetLegacyRedirect(searchParams);
if (legacy) return <Navigate replace to={legacy.to} state={{ from: legacy.from }} />;

<Route path="assets/new" element={<RequirePermission anyOf={["asset.manage"]}>...</RequirePermission>} />
<Route path="assets/:assetId" element={<RequirePermission anyOf={ASSET_READ_PERMISSIONS}>...</RequirePermission>} />

Convention: Detail Evidence Layout

What: Detail pages use one evidence-first structure so users can understand state, facts and consequences before acting.

Contracts:

  • Use this order for reimbursement, workflow task, procurement, contract, asset, project and payment details: status/next step, business summary, evidence materials, risk or validation signals, approval/processing history, actions.
  • The top overview shows business number or name, current state, critical amount or object facts, current handler or next node, and a clear return path.
  • The first content block is 当前状态与下一步: explain where the object is, who needs to act, what happens next and whether there are blockers or warnings.
  • The business summary block shows the fields needed before approval or review: requester, amount, project, purchase object, contract counterparty, asset owner, payment batch and similar module facts.
  • Evidence materials sit in the main content, grouped by business meaning: line items, PDF invoices, attachments, contract files, asset photos/numbers, receipts and acceptance materials.
  • Risk or validation sections appear only when there is a real signal such as budget issue, duplicate invoice, missing attachment, contract expiry, asset anomaly or payment failure.
  • Approval and processing history is for traceability. Keep a useful summary visible, but do not let full history crowd out current status and evidence.
  • PC high-risk actions live in the right command panel or separated action stack. Mobile high-risk actions live in the bottom action surface or a confirmation panel.
  • Approve, reject, return, pay, publish, delete and void actions must show the object summary and consequence before execution.
  • When evidence is incomplete, change the primary action to supplement material, view issues or an explained disabled state. Do not leave users guessing from a disabled button alone.
  • List and ledger rows show only evidence completeness summaries, missing evidence hints and a detail entry. Invoice preview, attachment download, contract file viewing, receipt download, evidence deletion and bulk packaging belong in detail workspaces, detail command panels, 更多, concrete role-only actions or authorized audit paths.
  • Sensitive evidence such as invoices, contracts, receipts and attachments must be permissioned; download and export belong in 更多 or audit paths when appropriate.
  • Detail evidence sections group materials by type. Do not mix invoices, ordinary attachments, contract files, receipts, asset materials and electronic-document indexes in one undifferentiated list.
  • Mobile details use a top focus card, sectioned cards and bottom safe-area actions. Do not copy PC two-column detail or wide tables into mobile.

Why: Detail pages are the place where users decide whether to approve, reject, pay or supplement materials. Consistent evidence order reduces misreads and accidental actions.

Tests Required:

  • cd client && npm run typecheck
  • cd client && npm run build
  • Focused Playwright or screenshot checks when changing detail overview, evidence sections, history placement, right command panels or mobile detail bottom actions.

Convention: Mobile Lightweight Surfaces

What: Mobile and Enterprise WeChat WebView routes are lightweight action surfaces. They are not compressed PC management consoles.

Contracts:

  • Mobile may support lightweight submit, supplement-material, status lookup, task handling, scanning, upload and message-deeplink flows.
  • Keep mobile pages to top status or focus card, card list or sectioned detail, and bottom safe-area primary action.
  • Mobile home should show compact status and today's actionable items. Do not add a full module directory or dashboard.
  • Mobile home finance summaries are conditional action cards. Render them only when the current finance/cashier user has pending payment, payment exception or risk-review counts to handle; do not show disabled finance placeholders or unconditional mobile 我的 finance anchors.
  • Mobile lists use search/status filtering, cards and one primary action. Do not use wide tables, column configuration, complex multi-column filters, full-scope management, admin configuration, batch export, bulk approval or bulk payment.
  • Mobile list search that filters the currently loaded list must expose the visible label and aria label 当前列表搜索; do not imply global search with labels such as 搜索报销 or 搜索采购 unless the page actually queries a global corpus.
  • Mobile details use a focus card, sectioned evidence and bottom actions. Do not copy PC two-column layouts.
  • Mobile forms keep necessary fields, material upload, field-local validation and a bottom action. Do not put admin configuration, workflow configuration, feature flags, statistics dashboards or bulk import into mobile forms.
  • Bottom actions must account for safe area and keyboard overlap. Button text must remain readable on narrow screens.
  • Dangerous mobile actions must open a confirmation panel that shows object summary and consequence.
  • Ordinary mobile paths must not show internal IDs, permission codes, configuration matrices or audit troubleshooting panels.
  • Enterprise WeChat remains the primary owner for daily HR/attendance actions such as leave, missed punch, outing, overtime and check-in. OA mobile shows mirror/history/exception/emergency takeover according to the business boundary.

Why: Most mobile use happens from notifications or quick context switches. Reducing mobile scope keeps the phone path fast and prevents PC admin complexity from leaking into WeChat.

Tests Required:

  • cd client && npm run typecheck
  • cd client && npm run build
  • Playwright or screenshot coverage for representative narrow viewports when changing mobile home, mobile lists, mobile details, bottom actions, upload surfaces or Enterprise WeChat WebView flows.
  • If true Enterprise WeChat device validation is unavailable, record it as paused manual validation; do not claim full WebView acceptance.

Scenario: Shared Mobile Sticky Action Surface

1. Scope / Trigger

  • Trigger: a mobile detail, approval or edit surface needs a bottom fixed primary-action area, or an existing bottom action area is changed.
  • Applies to mobile consumers and responsive business edit forms; it does not move business permission or state-machine decisions into the shared shell.

2. Signatures

  • Component: MobileStickyActionBar({ children, className?, contentClassName?, inlineFromMedium?, testId? }) from client/src/components/patterns/MobileStickyActionBar.tsx.
  • Page spacing constant: MOBILE_STICKY_ACTION_PAGE_CLASS.

3. Contracts

  • New or changed mobile bottom actions reuse MobileStickyActionBar; do not copy its fixed positioning, safe-area, grid or shadow classes into a page.
  • When a fixed action bar is rendered, the page's scroll/content container also receives MOBILE_STICKY_ACTION_PAGE_CLASS so the final field, evidence card or history row remains above the bar and device safe area.
  • Apply the page class conditionally when actions are conditional; an absent bar must not leave unexplained fixed-action padding.
  • The shared component owns coordinates, safe-area treatment, responsive inline conversion and action layout only. Consumers still own button text, permission/state visibility, confirmation, pending and disabled behavior.
  • inlineFromMedium may convert the action surface to an inline desktop/tablet row for shared edit templates; mobile detail routes normally keep the fixed form.
  • PaymentBatchesPage retains its existing responsive payment-specific bar as a known legacy exception until a payment-scoped task proves migration has no business or layout regression. It is not a pattern to copy.

4. Validation & Error Matrix

  • Bar visible + page class present -> final content remains reachable above the fixed surface.
  • Bar visible + page class missing -> spacing-contract failure.
  • Bar hidden + unconditional page class -> unnecessary-padding failure.
  • Consumer moves approval/payment state logic into the shared component -> ownership-contract failure.
  • Narrow viewport or device safe area -> actions remain readable and are not covered by the home indicator.

5. Good/Base/Bad Cases

  • Good: mobile approval and mobile reimbursement share the same action surface while each page keeps its own action matrix and confirmation behavior.
  • Base: BusinessEditTemplate uses inlineFromMedium and the same page-spacing constant when bottom actions exist.
  • Bad: paste a page-local fixed inset-x-0 bottom-0 wrapper and compensate with an unrelated hard-coded pb-24.

6. Tests Required

  • A source contract asserts mobile approval, mobile reimbursement and BusinessEditTemplate import the shared component and page-spacing constant.
  • Focused Playwright covers at least two mobile consumers, including final content reachability, visible/pending/disabled action states and a narrow viewport safe-area regression.
  • Run cd client && npm run typecheck && npm run build.

7. Wrong vs Correct

Wrong

<main className="pb-24">
  ...
  <div className="fixed inset-x-0 bottom-0">{actions}</div>
</main>

Correct

<main className={cn(showActions && MOBILE_STICKY_ACTION_PAGE_CLASS)}>
  ...
  {showActions ? <MobileStickyActionBar>{actions}</MobileStickyActionBar> : null}
</main>

Convention: Notifications Versus Tasks

What: Task queues and notifications have separate jobs. A task queue is for action; a notification center is for event awareness and navigation.

Contracts:

  • 待我审批 is an action list. Include only items that require the current user to process, supplement material, review, pay, approve, confirm or resolve an exception.
  • Task rows must have a clear next step, object summary, business state and processing entry.
  • Completed items, read-only progress, announcements, successful sync notices and ordinary system messages do not belong in the pending task list.
  • 通知中心 is an event inbox for status changes, reminders, failures, sync results, workflow transitions, invoice/attachment recognition results and Enterprise WeChat delivery results.
  • Notifications should support read/unread, view, jump target and failure explanation. Do not rebuild the full workflow task list inside the notification center.
  • Notification clicks must deep-link to the business detail, workflow task, mobile target page or failure explanation. Do not send users back to a generic notification list as the final destination.
  • Notification copy should explain what happened, who or what is affected, and the next useful step. Do not expose raw payloads, internal state codes or debug fields.
  • Workbench 今日待处理 aggregates only actionable items. Ordinary notifications, read-only progress, completed activity and announcements are not counted as pending work.
  • If a notification represents real work, the backend or aggregation layer should expose it as an explicit task item; the frontend should not infer every notification is a task.
  • The topbar bell is the notification entry. Do not add notification center as a default sidebar row.
  • Enterprise WeChat messages are an external delivery channel. Keep message content desensitized and deep-link into the OA business or task detail.

Why: Separating event awareness from action prevents Portal, task queue and notification center from showing the same thing in three different ways.

Tests Required:

  • cd client && npm run typecheck
  • cd client && npm run build
  • Focused Playwright coverage when changing task aggregation, notification center rows, topbar bell behavior, notification deep links or workbench 今日待处理 counts.

Convention: Sensitive Data And Exports

What: Contracts, finance, invoices, receipts, payments, assets and attachments are sensitive by default. Frontend surfaces show the minimum useful summary unless a permissioned workflow requires more.

Contracts:

  • Treat contract text and attachments, quotations, acceptance materials, invoices, receipts, payment information, bank accounts, supplier details, asset serial/location/owner data, personnel records, payroll/performance, audit records, original file names and export files as sensitive.
  • Workbench, notifications, ordinary lists, mobile cards and Enterprise WeChat messages show desensitized summaries only.
  • Ordinary surfaces must not show full bank/account numbers, full local paths, storage paths, raw payloads, full sensitive attachment names, Secrets, tokens or high-sensitive contract/finance details.
  • Previewing contract text, invoices, receipts and attachments requires server-side permission checks. Frontend visibility is not a security boundary.
  • High-sensitive previews should explain context or purpose when useful and record view evidence when the backend supports it.
  • Download, batch download, export, print, attachment packaging, contract text export and finance ledger export must be controlled by server permissions and data scope.
  • Export is never the primary first-screen action. Put exports in a concrete 导出 action, 更多 or role-only finance/admin panels.
  • Before exporting sensitive ledgers, explain scope and sensitive contents. Finance, contract, asset and invoice exports should write audit records.
  • Export file names use business numbers and dates. Do not concatenate complete personal accounts, complete supplier secrets or internal filesystem paths.
  • Audit sensitive operations with actor, time, object, action, scope and result where supported.
  • Permission-denied and failure messages use natural Chinese without leaking full paths, permission codes, raw URLs, payloads, Secrets or tokens.

Why: The product is for small teams, but the data is still financial, contractual and operationally sensitive. Small team convenience must not turn into broad accidental disclosure.

Tests Required:

  • cd client && npm run typecheck
  • cd client && npm run build
  • Focused Playwright or screenshot checks when changing attachment preview, contract/invoice/receipt surfaces, export controls, mobile summaries, notification copy or permission-denied states.

Convention: Permission Empty And Error States

What: Permission-denied, empty and error states use natural Chinese that explains cause and next step. Ordinary users should not inspect platform internals to understand what happened.

Contracts:

  • Hide unauthorized actions by default. If a route is opened directly or the user needs context, show a natural Chinese explanation and a real next step such as returning to workbench, viewing related records or contacting an admin.
  • Empty states are scoped to the current filter, status or section. Explain what is absent and show one useful next step; if there is no real next step, hide the section instead of adding instructional copy.
  • Error states explain business cause, impact and recovery path such as retry, supplement material, return to list or contact admin.
  • Ordinary user paths must not show raw 403, API route names, permission codes, internal state codes, route stacks, full paths, raw payloads or long technical error dumps.
  • Admin, audit and troubleshooting routes may show the minimum technical details needed to diagnose a problem, but the business reason and next step remain primary.
  • Mobile and notification targets use shorter copy, but must still include cause and next step.

Why: Small-team users do not have time to decode implementation details. Clear failure states reduce support work and avoid leaking internal platform structure.

Tests Required:

  • cd client && npm run typecheck
  • cd client && npm run build
  • Focused Playwright or screenshot checks when changing empty states, permission-denied pages, notification failure targets or shared error components.

Scenario: Ordinary User Error Message Resolution

1. Scope / Trigger

  • Trigger: a frontend component catches an API or JavaScript error and needs to show failure feedback on an ordinary employee, approver, finance or mobile path.
  • Explicit exception: client/src/pages/AuditLogsPage.tsx is a permissioned, read-only technical detail surface and may display the minimum API code beside its error message.

2. Signatures

  • Shared resolver: resolveErrorMessage(error: unknown, fallback = "操作失败,请稍后重试"): string from client/src/lib/resolveErrorMessage.ts.
  • API business mapping remains mapApiErrorMessage(code, details, fallbackMessage) in client/src/api/errorMessages.ts.

3. Contracts

  • Ordinary user feedback calls resolveErrorMessage instead of concatenating error.code, error.message, request methods or API paths locally.
  • API codes may still drive control flow, such as detecting an already-processed task or a disabled capability, but they must not be reused as display copy.
  • The resolver returns mapped natural Chinese, removes a leading technical code prefix and falls back when the candidate is empty, lacks natural Chinese or resembles a URL, API/filesystem path, HTTP request line, stack frame or raw JSON payload.
  • Feedback placement remains separate from message parsing: field/material problems are local, region load failures are persistent and retryable, and short success feedback may use toast.
  • AuditLogsPage is the only current code-plus-message exception. New admin or troubleshooting exceptions require an explicit technical detail area and a scoped Trellis decision; do not weaken the shared resolver.

4. Validation & Error Matrix

  • Known API code with a mapped business message -> show the mapped natural Chinese only.
  • Candidate starts with VALID_001: or 500: -> remove the prefix before display.
  • Candidate is English-only, contains /api/, a full path, HTTP method, stack frame or raw JSON -> show the caller-provided fallback.
  • Unknown value or empty message -> show the caller-provided fallback.
  • Audit-log load/export failure caused by ApiError -> the read-only audit error area may show <code>: <message>; ordinary pages may not.

5. Good/Base/Bad Cases

  • Good: a reimbursement page uses resolveErrorMessage(error, "加载报销失败,请稍后重试") and keeps the retry action beside the persistent error.
  • Base: a page uses isApiError(error) only to branch on a business condition, then resolves any displayed failure through the shared helper.
  • Bad: each page removes a code prefix with its own regex or displays `${error.code}: ${error.message}` on an ordinary path.

6. Tests Required

  • A source contract scans ordinary frontend consumers and rejects local code-plus-message concatenation while listing AuditLogsPage as the explicit exception.
  • Focused tests cover a mapped API error, a leading code prefix, a technical detail fallback and an unknown error.
  • Run cd client && npm run typecheck && npm run build.

7. Wrong vs Correct

Wrong

setError(isApiError(error) ? `${error.code}: ${error.message}` : "加载失败");

Correct

setError(resolveErrorMessage(error, "加载失败,请稍后重试"));

Convention: Dangerous Action Confirmation

What: Destructive, financial, approval, configuration, batch and external sync actions require meaningful confirmation. A generic 确定吗? dialog is not enough.

Contracts:

  • Treat delete, void, revoke, archive, restore, disable, publish, configuration switching, workflow switching and feature-flag changes as dangerous actions.
  • Treat pay, batch pay, sensitive export, batch download, contract text download and attachment packaging as dangerous or high-sensitive actions.
  • Treat approve, reject, return, transfer, batch approve and batch reject as approval-risk actions that need context before execution.
  • Treat external synchronization, retry external write and Enterprise WeChat/OA emergency-takeover switching as integration-risk actions.
  • Confirmation UI must show object summary: business number, name, requester or counterparty, amount or quantity and current state when relevant.
  • Confirmation UI must explain consequences: target state, reversibility, notifications, payment/approval/configuration impact and external-system effect.
  • Confirmation UI must show impact scope for batch, export, configuration and integration actions: selected count, data range, external system, Enterprise WeChat message or audit scope.
  • Rejection and return require a reason. Payment, publishing, batch and configuration actions may require a confirmation word, explicit checkbox or selected reason when risk is high.
  • Dangerous actions must be visually separated from normal view, edit, save and submit actions.
  • Mobile dangerous actions use bottom or full-screen confirmation panels, not instant execution from an easy-to-mistap control.
  • Confirmation failure must preserve user input and show result details plus the next repair action.
  • Audited actions should record actor, time, object, action, scope, reason and result where backend support exists.
  • Delete, withdraw, cancel, void and archive confirmations must use the exact business verb and explain the target state. Do not label cancel, void or archive as 删除.

Why: These actions can change finance, contracts, workflow, approvals, configuration and external integrations. Users need context before execution, and support needs an audit trail after execution.

Tests Required:

  • cd client && npm run typecheck
  • cd client && npm run build
  • Focused Playwright coverage when changing delete, payment, approval, publish/disable, batch, configuration or external-sync confirmations.

Convention: Integration Boundary And External Status

What: External-system state is conservative. OA shows facts confirmed by its own backend and explicit external acknowledgements; it does not present a queued request, frontend click or message attempt as final business completion.

Contracts:

  • OA owns reimbursement, procurement, contracts, projects, payments, AP, invoices, assets, sensitive attachments, audit and OA workflow handling.
  • Enterprise WeChat owns daily HR/attendance actions: check-in, leave, missed punch, outing and overtime. OA shows mirror, history, exception, audit and emergency-takeover records for those domains.
  • Agency accounting, bank, OCR and Enterprise WeChat messages are external capabilities or delivery channels. They do not replace OA server-side permissions or state machines. ERP is outside hyb-oa's product boundary and must not appear as a current or future user-facing integration.
  • Show final labels such as 已同步, 已发送, 已付款 and 识别完成 only when the relevant external system has returned a clear success acknowledgement.
  • Use in-progress labels such as 同步中, 等待回执, 银行处理中, 识别中 and 消息发送中 when the request has started but is not confirmed.
  • External failures must show source, impact and next step in natural Chinese, such as 银行回执未返回 or OCR 识别失败,需重新上传 PDF.
  • Do not expose raw external error codes, payloads, full URLs, secrets, tokens or debug traces to ordinary users.
  • External writes, payment execution, Enterprise WeChat resend and OCR retry must be controlled by permissions and state-driven actions.
  • Retry confirmation should explain which object will be retried, whether it may send duplicate external messages, external-system impact and audit effect.
  • External sync state should not block unrelated primary flows. If it blocks a flow, show the blocker and manual repair path.
  • Ordinary routes show summary, failure reason and next step. Admin, finance or troubleshooting routes may show deeper diagnostics when authorized.
  • HR/attendance routes should default to Enterprise WeChat record, sync mirror or exception handling copy. Do not expose OA daily leave/missed-punch paths unless OA emergency takeover is explicitly enabled.
  • Enterprise WeChat messages carry desensitized summary and OA deeplink only; they do not carry sensitive attachment text or final payment/approval actions.
  • If bank, agency accounting or another enabled integration is not connected or not confirmed, UI copy must not imply the capability already completed. Do not use ERP as a placeholder for future integration copy.

Why: External integrations are asynchronous and failure-prone. Conservative copy prevents users from trusting a state the system has not actually verified.

Tests Required:

  • cd client && npm run typecheck
  • cd client && npm run build
  • Focused Playwright coverage when changing Enterprise WeChat boundary copy, OCR status, bank/payment callbacks, accounting handover status, retry controls, external failure messages or integration diagnostics.

Convention: Verification And User-Testable Claims

What: Verification claims must describe the actual validation level. Local checks, screenshots and mock Playwright runs do not mean a user can test the change online.

Contracts:

  • Local build passing means only the local code built successfully.
  • Playwright passing means the specified automated path passed in the specified environment and data state.
  • Screenshot review passing means only the captured viewport and state were reviewed.
  • Deployment passing must identify the deployed environment, version or commit and health check result when available.
  • Say 可以线上测试 only after deployment, public access check, key-path smoke testing and necessary screenshot evidence are complete.
  • Before deployment, say 本地验证通过,尚未线上可测.
  • After deployment but before public access check, say 已部署,公网检查未完成.
  • If real-device or Enterprise WeChat WebView validation is unavailable, record it as paused manual validation. Do not substitute automation for full WebView acceptance.
  • A-class page changes should keep build, focused Playwright or screenshot evidence according to risk.
  • Changes touching permissions, state machines, approval, payment, Enterprise WeChat, external systems, sensitive downloads or production configuration need rollback or manual repair notes.
  • Updating this design/spec documentation does not mean pages were refactored, deployed or accepted for live trial.

Why: This keeps delivery status honest and prevents the user from trying a change that has not actually reached a public environment.

Tests Required:

  • Documentation-only changes: Trellis task validation, governance check and git diff --check.
  • Frontend implementation changes: cd client && npm run typecheck, cd client && npm run build, plus focused Playwright or screenshots based on affected surface.

Convention: A-Class PC Task-First Pages

What: A-class PC pages such as Portal, approvals, reimbursement list and reimbursement detail should make the current user task obvious before exposing secondary operations:

  • Treat the primary persona as a small-team operator wearing multiple hats, not a dedicated OA clerk. The first screen should reduce decision cost for a founder, finance/admin owner, project lead or employee who may switch roles during the same session.
  • First visible task surface should answer: what needs attention, what can be started, and where recent progress lives.
  • Keep one primary action in the first screen. Allow at most two secondary entries; move export, batch, delete, templates, complex filters, statistics and admin/finance tools into 更多, 高级 / 台账, detail actions, role-only panels or confirmation dialogs.
  • Do not turn small-team workflows into platform-builder surfaces by default: approval designers, broad configuration matrices, multi-level enterprise dashboards and batch admin consoles need explicit Trellis task scope.
  • Explanation text should only be default-visible for risk, exception or empty states. Do not use repeated subtitles or notice cards to explain normal workflow steps that the layout can express.
  • Low-frequency capabilities are hidden or moved, not removed, unless the current Trellis task explicitly deletes the capability.
  • Portal/workbench first screen should use a compact status summary plus 今日待处理; do not use a persistent welcome hero, fixed 报销进度 button, general announcement feed, or folded placeholder sections such as 团队与通知, 角色重点 or 试点观测摘要.
  • Workbench sections after 今日待处理 must be conditional: 团队卡点 only for real blockers, 管理提醒 only for real admin/trial issues, and 我的进度 only for reliable status updates. Empty data should disappear, not render as collapsed drawers or statistic cards.
  • Each workbench action row should state what the item is, why the current user is involved, current status/timing, and one next action. Do not represent raw application counts as progress unless the payload includes an actionable or status-level signal.
  • PC reimbursement list defaults to the initiator's own forms even for users with finance/approval roles. The first screen may use task cards for rejected corrections, drafts and reviewing progress; ledger/export/batch, proxy submit, templates and travel-linking remain secondary and must be hidden unless the current user has a real matching capability. Row workflow copy should distinguish 待你处理 from read-only 当前节点.

Why: hyb-oa serves small teams with overlapping roles. Core pages must make the next click clear for ordinary employees and approvers, while keeping finance and admin tools available without dominating the default path.

Tests Required:

  • cd client && npm run typecheck
  • cd client && npm run build
  • Focused Playwright or screenshot checks for changed A-class PC pages, asserting the task surface, primary action and secondary/advanced entry behavior.

Convention: Approval Task List Defaults

What: PC approval task lists such as /workflow/tasks should make 查看处理 the default row action. List-level 通过 is disabled by default unless a later Trellis task adds an explicit backend eligibility contract.

Contracts:

  • The row must answer what the item is, who requested it, why it is assigned to the current user, how long it has waited, and the public business reference.
  • History entry text should be explicit, such as 已处理记录; avoid user-facing 高级 / 历史 on the default task surface.
  • Time labels should prefer 已等待多久 and 接收于 over database-shaped labels such as 更新时间 when the source field is task creation/arrival time.
  • HR/attendance items owned by Enterprise WeChat, including leave, missed punch, outing, overtime and check-in related approvals, must not show OA list-level quick approval. OA may show mirror/history/status/exception context and emergency-takeover records according to the business boundary, but normal approval remains in Enterprise WeChat.
  • Reimbursement, procurement, travel, contract and other amount/evidence-heavy OA-owned tasks should route through 查看处理 first.
  • Business ledgers and ordinary business details must follow the same rule: show the task entry or progress, but do not place 通过, 驳回, 退回 or 转审 directly in reimbursement, procurement, contract, project or asset tables/details.
  • Future list-level quick approval requires the backend to expose an explicit eligibility flag such as quickApproveEligible=true, plus task-domain and audit semantics in the current Trellis task. Frontend heuristics based only on entity type, duration or row text are not enough.
  • If list quick approval is reintroduced later, its confirm dialog must still explain action consequences and include the specific task summary.

Why: Small-team approvers should build the habit of opening the task detail before approval, and the OA/Enterprise WeChat ownership boundary must stay clear for HR/attendance workflows.

Convention: Workbench Onboarding And Profile Scope

What: PC workbench and account settings should stay task-first, not become training or permission-maintenance surfaces:

  • Do not add a persistent 快速上手 / onboarding checklist to the workbench by default. If first-login guidance is needed, use a dismissible welcome dialog with at most three real business entries.
  • Welcome or onboarding entries must point to executable user tasks, such as 待我审批, 提交报销 or 请假; do not use explanatory entries like 了解流程, and do not route users to settings or notifications as a substitute for task guidance.
  • 个人设置 default content is personal profile, password/security, notification preference and Enterprise WeChat binding. Organization, report line and role summary may be folded under 账号详情; permission code lists and full employee dossier fields stay out of the default view. Do not add a default 查看员工档案 / employee-maintenance jump from the ordinary profile page; profile fields are read-only personal context, while dossier maintenance belongs to 人员 / 管理.
  • Keep account settings separate from admin configuration. 个人设置 may show personal profile, password/security, notification preferences and Enterprise WeChat binding only. Organization maintenance, role assignment, approval rules, feature flags, master data, audit logs, employee maintenance and system configuration belong under 管理, not the ordinary profile route.
  • Mobile 我的 and 个人设置 must not expose an admin matrix or raw internal labels. Hide 管理入口 / 人员管理 / 组织架构 / 系统设置 from mobile 我的, and map account status plus role codes to Chinese labels before display.

Why: Small teams need fewer default decisions. Persistent onboarding and permission-heavy profile pages make ordinary users inspect system internals before doing work.

Tests Required:

  • Focused Playwright coverage for changed workbench or profile routes.
  • Screenshot evidence for first-login welcome, workbench first screen and profile default view when changing these surfaces.
  • Workbench baseline screenshots must explicitly clear onboarding localStorage such as hyb_oa_show_welcome and hyb_oa_onboarding_active before capture, unless the test is intentionally verifying the welcome dialog.

Convention: Detail And Mobile Command Surfaces

What: PC detail pages and notification-like flows should reuse the Wave3 surface classes in client/src/index.css before adding page-local Tailwind card systems:

  • detail-workspace-grid for the main PC detail two-column workspace.
  • detail-command-panel, detail-fact-grid and detail-fact-card for compact status, summary, payment, export and evidence facts.
  • detail-evidence-card for attachments, invoices and other proof material.
  • detail-timeline and detail-timeline-item for approval progress and history.
  • detail-action-stack and detail-action-card for approve/reject/return, payment and other high-risk command groups.
  • notification-inbox, notification-row-card and notification-row-card--unread for PC notification streams; avoid reverting notifications to a desktop table unless a task explicitly requires tabular comparison.

Mobile key pages should reuse the mobile command classes in client/src/styles/mobile-touch.css:

  • mobile-focus-card for the current task, current list or account summary.
  • mobile-command-surface for tabs, search and filters.
  • mobile-list-card--focus and mobile-list-card--unread for state emphasis.

Why: Detail pages should feel like quiet command surfaces: status, summary, primary action, evidence and history have predictable places. Mobile pages should stay card/list based, with no PC tables, no horizontal overflow and no button text squeeze.

Tests Required:

  • cd client && npm run typecheck
  • cd client && npm run build
  • Focused Playwright screenshot coverage for changed PC detail, notification and mobile pages.
  • UI design rules check when changing shared visual classes.

Scenario: Auth Pages And PWA Updates

1. Scope / Trigger

  • Trigger: login, password change, password recovery or other unauthenticated forms are active while the PWA service worker finds a new frontend version.
  • Applies to client/src/pwa.ts, client/src/layouts/AuthLayout.tsx, and auth routes such as /login, /change-password, /forgot-password, and future reset-password pages.

2. Signatures

  • Event name: hyb-oa:pwa-update-ready.
  • Event detail: { applyUpdate?: () => void | Promise<void> }.
  • Route predicate: auth input pages must be checked by pathname before calling the captured controller-update refresh handler.

3. Contracts

  • Auth input pages must not auto-reload when a replacement native controller activates.
  • Auth input pages must show an explicit update prompt with a user-triggered refresh action.
  • Non-auth pages may keep automatic service-worker update behavior when the task does not change broader runtime policy.
  • Passwords and verification codes must stay only in React component state; do not write them to localStorage, sessionStorage, URL params, reports, or Trellis task evidence.
  • Password visibility controls must use icon buttons with accessible labels and preserve the current input value.

4. Validation & Error Matrix

  • PWA update on /login -> dispatch update-ready event and keep form state.
  • PWA update on /forgot-password -> dispatch update-ready event and keep email/code/password fields in memory.
  • User clicks refresh in the prompt -> run the captured update handler.
  • User dismisses prompt -> no reload and no form reset.
  • Password reset request returns success for any email -> show generic sent message; do not reveal whether the account exists.

5. Good/Base/Bad Cases

  • Good: user is typing a password, update prompt appears, typed values remain.
  • Base: user is on a normal portal page, PWA update can refresh per existing runtime behavior.
  • Bad: service-worker update auto-refreshes an auth form and clears credentials.
  • Bad: tests use real-looking personal email addresses or persist example passwords outside the test file.

6. Tests Required

  • Frontend typecheck and build.
  • Focused Playwright auth UX test asserting:
    • login password can be shown/hidden without losing value;
    • forgot-password flow calls request-code and reset endpoints;
    • auth-page PWA update prompt appears without clearing inputs.
  • UI design rules check when auth page layout changes.
  • Any Playwright test that renders /login, /forgot-password or another auth route with mocked /api/* responses must mock /api/auth/capabilities with emailDelivery.ready, emailLoginCode.enabled and passwordReset.enabled. Returning {} for unknown auth APIs can crash auth pages before the visual or navigation assertion actually runs.

7. Wrong vs Correct

Wrong

serviceWorker.register("/sw.js").then(() => {
  // Too late: navigation may have installed and activated the worker already.
  serviceWorker.addEventListener("controllerchange", reload);
});

Correct

function onActivatedReplacementController(applyUpdate: () => void) {
  if (isAuthInputPath(window.location.pathname)) {
    window.dispatchEvent(new CustomEvent(PWA_UPDATE_READY_EVENT, { detail: { applyUpdate } }));
    return;
  }
  applyUpdate();
}

Scenario: Live WebView Build Freshness

1. Scope / Trigger

  • Trigger: a production browser, PWA or Enterprise WeChat WebView can remain alive across deployment and continue running an old SPA bundle even though public index.html, service worker and immutable chunks are current.
  • A public bundle containing new copy proves deployment, not that every live WebView has loaded that bundle.

2. Signatures

  • Client build: document.documentElement.dataset.buildSha, sourced from VITE_BUILD_SHA and normalized to 12 hexadecimal characters.
  • Server check: GET /ready -> { data: { ok: true, buildSha: string } }.
  • Resume event: document visibilitychange with visibilityState="visible".
  • Prompt event: hyb-oa:pwa-update-ready with { applyUpdate }.

3. Contracts

  • Check only when a loaded page returns to the foreground; do not add a timer or poll continuously.
  • Request /ready with cache: "no-store"; compare only valid hexadecimal build identifiers and never display the identifiers to ordinary users.
  • A mismatch dispatches the existing update-ready event once per page lifecycle. The logged-in shell shows 发现新版本 with an explicit 点击刷新; it must not auto-reload an approval, form or auth input.
  • The refresh handler clears known PWA runtime/precache caches and then reloads the current URL. Existing auth-page update protection remains in force.
  • This monitor does not replace deployment build checks or real-device verification; it closes the live-client freshness gap after deployment.
  • The first deployment of this monitor cannot notify a WebView that is still executing an older bundle without the monitor. That rollout still requires an explicit manual refresh instruction; later deployments gain the guard.

4. Validation & Error Matrix

  • Client and server builds match -> no prompt.
  • Valid builds differ -> show one prompt.
  • /ready is unavailable, non-2xx, malformed or missing buildSha -> stay silent and preserve the current page.
  • Client build is development, unknown, empty or malformed -> stay silent.
  • Repeated foreground events after a mismatch -> no duplicate request or prompt in the same page lifecycle.

5. Good/Base/Bad Cases

  • Good: a manager returns to an old approval WebView, sees one refresh prompt, chooses refresh and then receives the deployed approval-detail bundle.
  • Base: the deployed and client builds match, so returning to the app adds no UI or network loop beyond the single readiness check.
  • Bad: infer that a missing button is a permission problem when adjacent UI from the same unconditional JSX block proves an old bundle is running.
  • Bad: force-refresh every foreground event or build a general update center.

6. Tests Required

  • Focused Playwright with synthetic builds covers mismatch, match and failed /ready responses.
  • Mismatch coverage asserts one prompt, no duplicate check in the same page, user-triggered reload, a 44px refresh target and no horizontal overflow at 375px portrait and phone landscape widths.
  • Run the full affected login/portal UX file, frontend typecheck and build.
  • Production acceptance still requires the representative Enterprise WeChat WebView to refresh and visibly receive the new business entry.

7. Wrong vs Correct

Wrong

setInterval(() => window.location.reload(), 60_000);

Correct

document.addEventListener("visibilitychange", () => {
  if (document.visibilityState === "visible") {
    void checkBuildFreshness();
  }
});

Scenario: Auth Page Layout And Login Entry Hierarchy

1. Scope / Trigger

  • Trigger: login, password recovery, forced password change or future unauthenticated auth forms need layout changes.
  • Applies to client/src/layouts/AuthLayout.tsx, shared auth components under client/src/components/auth/, and auth routes such as /login, /forgot-password and /change-password.

2. Component Contract

  • Auth routes should use one shared auth panel for brand, page title, subtitle, content and optional footer links.
  • AuthLayout owns only the auth page shell: background, width constraint, centering, PWA update prompt and route outlet. Its default variant remains a 460px auth form; /login may explicitly select the login variant to allow the approved desktop entry layout without widening password recovery or forced password change pages.
  • Page implementations own only their form fields, submit actions, validation messages and route-specific login-entry choices.

3. Visual Contracts

  • Do not nest a form card inside another login card.
  • Put the brand block inside the auth panel so logo, system name, title and form share one visual axis.
  • Keep password recovery, forced password change and login fallback-only panels stable around the 460px auth-shell width. When PC Enterprise WeChat login is enabled, /login may expand to a functional desktop split up to roughly 920px: Enterprise WeChat entry on the left and a form column capped around 404px on the right. This remains one shared auth panel, not a marketing split hero or nested form-card layout.
  • Place 忘记密码? beside the password label or directly below the password input on the right; do not bury it in help text or page footer.
  • Keep the DOM/tab order aligned with the main form flow even when CSS places 忘记密码? visually beside the password label: username/email input -> password input -> secondary controls. Do not render the forgot-password link before the password input in DOM order.
  • Use password visibility icon buttons with accessible labels and preserve the current input value.

4. Login Entry Hierarchy

  • Normal browser with pcWebLoginEnabled=true and a non-null pcLoginUrl: Enterprise WeChat is the preferred desktop entry. At 1024px and above, show it in the left functional column and keep account/password plus email-code in the right fallback column. Below 1024px, preserve the existing single-column form-first order and show the PC Enterprise WeChat entry afterward.
  • Normal browser without an available PC login URL: keep the existing 460px single-column form and show only the workbench boundary notice or other configured secondary provider; never render an empty desktop column or a disabled PC login button.
  • Enterprise WeChat WebView: Enterprise WeChat one-click entry is primary; account/password login remains available as the fallback path.
  • Enterprise WeChat entry changes must remain frontend entry hierarchy changes unless the task explicitly approves backend OAuth, binding or WeCom API work.
  • Email verification-code login is a separate login mode. Do not infer it from password-recovery verification-code flows without a backend login-code contract and abuse-control plan.

5. Tests Required

  • Frontend typecheck and build.
  • Focused auth E2E covering:
    • password visibility and forgot-password navigation on /login;
    • keyboard Tab from the username/email field lands on the password input, not the forgot-password link;
    • password recovery request-code and reset flow on /forgot-password;
    • 1024px and 1440px normal-browser PC Enterprise WeChat functional split, including column position and controlled form width;
    • 375px and 768px single-column form-first order without horizontal overflow;
    • PC login configuration unavailable falling back to the 460px single column;
    • WeCom WebView Enterprise WeChat entry as primary CTA with password fallback.
  • UI design rules check and Playwright screenshot evidence when the auth layout changes visibly.

6. Wrong vs Correct

Wrong

<AuthLayout>
  <Card>
    <Card>
      <LoginForm />
    </Card>
  </Card>
</AuthLayout>

Correct

<AuthPanel title="登录工作台" subtitle="账号密码登录">
  <LoginForm />
  <SecondaryLoginMethods />
</AuthPanel>

Wrong

<Label htmlFor="login-password">密码</Label>
<Link to="/forgot-password">忘记密码?</Link>
<PasswordInput id="login-password" />

Correct

<Label htmlFor="login-password">密码</Label>
<PasswordInput id="login-password" />
<Link className="row-start-1" to="/forgot-password">忘记密码?</Link>

Convention: Finance And Admin Console Surfaces

What: PC finance and C-class admin pages should reuse Wave4 shared classes from client/src/index.css before creating page-local card/table layouts:

  • finance-status-strip, finance-metric-card, finance-metric-card__label and finance-metric-card__value for compact admin summaries, analytics surfaces or true dashboards. Ordinary business ledger lists such as 付款 and 票据 should prefer BusinessListTemplate table surfaces instead of standalone summary chips/cards.
  • finance-workspace-grid, finance-command-panel, finance-list-card and finance-list-card--active for payment/AP/invoice operational workspaces.
  • admin-command-panel, admin-tile-grid, admin-tile-card and admin-workspace-grid for settings, workflow, feature-flag, user, master data, notification-channel and audit pages.

Why: Finance/admin pages are lower-frequency but high-risk operational surfaces. They need dense scanability, stable command regions and clear risk hierarchy without turning every page into a bespoke Tailwind card system.

Example:

<div className="finance-status-strip" data-testid="approval-rules-summary">
  <div className="finance-metric-card">
    <div className="finance-metric-card__label">启用规则</div>
    <div className="finance-metric-card__value">{enabledCount}</div>
  </div>
</div>

<BusinessTable testId="payment-batches-table">...</BusinessTable>

Tests Required:

  • cd client && npm run typecheck
  • cd client && npm run build
  • Focused Playwright coverage for changed finance/admin pages. The Wave4 baseline test is client/tests/e2e/pc-finance-admin-visual-wave4.spec.ts.
  • UI design rules check when changing these shared classes.

Scenario: Frontend Acceptance Spec Gate Registration

1. Scope / Trigger

  • Trigger: a frontend E2E spec is treated as a current acceptance contract rather than historical evidence or a manual-only diagnostic.

2. Signatures

  • Manifest entry: scripts/qa/route-manifest.json → acceptanceSpecs[].
  • Executable gate: bash scripts/qa/run-local-candidate.sh list|focused|full.

3. Contracts

  • A current acceptance spec must be referenced by an executed gate; merely existing under client/tests/e2e/ is not coverage.
  • list must resolve the spec, while focused or full must execute it and reject failed or skipped acceptance results.
  • DOM discovery may be dynamic to avoid duplicating selectors, but the expected business set, order or invariant must remain explicit. Never accept whatever the page happens to render.
  • Historical or unowned specs must be deliberately classified for later connection or deletion; do not silently present them as active coverage.

4. Validation & Error Matrix

  • Manifest entry missing or invalid → flow-coverage gate fails.
  • Acceptance spec skipped, failed or timed out → local-candidate result gate fails.
  • Dynamic collection with no fixed expectation → review failure even if the test is green.
  • Gate fixture cannot execute a newly called child script → fixture test fails and the child script must be added through command discovery or explicit setup.

5. Good/Base/Bad Cases

  • Good: discover current action tiles from the grid, then assert the fixed role-specific key/label set and layout invariants.
  • Base: a stable single control uses a direct data-testid assertion and is listed in acceptanceSpecs.
  • Bad: count whatever matching nodes exist, assert only their individual dimensions, and leave the spec outside every runner.

6. Tests Required

  • node --test scripts/qa/tests/check-flow-coverage.test.mjs scripts/qa/tests/check-minimal-profile.test.mjs scripts/qa/tests/verify-local-candidate-results.test.mjs.
  • bash scripts/qa/run-local-candidate.sh list must list the spec.
  • bash scripts/qa/run-local-candidate.sh focused or full must finish with zero failed/skipped acceptance tests.

7. Wrong vs Correct

Wrong

const actions = collectVisibleActions();
for (const action of actions) expect(action.height).toBeGreaterThanOrEqual(44);

Correct

const actions = collectVisibleActions();
expect(actions.map(({ key, label }) => ({ key, label }))).toEqual(EXPECTED_ACTIONS);
for (const action of actions) expect(action.height).toBeGreaterThanOrEqual(44);

Scenario: Management Route Metadata And Permission Disclosure

1. Scope / Trigger

  • Trigger: adding or changing a PC management landing item, management subpage, nested management detail, breadcrumb, sidebar ownership or recent-visit title.

2. Signatures

  • Registry: client/src/features/navigation/managementRoutes.ts.
  • Resolver inputs: pathname: string plus the current permissions: string[].
  • Router permissions: getManagementRoutePermissions(key).
  • Recent visits: writeRecentVisit(path, title, permissions) and readRecentVisits(permissions).

3. Contracts

  • The management landing cards, root-navigation permission union, sidebar active state, Topbar title/breadcrumb and recent-visit filtering derive from the same registry entry.
  • React Router keeps explicit component declarations and consumes the same registry permissions. Server authorization remains the hard access boundary; navigation metadata only enforces least disclosure.
  • Match the most specific route deliberately. A nested employee profile must resolve after its exact siblings and must not inherit a broader prefix by accident.
  • A user without the registered permission receives no restricted title, breadcrumb or recent-visit link. Revoked permissions also clean stale local recent-visit records.
  • The sidebar keeps one 管理 root entry. Do not spread low-frequency child pages back into the primary navigation.

4. Validation & Error Matrix

  • Registered route + allowed permission -> natural Chinese title, parent chain and management sidebar active state.
  • Registered route + denied permission -> no restricted metadata; Router and server guards still reject access.
  • Unknown registry key passed to the Router helper -> throw during development instead of silently using an empty permission list.
  • Newly declared application route missing from the route manifest -> local candidate route gate fails.

5. Good/Base/Bad Cases

  • Good: /people/:employeeId uses the registry for both 管理 / 人员 / 员工档案 and its system.users Router guard.
  • Base: /system/settings displays only 管理, with no self-referencing breadcrumb.
  • Bad: add a path to SystemSettingsPage, NavLinks, PAGE_TITLES and Router as four hand-maintained facts.

6. Tests Required

  • Registry contract covers unique keys, expected path inventory, parents, permissions, landing metadata and root permission derivation.
  • Permission-negative coverage asserts no title/breadcrumb disclosure and filters a previously stored recent visit after permission revocation.
  • Focused Playwright covers direct management deep links, sidebar active state, nested breadcrumbs and the management-root return link.
  • Run cd client && npm run typecheck && npm run build and bash scripts/qa/run-local-candidate.sh list.

7. Wrong vs Correct

Wrong

const MANAGEMENT_ACTIVE_PATHS = ["/people", "/system/users"];
<RequirePermission anyOf={["system.users"]}>...</RequirePermission>

Correct

isManagementPath(pathname, permissions);
<RequirePermission anyOf={getManagementRoutePermissions("people")}>...</RequirePermission>

Scenario: Authentication Password Input Sequential Focus

1. Scope / Trigger

  • Trigger: changing the shared password input or any login, password-change or password-recovery form.
  • Purpose: preserve the domestic continuous-form Tab flow without letting the inline visibility control create an extra sequential stop.

2. Signatures

  • Shared component: client/src/components/ui/password-input.tsx.
  • Visibility control: native button type="button" with tabIndex={-1}, dynamic aria-label, boolean aria-pressed and aria-controls=<input-id>.
  • Sequential focus follows DOM order; do not use positive tabIndex values.

3. Contracts

  • Pressing Tab from a password input skips its inline visibility button and reaches the next sequential form control in the DOM.
  • Existing links such as 忘记密码? remain keyboard reachable; do not remove unrelated controls merely to make a password-to-submit shortcut.
  • Mouse and touch can still toggle visibility. The label changes between 显示密码 and 隐藏密码, pressed state follows visibility, and the input value is preserved.
  • Login, password-change and password-recovery pages reuse the shared component and therefore keep one focus contract.
  • The focus change does not alter password validation, autofill, form submission, authentication APIs, email delivery or server configuration.

4. Validation & Error Matrix

  • Tab lands on the inline eye button -> sequential-focus regression.
  • Tab skips the next real input, link or submit control -> DOM-order regression; do not repair with a positive tabIndex.
  • Visibility click changes type but clears the value -> input-state regression.
  • Toggle lacks its dynamic label, pressed state or input association -> accessibility regression.
  • Enter from the confirmation password input does not submit the valid form -> form-keyboard regression.

5. Good/Base/Bad Cases

  • Good: reset form moves 新密码 -> 确认新密码 -> 重置密码, while either eye control remains clickable and preserves the password.
  • Base: login password moves to the existing 忘记密码? link because that link is the next DOM control, then continues to login submit.
  • Bad: keep the eye button in sequential Tab despite the approved form contract, remove all button semantics, or assign positive indices to force an artificial order.

6. Tests Required

  • Playwright asserts the login password reaches the forgot-password link, password-change fields traverse old/new/confirm/submit, and recovery fields traverse new/confirm/submit.
  • Assert tabindex=-1, aria-controls, dynamic aria-label, aria-pressed, click-to-toggle and value preservation.
  • Assert Enter from the valid confirmation-password field submits the form.
  • Run cd client && npm run typecheck && npm run build, focused authentication E2E, Trellis/process/sensitive gates and git diff --check.

7. Wrong vs Correct

Wrong

<button type="button" aria-label="显示密码" onClick={toggleVisible}>
  <Eye />
</button>

Correct

<button
  type="button"
  tabIndex={-1}
  aria-controls={inputId}
  aria-label={visible ? "隐藏密码" : "显示密码"}
  aria-pressed={visible}
  onClick={toggleVisible}
>
  <Eye aria-hidden="true" />
</button>

Scenario: Payment Detail Guides First Order Before Receipt

Scope / Trigger

The selected payment-batch detail after independent review, not the default payment ledger landing page. Reuse the existing desktop and mobile action model.

Signatures

PaymentBatchesPage.paymentActionModel, canExportPaymentOrder, paymentOrderExportCount, needsBankReceipt, canRegisterPaymentComplete. The backend remains authoritative for reviewed status, role and snapshot checks.

Contracts

  • A reviewed, exportable bank batch with no generated order and no receipt uses 下载银行制单 as its single primary action. Do not ask for a bank receipt before exposing the first order needed to make that payment.
  • Keep 上传银行回单 in 更多 for users who already paid manually; generating an order is not a new hard prerequisite for recording a legitimate payment.
  • After first generation, use 上传银行回单 as primary and put re-download in 更多. With an existing receipt, keep 登记付款完成 primary even if no order was generated. Unknown receipt state is not an empty receipt list.
  • Re-download requires a reason and the duplicate-bank-submission warning. Download success must not execute a payment, upload a receipt or mark paid.
  • Legacy/non-bank/non-exportable batches retain the existing manual flow. Do not add roles, bypass backend guards or change the payment state machine.

Validation / Error Matrix

State Primary Additional route
Reviewed, exportable, first order, no receipt Download order Manual receipt upload in more
Order generated, no receipt Upload receipt Re-download in more
Receipt already present Register payment complete Existing allowed order actions
Download fails Keep download step Reason dialog remains; no payment mutation
Receipt query fails No upload/complete primary inferred from absence Reload/error feedback

Good / Bad Cases

Good: finance downloads the approved batch file, then receives a receipt prompt. Bad: hiding first generation behind more while prompting for a not-yet-existing receipt; or requiring an export before accepting an already-completed manual payment.

Tests Required

Desktop/mobile first download, reason/cancel, received BIFF8 bytes, re-download warning and no payment mutation, manual upload via the actual file chooser, existing-receipt and non-bank behavior, and unknown-receipt refusal. Keep backend authorization and independent workbook parsing tests separate from browser mocks.

Wrong vs Correct

Wrong: a visible action or a generated file proves money was paid. Correct: file generation, bank execution and OA payment registration are distinct events. Synthetic browser/backend/container checks do not prove real bank acceptance.

Native PWA controller ownership (09-18 local concurrency fix)

  • Capture the current controller and subscribe to native controllerchange before asynchronous register(). Use one registration owner (injectRegister: false); do not combine generated registration with application reload ownership.
  • Wait for the observed controller to activate and verify it is still current. Ignore redundant/superseded/duplicate events. Initial installation dispatches offline-ready without reload; an existing controlled page applies a replacement once.
  • On auth input routes, dispatch the existing update-ready event and retain typed input until explicit user refresh. Background-to-foreground build freshness keeps its existing user prompt.
  • Controller update clears only app API/static runtime caches. Workbox owns precache revisions. Explicit logout/manual refresh can retain the broader existing cache-clear helper.
  • Failure witnesses record observations or null, not invented default false/zero. Build B evidence requires completed performance entries for B-only resources plus B-only cache entries; SPA navigation events are not a document reload count.
  • Validate native lifecycle unit cases, full auth UX and the same-profile A/B browser witness. Old running client code cannot be retroactively fixed; first rollout still needs freshness/refresh verification.