Title: Production Readiness Report Version: 1.0 Owner: [TENANT_CONFIGURATION_REQUIRED — Engineering Lead] Status: This pass complete; project is not fully production ready — see “Remaining blockers” Last reviewed: 2026-09-09 Next review: [TENANT_CONFIGURATION_REQUIRED] Reviewers: Engineering, Architecture, Security
Records what was actually broken, what was actually fixed, and what actually remains, for the “make every navigation work end-to-end” pass triggered by /interviews showing “Something went wrong loading this data.” See navigation-feature-gap-analysis.md for the full route-by-route trace this report summarizes.
/interviews, /offers, /green-form/*, /verification, /discrepancies, /approvals, /employee-conversion, /dashboard all showed a generic “Something went wrong loading this data” error or a documented “not available” placeholder. Any unmatched URL fell through to React Router’s unstyled default 404 (no route, no boundary).
Every one of the 7 workflow-module routes above failed for the same structural reason: the frontend page and its TanStack Query hook were fully built against an API contract that was never implemented on the backend — either no controller endpoint existed at all, or the endpoint existed but was permanently wired to a stub skill that always returns blocked. This was not a bug in the sense of broken code; it was genuinely unbuilt functionality, exactly as PROJECT_STATUS.md already honestly documented before this pass (“Scaffolded”). The generic error message was a symptom, not the cause — see item 9 for the separate, real fix to that symptom layer.
Two additional root-cause categories, found only once real implementation started:
"scheduled", "rescheduled") that don’t correspond to the backend’s real snake_case JSON convention or the real ref.* status codes (UPPER_SNAKE_CASE / PascalCase, depending on table). These would have produced silent undefined rendering, not crashes — caught only because each hook was rewritten against the real DTO and typechecked.ErrorState/DataTable now derive kind-specific icon, message, and retry-eligibility from the real ApiError.kind (401/403/404/409/422/429/5xx/network) instead of one hardcoded string. Threaded through all 22 list/detail pages.RouteErrorBoundary (errorElement) on every top-level route group — catches lazy-chunk-load failures and render crashes without blanking the whole app.NotFoundPage + wildcard path: "*" route — previously any unmatched URL fell through to React Router’s default.navigation-feature-gap-analysis.md for full detail per module)InterviewFeedbackTemplate seed row added.VerificationController (didn’t exist), real GET list/detail for discrepancies, real resolve and new reupload-request (1 new stored procedure) actions.EmployeeConversionController and ApprovalsController (neither existed), real 2-step gated conversion skill, real shortlist-approval skill, real cross-entity-type approval queue read.DashboardController — every tile is a real, live-computed count against real tables; sla_breaches is honestly 0 (no SLA tracking mechanism exists in this schema).| Endpoint | Status before | Status after |
|---|---|---|
GET /api/v1/interviews, GET .../{id} |
Did not exist | Real |
POST /api/v1/interviews, POST .../{id}/feedback |
Stub | Real |
GET /api/v1/offers, GET .../{id} |
Did not exist | Real |
POST /api/v1/offers, .../approve, .../send, .../acceptance |
Stub | Real |
GET/POST /api/v1/green-forms/by-token/{token}, GET .../submission/{id}, POST .../submissions |
Did not exist | Real (anonymous, token-authorized) |
POST /api/v1/green-forms/{applicationId}/issue-link |
Stub | Real |
GET /api/v1/verification, GET .../{applicationId} |
Did not exist (no controller) | Real |
GET /api/v1/discrepancies, GET .../{id} |
Did not exist | Real |
POST /api/v1/discrepancies/{id}/resolve |
Stub | Real |
POST /api/v1/discrepancies/{id}/reupload-request |
Did not exist | Real |
GET /api/v1/employee-conversion, GET .../{applicationId} |
Did not exist (no controller) | Real |
POST /api/v1/applications/{id}/convert-to-employee |
Stub | Real |
POST /api/v1/applications/{id}/shortlist-approval |
Stub | Real |
GET /api/v1/approvals |
Did not exist (no controller) | Real |
POST /api/v1/approvals/{id}/decision |
Did not exist | Real for offer.Offer/onboarding.Discrepancy; honest 422 for other entity types |
GET /api/v1/dashboard/summary |
Did not exist | Real |
Left as documented stubs (out of scope this pass, each comparable in size to one module above): POST /documents/{id}/verify, POST /applications/{id}/progression-approval, candidate matching (match_candidates_skill), admin user create/edit/activate/deactivate/role-assignment.
ApproveOffer/SendOffer/RecordAcceptance/SubmitFeedback/Resolve/RequestReupload endpoints previously returned 200 OK unconditionally, even when the underlying skill failed — now return 422 on failure, matching how create endpoints already behaved.TenantId and is independently enforced by row-level security (verified live — see item 9).db_hr_public_token_resolver) and a dedicated login-less user (db_hr_green_form_token_resolver), granted EXECUTE on exactly the 2 token-resolution procedures (never schema- or table-wide), and extended security.fn_tenant_access_predicate with one new, documented exemption clause. Authorization for these procedures comes from possession of an unguessable token (the row’s own primary key), resolved server-side — never a client-supplied tenant id. This was caught and fixed only because an initial EXECUTE AS OWNER attempt silently returned 404 at runtime (not a compile error) — the fix was verified with a real anonymous HttpClient carrying no Authorization header at all.ref.InterviewFeedbackTemplate had zero seed rows despite InterviewFeedback.InterviewFeedbackTemplateId being a NOT NULL foreign key — feedback submission was structurally impossible even after the endpoint existed. Added the missing seed row to 06-seed-recruitment-configuration.sql.
59 backend integration tests now pass (35 pre-existing + 24 new), all against a real LocalDB instance, none mocked:
| Test file | Coverage |
|---|---|
InterviewSchedulingAndFeedbackTests.cs |
Schedule→list→get, feedback→status change, wrong-role→403, not-found→404, no-token→401 |
OfferLifecycleTests.cs |
Full create→approve×2 (2-step matrix)→send→accept lifecycle with real state checks at each step, send-before-approval correctly rejected (422), wrong-role→403 |
GreenFormLifecycleTests.cs |
Full anonymous issue→get→submit→status flow with a real unauthenticated HttpClient, resubmission correctly rejected, unknown-token→404 |
DiscrepancyAndVerificationTests.cs |
List/resolve/reupload/wrong-role/not-found for discrepancies, verification queue read |
EmployeeConversionAndApprovalsTests.cs |
Approvals queue read, ineligible-candidate conversion correctly refused server-side, wrong-role→403 |
DashboardSummaryTests.cs |
Real non-fabricated counts, no-token→401 |
Frontend: 40 Vitest tests pass (2 pre-existing tests fixed because they used stale mock shapes that no longer matched the real DTOs), npm run typecheck/lint/build all clean.
Cors:AllowedOrigins config-driven; empty in appsettings.json, localhost:5173 in appsettings.Development.json) — no CORS policy existed at all before this pass.422 instead of a false 200 OK (see item 5).Math.Clamp(limit, 1, 100)).TRY/CATCH, an audit-event write, and a uniform Success/Message/EntityId/ErrorCode result — consistent with the existing ADR-006 pattern, not a new one invented for this pass.PROJECT_STATUS.md before this pass, not attempted here (comparable in size to one of the 6 modules built above).POST /documents/{id}/verify) — still a stub; no onboarding.VerificationCheck/VerificationResult write path exists.InterviewOutcome.match_candidates_skill) — still a stub; a genuinely large AI-matching feature, unchanged by this pass.offer.Offer and onboarding.Discrepancy; TAN, shortlist, and employee-conversion approvals still go through their own dedicated endpoints rather than the generic /approvals/{id}/decision route.npx playwright install (e2e browser binaries) not run — from the earlier platform-upgrade pass, still outstanding.PROJECT_STATUS.md.No new environment variables were introduced. Cors:AllowedOrigins (new config key, not an env var) defaults to empty (closed) and must be set explicitly for any non-same-origin production deployment — see appsettings.json.
# Backend
dotnet build HrAutomation.slnx
dotnet test tests/HrAutomation.Tests/HrAutomation.Tests.csproj # expect 59/59 passing, real LocalDB required
dotnet run --project src/HrAutomation.Api/HrAutomation.Api.csproj --urls "http://localhost:5219"
curl http://localhost:5219/health # expect {"status":"Healthy",...}
# Frontend (separate terminal)
cd src/HrAutomation.Web
npm install
npm run typecheck && npm run lint && npm run build && npm run test
npm run dev # http://localhost:5173
# Live check of a previously-broken route (requires the backend running):
# obtain a dev token, then GET /api/v1/interviews with it — see
# HrAutomation.Web/README.md "Development proxy" for the VITE_AUTH_MODE=devToken flow.
Per this task’s own instruction: the project is not fully production ready. It is materially more complete than before this pass — 7 previously-broken navigation routes now work end-to-end against real data, with real tests proving it — but admin user management, document verification, progression approval, and AI-assisted candidate matching remain genuinely unbuilt, and production authentication (DEC-002, OIDC vs. BFF) is still unresolved. See PROJECT_STATUS.md for the authoritative, continuously-updated capability table.
| Version | Date | Author | Change |
|---|---|---|---|
| 1.0 | 2026-09-09 | Navigation/feature reliability pass (Claude Code) | Initial creation |