Files
yellow-bank-soal/FRONTEND_MIGRATION_AUDIT_REPORT.md
2026-06-20 01:43:39 +07:00

31 KiB

Frontend Migration Audit Report

Date: 2026-06-19
Project: Yellow Bank Soal / IRT Bank Soal
Scope: Migration from root-level Python/FastAPI admin UI to backend/ plus new React frontend/
Auditor: Codex

1. Executive Summary

The React frontend is scaffolded and builds successfully, but the migration is not yet feature-complete or integration-safe. The biggest risks are API address drift, tenant/website context bugs, missing parity with the legacy Python admin workflows, and placeholder React pages that appear functional but do not call real backend APIs.

Current readiness assessment: not production-ready as the primary replacement for the Python admin UI.

Top findings:

Priority Finding Impact
P0 Local frontend API base URL omits /api/v1 Most API calls 404 in npm run dev and any environment using frontend/.env.
P0 System admin website scope starts as website_id=0 and React Query keys ignore website selection First dashboard loads empty or wrong scoped data; switching websites can show stale data.
P0 Several React API calls target nonexistent or renamed backend endpoints Reports, normalization, and Excel import workflows are broken.
P1 Student tryout portal from the migration plan is absent Core learner flow is not migrated to React.
P1 AI generation UI has incomplete save/review/batch behavior Operators can generate previews, but core review and batch workflow parity is missing.
P1 Unsafe dangerouslySetInnerHTML use without sanitization Imported or AI-generated HTML can become an admin XSS risk.
P2 Multiple legacy admin features are missing or placeholders Hierarchy, question quality, question details, password update, exports, and settings are incomplete.

The build result is positive: npm run build completed successfully. This means the current issues are mainly behavioral and integration defects, not TypeScript compilation blockers.

2. Audit Scope And Methodology

Reviewed:

  • Repository restructure from root app/ to backend/app/ and new frontend/.
  • Current React routes, pages, state store, API client, and Docker/Nginx configuration.
  • Current FastAPI router definitions and generated OpenAPI paths.
  • Last committed Python admin surface via git show HEAD:app/admin_web.py.
  • Existing planning documents: REACT_Migration_Plan.md, ADMIN_TRYOUT_RESTRUCTURE_PLAN.md, and UX_AUDIT_ADMIN_FLOW.md.

Verification performed:

  • npm run build inside frontend/: passed.
  • FastAPI OpenAPI generation from backend/app/main.py: produced 55 paths.
  • Static endpoint comparison between React api.get/post/put/delete calls and backend route definitions.

Not performed:

  • Full browser E2E against a running backend/database.
  • Live authentication, import, AI generation, or report generation.
  • Full backend test suite run.

3. Current Architecture Snapshot

The current repository is in an uncommitted migration state. Git sees the old root-level Python files as deleted and backend/ plus frontend/ as new untracked folders.

React frontend:

  • Admin-only route shell currently lives in frontend/src/App.tsx.
  • API helper is frontend/src/lib/api.ts.
  • Global website and auth token state is persisted in frontend/src/store/useAppStore.ts.
  • The admin UI has pages for Dashboard, Questions, Tryouts, Reports, Settings, AI Generation, Import, and nested Tryout workspaces.

Backend:

  • Main FastAPI app lives in backend/app/main.py.
  • JSON APIs are generally under /api/v1.
  • Legacy Python admin HTML router is still mounted at /admin when ENABLE_ADMIN=true.
  • Import/export router hardcodes /api/v1/import-export inside the router prefix rather than relying on settings.API_V1_STR.

4. Verification Results

Check Result Notes
React build Passed tsc -b && vite build completed. Vite warned that the main JS chunk is larger than 500 kB.
FastAPI OpenAPI paths Passed OpenAPI generated 55 paths.
API route parity Failed Multiple frontend calls do not map to backend paths or methods.
Feature parity with legacy Python admin Partial Several legacy workflows are absent, placeholders, or only implemented as HTML admin routes.
Local development readiness Failed frontend/.env and backend CORS settings do not match the default Vite dev setup.

5. Backend API Paths Observed

The current OpenAPI schema exposes these relevant JSON paths:

