The organisation runs four staff roles - system_administrator,
recruiter, hiring_manager, department_head. Migration 026 soft-deletes
hr_administrator, interviewer and ceo (listings filter on is_deleted,
so they vanish from Access Control and every role picker). candidate
stays: it is the applicant account type every candidate user sits on,
not a staff role. Guarded: a role that still has live members is left
untouched until they are reassigned, and the idempotent migration picks
it up on a later boot.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Backend: new GET /analytics/applications-per-job/fetch (analytics.view) counting
inbox + manual-upload applications per job on the same basis as the funnel, with
zero-application open reqs included. Per-job group-bys added to Inbox_Messages,
Manual_UPLOAD_CANDIDATE; JobPosts.list_open_reqs for the zero-fill.
Frontend: Dashboard rewritten as a filterable summary (Week/Month/Quarter/Year +
department) - My Tasks, Today's Schedule and Quick Actions removed; 8 KPI tiles
(Hires and Offers Sent now shown); Applications per Job hero row-list; pipeline
card refed from /analytics/funnel so it obeys filters; Offer Book, Needs
Attention and Recent Activity cards added. ChartCard and time ranges extracted
to shared modules used by both Dashboard and Analytics.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- talent_runs.cost_usd records each run's actual charge (usageTotalUsd),
accumulated across the broadened re-run ladder (migration 018).
- GET /talent/account: live balance and cycle spend from Apify
/users/me/limits (nulls when Apify is unreachable) plus the observed
$/profile over recorded runs.
- Header shows Balance / Spent / $-per-profile chips that refresh with
every search; the run status line shows the last search's cost and the
confirm dialog estimates from the live average.
- Responsive: toolbar controls stack edge-to-edge and candidate card
footers wrap instead of clipping their buttons on phones.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Manual outreach funnel on talent_profiles (sourced -> shortlisted ->
contacted, one-step undo) tracked per profile with who/when stamps.
New PATCH /talent/profiles/outreach behind talent.edit; Find Talent
screen gains All/Shortlisted/Contacted tabs, a star to shortlist and
a check to record that the recruiter messaged the person on LinkedIn
(the app itself sends nothing). Re-runs never reset outreach state.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Deploy to S3 / deploy (push) Successful in 37sDetails
The banked PDF now lives IN the database (new cv_bank_files table,
manual migration 010, auto-applied at startup) instead of the container
filesystem, so production redeploys cannot lose a stored CV; upload
writes row + bytes in one commit and creates no disk file at all. New
GET /candidate/cv-bank/file serves the bytes for both download and the
in-app preview.
Per user request, banking a CV with a detectable email also creates or
reactivates the candidate account (same pattern as manual add, no setup
email), so banked people appear on the Candidates screen; a CV without
an email still banks fine, account-less. Existing bank rows were
backfilled locally.
E2E-verified: upload -> bank row + DB bytes + zero disk files, preview
and download served from the DB, candidate visible on the Candidates
screen.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Deploy to S3 / deploy (push) Successful in 36sDetails
Storing a CV without a job no longer rides the cv_upload pipeline, which
created a candidate user account and an inbox row — stored CVs were
leaking into the Candidates screen. New cv-bank endpoints instead write
apply_via=cv_bank rows in manual_upload_candidate (nullable user/job
FKs): file + parsed text only, no account, no inbox entry, no scoring,
and email is optional (captured when the CV contains one). The CV Import
screen now shows the bank itself below the dropzone — browse, download
(existing /documents/download route) and delete.
E2E-verified: uploads land as BANKED rows with user_id NULL, zero new
accounts or inbox rows, UI delete works, Candidates screen unchanged.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Deploy to S3 / deploy (push) Successful in 36sDetails
The job picker gains a No job / store in CV bank option: files upload
one-per-request through the existing POST /candidate/cv_upload pipeline
(email auto-detected from the CV, saved as an UNASSIGNED inbox item with
background job suggestions) instead of being scored. Queue rows show
Stored plus the detected email; a CV with no detectable email fails
alone with a clear message. In this mode the scored grid gives way to a
panel linking Job Matching and the Inbox, where stored CVs are browsed
and later assigned. Also guarded the matcher enqueue in ingest_upload:
with the broker down the upload used to 500 after the row was inserted.
E2E-verified: two CVs stored (emails detected), rows visible in DB with
assigned_job_post_id NULL, both surfaced in Job Matching and the Inbox.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Deploy to S3 / deploy (push) Successful in 31sDetails
Production containers have ephemeral filesystems, so the disk-backed
image from the previous commit would vanish on redeploy. New
job_post_images table (bytea, PK = job_posts FK so re-upload replaces),
created everywhere by manual migration 009 which run_manual_sql applies
automatically at startup — prod boots with DB_AUTOGENERATE=false and
never autogenerates tables. Upload/fetch endpoints unchanged for the
frontend; fetch now serves bytes from the row. The one locally stored
disk image was imported into the table and backend/uploads removed.
E2E re-verified: create-with-image 200, fetch 200, cover renders.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Deploy to S3 / deploy (push) Successful in 32sDetails
The create modal collected an image but dropped it. Now POST
/job/image/upload stores it on disk keyed by the post id (uuid-validated,
5 MB / png-jpg-webp-gif, replace-on-reupload; no DB migration) and GET
/job/image/fetch serves it. The create flow uploads right after the row
exists, image failure downgrades to a toast instead of failing the create,
and Job Details renders the cover via an authorized blob fetch.
E2E-verified: upload 200, fetch 200, cover renders in Job Details.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Deploy to S3 / deploy (push) Successful in 36sDetails
Dropped from the create and edit modals, the detail drawer, the frontend
job mapper, and the composed publish text (which otherwise printed
Salary: Anonymous on every post). Backend model/API still tolerate the
column, so older clients and stored rows are unaffected.
UI-verified live: modal has no salary field, create POST carries no
salary key, cover-image upload previews and the post lands successfully.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Updated the `app.py` file to replace the generic `Sheet` service with specific services: `SheetHealth`, `SheetRead`, `SheetImport`, and `SheetWrite` for better clarity and functionality.
- Modified the `tasks.py` file to utilize the `SheetImport` service for handling sheet import tasks.
- Enhanced the `views.py` file by introducing a new class hierarchy for sheet operations, improving code organization and readability.
These changes improve the structure and maintainability of the Google Sheets integration.
Live case: the job titled "Generative Engineer" sourced a pool titled
"Generative AI Engineer" -- the exact phrase never occurs in that title,
so every genuine match fell through to the scattered 35 tier and the
whole pool compressed into the 40s. A current title containing every
job-title token now earns the full 55. The headline tier stays
phrase-only so keyword-stuffed headlines still cap at the 35 tier.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>