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/tobackend/app/and newfrontend/. - 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, andUX_AUDIT_ADMIN_FLOW.md.
Verification performed:
npm run buildinsidefrontend/: passed.- FastAPI OpenAPI generation from
backend/app/main.py: produced 55 paths. - Static endpoint comparison between React
api.get/post/put/deletecalls 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
/adminwhenENABLE_ADMIN=true. - Import/export router hardcodes
/api/v1/import-exportinside the router prefix rather than relying onsettings.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:5defaults tohttp://localhost:8000.frontend/.env:1setsVITE_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"indocker-compose.yml:62, so Docker and local dev behave differently.
Impact:
- Running
npm run devor using the checked-infrontend/.envmakes calls such as/auth/admin-login,/websites, and/admin/dashboard/statshit 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.examplewith the same value. - Add a startup assertion or dev console warning if
VITE_API_URLdoes not end in/api/v1. - Consider making the Axios helper append
/api/v1itself 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=0inbackend/app/routers/auth.py:50-55. - The comment says this placeholder should produce global access, but
require_website_authreturnsauth.website_idwhenever it is notNoneinbackend/app/core/auth.py:147-150. WebsiteSelectorauto-selects the first website asynchronously infrontend/src/components/WebsiteSelector.tsx:25-29.- Dashboard query key is
['dashboard-stats']and does not includewebsiteIdinfrontend/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 website0and 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=Nonefor global system admin or makewebsite_id=0explicitly mean global access. - In React, gate website-scoped queries until
websiteIdis set, except for the websites list itself. - Include
websiteIdin every website-scoped React Query key, for example['dashboard-stats', websiteId]. - Invalidate website-scoped queries when
WebsiteSelectorchanges.
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-performanceinfrontend/src/pages/admin/reports/index.tsx:14,:58, and:88. - Backend exposes
/reports/calibration/status,/reports/items/analysis, and/reports/student/performanceinbackend/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}/configinfrontend/src/pages/admin/tryouts/Normalization.tsx:22. - Backend route is
/tryout/{tryout_id}/configinbackend/app/routers/tryouts.py:34-44. - React posts
/tryouts/{id}/normalizationwith{ rataan, sb, mode }infrontend/src/pages/admin/tryouts/Normalization.tsx:39-45. - Backend expects
PUT /tryout/{tryout_id}/normalizationwith fieldsnormalization_mode,static_rataan, andstatic_sbinbackend/app/routers/tryouts.py:109-120. - React calls
/tryouts/{id}/normalization/recalculateinfrontend/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-importinfrontend/src/pages/admin/import/index.tsx:17-23. - React posts confirmation to
/import-export/snapshot-questions/promote-bulkinfrontend/src/pages/admin/import/index.tsx:31-35. - Backend Excel import APIs are
/api/v1/import-export/previewand/api/v1/import-export/questionsinbackend/app/routers/import_export.py:53-62and: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/previewand confirm to/import-export/questionswith requiredtryout_id. - If snapshot promotion is required in React, add a JSON API for selected snapshot question IDs and update the UI accordingly.
- Hide
/admin/importuntil 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-85describes 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-66only defines/loginand/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 : 1infrontend/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
dangerouslySetInnerHTMLinfrontend/src/pages/admin/tryouts/QuestionManagement.tsx. - React renders AI preview stem/options/explanation with
dangerouslySetInnerHTMLinfrontend/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
SafeHtmlcomponent and forbid directdangerouslySetInnerHTMLin 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
.envallowshttp://localhost:3000andhttp://localhost:8000inbackend/.env:15. - Vite dev normally serves at
http://localhost:5173. REACT_Migration_Plan.md:53explicitly calls out adding frontend dev origins.
Impact:
- Local
npm run devcan 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:5173toALLOWED_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-47displays...for all metrics.frontend/src/pages/admin/questions/QuestionQuality.tsx:61-65says diagnostic charts are coming soon.- Legacy Python admin had a real
/admin/question-qualityview 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/statusper 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/passwordand 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}/configplus 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.mdandADMIN_TRYOUT_RESTRUCTURE_PLAN.mdidentified 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-28planned singular/admin/tryout/{tryout_id}/...route depth.- Current React uses plural
/admin/tryouts/:id/...infrontend/src/App.tsx:55-60. - Planned question workspace route includes question id, but current route is
/admin/tryouts/:id/questions/ai-workspacewithout 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
questionIdin 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.pystill includesadmin_web_routerwhen admin is enabled.- Docker serves React at port
3000and backend at port8000. - Backend still owns
/admin/*on port8000; React owns/admin/*on port3000.
Impact:
- If production routing later places frontend and backend behind one host,
/adminrouting 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.
- Frontend owns
- 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-IDbased 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
websiteIdin 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. |
9. Recommended Remediation Plan
Phase 0: Stop the bleeding
- Fix
VITE_API_URLto include/api/v1infrontend/.env, Docker build args, and.env.example. - Add
http://localhost:5173to backend CORS for Vite dev. - Fix system-admin website scoping so no-header system admin is global or explicitly blocked until a website is selected.
- Gate website-scoped React queries until
websiteIdis available. - Add
websiteIdto all scoped query keys.
Phase 1: Repair broken API contracts
- Fix Reports paths and require a selected tryout.
- Fix Normalization GET/PUT paths and payload schema.
- Remove or fix the broken Excel import page.
- Decide whether snapshot promotion needs a JSON API and add it if React owns the workflow.
- Add API contract tests that compare frontend endpoint constants against OpenAPI paths.
Phase 2: Recover feature parity
- Implement real Tryout Settings.
- Implement Question Detail and Variant Detail pages.
- Implement AI run history, review filters, batch generation, and bulk review.
- Implement Question Quality with real metrics.
- Implement Data Overview/Hierarchy in React.
- Wire report export buttons.
Phase 3: Student portal
- Add learner tryout listing.
- Add active session page using
/session/{id}/next_itemand/submit_answer. - Add server-synced timer from
expires_at. - Persist session recovery state.
- Add completion and result pages.
Phase 4: Migration hardening
- Decide the final production routing split between frontend
/admin/*and backend/api/v1/*. - Disable or move legacy Python admin once React parity is complete.
- Add Playwright smoke tests for login, website switch, import preview, tryout drilldown, AI preview, normalization save, and reports.
- 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.