/api/v1/auth/admin-login
/api/v1/websites
/api/v1/admin/dashboard/stats
/api/v1/admin/questions
/api/v1/admin/templates
/api/v1/admin/tryouts/{tryout_id}/questions
/api/v1/admin/tryouts/{tryout_id}/attempts
/api/v1/admin/ai/models
/api/v1/admin/ai/generate-preview
/api/v1/admin/ai/generate-save
/api/v1/admin/ai/pending-reviews
/api/v1/admin/ai/review/{item_id}
/api/v1/import-export/preview
/api/v1/import-export/questions
/api/v1/import-export/export/questions
/api/v1/import-export/tryout-json/preview
/api/v1/import-export/tryout-json
/api/v1/tryout/
/api/v1/tryout/{tryout_id}/config
/api/v1/tryout/{tryout_id}/normalization
/api/v1/tryout/{tryout_id}/calibration-status
/api/v1/reports/student/performance
/api/v1/reports/items/analysis
/api/v1/reports/calibration/status

Legacy HTML-only admin paths still exist under /admin, including:

/admin/hierarchy
/admin/question-quality
/admin/calibration-status
/admin/item-statistics
/admin/session-overview
/admin/snapshot-questions
/admin/snapshot-questions/promote-bulk
/admin/basis-items
/admin/basis-items/{basis_item_id}
/admin/basis-items/{basis_item_id}/generate
/admin/basis-items/{basis_item_id}/review-bulk
/admin/questions/{item_id}
/admin/questions/{item_id}/generate
/admin/questions/{item_id}/generate/review-bulk

Those HTML paths are not a substitute for React JSON API parity unless the React app intentionally navigates users back into the legacy admin UI.

6. Endpoint Compatibility Matrix

This matrix assumes the frontend base URL is configured as http://localhost:8000/api/v1, as Docker currently does. If the base URL is http://localhost:8000, most rows fail one level earlier because /api/v1 is missing.

React call Backend status Migration status
/auth/admin-login Exists as /api/v1/auth/admin-login OK when base URL includes /api/v1.
/websites Exists as /api/v1/websites OK when base URL includes /api/v1.
/admin/dashboard/stats Exists as /api/v1/admin/dashboard/stats Path OK, but website scoping can return empty/stale data.
/tryout/ Exists as /api/v1/tryout/ OK when base URL includes /api/v1.
/admin/questions Exists as /api/v1/admin/questions OK when base URL includes /api/v1.
/admin/templates Exists as /api/v1/admin/templates Path OK; verify runtime lazy relationship behavior.
/admin/tryouts/{id}/questions Exists as /api/v1/admin/tryouts/{tryout_id}/questions OK when base URL includes /api/v1.
/admin/tryouts/{id}/attempts Exists as /api/v1/admin/tryouts/{tryout_id}/attempts OK when base URL includes /api/v1.
/admin/ai/models Exists as /api/v1/admin/ai/models OK when base URL includes /api/v1.
/admin/ai/generate-preview Exists as /api/v1/admin/ai/generate-preview Path OK; payload includes unsupported operator_notes in one page but Pydantic ignores extras by default.
/admin/ai/generate-save Exists as /api/v1/admin/ai/generate-save Path OK; React passes placeholder slot and can cause duplicate/conflict behavior.
/import-export/tryout-json/preview Exists as /api/v1/import-export/tryout-json/preview OK when base URL includes /api/v1.
/import-export/tryout-json Exists as /api/v1/import-export/tryout-json OK when base URL includes /api/v1.
/reports/calibration-status Backend has /api/v1/reports/calibration/status?tryout_id=... Broken. Wrong path and missing required tryout_id.
/reports/item-analysis Backend has /api/v1/reports/items/analysis?tryout_id=... Broken. Wrong path and missing required tryout_id.
/reports/student-performance Backend has /api/v1/reports/student/performance?tryout_id=... Broken. Wrong path and missing required tryout_id.
/tryouts/{id}/config Backend has /api/v1/tryout/{id}/config Broken. Uses plural tryouts.
POST /tryouts/{id}/normalization Backend has PUT /api/v1/tryout/{id}/normalization Broken. Wrong path, method, and payload schema.
/tryouts/{id}/normalization/recalculate No JSON API found Broken.
/import-export/tryout-import No JSON API found Broken. Should likely use /import-export/preview or /import-export/questions.
/import-export/snapshot-questions/promote-bulk No JSON API found Broken. Legacy equivalent is HTML form POST /admin/snapshot-questions/promote-bulk.

