D. Frontend¶
Parent: Code Review Index Priority: the surface pilots actually touch; several low-cost, high-lift wins Surface affected:
web/
The TypeScript config is strict (good), types in web/src/types/ are thorough (good), but state and data fetching are ad-hoc and several components have exceeded the size where local reasoning works.
April 2026 update. The Comments tab UI redesign on branch
codex/dfm-injection-molding-rules-bar-refinement-comments(8a61fd3) added: - newweb/src/utils/markdown.tsmodule wrappingmarked+DOMPurifywith a safe tag/attr allowlist and@mentionchip substitution; -marked+dompurify+@types/dompurifyas new npm deps; -ReviewTicketnow includesassignee,resolvedAt,resolvedBy; -AnalysisFocusPayloadgainedbadge+isolate_levelfields for numbered-badge and isolate-geometry viewer behavior.None of this yet addresses the items below —
ReviewPanel.tsxis still 1,400+ lines andApp.tsxstill carries the ~50-useStateworkspace state. Treat the comments redesign as cosmetic + feature work that leaves the D1 (store) and D2 (API client) refactors still open.
D1 — No central store; ~50 useState in App.tsx with deep prop drilling¶
- Status: Open
- Impact: H | Complexity: M | Time: 2 W
- Files:
web/src/App.tsx(2915 lines;useStateblock ~line 432,clearWorkspaceState~560-605), every sidebar component
Finding. App.tsx holds the workspace state (model, components, profiles, tickets, reviews, vision views, …) as ~50 useStates and drills each into sidebars and workspaces. clearWorkspaceState manually resets 30+ setters. There are 226 useState calls across web/src/ total.
Action for Codex.
1. Introduce Zustand (small, zero-boilerplate) for workspace state. Create web/src/stores/:
- useWorkspaceStore — model, components, visibility, profiles, session snapshot.
- useReviewStore — tickets, reviews, pins.
- useDfmStore — active DFM session, findings, PDF export state.
- useVisionStore — view catalog, pasted images, active report.
2. Replace useState + prop-drilling with store selectors inside the consuming components.
3. Keep App.tsx as a composition root and router — target < 600 lines.
4. Add a reset() action per store to replace clearWorkspaceState.
Acceptance criteria.
- App.tsx < 600 lines; no useState block holding more than 5 unrelated values.
- DfmSidebar, VisionAnalysisSidebar, ReviewPanel consume stores directly; App.tsx no longer drills their state.
- Playwright specs still pass.
Depends on: D2 (API client) — do as one coordinated migration.
D2 — Fetch is duplicated across 27+ call sites; only DraftLint has a service¶
- Status: Open
- Impact: H | Complexity: M | Time: 1 W
- Files:
web/src/App.tsx(27 fetch sites),web/src/components/DfmSidebar.tsx(5),web/src/components/DesignReviewWorkspaceReference.tsx(4),web/src/components/ModelViewer.tsx(4), plusBatchModeWorkspace,DrawingPage,VisionAnalysisSidebar,ReportTemplateBuilderSidebar,FusionAnalysisSidebar,CncAnalysisSidebar. Onlyweb/src/services/draftlintClient.tsis centralized.
Finding. Every component builds its own fetch(apiBase + "/api/...", { method: ... }) call with ad-hoc error handling, retry, and response parsing. This guarantees inconsistent error UX and duplicated bugs. There is also no caching of server state — every render potentially refetches.
Action for Codex.
1. Introduce TanStack Query (@tanstack/react-query) for server-state caching.
2. Expand web/src/services/ into per-resource API clients:
- modelsClient.ts, reviewsClient.ts, dfmClient.ts, visionClient.ts, fusionClient.ts, cncClient.ts, partFactsClient.ts, canonicalSceneClient.ts.
3. Each client exports typed functions that return parsed, validated responses (use the types already in web/src/types/).
4. Wrap each client function in a useXxxQuery / useXxxMutation hook under web/src/hooks/.
5. Remove all direct fetch(apiBase + ...) from components.
Acceptance criteria.
- grep -rnE "fetch\(apiBase|VITE_API_BASE_URL" web/src/components/ returns nothing.
- Every API call goes through a service + hook.
- At least one stale-while-revalidate flow (e.g. part-facts polling) is driven by React Query.
Depends on: D1.
D3 — No app-level error boundary; API errors disappear silently¶
- Status: Open
- Impact: H | Complexity: L | Time: 1 D
- Files:
web/src/App.tsx(root), newweb/src/components/ErrorBoundary.tsx, newweb/src/components/Toaster.tsx
Finding. The only error boundary is ViewerErrorBoundary inside ModelViewer.tsx. Everything else that throws surfaces as an unstyled blank area, or silently logs to console, or writes to a bespoke state variable (profileError, busyAction). Pilots will report "it didn't work" without any path to the cause.
Action for Codex.
1. Add <ErrorBoundary> at the root of App.tsx that renders a friendly fallback with a "Copy error details" button that copies the error + recent request IDs.
2. Add a Toaster (e.g. sonner or a 30-line custom one) at the root.
3. In the API client layer (D2), route every non-2xx response to toast.error(...) with a human-readable message and the server request_id.
4. On 401/403, auto-redirect to a sign-in prompt.
5. On 429, show "Rate limited — retry in Xs" using the Retry-After header.
Acceptance criteria.
- Throwing inside any component shows the fallback, not a blank page.
- A failed API call produces a visible toast with server request_id (paired with A6/B7).
- The fallback UI includes a "Reload" action and a request-ID trail.
Depends on: A6 (request IDs), B7 (structured logging / request IDs in response).
D4 — No code splitting; Three.js and every sidebar is eager¶
- Status: Open
- Impact: M | Complexity: L | Time: 1 D
- Files:
web/vite.config.ts,web/src/App.tsx(all imports at top),web/src/components/*Sidebar.tsx
Finding. App.tsx imports every workspace mode and every sidebar synchronously at the top. three, @react-three/fiber, @react-three/drei, plus ~10 heavy sidebars are all in the main bundle even when the pilot only uses one mode.
Action for Codex.
1. Convert mode workspaces and sidebars to React.lazy(() => import("./components/XxxSidebar")).
2. Wrap them in <Suspense fallback={<Spinner />}>.
3. Configure Vite build.rollupOptions.output.manualChunks to split vendor (three, @react-three/*) into its own chunk.
4. Add vite-bundle-visualizer to scripts; run it once and commit the starting size in docs/design-review/bundle.md.
Acceptance criteria.
- npm run build shows per-mode chunks named by component.
- Initial JS payload (app shell without any mode) is <= 250 KB gzipped.
Depends on: none.
D5 — 2 Playwright specs, zero unit tests¶
- Status: Open
- Impact: H | Complexity: L | Time: ongoing (1 W kickoff)
- Files:
web/tests/, newweb/src/**/__tests__/
Finding. Only dfm-benchmark-overlay.spec.ts and expert-dfm-single-surface.spec.ts exist. Normalization helpers in web/src/utils/ (normalizeComponents, normalizeStringMap, normalizeComponentProfilesMap, PartFacts shapers) have no tests — these are exactly the places a silent regression will cost a pilot.
Action for Codex.
1. Add Vitest + @testing-library/react to devDependencies.
2. Configure web/vitest.config.ts with jsdom environment.
3. Write unit tests for every helper in web/src/utils/ first — start with dfmProfiles.ts normalization.
4. Add a small test/ folder convention; CI runs npm test alongside npm run test:e2e.
5. Component tests for CommentForm, ReviewStartForm, ModeMenu (smaller, tractable).
Acceptance criteria.
- npm test runs unit tests and passes.
- CI gate runs unit tests on every PR.
- Coverage report produced (even if not gated yet).
Depends on: E1 (CI wiring).
D6 — No ESLint, no Prettier¶
- Status: Open
- Impact: M | Complexity: L | Time: 0.5 D
- Files:
web/package.json, newweb/.eslintrc.cjs,web/.prettierrc.json
Finding. Only the TypeScript compiler catches issues. No lint rules for exhaustive-deps, no accessibility rules, no consistent formatting.
Action for Codex.
1. Add:
- eslint, @typescript-eslint/parser, @typescript-eslint/eslint-plugin
- eslint-plugin-react, eslint-plugin-react-hooks, eslint-plugin-jsx-a11y
- prettier, eslint-config-prettier
2. Commit .eslintrc.cjs with the above and a short project-specific ruleset.
3. Commit .prettierrc.json and a .prettierignore.
4. Add lint, lint:fix, format scripts to package.json.
5. Run once to fix the existing codebase; commit as a standalone formatting PR.
6. Add eslint to CI (see E3).
Acceptance criteria.
- npm run lint exits 0.
- react-hooks/exhaustive-deps catches at least the known missing-dep warnings.
Depends on: E3.
D7 — Accessibility is ad-hoc¶
- Status: Open
- Impact: M | Complexity: M | Time: 3–5 D
- Files:
CommentForm.tsx,ModeMenu.tsx,Toolbar.tsx,RailTabIcon.tsx, others
Finding. Some components use role="dialog" + aria-modal (good), others rely on unlabeled icon buttons. No focus trap in modals. No live regions for status updates. Keyboard shortcuts are limited to Escape in a few places.
Action for Codex.
1. Install focus-trap-react; apply to CommentForm, ReviewStartForm, ModeMenu, any popover.
2. Audit with eslint-plugin-jsx-a11y (after D6).
3. Add aria-label to every icon-only button (Toolbar, RailTabIcon, sidebar triggers).
4. Add an aria-live="polite" region for async status messages (DFM review progress, upload status).
5. Add a "Skip to content" link at the top of App.tsx.
6. Manually test keyboard-only navigation through the main flow.
Acceptance criteria.
- eslint-plugin-jsx-a11y clean.
- A keyboard-only user can upload a STEP, open DFM, run review.
Depends on: D6.
D8 — DesignReviewWorkspace.tsx is a 1-line re-export; finish or delete¶
- Status: Open
- Impact: L | Complexity: L | Time: 0.5 D
- Files:
web/src/components/DesignReviewWorkspace.tsx(1 line),web/src/components/DesignReviewWorkspaceReference.tsx(3044 lines)
Finding. Looks like a stalled refactor where the intent was to replace the reference impl. Right now it's pure indirection.
Action for Codex.
1. Decide: is a new implementation in progress? If yes, finish it. If no, delete DesignReviewWorkspace.tsx and rename DesignReviewWorkspaceReference.tsx → DesignReviewWorkspace.tsx.
2. Update all imports.
Acceptance criteria.
- Only one DesignReviewWorkspace file exists; build passes.
Depends on: none.
D9 — vite.config.ts is skeletal; env vars undocumented¶
- Status: Open
- Impact: L | Complexity: L | Time: 0.5 D
- Files:
web/vite.config.ts,web/.env.example(new)
Finding. No manualChunks, no bundle analysis, no VITE_* schema. Required env vars (VITE_API_BASE_URL, VITE_PORT) are documented only in project.md.
Action for Codex.
1. Add manualChunks (see D4).
2. Add web/.env.example with every VITE_* var commented.
3. At app startup, validate required import.meta.env.VITE_* and surface a user-visible error if missing.
Acceptance criteria.
- App refuses to boot if VITE_API_BASE_URL is missing.
- .env.example exists and is referenced from the README.
Depends on: D4.