diff --git a/docs/architecture/cv-builder.md b/docs/architecture/cv-builder.md new file mode 100644 index 0000000..9e0df43 --- /dev/null +++ b/docs/architecture/cv-builder.md @@ -0,0 +1,88 @@ +# 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. diff --git a/docs/architecture/cv-theme-engine.md b/docs/architecture/cv-theme-engine.md new file mode 100644 index 0000000..57803ee --- /dev/null +++ b/docs/architecture/cv-theme-engine.md @@ -0,0 +1,53 @@ +# CV Theme Engine + +> Phase 4 (2026-07-18). How CV themes work and how to add one. Companion to +> `docs/architecture/cv-builder.md`. Verified against code. + +## The rule + +**A theme is data, not code.** There is exactly one renderer — `ThemedCvRenderer` +(`JobTrackerApi/Services/ThemedCvRenderer.cs`) — with a single render path. Every visual difference +between themes is expressed by the fields of a `CvTheme` record (`Models/CvTheme.cs`). Adding a theme +never touches the renderer. + +This replaces the previous approach (`CvTemplateRenderer`, one hand-written HTML method per template), +which is retained only for the legacy tailored-draft flow and is not used by the builder. + +## `CvTheme` fields (the knobs) + +| Group | Fields | +|---|---| +| Identity | `Id`, `Name`, `Category`, `Description` | +| Layout | `Layout` (`single` \| `sidebar-left` \| `sidebar-right` \| `header-band`), `SidebarWidthMm`, `SidebarSections` | +| Palette | `Accent`, `Ink`, `Muted`, `Line`, `Paper`, `SidebarBg`, `SidebarInk`, `HeadingColor` | +| Typography | `HeadingFont`, `BodyFont`, `NameSizePt`, `HeadingSizePt`, `BodySizePt`, `LineHeight` | +| Spacing | `PageMarginMm`, `SectionGapMm`, `EntryGapMm` | +| Styling | `HeaderStyle` (`plain`\|`band`\|`centered`\|`kicker`), `HeadingStyle` (`caps-rule`\|`underline`\|`plain`\|`bar`), `PhotoShape` (`none`\|`square`\|`rounded`\|`circle`), `DefaultIcons` | + +The renderer computes CSS variables from these plus the variant's runtime overrides (accent, fonts, +density, page size, photo, icons) and picks one of the layout wrappers. `SidebarSections` decides which +section keys move to the sidebar for the two-column layouts. + +## Adding a theme + +1. Append one `CvTheme { … }` to `CvThemeCatalog.Themes` (`Models/CvTheme.cs`). Only override the + fields that differ from the defaults. +2. Nothing else. It appears in `GET /api/cv/themes`, the Customize tab picker, and renders. + +Add a `CvBuilderTests.Every_catalog_theme_renders_valid_html` already loops the whole catalog, so a new +theme is smoke-tested automatically. + +## Shipped themes (8) + +`modern` (header-band), `minimal` (single), `executive` (single, serif), `technical` (sidebar-left, +dense), `ats-classic` (single, no graphics), `nordic` (sidebar-right), `elegant` (single, editorial), +`creative` (sidebar-left, bold). + +## Deliberately not here (yet) + +- **Rich-text inside bullets.** The render model treats bullets as plain strings, so the builder's text + areas are plain text. Adding bold/italic/links means the render model carries inline runs and the + renderer emits them — a self-contained extension, no theme changes. +- **PDF page numbers.** `ShowPageNumbers` is carried through settings but the actual footer is a + Playwright `footerTemplate` concern (the PDF exporter), not CSS paged-media (Chromium's margin boxes + are unreliable). Wire it in `PlaywrightCvPdfExporter` when needed. diff --git a/docs/implementation-roadmap.md b/docs/implementation-roadmap.md index eba4e1c..345d67b 100644 --- a/docs/implementation-roadmap.md +++ b/docs/implementation-roadmap.md @@ -99,6 +99,22 @@ Goal: one professional source of truth that can actually feed outputs. Goal: `Content Tab → Customise Tab → Preview → Export` (guide `:312`). **This is the largest item in the plan.** The existing inert tab makes it look nearly done; it is not started. +> **Foundation SHIPPED 2026-07-18** (commits `a3e18e4` backend, `158dd02` frontend). Data-driven +> theme engine + variant model + 3-tab builder + live preview + public CV, all consuming the master +> profile (never duplicating it). See `docs/architecture/cv-builder.md` and +> `docs/architecture/cv-theme-engine.md`. Status: 4.1 ✅ `CvTheme` model. 4.2 ✅ one +> `ThemedCvRenderer` (the old `CvTemplateRenderer` stays only for the legacy tailored-draft flow). +> 4.3 ✅ 8 themes as data. 4.4 ✅ Content tab reorder/hide/rename + custom sections (up/down +> controls with keyboard support; **native drag-and-drop deferred** — no dnd dependency added yet). +> 4.5 ✅ Customise tab (theme, accent, fonts, density, page size, photo/icons/page-numbers). 4.6 ✅ +> live preview (server render on a 350 ms debounce; **not yet client-side** — see below). 4.7 ✅ PDF +> export wired into the builder. Plus: autosave + version history/restore, AI-assist (suggestion-only), +> public CV at `/cv/{slug}`. **Remaining polish:** 4.8 split `ProfileCvController`; client-side preview +> (4.6 currently server round-trip); native drag-and-drop; rich-text bullets; PDF page-number footer; +> per-item override UI in the Content tab (the API + resolver support it; the editor exposes section- +> level controls). **Known limitation:** external direct loads of `/cv/{slug}` bounce to root (SPA +> deep-link issue, app-wide) — fix in the frontend static-export/routing config. + | # | Task | Priority | Difficulty | Dependencies | Expected value | |---|---|---|---|---|---| | 4.1 | **Design the `CvTheme` model** — layout, columns, header position, font family, base size + per-element deltas, spacing, margins, accent + application targets, icon style, photo settings | **P1** | **M** | 3.4 | **The keystone.** Everything else in Phase 4 depends on themes being *data*. Modelled on FlowCV's proven control set (report §7), trimmed to ~12 controls per the guide's "avoid excessive configuration". |