7. Findings

P0-01: Local API base URL omits /api/v1

Severity: P0
Category: API routing / environment configuration
Evidence:

  • frontend/src/lib/api.ts:5 defaults to http://localhost:8000.
  • frontend/.env:1 sets VITE_API_URL=http://localhost:8000.
  • Backend JSON admin APIs are exposed under /api/v1/....
  • Docker build uses VITE_API_BASE_URL: "http://localhost:8000/api/v1" in docker-compose.yml:62, so Docker and local dev behave differently.

Impact:

  • Running npm run dev or using the checked-in frontend/.env makes calls such as /auth/admin-login, /websites, and /admin/dashboard/stats hit the wrong backend URLs.
  • Developers can see a compiling app but get login/API failures at runtime.
  • Bugs may be masked in Docker while reappearing in local development or other deployments.

Recommendation:

  • Standardize one env var name, preferably VITE_API_URL, and set it to the full API root: http://localhost:8000/api/v1.
  • Add frontend/.env.example with the same value.
  • Add a startup assertion or dev console warning if VITE_API_URL does not end in /api/v1.
  • Consider making the Axios helper append /api/v1 itself so pages never depend on a base URL convention.

P0-02: System-admin website scope and React Query cache can show empty or stale tenant data

Severity: P0
Category: Multi-tenant data isolation / state management
Evidence:

  • Login issues a system-admin token with website_id=0 in backend/app/routers/auth.py:50-55.
  • The comment says this placeholder should produce global access, but require_website_auth returns auth.website_id whenever it is not None in backend/app/core/auth.py:147-150.
  • WebsiteSelector auto-selects the first website asynchronously in frontend/src/components/WebsiteSelector.tsx:25-29.
  • Dashboard query key is ['dashboard-stats'] and does not include websiteId in frontend/src/pages/admin/Dashboard.tsx:45-50.
  • Other query keys also omit websiteId, including ['tryouts'], ['admin-questions'], and ['ai-pending-reviews'].

Impact:

  • First-load dashboard requests can run before the selector sets X-Website-ID; backend may interpret the request as website 0 and return empty data.
  • Switching websites can leave cached data from the prior website because React Query keys do not include the website id.
  • Multi-tenant admin data can appear wrong even when the API endpoint is otherwise correct.

Recommendation:

  • Fix backend system-admin semantics: use website_id=None for global system admin or make website_id=0 explicitly mean global access.
  • In React, gate website-scoped queries until websiteId is set, except for the websites list itself.
  • Include websiteId in every website-scoped React Query key, for example ['dashboard-stats', websiteId].
  • Invalidate website-scoped queries when WebsiteSelector changes.

P0-03: Reports page calls nonexistent backend paths and omits required filters

Severity: P0
Category: API contract / reporting
Evidence:

  • React calls /reports/calibration-status, /reports/item-analysis, and /reports/student-performance in frontend/src/pages/admin/reports/index.tsx:14, :58, and :88.
  • Backend exposes /reports/calibration/status, /reports/items/analysis, and /reports/student/performance in backend/app/routers/reports.py:68-80, :172-184, and :231-241.
  • Each backend report endpoint requires tryout_id.
  • React report export buttons have no handlers in frontend/src/pages/admin/reports/index.tsx:29-31, :73-75, and :103-105.

Impact:

  • All three report tabs fail at runtime.
  • Even after path correction, the page needs tryout selection or route context because backend requires tryout_id.
  • Export buttons are misleading because they do not call the export APIs.

Recommendation:

  • Use the backend paths:
    • /reports/calibration/status?tryout_id={id}
    • /reports/items/analysis?tryout_id={id}
    • /reports/student/performance?tryout_id={id}
  • Add tryout selector/context to the Reports page.
  • Wire export buttons to /reports/.../export/{format} endpoints.
  • Render real report tables from response fields instead of placeholder text.

P0-04: Tryout normalization page uses wrong paths, method, payload, and silent fallback

