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

616 lines
31 KiB
Markdown

# 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:
```text
/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:
```text
/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. |
## 9. Recommended Remediation Plan
### 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.