feat(career): CV builder backend — data-driven theme engine + variant model
CI and Deploy / test (push) Failing after 1m51s
CI and Deploy / deploy (push) Has been skipped

Phase 4 foundation. A CvVariant is a lens over the master CareerProfile
(section order/visibility, per-item overrides keyed by ItemKey, theme +
builder settings) — it references career data, never duplicates it. One
renderer (ThemedCvRenderer) draws every theme; a theme is pure data
(CvThemeCatalog, 8 professional themes), so adding a theme needs no renderer
change. Autosave version history + non-destructive restore, public CV via
/api/public-cv/{slug} (anonymous, noindex, filter-bypassing owner load), and
an AI-assist endpoint reusing the existing provider abstraction (suggestions
only, never auto-applied).

- Models: CvVariant/CvVariantVersion, CvVariantSettings, CvTheme + catalog
- Services: CvVariantResolver (profile+lens -> render model), ThemedCvRenderer,
  CvVariantService, CareerProfileService.LoadStructuredForOwnerAsync (public)
- API: CvVariantController (/api/cv), PublicCvController (/api/public-cv)
- Migration AddCvVariants (2 self-contained tables; verified applied on the
  running container), 16 tests (resolver/renderer/service), 296 backend green

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
cesnimda
2026-07-18 09:52:58 +02:00
parent 707d8c59d2
commit a3e18e4b44
15 changed files with 3842 additions and 0 deletions
@@ -795,6 +795,89 @@ namespace JobTrackerApi.Migrations
b.ToTable("CvUploadArtifacts");
});
modelBuilder.Entity("JobTrackerApi.Models.CvVariant", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<DateTimeOffset>("CreatedAtUtc")
.HasColumnType("TEXT");
b.Property<bool>("IsPublic")
.HasColumnType("INTEGER");
b.Property<int?>("JobApplicationId")
.HasColumnType("INTEGER");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("TEXT");
b.Property<string>("OwnerUserId")
.IsRequired()
.HasColumnType("TEXT");
b.Property<string>("PublicSlug")
.IsRequired()
.HasColumnType("TEXT");
b.Property<string>("SettingsJson")
.IsRequired()
.HasColumnType("TEXT");
b.Property<DateTimeOffset>("UpdatedAtUtc")
.HasColumnType("TEXT");
b.Property<int>("Version")
.HasColumnType("INTEGER");
b.HasKey("Id");
b.HasIndex("JobApplicationId");
b.HasIndex("PublicSlug")
.IsUnique();
b.HasIndex("OwnerUserId", "UpdatedAtUtc");
b.ToTable("CvVariants");
});
modelBuilder.Entity("JobTrackerApi.Models.CvVariantVersion", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<DateTimeOffset>("CreatedAtUtc")
.HasColumnType("TEXT");
b.Property<int>("CvVariantId")
.HasColumnType("INTEGER");
b.Property<string>("OwnerUserId")
.IsRequired()
.HasColumnType("TEXT");
b.Property<string>("SettingsJson")
.IsRequired()
.HasColumnType("TEXT");
b.Property<string>("Source")
.IsRequired()
.HasColumnType("TEXT");
b.Property<int>("Version")
.HasColumnType("INTEGER");
b.HasKey("Id");
b.HasIndex("CvVariantId", "Version");
b.ToTable("CvVariantVersions");
});
modelBuilder.Entity("JobTrackerApi.Models.GmailConnection", b =>
{
b.Property<int>("Id")
@@ -1803,6 +1886,27 @@ namespace JobTrackerApi.Migrations
b.Navigation("Artifact");
});
modelBuilder.Entity("JobTrackerApi.Models.CvVariant", b =>
{
b.HasOne("JobTrackerApi.Models.JobApplication", "JobApplication")
.WithMany()
.HasForeignKey("JobApplicationId")
.OnDelete(DeleteBehavior.SetNull);
b.Navigation("JobApplication");
});
modelBuilder.Entity("JobTrackerApi.Models.CvVariantVersion", b =>
{
b.HasOne("JobTrackerApi.Models.CvVariant", "CvVariant")
.WithMany()
.HasForeignKey("CvVariantId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("CvVariant");
});
modelBuilder.Entity("JobTrackerApi.Models.InterviewPrepNote", b =>
{
b.HasOne("JobTrackerApi.Models.JobApplication", "JobApplication")