Severity: P0
Category: API contract / scoring configuration
Evidence:

  • React fetches /tryouts/{id}/config in frontend/src/pages/admin/tryouts/Normalization.tsx:22.
  • Backend route is /tryout/{tryout_id}/config in backend/app/routers/tryouts.py:34-44.
  • React posts /tryouts/{id}/normalization with { rataan, sb, mode } in frontend/src/pages/admin/tryouts/Normalization.tsx:39-45.
  • Backend expects PUT /tryout/{tryout_id}/normalization with fields normalization_mode, static_rataan, and static_sb in backend/app/routers/tryouts.py:109-120.
  • React calls /tryouts/{id}/normalization/recalculate in frontend/src/pages/admin/tryouts/Normalization.tsx:53-56, but no matching JSON API was found.
  • The page catches config load failures and silently displays defaults in frontend/src/pages/admin/tryouts/Normalization.tsx:21-26.

Impact:

  • Operators can believe they changed normalization settings when the requests actually failed or hit nonexistent endpoints.
  • Silent defaults can overwrite user trust in scoring configuration by hiding missing data.
  • Normalization is core to NM/NN scoring, so this is a production blocker.

Recommendation:

  • Change GET to /tryout/{id}/config.
  • Change save to PUT /tryout/{id}/normalization.
  • Send backend schema names: normalization_mode, static_rataan, static_sb.
  • Remove silent fallback for API failures; show an error state.
  • Either add a backend recalculation endpoint or remove the button until the API exists.

P0-05: Excel import page is wired to nonexistent endpoints

Severity: P0
Category: Import workflow / API contract
Evidence:

  • React posts preview/upload to /import-export/tryout-import in frontend/src/pages/admin/import/index.tsx:17-23.
  • React posts confirmation to /import-export/snapshot-questions/promote-bulk in frontend/src/pages/admin/import/index.tsx:31-35.
  • Backend Excel import APIs are /api/v1/import-export/preview and /api/v1/import-export/questions in backend/app/routers/import_export.py:53-62 and :150-160.
  • Snapshot promotion currently exists only in the legacy HTML admin as /admin/snapshot-questions/promote-bulk.

Impact:

  • The standalone Excel Import page cannot complete its workflow.
  • Users have two import surfaces: a working JSON import modal under Tryouts and a broken Excel import page under /admin/import.
  • The comments in the React file explicitly show uncertainty about endpoint names.

Recommendation:

  • Decide whether Excel import remains in the React admin.
  • If yes, wire preview to /import-export/preview and confirm to /import-export/questions with required tryout_id.
  • If snapshot promotion is required in React, add a JSON API for selected snapshot question IDs and update the UI accordingly.
  • Hide /admin/import until the contract is implemented.

P1-01: Student tryout portal is missing from React

