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 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>