Extends the interview-prep persistence pattern (previous commit) to
the other two AI-generated per-job outputs that were re-running their
full AI call chain on every tab open: candidate-fit (4 AI calls) and
focus-plan (4 AI calls). Across all three tabs that's 9 AI calls fired
every single time a user revisits a job's AI workspace tabs.
Generalized into AiWorkspaceNote (OwnerUserId, JobApplicationId,
NoteType, ResultJson) rather than duplicating InterviewPrepNote's
per-field-column shape: CandidateFitDto and FocusPlanDto are irregular
and nested (up to 13 fields including a nested guidance object),
where per-field columns would be unreasonable. One table, keyed by
note type, serving both.
Same rules as interview prep: reuse across calls, regenerate when the
attachment selection changes, regenerate on explicit refresh. Frontend
gets the same "Regenerate" button on both tabs.
3 new tests (persist+reuse for both, refresh for candidate-fit).
Verified against the real dev DB.
Interview prep re-ran its AI call every time the tab opened -- flagged
in the product teardown as work evaporating on every re-open (cost,
latency, and non-determinism for no reason). GetInterviewPrep now
persists one note per job application and reuses it on subsequent
reads, only regenerating when the selected attachment context changes
or a refresh is explicitly requested.
- InterviewPrepNote: one row per (owner, job), keyed additionally by
an attachment-selection fingerprint so picking different attachments
correctly triggers a fresh brief without needing an explicit flag.
- GetInterviewPrep gained a `refresh` query param; the frontend adds a
small "Regenerate" button as the explicit escape hatch for when the
underlying job/notes have changed since the note was written.
- Both SQLite (dev) and MySQL/MariaDB (prod) reconciler dialects.
- 3 new tests: reuse across calls, refresh regenerates, attachment
context change regenerates. Verified against the real dev DB.
Continuing the product/UX review's deferred items:
- Every top-level view now gets a one-line subtitle under its title
(Dashboard/Jobs/Kanban/Reminders/Correspondence/Gmail review) stating
what that specific view is for, instead of navigation being the only
signal of what each page does.
- Correspondence inbox and Gmail review queue cross-link to each other
instead of being two unexplained flat sidebar items -- kept both nav
entries (renaming/nesting risked breaking muscle memory) but made the
relationship between them explicit in the UI itself.
- Kanban board switches to a horizontal scroll-snap row on phone-width
viewports instead of stacking all 5 columns vertically, which meant
a lot of scrolling to see anything past "Applied".
- Match-score ring gets an aria-label with the actual percentage --
it was two nested decorative CircularProgress elements with no
accessible text. (Keyboard-accessible status changes on kanban cards
were already covered by the existing "..." menu -- no gap there.)
Implements the six "propose first" items from the product/UX review:
- "/" now redirects to /dashboard instead of the empty /jobs table --
a new user's first screen is now an overview with orientation, not
a data table with zero rows and four filter dropdowns.
- Jobs table gets a real first-time empty state (distinct from "no
results match your filters") pointing at Add Job and the bookmarklet,
instead of a bare "No jobs found."
- Match Score card and Candidate Fit tab now each get a one-line
caption explaining what they are and how they differ (deterministic
keyword coverage vs. AI opinion) -- they previously sat side by side
with no explanation of why there are two.
- Google sign-in hint now reflects self-serve signup when
Auth:AllowRegistration is on, instead of always implying you need an
existing linked account.
- Quick Search button now shows its keyboard shortcut (Ctrl+K / ⌘K)
inline instead of being undiscoverable.
Second UI-rework pass. The job workspace mockup's signature element is
a donut "coverage" ring for the deterministic CV match score; the app
had a linear progress bar instead. Replaced with a layered
CircularProgress ring (track + value arc, percentage centered) while
keeping every existing feature (band chip, matched/missing keyword
chips, section coverage) -- this is a pure visual upgrade to the
existing MatchScoreCard, not a feature reduction to match the mockup's
simpler single-panel layout.
Fixed match-score-panel.test.tsx's no-signal-state assertion, which
expected the removed inline "—" placeholder; restored it outside the
ring's conditional render.
When a job workspace opens, loads /status-suggestion and shows a
dismissible banner when a recent inbound email implies a status move
("This email looks like a move to Interview"). Applying it PATCHes the
status; nothing changes without the user's click.
- StatusSuggestion type + load-on-open effect + apply handler
- warning-toned banner shown above tab content on any tab
- EN/NB translations; README endpoint docs
- 2 frontend tests; full suite green (21 suites / 48 tests)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Adds a MatchScoreCard at the top of the Candidate Fit tab that loads the
deterministic /match-score endpoint independently of the slow AI
narrative, so users see a reproducible score, matched/missing keyword
chips, and per-section coverage immediately.
- MatchScore types + cached, attachment-independent load effect
- graceful 'not enough signal' state
- EN/NB translations
- frontend panel test (matched/missing/section + degraded state)
- backend integration tests for GetMatchScore (happy path + missing CV)
- README endpoint reference
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>