Standard card language instead of the email-head strips: Contact &
application, Profile and Matching roles are now proper cards with
card-heads. The roles panel collapses three overlapping controls into
one flow - a single Browse roles action, with Assign appearing only
once a selection exists (a permanently disabled primary button read as
broken UI). Sheet provenance moves off the floating header corner into
the Imported from ... row N caption, and the action bar gains a
divider (email pane too, for consistency).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The combined stream leads the channel pills (All / Email / Sheet Forms)
and is the channel the inbox opens on.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Third channel pill beside Email / Sheet Forms. Both sources are fetched
unpaged (their endpoints read a missing top/limit as no LIMIT), merged
newest-first, and paged client-side - per-source skip/top cannot compose
into a correct global page. Rows carry kind (email/form) so the detail
pane, mark-read (email rows only get checkboxes; sheet rows have no
mailbox read state) and the kind-aware state/duplicate mutations all
pick the right endpoint per row. Tabs collapse to the shared set (no
Unread), counts and totals sum both sources, and the sheet filter spans
every sheet tab on this channel.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The unpaged switch lived only in the bottom Per-page dropdown and went
unnoticed. The bulk bar now carries a Show all (N) / Show paged button
(hidden while rows are ticked, since the bar swaps to selection
actions); it drives the same pageSize state, so the dropdown stays in
sync. The bar wraps now - four controls no longer fit one line in the
34%-wide split column at laptop widths.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Integrates the Per-page dropdown (10/50/100) with the inbox All option:
All is now a fourth dropdown entry (opt-in via allowAll) instead of a
separate button. Restores what the stale-branch merges on main clobbered
from PR #46: mobile.test.mjs, the test:mobile script and the
puppeteer-core devDependency.
PageSizeField gains an opt-in All toggle beside the numeric input
(sentinel value all; the input disables while active). Only the
Recruitment Inbox opts in: with All active the fetch omits top/limit,
which /inbox/all-applications and /sheet/form-data/fetch already read
as unpaged (Query(None) -> no LIMIT), so the whole tab renders as one
page for both the Email and Sheet Forms channels. Every other table
keeps its existing paged-only control.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Audited all 25 routes at 320/375/390/430/768 with browser automation.
Root cause of most overflows: grid items default min-width:auto, so one
wide intrinsic child (a select option, the 620px RBAC matrix) pushed
whole cards past the viewport.
- .grid > * / .rbac-layout > * min-width:0 (CV Import, Interviews,
Tasks, Access Control card blowouts)
- calendar: minmax(0,1fr) columns — SAT was clipped off-screen at 320
- .seg wraps (Tasks filters), .card-head wraps <=900
- sticky mobile table column: first column (identity), was nth-child(2)
- tooltips display:none until hover + .content overflow-x clip (killed
a 7px sideways wiggle from invisible tooltip boxes)
- Ask Analytics form: design-system input styling, stacks on phones
- bar/grouped-bar charts thin x labels by slot width (overlap on phones)
- Candidates sort label+select wrap as one unit
- new: mobile.test.mjs + npm run test:mobile (puppeteer-core devDep,
drives installed Chrome) — fails on sideways scroll or runtime errors
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>
- 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
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>
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>
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>
- 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>
Dashboard, Candidates, Jobs, Settings, Inbox, Interviews, Matching,
Managers, JobCandidates — bounded checklist per screen:
- Hand-rolled .page-head blocks replaced with <PageHeader/>. Fixes
Matching, whose bare <h1> rendered in the wrong face and weight.
- Candidates drops its verbatim copy of the DataTable thead for
DataTableHead; clickable rows use .row-click.
- Interviews deletes its local STATUS_CLASS shadow (Badge auto-maps
identically); Dashboard imports the shared PRIORITY_CLASS.
- Icon-only .act-btn buttons gain aria-labels mirroring their tooltips.
- Emails no longer overflow: Settings/Candidates clip with full text in
title=""; Managers shows the full address again instead of chopping
the domain off the data.
- Mechanical inline-style swaps to the new utilities (mb-*, mt-*,
flex-1, justify-between, flex-wrap) on exact matches only.
- Settings uses its imported EmptyState instead of a hand-rolled copy;
JobCandidates embedded title uses .page-title-sm instead of an inline
font size.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
className="input" matches no CSS rule, so the filter fell back to the
browser default ~180px width and clipped its own placeholder ("Filter
by name, headline, com"). Swap it for the toolbar-search pattern every
other screen uses: search icon, full-width input, 340px cap.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Scheduling an interview record did not move the pipeline stage, so
recruiters who booked an interview from the profile still saw the Forms
tab locked (reported on a Shortlist-stage candidate with a completed
Technical interview). The gate now passes when the stage is
INTERVIEW/OFFER/HIRED OR the application has any interview on record —
enforced server-side in candidate_forms create (manual-upload candidates
still unlock by stage alone, since interviews link only to inbox rows)
and mirrored in the Forms tab hint.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Deploy to S3 / deploy (push) Successful in 37sDetails
Backend: new candidate_forms domain (requisition, interview analysis,
cultural fit) with XOR inbox/manual keys, server-recomputed section
averages and combined summary, INTERVIEW-stage gate (409), history
events, INTERVIEWS_* permissions + 008 RBAC seed; offers table gains
the seven Annexure-J fields.
Frontend: Forms tab in the candidate profile (paper-exact labels from
/forms/definitions, rating tables, score summary tiles, completion
dots); profile converted to a full page at /candidate/:userId opened
from Candidates, Talent Pool and Pipeline; live Advance Stage now calls
PATCH /candidate/stage; workflow-ordered tabs; responsive pass verified
by headless-Edge screenshots at 375-2400px; Stars import crash fix in
Interviews; Matching tab strip wraps on phones.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Dashboard/chart audit (senior-analyst pass over the live app):
- Pipeline bars were all 0% while the doughnut showed candidates: the
percentage base was the first stage count (PROCESS, empty). Bars are
now each stage share of the active total, REJECTED is excluded
(matching the Analytics card), and stages sort in pipeline order.
- KPI trend chips no longer show a green up-arrow beside an empty delta;
for lower-is-better metrics (time to hire, cost per hire) the colour
tracks goodness while the arrow tracks the data direction.
- Closed Jobs tile no longer adds hires to closed requisitions and no
longer hardcodes an up arrow; Offers Accepted drops the hires-series
sparkline that plotted the wrong metric.
- charts.js: integer-friendly Y ticks (no more 0,1,1,2,2), edge-hugging
X labels (last month was clipped), and label thinning on packed axes.
Find Talent: nav item and page title renamed from Talent to match the
action button and to distinguish it from Talent Pool.
Already-applied matching (Find Talent x ATS):
- linkedin_utils.py: shared /in/<slug> extraction that survives PDF
mangling (percent-escapes, no scheme, trailing punctuation, /pub/).
- inbox_messages.linkedin_slug + manual_upload_candidate.linkedin_slug
(indexed; empty = scanned-none, NULL = unscanned) written as CVs are
processed and lazily backfilled in bounded batches at fetch time.
- talent/matching.py annotates profile payloads with already_applied
(source, status, job, same_job, applications); profile cards and the
detail modal show a green Already-applied / amber In-ATS-other-job
chip.
Verified live: seeded CV mention matched its sourced profile with zero
false positives across 50 real Apify profiles; 57 backend tests and the
frontend build + smoke suites pass.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Builds the four remaining analytics gaps from the architecture plan:
- Report library (REQ-ANL-03): new backend/reports module - saved reports
as parameterisations of the governed analytics queries (never free-form
SQL), rolling window_days filters, recorded runs, CSV export via
Content-Disposition. Reports screen gains the library card with
create/run/export/delete plus a results modal.
- Natural-language analytics (REQ-ANL-05, ADR-0010): POST /analytics/ask
maps a question onto one whitelisted intent, runs the same governed
query the dashboard uses, then narrates the numbers. Ask Analytics card
on the Analytics screen; LLM outages 503 without touching the charts.
- Time-to-hire baseline (REQ-ANL-08): KPIs surface tth_baseline_* from
org setting analytics.tth_baseline ({days,source}); not hardcoded
per OPEN-12. analytics added to org-settings categories. Reports TTH
card shows the delta when set.
- Source spend (REQ-ANL-09 cost side): hiring costs can be tagged with a
source channel; source performance returns tagged spend and
cost-per-application, including spend-only rows for channels with zero
applications (wasted spend must stay visible). Log-cost modal and
Source Performance table on Reports; untagged spend stays
cost-per-hire only.
Verified live: migration autogenerated and applied (saved_reports,
report_runs, hiring_costs.source_channel_id), 50 backend tests, vite
build + smoke/token/theme suites, and a headless-browser drive of both
screens end to end.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Adjusted `VITE_API_BASE` in Dockerfile and docker-compose.yml to allow same-origin requests, enhancing compatibility with nginx proxy settings.
- Increased the `top` query limit in `app.py` to 500 to accommodate frontend requirements while ensuring consistency across job fetching in `Jobs.jsx` and `Managers.jsx`.
- Updated nginx configuration to properly proxy API requests, preventing incorrect responses for job-related endpoints.
These changes streamline the interaction between the frontend and backend, ensuring a smoother user experience when fetching job data.
- Introduced a new `MailboxSyncRun` model to manage mailbox synchronization jobs, allowing for tracking of sync status and results.
- Implemented API endpoints for starting mailbox sync and fetching sync status, enabling asynchronous processing of email synchronization.
- Updated the `Email` service to handle mailbox sync operations, including enqueueing tasks and managing sync results.
- Enhanced frontend components to initiate mailbox sync and display sync status, improving user experience and feedback during the sync process.
- Added necessary query keys and local storage management for tracking sync runs in the UI.
This commit enhances the application's email synchronization capabilities, providing a more robust and user-friendly experience for managing mailbox data.
- Added a new `CandidateHistory` model to maintain an append-only audit log for candidate actions.
- Introduced `HistoryRecorder` functionality to log significant events across various modules, including interviews, feedback, and notes.
- Updated existing methods to include `current_user` for tracking who performed actions.
- Enhanced the `EMAIL_URL` configuration in `docker-compose.yml` to allow for environment variable overrides.
- Updated frontend components to support fetching and displaying candidate history.
This commit improves the application's ability to track changes and actions related to candidates, enhancing accountability and transparency.
- Updated the interview model to include `graph_event_id` and `web_link` for calendar integration.
- Added methods for setting calendar events and retrieving job titles associated with interviews.
- Modified serializers to include new fields in the interview response.
- Enhanced the job post model to include applicant counts.
- Updated frontend components to handle new fields and improve user experience with calendar event management.
- Introduced image upload functionality in job forms with validation for file types and sizes.
- Improved dashboard and calendar UI for better data presentation.
This commit streamlines interview scheduling and job management processes, enhancing overall application functionality.
- GET /jobs/export (jobs.export): styled .xlsx via openpyxl — branded
banner, dark-green frozen header with auto-filter, zebra rows, color-
coded status, bulleted requirements/nice-to-have, date formatting.
Honors the same filters as /jobs/fetch.
- Export button on the Jobs screen now downloads the file through the
authenticated downloadFile helper, carrying the active UI filters,
with an Exporting... busy state (was a fake success toast).
- openpyxl pinned in backend requirements.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Deploy to S3 / deploy (push) Successful in 33sDetails
- New POST /job/assist-field (job_board.create OR jobs.edit): per-field
AI fix/suggest via backend/job_assist package on the shared llm_call.
Suggest is gated on title + experience anchors; decisive typo repair;
provider failures return a generic 503.
- AiFieldAssist component: sparkle button per field with fix/suggest,
loading, preview-before-apply, abort on close; wired into JobForm and
EditJobForm with per-field suggest hints.
- Create Job no longer requires a Buffer channel: without channel_id or
platform the backend saves an internal-only requisition (platform
internal) and never calls Buffer; publishing stays on the Job Board.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>