Severity: P1
Category: Feature parity / core business flow
Evidence:

  • REACT_Migration_Plan.md:73-85 describes Phase 3 Student Portal construction: tryout listing, exam session, async answer submission, state recovery, server timer, and result page.
  • Current frontend/src/App.tsx:38-66 only defines /login and /admin/* routes.
  • No student session routes or pages were found under frontend/src/pages.

Impact:

  • The React migration does not yet cover the learner-facing tryout experience.
  • If the goal is full Python frontend replacement, core user-facing functionality remains unmigrated.

Recommendation:

  • Add student routes for tryout listing, active session, answer submission, completion, and result summary.
  • Use existing backend session APIs under /api/v1/session.
  • Add E2E coverage for refresh recovery and server-synced timer behavior.

P1-02: AI generation workflow is incomplete and can save invalid variants

Severity: P1
Category: AI generation / operator workflow
Evidence:

  • Global AI page uses manual basis item id and comments that a real template selector is missing in frontend/src/pages/admin/ai/Workspace.tsx:26-27.
  • Global AI page has "Discard" and "Save & Queue Review" buttons with no handlers in frontend/src/pages/admin/ai/Workspace.tsx:138-140.
  • Tryout AI workspace saves generated questions with slot: basisItem ? 1 : 1 in frontend/src/pages/admin/tryouts/AIWorkspace.tsx:64-76.
  • Tryout AI workspace "Review Variants" and "Batch Generation" tabs are placeholder text in frontend/src/pages/admin/tryouts/AIWorkspace.tsx:233-253.
  • Legacy Python admin supported batch count, operator notes, note inclusion, run history, filters, review-bulk, and variant detail pages.

Impact:

  • Operators cannot reliably save variants with correct slot linkage.
  • Batch generation and review parity are missing.
  • Duplicate slot conflicts are likely because saved AI variants always use slot 1.

Recommendation:

  • Use the selected basis item's real slot, tryout_id, website_id, and source snapshot metadata.
  • Add JSON APIs if needed for batch generation, run history, review filtering, and bulk review.
  • Disable save buttons until all required fields are present.
  • Remove or implement the global AI workspace to avoid two partial AI workflows.

P1-03: Imported and generated HTML is rendered without sanitization

Severity: P1
Category: Security / XSS
Evidence:

  • React renders question HTML with dangerouslySetInnerHTML in frontend/src/pages/admin/tryouts/QuestionManagement.tsx.
  • React renders AI preview stem/options/explanation with dangerouslySetInnerHTML in frontend/src/pages/admin/tryouts/AIWorkspace.tsx:180-200.
  • The migration plan explicitly calls out HTML sanitization as a security checklist item in REACT_Migration_Plan.md:204-208.

Impact:

  • Imported Sejoli payloads or AI-generated content could inject scripts or unsafe markup into admin pages.
  • Admin XSS is high impact because admins hold cross-website operational access.

Recommendation:

  • Add a sanitizer such as DOMPurify.
  • Create a single SafeHtml component and forbid direct dangerouslySetInnerHTML in pages.
  • Sanitize on render and consider backend-side validation for stored HTML.

P1-04: CORS config does not include the default Vite dev origin

Severity: P1
Category: Local development / environment configuration
Evidence:

  • Backend .env allows http://localhost:3000 and http://localhost:8000 in backend/.env:15.
  • Vite dev normally serves at http://localhost:5173.
  • REACT_Migration_Plan.md:53 explicitly calls out adding frontend dev origins.

Impact:

  • Local npm run dev can fail with CORS errors even after the API base URL is corrected.
  • Developers may mistakenly debug auth/API code when the root cause is CORS.

Recommendation:

  • Add http://localhost:5173 to ALLOWED_ORIGINS.
  • Keep Docker/static origin and Vite dev origin both represented in .env.example.

P2-01: Question quality page is a static placeholder

Severity: P2
Category: Missing feature / reporting parity
Evidence:

  • frontend/src/pages/admin/questions/QuestionQuality.tsx:14-47 displays ... for all metrics.
  • frontend/src/pages/admin/questions/QuestionQuality.tsx:61-65 says diagnostic charts are coming soon.
  • Legacy Python admin had a real /admin/question-quality view that computed calibrated totals and per-tryout readiness.

Impact:

  • Operators lose the prior calibration diagnostics workflow.
  • The page appears present but does not provide operational data.

Recommendation:

  • Either wire this page to /reports/calibration/status per selected tryout or add a dashboard-level quality summary API.
  • Replace placeholder cards with real metrics and loading/error states.

P2-02: Tryout settings and general/security settings are placeholders

Severity: P2
Category: Missing feature / admin configuration
Evidence:

  • Tryout settings page contains only placeholder text in frontend/src/pages/admin/tryouts/TryoutSettings.tsx:14-16.
  • Security settings form has inputs and button but no mutation in frontend/src/pages/admin/settings/index.tsx:151-176.
  • General settings tab is placeholder text in frontend/src/pages/admin/settings/index.tsx:203-211.
  • Legacy Python admin had /admin/password and website management.

Impact:

  • Operators cannot update tryout scoring/selection/AI settings from React.
  • Password update looks available but does nothing.

Recommendation:

  • Implement tryout settings using /tryout/{id}/config plus update endpoints for scoring mode, selection mode, AI generation, and calibration thresholds.
  • Add or expose a JSON password-change endpoint, or hide Security until implemented.
  • Replace "General" with concrete settings or remove the tab.

P2-03: Hierarchy/data overview was not migrated

Severity: P2
Category: Missing feature / operator orientation
Evidence:

  • Legacy Python admin exposed /admin/hierarchy.
  • UX_AUDIT_ADMIN_FLOW.md and ADMIN_TRYOUT_RESTRUCTURE_PLAN.md identified hierarchy visibility as important.
  • Current React sidebar has Dashboard, Questions, Tryouts, Reports, Settings only in frontend/src/layouts/AdminLayout.tsx:10-16.
  • No React hierarchy page exists.

Impact:

  • Operators lose the data relationship map for Website -> Tryout -> Snapshot -> Basis Item -> AI Run -> Variant.
  • This was specifically identified as important for reducing confusion after import and AI generation.

Recommendation:

  • Add a React Data Overview/Hierarchy page.
  • Expose a JSON hierarchy API instead of relying on legacy HTML.
  • Link it from Dashboard or Tryouts, not only Settings.

P2-04: Route structure deviates from the planned tryout-centric URL model

Severity: P2
Category: Navigation / route consistency
Evidence:

  • ADMIN_TRYOUT_RESTRUCTURE_PLAN.md:19-28 planned singular /admin/tryout/{tryout_id}/... route depth.
  • Current React uses plural /admin/tryouts/:id/... in frontend/src/App.tsx:55-60.
  • Planned question workspace route includes question id, but current route is /admin/tryouts/:id/questions/ai-workspace without question id.
  • TryoutLayout tabs omit Normalization from the visible tab list even though the route exists.

Impact:

  • URL semantics differ from the planned hierarchy.
  • AI workspace lacks clear parent question context.
  • Users navigating to Normalization see a page that is not represented in the tab state.

Recommendation:

  • Decide on singular or plural route convention and align docs, React routes, and links.
  • Include questionId in AI workspace routes.
  • Add a visible Normalization tab or move normalization under Settings consistently.

P2-05: Legacy Python admin remains mounted, creating deployment ambiguity

Severity: P2
Category: Deployment / migration completeness
Evidence:

  • backend/app/main.py still includes admin_web_router when admin is enabled.
  • Docker serves React at port 3000 and backend at port 8000.
  • Backend still owns /admin/* on port 8000; React owns /admin/* on port 3000.

Impact:

  • If production routing later places frontend and backend behind one host, /admin routing can easily point to the wrong application.
  • Operators may accidentally use two different admin UIs with different feature coverage.

Recommendation:

  • Define production routing explicitly:
    • Frontend owns /admin/*.
    • Backend owns /api/v1/*, /docs, /health, and possibly legacy admin only behind a temporary fallback path.
  • Add a migration flag to disable legacy admin once React parity is reached.

P2-06: Query invalidation and cache keys are not website-aware

Severity: P2
Category: State management / data freshness
Evidence:

  • Examples: ['dashboard-stats'], ['tryouts'], ['admin-questions'], ['ai-pending-reviews'].
  • The API interceptor changes X-Website-ID based on Zustand state, but React Query cache keys do not reflect that state.

Impact:

  • After switching websites, React Query can return prior website data without refetching.
  • The visible WebsiteSelector can imply a different scope than the data currently shown.

Recommendation:

  • Include websiteId in all website-scoped query keys.
  • Add a small helper for scoped keys to avoid drift.
  • Consider clearing scoped query cache on logout and website switch.

P3-01: Several buttons look actionable but do nothing

Severity: P3
Category: UX polish / trust
Evidence:

  • Report export buttons have no click handlers.
  • Excel "Download Template" button has no click handler.
  • Global AI "Discard" and "Save & Queue Review" buttons have no click handlers.
  • Settings "Update Password" button has no click handler.

Impact:

  • The UI feels more complete than it is, which can mislead testers and operators.

Recommendation:

  • Remove disabled/nonfunctional controls or wire them to real mutations/downloads.
  • Prefer disabled buttons with explanatory tooltip only when the missing backend is intentional.

8. Feature Parity Checklist

Area Legacy Python admin React status Notes
Login/logout Present Partial JWT login works by path only if base URL includes /api/v1; no remember-me equivalent.
Dashboard Present Partial React has KPI cards, but first-load website scoping and query key issues affect data.
Website management Present Partial React CRUD exists; confirm delete cascade semantics and query invalidation.
Tryout import JSON Present Mostly present Modal maps to real JSON endpoints when base URL includes /api/v1.
Excel import Present via API Broken React page calls nonexistent endpoints.
Snapshot question promotion Present as legacy HTML Missing JSON/React React calls nonexistent API.
Global question list Present with filters/detail Partial React list exists, but filters and detail page are missing.
Question detail Present Missing No React route/page.
Question quality Present Placeholder Static cards only.
Tryout list/tree Present/planned Partial Accordion exists; average NM/NN and some plan details missing.
Tryout attempts Present Present basic Filtered table exists.
Normalization Present Broken Wrong API contract.
Tryout settings Present via backend fields Placeholder No real form.
AI basis workspace Present Partial Preview and single save partially exist; batch/review/run history missing.
AI pending review Present Partial List and approve/reject exist; preview/detail missing.
Variant detail Present Missing No React page.
Bulk variant review Present Missing No React workflow.
Hierarchy/data overview Present Missing Important operator context lost.
Reports dashboard Present Broken/placeholder Wrong endpoints and no tryout filters.
Report exports Present in backend Missing in React Buttons not wired.
Password update Present in legacy HTML Placeholder No API/mutation.
Student tryout portal Planned Missing No React student/session routes.

Phase 0: Stop the bleeding

  1. Fix VITE_API_URL to include /api/v1 in frontend/.env, Docker build args, and .env.example.
  2. Add http://localhost:5173 to backend CORS for Vite dev.
  3. Fix system-admin website scoping so no-header system admin is global or explicitly blocked until a website is selected.
  4. Gate website-scoped React queries until websiteId is available.
  5. Add websiteId to all scoped query keys.

Phase 1: Repair broken API contracts

  1. Fix Reports paths and require a selected tryout.
  2. Fix Normalization GET/PUT paths and payload schema.
  3. Remove or fix the broken Excel import page.
  4. Decide whether snapshot promotion needs a JSON API and add it if React owns the workflow.
  5. Add API contract tests that compare frontend endpoint constants against OpenAPI paths.

Phase 2: Recover feature parity

  1. Implement real Tryout Settings.
  2. Implement Question Detail and Variant Detail pages.
  3. Implement AI run history, review filters, batch generation, and bulk review.
  4. Implement Question Quality with real metrics.
  5. Implement Data Overview/Hierarchy in React.
  6. Wire report export buttons.

Phase 3: Student portal

  1. Add learner tryout listing.
  2. Add active session page using /session/{id}/next_item and /submit_answer.
  3. Add server-synced timer from expires_at.
  4. Persist session recovery state.
  5. Add completion and result pages.

Phase 4: Migration hardening

  1. Decide the final production routing split between frontend /admin/* and backend /api/v1/*.
  2. Disable or move legacy Python admin once React parity is complete.
  3. Add Playwright smoke tests for login, website switch, import preview, tryout drilldown, AI preview, normalization save, and reports.
  4. Add a route/API smoke test that verifies every visible navigation target and button either works or is intentionally disabled.

10. Suggested Test Plan

Minimum tests before considering the React migration complete:

Test Expected result
npm run build Passes with no TypeScript errors.
Login with local env Hits /api/v1/auth/admin-login, stores token, lands on dashboard.
First dashboard load Waits for or uses a real selected website, never website 0.
Website switch Dashboard, tryouts, questions, reports, and AI pending reviews refetch for the selected website.
Tryout JSON preview/import Calls /api/v1/import-export/tryout-json/preview and /tryout-json; new tryouts appear.
Excel import Calls real preview and import endpoints or the page is hidden.
Normalization save GET /api/v1/tryout/{id}/config, PUT /api/v1/tryout/{id}/normalization, visible success/error state.
Reports Requires tryout context and loads real calibration/item/student data.
AI preview/save Saves with correct basis slot and displays generated variant in review queue.
AI review/bulk Approve/reject/archive works and status updates are visible.
XSS smoke Imported HTML and AI HTML are sanitized before rendering.
Student session Start/resume/answer/complete/result works with server timer.

11. Final Assessment

The migration has a good foundation: React, routing, TanStack Query, Zustand, shadcn-style components, Docker/Nginx serving, and a number of admin pages are already present. The main risk is that the UI currently looks further along than its backend integration really is.

The highest leverage next move is to stabilize the API boundary: fix the /api/v1 base URL, align endpoint paths and methods, make website scoping deterministic, and add website-aware query keys. Once that is done, the team can safely fill the larger parity gaps without chasing confusing 404s, empty dashboards, or stale tenant data.