- Assignees restricted to recruiter-role users (role id resolved from the
roles table per request, not hardcoded); server 422s any other role on
create and update
- New GET /tasks/assignees/fetch: active recruiter users for the picker,
separate from /users/fetch so assigning never needs rbac_users.view
- Creation gated to system_administrator / hr_administrator / recruiter:
allowed role ids searched from the DB and compared to the callers
role_id, on top of the tasks.create permission tag
- manual/005_tasks_rbac_restrict.sql: permission DB follows suit -
hiring_manager / department_head / ceo swap tasks_management for a
view-only tasks_viewer bundle (idempotent, auto-applies at boot)
- UI: assignee dropdown lists recruiters from the new endpoint with
required-field validation; recruiters get an assign-to-me default; the
New Task button honours the creator roles
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- New backend/tasks/ package (offer/ house style): tasks table
(title/status/priority/due_date/assignee_id -> users.id, optional
inbox_id/job_post_id links, soft delete), routes /tasks/fetch|create|
update|delete guarded by new tasks.* permission tags
- Assignees validated against users+roles: must exist, not deleted, not
candidate-role; omitted assignee defaults to the caller; responses carry
assignee_name/assignee_role from one batched join
- TASKS permission module (104 -> 112 tags); manual/004_tasks_rbac.sql
seeds the tags + tasks_management bundle onto 6 staff roles (auto-applies
at startup)
- Tasks.jsx cut over from seed to live: real create/complete/reopen with
optimistic flip, two-click delete in the detail modal, assignee picker
listing real users with roles; live sidebar badge (open-task total);
route now requires tasks.view
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- inbox_messages.ats_score/ats_band written on every completed inbox score;
inbox.ats_id always points at the current ats_results row
- ats_results now holds the supersede-chained history for BOTH inbox and
upload scores (new candidate_id link, inbox_id nullable for uploads)
- migrations/manual/*.sql apply automatically at startup, tracked once per
database in manual_migrations - developers just pull and boot
- 002_backfill_inbox_ats.sql backfills pre-existing scores
- Add Candidate modal: Matching-style role picker above the CV dropzone,
and the CV is scored via /candidate/score after creation
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Conflict resolutions:
- backend/job/app.py: dropped duplicate pydantic import (already present below)
- backend/job/candidate/views.py: union of both sides — kept Talha's
_recommendation/_scores_by_message helpers alongside main's manual-upload
create_candidate, merged import lists and module-level config
- frontend/src/lib/apiClient.js: both sides made the same FormData fix;
kept main's version that reuses the shared multipart const
- frontend/src/screens/Candidates.jsx: kept Talha's live-data screen and
ported main's AddCandidate modal onto it — server POST via
candidatesApi.createManual with live job posts, seed-shaped buildRow
replaced by a candidates query invalidation; seed-only BulkAssign dropped
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The decoded attachment is already the CV and the job post is already the JD, so
scoring now fires from the data we have:
- After the agent matches an inbox message, score_message_against_job runs
inline in the match task - against the assigned job if one is set, else the
top suggested job. Failures log and never fail the match.
- When a recruiter assigns a job post, the new inbox.score_message task is
queued in the background so the request never waits on OpenAI.
Idempotent by (message, job): an already-completed score is never paid for
twice. Rows are attributed to the job post owner since background tasks have
no request user. CV Import and the profile Score-with-ATS button remain as
manual fallbacks.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Main stubbed ai_score/recommendation as None in the candidate-profile payload;
fill them from the scoring engine's candidates table, joined by inbox message
(one batched query for the list, assigned-job-preferred for the detail). The
detail payload also gains matched/missing keywords, the critique, and which job
the score was against.
Frontend: TalentPool cards and the profile hero prefer the real score over the
seed placeholder, and the profile grows a Score-with-ATS button (shown when the
candidate has an assigned job and an inbox message) that runs the existing
score_inbox pipeline and repaints via the detail refetch.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Union resolution, same policy as the previous merge — both feature sets kept:
- app.py: scoring routes kept; main's richer GET /job/fetch (search/top/skip/ids)
adopted, with the dual JOB_BOARD_VIEW-or-CANDIDATES_VIEW permission restored so
recruiters with only candidate rights keep the CV Import job picker.
- candidate/models.py: Candidates (scoring) coexists with main's new Interviews,
Notes, Activity, Feedback models.
- candidate/serializers.py: serialize_candidate kept alongside main's
detail-mode serialize_candidate_profile.
- CandidateProfile.jsx: main's rich tabbed profile (interviews/notes/feedback/
activity) is now THE CandidateProfile, used by TalentPool; the scored-CV modal
moved to ScoredCandidateProfile.jsx, used by Candidates.jsx. The two payload
shapes share almost no fields, hence two components.
Backend imports verified; frontend builds clean.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>