# Web Frontend QA Report ## 1. Executive Summary **Overall health: Solid at the contract/logic layer, blocked at the browser layer.** - **Areas tested:** 15 (12 functional+API areas, 2 full-LLM E2E areas, 1 cross-route browser smoke pass). - **Backend contract + component logic:** Largely sound. Happy paths, validation (422), error envelopes (404/409), snake_case alignment, optimistic-update/rollback, and a11y structure mostly check out across all areas. - **Full LLM E2E that genuinely ran:** write-sse (real Kimi draft stream, 1261 tokens), toolbox-generators (all 12 generators + 3 ingest flows), style (16-dim fingerprint + refine), chain (API + awaiting/resume seam verified against real jobs). - **What could NOT be verified (environmental):** The running Next dev server had a corrupted `.next` build cache — every `/projects/[id]/*` SSR route returned HTTP 500 ("Cannot find module openapi-fetch@0.13.8.js"), and client JS chunks 404'd so React never hydrated. This blocked ALL in-browser/visual/keyboard/a11y verification. Root cause: stale build + two concurrent `next dev` processes; not a source defect. Fix: kill duplicate server, `rm -rf apps/web/.next`, restart. **Issue counts (de-duplicated): 23 total** - **CRITICAL: 2** (1 functional, 1 build-cache infra) - **HIGH: 4** (1 functional, 3 infra — same root cause) - **MEDIUM: 6** - **LOW: 11** The two functional CRITICAL/HIGH items are real and independent of the build issue. The build-cache 500 is a single environmental root cause counted once as CRITICAL (smoke pass) and re-observed as HIGH across multiple areas. ## 2. All Recorded Errors (de-duplicated, CRITICAL → LOW) | Severity | Area / Component | Symptom | Repro | |---|---|---|---| | CRITICAL | write-sse — `routers/projects.py::stream_draft` | POST /draft for a NON-EXISTENT project returns 200 + a full LLM-generated chapter instead of 404 — burns a paid LLM call; sibling endpoints correctly 404 | `curl -X POST /projects/0000…0000/chapters/7/draft -H 'Accept: text/event-stream'` → 200 + token/done | | CRITICAL | Build cache — `apps/web/.next` (all 10 project routes + hydration) | Corrupt `.next`: server bundles require missing `vendor-chunks/openapi-fetch@0.13.8.js` → all `/projects/[id]/*` routes 500; client JS chunks 404 → React never hydrates → entire app non-interactive | `curl localhost:3000/projects/{PID}/write` → 500; type valid title in wizard → 下一步 stays disabled | | HIGH | ProjectWizard.tsx (StepPremise + StepProtagonist) | Step 3 (立意) and Step 4 (主角/金手指) bind to the SAME `form.premise` field — step-4 input silently overwrites step-3; both cannot be saved | /projects/new → type 立意 (step3) → type 主角 (step4) → submit → only step-4 text persists | | HIGH | Build-cache 500 (re-observed in foreshadow / codex / skills / review / nav / chain) | Same missing-openapi-fetch vendor chunk 500s every `/projects/[id]/*` route, blocking browser QA | `curl localhost:3000/projects/{PID}/{route}` → 500 | | HIGH | GET /skills backend registry | Skills registry empty (`{"skills":[]}`); loads only from DB `skills` table with no seed rows and no POST/seed endpoint → SkillsPage permanently shows "暂无已注册技能" | `curl localhost:8000/skills` → `{"skills":[]}` | | HIGH | env — duplicate `next dev` processes | Two concurrent `next dev` on same `apps/web` clobber shared `.next` → likely root cause of the corrupt build | `ps aux \| grep 'next dev'` → two PIDs | | HIGH | rules — `routers/rules.py` + `rule_repo.py` | POST rule to non-existent project returns 500 (unhandled FK violation) instead of 404 | `curl -X POST /projects/{random-uuid}/rules -d '{"level":"project","content":"x"}'` → 500 | | HIGH | ReviewReport.tsx + style.md prompt + review_node | One-click 回炉 can target wrong/empty paragraph: style-auditor LLM segment idx and frontend `split(/\n{2,}/)` paragraph index use unrelated splitting schemes (no shared contract) | Draft with single-newline segments → style segments idx>0 → 回炉 refines wrong/empty para | | MEDIUM | OutlineEditor.tsx + server.ts fetchOutline | 卷号 volume selector is misleading: GET ignores volume filter, editor always renders ALL volumes; picker only affects POST generate | Change 卷号 to 2 → all volumes still listed | | MEDIUM | useOutline.ts:44-49 | 422 validation errors read `apiError.error.code`, but 422 uses `{detail:[...]}` → degrades to generic "OUTLINE_FAILED" message | POST /outline volume=0 → generic error, validation cause lost | | MEDIUM | settings PUT /settings/providers + ProvidersSettings | PUT accepts arbitrary/unknown tier+provider (no enum/whitelist), upsert-only with no DELETE → bogus tier_routing row persists invisibly, pollutes gateway config | `PUT {tier_routing:[{tier:'bogus',…}]}` → 200, persists, unremovable via API | | MEDIUM | backend char persist/read + CodexPage.tsx | Character relations silently dropped on persist/read (GET returns `relations:[]` despite ingest); codex list never renders relations anyway | POST character with relations → GET → `relations:[]` | | MEDIUM | backend POST /projects/{id}/characters | Character ingest non-idempotent: re-ingesting same name creates duplicate row → duplicate chips (UI dedup only session-vs-persisted) | POST same card twice → GET count 2 | | MEDIUM | foreshadow PATCH to_status (backend) | Invalid `to_status` value returns 500 INTERNAL instead of 422 VALIDATION; `to_status` is a free string, not enum-validated | `PATCH /foreshadow/FS-01 -d '{"to_status":"BOGUS"}'` → 500 | | MEDIUM | ChainPage.tsx:61 + ChainAdjudication.tsx:102 | Chain run/resume failures call `friendlyError(undefined)` — discard error code/message; 503 LLM_UNAVAILABLE loses its "go to settings" action link | Trigger run with no provider → generic "出错了" toast, no settings link | | MEDIUM | generation.py list_rules (GET rules) | GET rules for non-existent project returns 200 `{"rules":[]}` instead of 404, masking bad project ids | `curl /projects/{random-uuid}/rules` → 200 empty | | LOW | rules / templates / foreshadow / projects / codex (CRUD gaps) | No DELETE/edit endpoint for rules; no DELETE /projects; whitespace-only content accepted server-side (min_length before trim) on rules/projects/foreshadow → unremovable junk rows | `curl -X POST .../rules -d '{"content":" "}'` → 201 | | LOW | useDraftStream.ts (pre-stream error) | Pre-stream error envelope: extracts code+message but drops `error.request_id` → weakens end-to-end greppability | Force 503 before stream → surfaced error has no request_id | | LOW | FastAPI 422 envelope inconsistency (write-sse, outline, accept, injection) | 422 errors return raw FastAPI `{detail:[...]}` instead of project `{error:{code,message,request_id}}` envelope → no request_id, no code mapping | `curl /projects/{PID}/chapters/abc/injection` → 422 `{detail:…}` | | LOW | apps/api accept endpoint (useAccept.ts) | Accept 422 (empty final_text) uses raw FastAPI detail shape, not standard envelope (subset of above, accept-specific) | POST accept `{final_text:'',decisions:[]}` → 422 raw detail | | LOW | ProvidersSettings.tsx testConnection | Test-connection collapses distinct 404 "needs key" vs 503 "probe failed" into one generic toast | Click test on unconfigured vs unreachable provider → identical toast | | LOW | ProjectWizard.tsx submit + cards.ts + useGenerator | Create/generate failures show generic toast; backend validation detail / specific message discarded | Force a 422 → generic toast, backend reason lost | | LOW | CommandPalette.tsx | Incomplete combobox a11y: input not `role=combobox`, no `aria-activedescendant`; `