Skip to content

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: - new web/src/utils/markdown.ts module wrapping marked + DOMPurify with a safe tag/attr allowlist and @mention chip substitution; - marked + dompurify + @types/dompurify as new npm deps; - ReviewTicket now includes assignee, resolvedAt, resolvedBy; - AnalysisFocusPayload gained badge + isolate_level fields for numbered-badge and isolate-geometry viewer behavior.

None of this yet addresses the items below — ReviewPanel.tsx is still 1,400+ lines and App.tsx still carries the ~50-useState workspace 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; useState block ~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), plus BatchModeWorkspace, DrawingPage, VisionAnalysisSidebar, ReportTemplateBuilderSidebar, FusionAnalysisSidebar, CncAnalysisSidebar. Only web/src/services/draftlintClient.ts is 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), new web/src/components/ErrorBoundary.tsx, new web/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/, new web/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, new web/.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.tsxDesignReviewWorkspace.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.