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>
Backlog item 4 (Wave 3, first sub-item). HasResume/HasCoverLetter/HasPortfolio/
HasOtherAttachment were manually-editable checkboxes in EditJobDialog,
completely independent of whether a file was actually attached -- classic
drift: mark 'resume ready' by hand, later delete the resume attachment, flag
stays stuck true forever. User confirmed (asked directly, since removing the
manual-override capability is a product decision, not purely technical):
make them fully computed from Attachments, no manual override.
- AttachmentsController.RecomputeAttachmentFlagsAsync: the single place these
four fields get written now, called after every attachment mutation
(upload, delete, Purpose change) that could affect them. Deliberately kept
as persisted columns (not [NotMapped] computed properties reading the
Attachments navigation collection) -- ~15 query sites build JobApplication
DTOs without .Include(Attachments), so a live-computed property would
silently return false everywhere instead of throwing, the worst kind of
bug. Recomputing at the one write funnel avoids touching any read path.
- Removed HasResume/etc from CreateJobApplicationRequest/
UpdateJobApplicationRequest -- no longer client-settable.
- EditJobDialog: removed the manual checkboxes, kept the (now genuinely
accurate) read-only status chips.
- AddJobModal: stopped sending has*-flags at job-creation time; the
follow-up attachment upload call now sets them correctly via the same
recompute path.
Caught a real bug while testing this: the Purpose-change path recomputed
before saving the Purpose change, so a fresh query missed the pending edit
and the flags never updated. Fixed by committing the mutation before
recomputing.
3 new backend tests (purpose-change sets flag, delete clears flag,
non-primary purpose counts as "other"). 172/172 backend, 25/25 frontend
suites (57 tests) green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Adds SalaryMin/SalaryMax/SalaryCurrency/SalaryPeriod alongside the
existing free-text Salary field (kept for back-compat and display).
- JobApplication model + idempotent column bridging for SQLite and MySQL
- Create/Update DTOs with NormalizeSalary (clamps negatives, swaps
inverted min/max, uppercases currency, whitelists period)
- JobApplicationDto exposes the fields; CSV export gains 4 columns
- UI: add/edit dialogs get min/max/currency/period inputs; job table
renders a formatted range via shared salary.ts formatter (falls back
to free-text when structured values are absent)
- EN/NB translations; backend + full frontend suites green
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>