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 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.
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>
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>
- New ui/PageHeader.jsx emitting the existing .page-head structure;
screens adopt it in the next two passes.
- DataTable: sortable thead extracted as DataTableHead (Candidates can
drop its verbatim copy) and an onRowClick prop with keyboard support.
- Tabs: index-based ids, aria-controls, roving tabindex, arrow-key and
Home/End navigation; TabPanel labels its panel.
- Toast root announces politely to screen readers (was silent).
- Dropdown stamps aria-expanded/aria-haspopup on every trigger.
- primitives: shared PRIORITY_CLASS (Dashboard and Tasks had identical
private copies).
- Shell: skip link to #main-content, sidebar nav labeled Primary,
clickable notification rows are real buttons now.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Type scale (--fs-2xs..--fs-3xl) and line-height tokens; the 20-size
census collapses onto 9 steps. Spacing scale (--space-1..8) plus --gap
keeping the signature 18px rhythm. Colors untouched.
- Base h1-h6 rules: bare headings used to render at UA defaults, larger
and bolder than the styled card titles beside them.
- One eyebrow-label rule replacing eleven near-identical declarations at
five sizes and five letter-spacings.
- High-traffic families retrofitted to tokens (page/card/table/form/btn/
tabs/badge/sidebar/kpi); rare bolt-on sections left alone.
- New utilities: truncate, clamp-2, justify-between, flex-wrap, flex-col,
flex-1, min-w-0, w-full, mt/mb-8/12/16/24, cell-clip, row-click.
- Defined phantom classes shipped in JSX (.dt, .route-loading,
.tab-count) and a .skip-link.
- Email/role cells clip with ellipsis on all viewports, not only <640px.
- Content measure capped at 1440px on every desktop, not only >=1600.
- Sidebar: clearer group separation, active item weight 600.
- main.jsx design-contract comment amended: colors stay frozen,
typography/spacing re-tokenized.
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>
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>
- Deleted the `.env.example` file to centralize configurations in `backend/.env`.
- Adjusted `docker-compose.yml` and `docker-compose.dev.yml` for improved environment variable handling.
- Enhanced nginx configuration for better API request management.
These changes streamline the setup process and improve configuration management.
Deploy to S3 / deploy (push) Successful in 34sDetails
Both branches appended at EOF; the merge kept only main's hunk and its
missing trailing newline. Root-anchored so backend/candidate_forms/
(the forms domain package) stays tracked.
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>