- 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>
- List column yields (34%, floor 280px) instead of holding 420px, so the
detail pane stays readable in the 980-1200px band.
- Detail pane is a size container: its two-column info grid collapses on
the pane own width, and long names/emails wrap instead of clipping.
- Tab strip wraps on narrow screens instead of running off-canvas.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
When no job board matches, the chip now says Email instead of the
full address; the address stays in the hover title and the opened
message. Board/referral chips are unchanged.
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 35sDetails
- Jobs list: close/reopen action per requisition with confirm on close
- Settings: real timezone and currency option lists
- Assigned-role chip: readable color pairing on soft background
- Scrollbars: Chromium honors the styled ::-webkit-scrollbar again
(scrollbar-color now gated to Firefox), slightly wider hit area
- Topbar hamburger: hidden on desktop (specificity fix) and the drawer
closes itself when the window grows past the mobile breakpoint
- Content scroller: removed overscroll containment that killed wheel
scrolling everywhere except the scrollbar thumb
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 37sDetails
Each bank row gains an eye action that opens the PDF in a modal iframe.
fetchBlobUrl learned an optional MIME re-type: /documents/download sends
octet-stream, which would trigger a download instead of the inline
viewer, so the blob is re-wrapped as application/pdf. Object URL revoked
on close. Download and remove actions unchanged.
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 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>
Deploy to S3 / deploy (push) Successful in 36sDetails
Several API prefixes double as SPA routes (/jobs, /inbox, /pipeline, ...);
refreshing the browser on one proxied the navigation to the backend and
500d. Navigations send Accept: text/html and now bypass to /index.html,
while fetch/XHR traffic still proxies. VITE_API_TARGET repoints the proxy
when the API is not on 8000.
Verified: direct loads of /jobs, /inbox, /pipeline, /analytics, /offers
render the correct screen with the session intact; /health still proxies.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Full-app verification tour (25 screens, light + dark) confirmed Inter
Tight/Belleza/Inter render correctly; the only stragglers were inline
weight-800 numbers with px tracking in four screens, now on the same
display face, weights, and em tracking as the CSS system.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Belleza (single-weight decorative face) read thin and informal against
the dense data UI. Headings now use Inter Tight semibold with negative
tracking; hero metrics join the display face at proper optical sizes.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The 380px queue rail left .ii-main only 148px: an email-style sender
name (no break points, never truncated) painted straight over the
timestamp, and the meta chips wrapped one per line. Three changes,
verified live against the dev server:
- Sender names truncate with the full value in title="" (Inbox and
Matching, which reuses the row classes).
- Older-than-a-week list timestamps show the date only, like Outlook —
the date+time form was ~105px and the detail pane has the full
instant. .ii-main grows 148 -> 195px; both status chips fit one row.
- .ii-name gets min-width: 0 and its size joins the type scale.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Skeleton loading system: .skeleton shimmer primitives in CSS (theme-
aware via --border) plus a SkeletonRows component, adopted at the
main-content loading states of Jobs, Candidates, Inbox, Interviews,
Offers, Assessments, Tasks, Managers, Notifications. Small card-level
loading states keep their EmptyState text.
- ErrorBoundary around the route outlet, keyed by pathname: a throw in
one lazy screen shows a recoverable error state instead of blanking
the whole app; navigating away resets it.
- auth.css stops hardcoding the Belleza stack twice; both sites use
var(--font-display) so the display face has one definition.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- .g-3 grids step 3 -> 2 columns at 1200px and 2 -> 1 at 900px; they
used to jump straight to one column, wasting the whole 900-1200 band
(Managers cards, Help topics, JobBoard destinations).
- The four scattered trailing @media 640px fragments (Dashboard v2
grid, candidate page, hiring forms) merged into ONE labeled block at
the end of the file, positioned after every base rule; the paired
400px override moves with it so small-phone rules still win. The
core shell 640 block stays put so the landscape-phone height rules
keep their existing precedence.
- Every breakpoint labeled; responsive section header updated.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- All 28 <div className="form-section-title"> section markers are real
<h3> headings now — the class carries every visual property, so this
is semantics-only, zero visual change.
- Remaining icon-only .act-btn buttons labeled: CandidateProfile
(download/edit note/revise scorecard, incl. one that had neither
tooltip nor label), CandidateForms amend button.
- PasswordField show/hide toggle is keyboard-reachable (tabIndex -1
removed) with aria-pressed state.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
AiAssistant, AiStudio, Analytics, Assessments, Calendar, CvImport, Help,
JobBoard, Notifications, Offers, Pipeline, Rbac, RecruiterHub, Reports,
Talent, TalentPool, Tasks — same bounded checklist:
- Every remaining hand-rolled .page-head replaced with <PageHeader/>;
zero copies of the block remain in the codebase.
- JobBoard fixes its non-functional inline ellipsis (overflow without
white-space never truncated) with .truncate + title.
- Tasks imports the shared PRIORITY_CLASS and labels its unlabeled
card-head add button; remaining .act-btn tooltips mirrored into
aria-labels (Assessments, JobBoard, Notifications, Offers, Talent).
- Assessments swaps a hand-rolled empty state for the imported
EmptyState; Help drops an inline h2 font size the base rule now covers.
- Mechanical inline-style swaps to utilities on exact matches.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>