feat: Phase 0 foundation — Job entity, expanded pipeline, AI service lockdown, DateApplied history
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>
This commit is contained in:
@@ -47,9 +47,14 @@ namespace JobTrackerApi.Services
|
||||
var status = job.Status ?? "Applied";
|
||||
if (status == "Interviewing") status = "Interview";
|
||||
|
||||
// Nothing has been submitted in a pre-application stage, so there is nobody to chase
|
||||
// and nobody to be ghosted by. Guard before any date maths: a Saved job has no
|
||||
// DateApplied, and treating that as "very old" would silently auto-ghost it.
|
||||
if (JobPipeline.IsProspect(status)) return new FollowUpDecision(false, null, false);
|
||||
|
||||
// Last activity: any explicit follow-up date, response date, feedback request, or correspondence message.
|
||||
var last = Max(
|
||||
job.DateApplied,
|
||||
job.DateApplied ?? job.SavedAt,
|
||||
job.ResponseDate,
|
||||
job.FollowUpAt,
|
||||
job.FeedbackRequestedAt,
|
||||
@@ -61,7 +66,11 @@ namespace JobTrackerApi.Services
|
||||
// Applied: if no response and enough time passed since applied.
|
||||
if (string.Equals(status, "Applied", StringComparison.OrdinalIgnoreCase) && !job.ResponseReceived)
|
||||
{
|
||||
var daysSinceApplied = (now - job.DateApplied).TotalDays;
|
||||
// An Applied job should always have DateApplied. Fail safe rather than fall back to
|
||||
// a synthetic date, which could ghost the job on the next rules pass.
|
||||
if (job.DateApplied is null) return new FollowUpDecision(false, null, false);
|
||||
|
||||
var daysSinceApplied = (now - job.DateApplied.Value).TotalDays;
|
||||
if (daysSinceApplied >= s.AppliedFollowUpDays)
|
||||
return new FollowUpDecision(true, $"No reply after {s.AppliedFollowUpDays}d", daysSinceApplied >= s.AppliedGhostDays);
|
||||
return new FollowUpDecision(false, null, daysSinceApplied >= s.AppliedGhostDays);
|
||||
|
||||
Reference in New Issue
Block a user