feat: integrate Career Workspace foundation from feature/career-workspace
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>
This commit is contained in:
@@ -32,6 +32,10 @@ namespace JobTrackerApi.Data
|
||||
public DbSet<TwoFactorRecoveryCode> TwoFactorRecoveryCodes => Set<TwoFactorRecoveryCode>();
|
||||
public DbSet<TrustedDevice> TrustedDevices => Set<TrustedDevice>();
|
||||
public DbSet<UserSession> UserSessions => Set<UserSession>();
|
||||
public DbSet<CareerProfile> CareerProfiles => Set<CareerProfile>();
|
||||
public DbSet<CareerProfileVersion> CareerProfileVersions => Set<CareerProfileVersion>();
|
||||
public DbSet<InterviewPrepNote> InterviewPrepNotes => Set<InterviewPrepNote>();
|
||||
public DbSet<AiWorkspaceNote> AiWorkspaceNotes => Set<AiWorkspaceNote>();
|
||||
|
||||
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
||||
{
|
||||
@@ -216,6 +220,57 @@ namespace JobTrackerApi.Data
|
||||
|
||||
modelBuilder.Entity<UserSession>()
|
||||
.HasIndex(x => x.UserId);
|
||||
|
||||
// Career Workspace foundation (docs/career-workspace-implementation-roadmap.md Phase F1).
|
||||
// One CareerProfile per user for now -- see roadmap "Not now: multiple profiles per user".
|
||||
modelBuilder.Entity<CareerProfile>()
|
||||
.HasQueryFilter(x => CurrentUserId != null && x.OwnerUserId == CurrentUserId);
|
||||
|
||||
modelBuilder.Entity<CareerProfile>()
|
||||
.HasIndex(x => x.OwnerUserId)
|
||||
.IsUnique();
|
||||
|
||||
modelBuilder.Entity<CareerProfileVersion>()
|
||||
.HasQueryFilter(x => CurrentUserId != null && x.OwnerUserId == CurrentUserId);
|
||||
|
||||
modelBuilder.Entity<CareerProfileVersion>()
|
||||
.HasIndex(x => new { x.OwnerUserId, x.CareerProfileId, x.Version });
|
||||
|
||||
modelBuilder.Entity<CareerProfileVersion>()
|
||||
.HasOne(x => x.CareerProfile)
|
||||
.WithMany()
|
||||
.HasForeignKey(x => x.CareerProfileId)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
|
||||
// Interview prep persistence (career-workspace-implementation-roadmap.md Phase F5).
|
||||
modelBuilder.Entity<InterviewPrepNote>()
|
||||
.HasQueryFilter(x => CurrentUserId != null && x.OwnerUserId == CurrentUserId);
|
||||
|
||||
modelBuilder.Entity<InterviewPrepNote>()
|
||||
.HasIndex(x => new { x.OwnerUserId, x.JobApplicationId })
|
||||
.IsUnique();
|
||||
|
||||
modelBuilder.Entity<InterviewPrepNote>()
|
||||
.HasOne(x => x.JobApplication)
|
||||
.WithMany()
|
||||
.HasForeignKey(x => x.JobApplicationId)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
|
||||
// Generic AI workspace note persistence (candidate fit, focus plan) -- same rationale as
|
||||
// InterviewPrepNote above, generalized because these DTOs are irregular/nested enough that
|
||||
// per-field columns would be unreasonable.
|
||||
modelBuilder.Entity<AiWorkspaceNote>()
|
||||
.HasQueryFilter(x => CurrentUserId != null && x.OwnerUserId == CurrentUserId);
|
||||
|
||||
modelBuilder.Entity<AiWorkspaceNote>()
|
||||
.HasIndex(x => new { x.OwnerUserId, x.JobApplicationId, x.NoteType })
|
||||
.IsUnique();
|
||||
|
||||
modelBuilder.Entity<AiWorkspaceNote>()
|
||||
.HasOne(x => x.JobApplication)
|
||||
.WithMany()
|
||||
.HasForeignKey(x => x.JobApplicationId)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user