§ unswayed-backend
Dashboards & auto-apply (Phase 17)
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 thearchivebucket (there is no separate "rejected" status;archiveis the reject/closed bucket).interviewOffers—interview_invite+interview_accept.topMatches(≤3) — the latest-version-per-job rows from MERITcandidate_scoresfor 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):candidateIdis the UCN identifier (masked) unless that candidate is shortlisted (theCandidateScore.shortlistedflag 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(toggleisAutoApplyActive),GET …/settings/auto-apply(read),GET /api/v1/applicant/auto-apply/logs(paginatedAutoApplyLog). - The cron (
AUTO_APPLY_CRON, default0 0 * * *) is wrapped inwithAdvisoryLock(the Phase-16 leader-guard) so only one instance runs it. AutoApplyService.runBatch()fetches opted-in (isAutoApplyActive+isCompleted) applicants, selects from livecandidate_scores(totalScore ≥ AUTO_APPLY_MIN_SCORE, default 80; jobtype=internal+ active + not-yet-applied), and submits each via the existingApplyService.apply— the same validated path a manual apply uses. Each attempt is logged toauto_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). DashboardsModuleprovides its ownEmployerContextService(Prisma-only) to resolve the employer;AutoApplyModuleimportsApplicationsModule(now exportingApplyService) +NotificationsModule.- All three are bare
/api/v1/*— return the object directly, no envelope.
Subpages