Keep follow-up draft generation but remove the direct application SMTP delivery boundary. Route users to the provider-aware Job email flow and return 410 for legacy API callers.
- consolidate API ownership and remove dead vendor code
- add Stripe billing, learning paths, and public CV hardening
- add migration, recovery, security, audit, and browser gates
Every JobApplication gets a dedicated workspace at /applications/{id} — a
surface, not a new data store. It owns no data and duplicates none: CV comes
from the Phase 4 CvVariant lens, analysis/match/interview from the existing
AiWorkspacePanel, documents from Attachments, communication from
Correspondence, activity from JobEvent, stage semantics from JobPipeline. No
career data is copied and nothing here writes.
- GET /api/jobapplications/{id}/workspace: one aggregate read (role, company,
stage, dates, attached CV variant, cover letter, documents, AI history,
recent activity) replacing the page fanning out across endpoints
- Next recommended action: ordered rules answering "what do I do next?", the
core product principle for this phase
- ApplicationWorkspacePage: left nav + linkable ?section=, reusing the existing
component for each domain; later-milestone sections say so rather than faking
- Entry point from the job dialog via an optional onOpenWorkspace callback —
the dialog must not depend on router context (it is mounted without a
<Router> in several suites), so the caller owns navigation
- 8 backend tests (aggregate, CV variant surfacing, counts, activity ordering,
next-step rules, tenant scoping)
Local: 314 backend, 88/88 frontend (31 suites), tsc clean, production build ok.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Phase 5 frontend. A new "AI Workspace" tab in the job details dialog hosts the
five suggestion modules (Job Analysis, Career Match, Cover Letter with tone,
Interview Prep, Application Review) with a generate flow, a dependency-free
markdown renderer for results, and a history sidebar (reuse / compare / copy /
delete). Everything is suggestion-only — copy to keep; nothing auto-applies.
- aiWorkspace.ts (types + API), components/AiWorkspacePanel.tsx,
components/Markdown.tsx (no HTML injection surface — renders React nodes)
- mounted as the last tab in JobDetailsDialog (index-safe, no reindexing)
- 3 tests (generate flow, cover-letter mode, markdown); tsc + build clean
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Recover the F1 Career Profile foundation + AI-workspace persistence from the
unmerged feature/career-workspace branch, so Phase 2 builds on the documented,
tested target state instead of re-deriving it. Foundation only — CV Builder
commits (variants, ATS badge, rewrite diff) stay deferred per "do not build CV
Builder yet". See docs/career-workspace-branch-assessment.md.
Squashed from 3 branch commits (235e291, 5916f09, 00a035e), resolved against
main + Phase 0:
- CareerProfile + CareerProfileVersion (append-only history), dual-written from
every profile save path via CareerProfileService. ApplicationUser.
ProfileCvStructureJson stays authoritative; the tables mirror it. Stable item
IDs assigned to jobs/education/certifications/projects (the prerequisite for
future variant lineage). CvDateNormalizer for free-text -> YYYY-MM.
- InterviewPrepNote + AiWorkspaceNote: cache AI interview prep / candidate fit /
focus plan keyed by an attachment-context signature, so they stop regenerating
(and re-spending the provider) on every open.
Conflict resolutions (union, favouring current code + Phase 0):
- JobTrackerContext / StartupInitializationExtensions: kept Phase 0's tables and
reconciler blocks, added the career/interview/ai-note tables (both SQLite and
MySQL dialects).
- ProfileCvController: dropped the branch's in-file DTO records (main defines them
in ProfileCvDtos.cs) and the LayoutFamily/AtsRating template fields (deferred
ATS-badge work), keeping main's 7-arg CvTemplateDescriptor.
- JobApplicationsController: kept the branch's cache-check, restored main's
AsNoTracking on the read-only user load.
Tables ship empty (verified dev); nothing to migrate.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Unblocks the documented core workflow and closes the AI-service exposure,
without changing existing behaviour.
Job/JobApplication split (additive; see ADR-002):
- New Job entity (the opportunity) with owner-scoped query filter; nullable
JobApplication.JobId FK. Nothing reads Job yet.
- Migration AddJobEntityAndProspectStages, hand-edited to drop reconciler-owned
tables the scaffolder re-emitted; verified against the real dev DB.
Pipeline: 10 internal stages across three concerns kept separate —
PipelineStage (workflow) / PipelineGroup (UI: NotApplied/Active/Closed) /
PipelineCategory (analytics). Adds Saved/Interested/Preparing/Withdrawn;
keeps Waiting and Ghosted. Kanban shows 3 grouped columns; cards keep a stage
chip and full transitions; drag applies only safe transitions (never infers
Ghosted/Withdrawn).
DateApplied nullable + SavedAt. Cleared when leaving Applied so analytics stay
accurate; the discarded date is preserved as an AppliedDateCleared JobEvent.
AI service lockdown: no host port; private ai_internal network (backend is the
only other member); X-Ai-Service-Token required on all non-/health endpoints;
AI_SERVICE_TOKEN mandatory via compose. Verified backend-only against the live
stack.
Also carries two pre-existing working-tree files (views/ProfilePage.tsx,
views/CareerWorkspacePage.tsx) so the tree is clean for the branch integration.
Tests: +40 backend (247 total), +5 sidecar (16), +15 frontend.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Restyle JobDetailsDialog.tsx (04-job-workspace.png mockup) within its
existing dialog/tab structure -- the real app splits Correspondence,
Attachments, and Candidate Fit into separate tabs rather than the
mockup's single-screen 2x2 card grid, so this is a visual-language
pass over the existing IA, not a restructure:
- Header: bolder title (h5/800), heavier status chip, cleaner
no-underline tab styling.
- Every flat bordered "fake card" Box (11 instances across all tabs,
plus the 2 in the Overview strategy-snapshot panel) becomes a
floating shadow card with no border, matching every other screen
redesigned this session.
- The two genuinely AI-generation actions (Generate Strategy Snapshot,
and by extension the shared GradientButton component) get the
mockup's signature gradient CTA treatment; the confirm-gated
"Refresh AI summary" action stays a plain outlined button so the
gradient doesn't get diluted by a second use on the same tab.
Also fixes a real bug surfaced by actually using GradientButton for
the first time: its sx callback read theme.vars.customShadows, which
throws when a component renders without this app's ThemeProvider --
true in production always, but true in every test in this repo (none
of them wrap with a ThemeProvider), so every test touching a
GradientButton or one of these restyled boxes crashed. Fixed by using
a static shadow value instead of a theme.vars lookup in both the
component and this file, matching the fact that inline sx callbacks
execute against whatever theme is in context (unlike theme.components
styleOverrides, which only run when this app's real theme is actually
provided).
Verified: tsc clean, full suite green (65/65, including 4 test files
that render this exact dialog). Live check: booted the backend and
loaded the dashboard through a fresh Next.js dev server + cache
(cleared .next after chasing what turned out to be a stale console-log
history in the Browser pane tooling, not a real compile error) --
confirmed real data renders with no actual runtime errors.
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>