CODEX // oaoisme wiki

§ unswayed-backend

Dashboards & auto-apply (Phase 17)

updated 2026-06-24

Dashboards & auto-apply (Phase 17)

UN-157 / UN-158 / UN-156 · ADR-0047. The first of the new UN-156…165 backlog: two home-screen dashboards and the Lexi auto-apply service. All three live on the /api/v1/* surface (bare camelCase, no {status,message,data} envelope), and the actor is resolved from the auth user (no path {userId}).

Applicant dashboard — GET /api/v1/applicant/dashboard

One aggregated, no-N+1 call returning everything the applicant home screen needs:

  • activeApplicationCount — applications not in the archive bucket (there is no separate "rejected" status; archive is the reject/closed bucket).
  • interviewOffersinterview_invite + interview_accept.
  • topMatches (≤3) — the latest-version-per-job rows from MERIT candidate_scores for this applicant, on active jobs they have not already applied to, ordered by score.
  • recentJobApplications (≤5) — newest first, each tagged with its match score if scored.
  • profileCompletionPercent — a new server-side calc: a documented 10-field required set (name, about, experience level, address, skills, resume present, education present, filter preferences, job preferences), 10% each. This is the engagement nudge.

Employer dashboard — GET /api/v1/employer/dashboard

Aggregated hiring metrics, batched (no per-job query loop):

  • activeListingsCount, totalCandidates (distinct applicants), newPositions (last 30 days), candidateGrowth (this calendar month vs last — {percentageChange, direction, comparedTo}), candidateMatchCount (scores > 90), activeListings (per-job applicant count + top match score).
  • bestMatches (≤5) — the top scores across the employer's active jobs. Controlled disclosure (ADR-0041): candidateId is the UCN identifier (masked) unless that candidate is shortlisted (the CandidateScore.shortlisted flag or an application status at/past shortlisted), in which case the real id is disclosed.

Lexi auto-apply (UN-156)

When an applicant opts in, a nightly job auto-submits applications to internal jobs whose MERIT score clears a threshold.

  • Endpoints: PATCH /api/v1/applicant/settings/auto-apply (toggle isAutoApplyActive), GET …/settings/auto-apply (read), GET /api/v1/applicant/auto-apply/logs (paginated AutoApplyLog).
  • The cron (AUTO_APPLY_CRON, default 0 0 * * *) is wrapped in withAdvisoryLock (the Phase-16 leader-guard) so only one instance runs it.
  • AutoApplyService.runBatch() fetches opted-in (isAutoApplyActive + isCompleted) applicants, selects from live candidate_scores (totalScore ≥ AUTO_APPLY_MIN_SCORE, default 80; job type=internal + active + not-yet-applied), and submits each via the existing ApplyService.apply — the same validated path a manual apply uses. Each attempt is logged to auto_apply_logs (success / skipped / failed); one in-app notification fires per applicant batch.

Why it reuses ApplyService (ADR-0047)

The cron does not re-implement apply or scoring. It builds an ApplyDto from the applicant's stored profile (their primary ApplicantResume + EEO defaults) and calls the real service — so auto-applied applications go through identical validation, the application-number retry, and the post-commit notify. An applicant with no resume is skipped (ApplyService requires one). The flow is idempotent: the already-applied guard makes a re-run a no-op.

Gotchas

  • Dashboards add no models — they read existing data. Only auto-apply adds schema (User.isAutoApplyActive + AutoApplyLog).
  • DashboardsModule provides its own EmployerContextService (Prisma-only) to resolve the employer; AutoApplyModule imports ApplicationsModule (now exporting ApplyService) + NotificationsModule.
  • All three are bare /api/v1/* — return the object directly, no envelope.

Subpages