Files
jobtrackingapp/docs/architecture/cv-builder.md
T
cesnimda 17edf19f89
CI and Deploy / test (push) Failing after 1m53s
CI and Deploy / deploy (push) Has been skipped
docs(architecture): document CV builder, theme engine, Phase 4 status
cv-builder.md (variant model, rendering pipeline, API, builder workflow,
extension points, known deep-link limitation) + cv-theme-engine.md (how a
theme is data and how to add one). Roadmap Phase 4 marked foundation-shipped
with the remaining polish itemised.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-18 10:07:43 +02:00

4.7 KiB

CV Builder (Career Workspace Builder)

Phase 4 (2026-07-18). The builder that turns the master career profile into themed, exportable, optionally-public CVs. Companion to docs/architecture/cv-theme-engine.md, docs/architecture/career-profile-model.md, and MASTER_IMPLEMENTATION_GUIDE.md. Verified against code and a running container.

Source-of-truth chain

CareerProfile (the only editable career source)
   ↓ read (never copied)
CvVariant       — a LENS: section order/visibility, per-item overrides, theme + builder settings
   ↓ resolve
CvRenderModel   — presentation projection (CvVariantResolver)
   ↓ render (one path, theme = data)
HTML            — ThemedCvRenderer
   ↓ export / share
PDF  |  Public CV (/cv/{slug})

The master profile is never written by the builder. A variant references career items by their stable ItemKey; overriding a bullet stores the override on the variant, leaving the profile untouched (CvBuilderTests.Resolver_applies_item_override_bullets_without_touching_the_master).

Variant model

CvVariant (Models/CvVariant.cs) + CvVariantVersion (autosave history). The whole lens lives in one SettingsJson blob (CvVariantSettings, Models/CvVariantSettings.cs) because it is edited and saved as a unit — never queried field-by-field. A variant stores:

  • ThemeId + overrides: accent, heading/body font, density, page size, photo/icons/page-numbers.
  • Sections: ordered list with per-section Hidden + optional renamed Title.
  • Overrides: keyed by ItemKey{ Hidden, Title, Subtitle, Bullets } (per-item, job-specific).
  • CustomSections: variant-only sections not in the master profile.

PublicSlug (unguessable, globally unique) backs the public URL. IsPublic is off by default.

Rendering pipeline

  1. CvVariantResolver.Build(profile, settings, fallbackName, photoDataUrl)CvRenderModel. Applies order, visibility, item overrides and custom sections. Pure projection, owns nothing.
  2. ThemedCvRenderer.Render(model, theme, settings) → HTML. One render path; the theme (data) drives layout/palette/typography/spacing. See cv-theme-engine.md.
  3. PlaywrightCvPdfExporter (reused unchanged) turns the HTML into a PDF.

API

Authenticated (/api/cv, CvVariantController): GET themes; GET/POST variants; GET/PUT/DELETE variants/{id}; POST variants/{id}/duplicate; PUT variants/{id}/public; GET variants/{id}/versions, POST …/versions/{v}/restore; GET variants/{id}/preview and POST preview (live preview of unsaved settings); POST variants/{id}/export-pdf; POST ai/assist.

Anonymous (/api/public-cv/{slug}, PublicCvController): serves a public variant read-only, X-Robots-Tag: noindex. Loads the owner's profile via LoadStructuredForOwnerAsync (bypasses the tenant filter — there is no current user). Unknown/private slug → 404 (CvBuilderTests.Private_variant_is_not_served_publicly).

Builder workflow (frontend)

/career/builder lists variants (CvBuilderPage); the editor (CvBuilderEditor) is three tabs — Content, Customize, AI Tools (plus History) — beside an always-on live preview that re-renders through POST /api/cv/preview on a 350 ms debounce. Edits autosave on an 800 ms debounce (source: autosave), appending a version each save. /cv/:slug (PublicCvPage) renders a public CV in a sandboxed iframe.

AI

POST /api/cv/ai/assist reuses the existing ISummarizerService provider abstraction. It returns a suggestion the user copies in themselves — it never mutates the profile or the variant. Every prompt carries "preserve every factual claim — never invent" (MASTER_IMPLEMENTATION_GUIDE AI philosophy).

Extension points

  • New theme: append a CvTheme to the catalog (cv-theme-engine.md). No renderer change.
  • New section kind: add a builder in CvVariantResolver + a Kind branch in ThemedCvRenderer.
  • DOCX export: add an IDocxExporter consuming the same CvRenderModel; the model is format-neutral.
  • Rich text / page numbers: see cv-theme-engine.md "Deliberately not here (yet)".

Known limitation

Direct external loads of any deep link (including /cv/{slug}) currently bounce to the app root — the CSR lift-and-shift Next.js static-export setup only prerenders /, so a hard navigation to a deep path doesn't reach the react-router route (in-app client navigation works). This affects the whole SPA, not just public CVs, but public CVs are the first feature that depends on external direct links working. Fix path: serve index.html for all non-/api paths so react-router owns routing (nginx already has try_files $uri /index.html; the redirect is client-side in the Next shell). Tracked as a follow-up — see docs/architecture/frontend.md / the Next.js migration note.