feat(workspace): unified application checklist (Phase 5 milestone 2)
CI and Deploy / test (push) Failing after 1m8s
CI and Deploy / deploy (push) Has been skipped

Evolve the existing readiness workflow into one persisted, user-controlled
checklist rather than adding a second tracker.

ApplicationChecklistItem records only completion state and user intent. Each
default system item carries a stable SystemKey and an AutoSignal — the same
signal /readiness already computed — and re-syncs on every read: a satisfied
signal auto-completes the item, a reverted signal reopens it, and a manual tick
always wins. Users can add, reorder, dismiss and delete.

Readiness is refactored into a projection of the checklist (score = completion
percentage, completed/missing = live items by status). Its DTO shape and the
workflowSignal/reminders health view are unchanged, so no API contract breaks.

The workspace's next recommended action now comes from the first pending
checklist item in category priority order (preparation, submission, follow-up,
interview, custom), replacing the parallel ruleset — so the overview can never
recommend something already ticked off, and a user's own task can be next.

The table follows the established MariaDB-safe path: the scaffolded migration is
a no-op and the idempotent reconciler owns the DDL for both providers. Verified
on MariaDB 11 — auto_increment PK, varchar/datetime(6)/tinyint(1) columns, both
indexes inside the key limit, cascade delete, unique system key per application,
and NULL system keys not colliding for custom items.

329 backend tests, 94 frontend tests, type check, production build and both
Docker builds pass locally.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
cesnimda
2026-07-19 11:15:46 +02:00
parent e55a6e86b7
commit 3a906b881e
18 changed files with 3783 additions and 76 deletions
@@ -28,9 +28,11 @@ namespace JobTrackerApi.Controllers
private readonly AnalyticsService _analytics;
private readonly IJobCvMatchService _matchService;
private readonly IMemoryCache _cache;
private readonly IApplicationChecklistService _checklist;
public JobApplicationsController(JobTrackerContext db, ISummarizerService summarizer, IAppEmailSender email, UserManager<ApplicationUser> users, ILogger<JobApplicationsController> logger, ICvTemplateRenderer? cvTemplateRenderer = null, ICvPdfExporter? cvPdfExporter = null, AnalyticsService? analytics = null, IJobCvMatchService? matchService = null, IMemoryCache? cache = null)
public JobApplicationsController(JobTrackerContext db, ISummarizerService summarizer, IAppEmailSender email, UserManager<ApplicationUser> users, ILogger<JobApplicationsController> logger, ICvTemplateRenderer? cvTemplateRenderer = null, ICvPdfExporter? cvPdfExporter = null, AnalyticsService? analytics = null, IJobCvMatchService? matchService = null, IMemoryCache? cache = null, IApplicationChecklistService? checklist = null)
{
_checklist = checklist ?? new ApplicationChecklistService(db);
_db = db;
_summarizer = summarizer;
_email = email;
@@ -1771,20 +1773,22 @@ Candidate master CV:
var followUpDecision = RulesEngine.Evaluate(settings, job, now, lastMessageAt);
var workflowSignal = BuildWorkflowSignal(job, followUpDecision);
var completed = new List<string>();
var missing = new List<string>();
// Phase 5 Milestone 2: readiness no longer runs its own parallel checklist. The persisted
// application checklist is the one workflow surface; readiness projects it into the score /
// completed / missing / reminders health view the dialog and dashboard already consume, so
// the two can never disagree. The DTO shape is unchanged on purpose.
// docs/architecture/application-workspace.md.
var checklist = job.OwnerUserId is null
? null
: await _checklist.GetAsync(job.OwnerUserId, id, cancellationToken);
var live = checklist?.Items.Where(i => i.Status != ChecklistStatuses.Dismissed).ToList()
?? new List<ChecklistItemDto>();
if (workflowSignal.HasTailoredCv) completed.Add("Tailored CV saved"); else missing.Add("Tailor your CV for this role");
if (!string.IsNullOrWhiteSpace(job.CoverLetterText)) completed.Add("Cover letter draft ready"); else missing.Add("Create a cover letter draft");
if (job.HasPortfolio) completed.Add("Portfolio attached"); else missing.Add("Consider adding a relevant portfolio example");
if (!string.IsNullOrWhiteSpace(job.Company?.RecruiterEmail)) completed.Add("Recruiter contact available"); else missing.Add("Capture recruiter contact details if possible");
if (!string.IsNullOrWhiteSpace(job.NextAction)) completed.Add("Next action captured"); else missing.Add("Write the next action so follow-up is clear");
if (job.FollowUpAt is not null) completed.Add("Follow-up scheduled"); else missing.Add("Schedule a follow-up date");
if (workflowSignal.HasSavedApplicationAnswerDraft) completed.Add("Saved application answers available"); else missing.Add("Save application answers for this role");
if (workflowSignal.HasInterviewPrepNotes || !IsInterviewStage(job.Status)) completed.Add("Interview prep notes captured"); else missing.Add("Capture interview prep notes before the interview");
var completed = live.Where(i => i.Status == ChecklistStatuses.Done).Select(i => i.Title).ToList();
var missing = live.Where(i => i.Status == ChecklistStatuses.Pending).Select(i => i.Title).ToList();
var reminders = BuildReadinessReminders(job, workflowSignal);
var score = Math.Clamp(completed.Count * 12 + (string.IsNullOrWhiteSpace(job.Description) ? 0 : 10), 20, 100);
var score = checklist?.Progress.Percent ?? 0;
var level = score >= 80 ? "Ready" : score >= 60 ? "Needs polish" : "Needs work";
return Ok(new ReadinessDto(score, level, completed, missing, reminders, workflowSignal));