Files
jobtrackingapp/docs/application-discovery-report.md
cesnimda b176a44627 docs: reorganize tree, restore architecture + research from archive, add Phase 0 reports
Active docs/ was stub scaffolding while the real docs sat in docs/_archive/.
Restore and correct them, and record the Phase 0 work.

- docs/architecture/current.md: verified system map (from archived SYSTEM_OVERVIEW,
  9 corrections against code).
- docs/research/competitors.md: sourced competitor analysis (from archived
  PRODUCT_RESEARCH, feature matrix corrected).
- docs/decisions/ADR-002-job-application-model.md: the Job/JobApplication split.
- docs/application-discovery-report.md, docs/implementation-roadmap.md,
  docs/phase-0-foundation-report.md, docs/career-workspace-branch-assessment.md.
- Remove 10 zero-byte placeholder files that advertised content that never existed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-17 17:04:32 +02:00

54 KiB
Raw Permalink Blame History

Jobjakt — Application Discovery Report

Date: 2026-07-17 Method: read-only audit. Every claim below verified against code at the cited path, not against documentation. Scope: full application (backend, frontend, AI sidecar, infra, docs, competitor research).


1. Executive summary

Jobjakt is a substantially more complete product than its documentation suggests, built around a genuinely differentiated core: a self-hosted, local-AI job tracker with Gmail correspondence import and a human review queue. That combination is ahead of the paid market (Teal, Huntr, Jobscan all lack inbox auto-tracking). The backend is mature — 19 controllers, ~9.3k lines of controller code, ~7.5k lines of services, 36 test classes including dedicated authorization and SSRF-guard suites, real multi-tenancy via EF global query filters, 2FA, trusted devices, session management, and a working CI/CD pipeline to production.

The gap is not capability. It is three structural mismatches between what the product says it is and what the code models:

  1. There is no Job entity. The only entity is JobApplication, and the pipeline's first stage is Applied (JobTrackerApi/Services/JobPipeline.cs:24). The glossary states "A Job may exist before an application is submitted" and the target workflow is Add Job → prepare CV → prepare cover letter → submit → track. The data model cannot represent a job you have not yet applied to. This single fact blocks the top-priority workflow in docs/MASTER_IMPLEMENTATION_GUIDE.md.
  2. The CV Builder does not exist. CareerWorkspacePage renders a "CV Builder" tab that passes a careerView prop to ProfilePage — and that prop is destructured and never read (job-tracker-ui/src/views/ProfilePage.tsx:231). Both tabs render identical content. CV "themes" are five hardcoded C# string-interpolated HTML functions (JobTrackerApi/Services/CvTemplateRenderer.cs:22), which cannot express any of the content/design separation the guide requires.
  3. The documentation is a scaffold, not a description. 139 active doc files; the median is ~480 bytes — a title and one generic sentence. docs/research/flowcv-analysis.md opens with "TODO: Complete documentation." The four ADRs in docs/_archive/decisions/ are 0 bytes. Meanwhile docs/_archive/SYSTEM_OVERVIEW.md (20 KB) and docs/_archive/PRODUCT_RESEARCH.md (13 KB) are excellent, evidence-based, and mostly still accurate. The real documentation was archived and replaced with stubs.

Nothing here needs a rewrite. The build order that follows is: fix the data model so the core workflow is representable, then delete the fake CV Builder and build a real one on a data-driven theme system.

Severity-ranked headline issues

# Issue Impact Evidence
1 No pre-application Job state; pipeline starts at Applied Blocks the guide's #2 priority workflow entirely Services/JobPipeline.cs:24
2 CV Builder tab is inert — careerView prop never read Ships a visible feature that does nothing views/ProfilePage.tsx:231
3 AI sidecar has zero auth, port 8001 published to host Unauthenticated LLM access; burns Gemini/Groq API key docker-compose.yml, tools/summarizer/app.py
4 DataProtection keys remain in git history Untracked in 519c32e but recoverable; rotation still open git log --all -- JobTrackerApi/keys
5 Active docs are stubs; real docs archived Every future AI session starts from fiction wc -c docs/**/*.md
6 Two god controllers (2313 + 2249 lines) Violates the guide's own "avoid massive controllers" rule Controllers/JobApplications*.cs, ProfileCv*.cs

2. Current product understanding

2.1 What it actually is

A self-hosted, multi-user job application tracker with a local-AI career assistant bolted on correctly — the AI is grounded in the user's real parsed CV rather than free-floating, which is the architecturally right answer to the "AI slop" complaint that dominates competitor reviews.

Production deployment: https://jobs.cesnimda.uk via Gitea Actions → SSH → Docker Compose.

2.2 Target users

Matches the documented personas, with one caveat: the app currently serves exactly one persona — the owner. Auth:AllowRegistration defaults to false (Controllers/AuthController.cs:135), so registration returns HTTP 403 unless explicitly enabled. Multi-tenancy is implemented (every entity carries OwnerUserId, enforced by global query filters); it is simply not open.

The job-import plugins are Norwegian-market specific: Finn, NAV, Jobbnorge, LinkedIn (Services/JobImport/Plugins/). This is a real, unstated product decision — Jobjakt is currently a Norway-focused tracker.

2.3 Current feature reality vs the stated hierarchy

The guide's hierarchy is CORE: Job Tracking → Applications → Workflow → Follow-ups → Communication, then SUPPORTING: Career Profile → Master CV → CV Builder → Cover Letters → Portfolio → Interview Prep, then ENHANCEMENT: Job Discovery.

Measured against the code:

Layer State
Job Tracking Strong — but starts at "Applied", so the pre-application half is missing
Applications Strong — 38 endpoints, rich lifecycle
Application Workflow 🟡 The add-job wizard exists; the pipeline it feeds cannot hold a not-yet-applied job
Follow-ups Strong — reminders, rules engine, auto-ghosting, AI drafts, send
Communication Tracking Best-in-class — Gmail OAuth + review queue, IMAP, MS Graph
Career Profile 🟡 Exists as a JSON blob, not a queryable model
Master CV Upload → OCR → parse → structured profile works
CV Builder Does not exist (inert tab)
Cover Letters AI generation + drafts exist
Portfolio Attachment slot only; no portfolio model
Interview Preparation Endpoint exists (/interview-prep)
Job Discovery Zero code

The inversion risk the guide warns about has not happened. Job tracking is decisively the strongest part of the app. The CV system is the weakest. If anything, the docs over-invest in CV builder prose relative to what the product needs.

2.4 Current user journey (as actually coded)

Owner logs in (registration off)
  ↓
Dashboard — stats + dismissible 2-item checklist
  ↓
Add Job wizard: URL → preview/extract → verify → CV → cover letter → portfolio → files
  ↓
Job saved directly as "Applied"        ← the workflow's premise is broken here
  ↓
Track: status / follow-ups / correspondence / AI assists

3. User journey analysis

3.1 New user journey

Current:

  • Landingviews/LandingPage.tsx (265 lines), redirects authenticated users.
  • Registration — endpoint exists (POST /api/auth/register) but is 403 by default, and there is no /register route in the router. Sign-up is folded into LoginPage.tsx.
  • Login — solid: email/password, Google, Microsoft, 2FA challenge, trusted devices.
  • First setupOnboardingChecklist.tsx: two items (upload CV, add a job), dismissible to localStorage, auto-hides when both are done.

Target: Signup → Create profile → Add/import CV → Add first job → Prepare application

Missing steps:

  • Signup is off and has no dedicated screen.
  • No "create profile" step — the profile is a side-effect of CV upload.
  • No email-verification gate in the flow (POST /auth/verify-email exists; nothing forces it).
  • No "connect email" step, despite Gmail import being the strongest differentiator — it is buried in /settings/connected-accounts.
  • No "prepare application" step — the wizard ends at save.

Confusing areas:

  • The onboarding checklist infers "has CV" from profileCvText being non-empty (OnboardingChecklist.tsx:31) — a text field, not the structured profile. A user with a parsed profile but empty text would be told to upload a CV again.
  • Dismissal is permanent per-user in localStorage; there is no way back.
  • /profile and /career both render ProfilePage, differing only by a careerOnly boolean. Two nav destinations, one component, overlapping content — this is precisely the "everything should have one obvious place" rule being broken.

UX problems: onboarding is a checkbox list, not a guided flow. The guide asks users to always know "what happens next"; the checklist answers that twice and then never again.

3.2 Job workflow

Current — genuinely good, and better than the docs claim:

  • Adding jobsAddJobModal.tsx (618 lines) implements a 6-step wizard: ["Add job", "Review details", "CV", "Cover letter", "Portfolio", "Additional files"] with skippable optional steps (AddJobModal.tsx:368). Landed in commit aa3567d.
  • ImportingPOST /api/jobimport/previewUniversalJobParser + per-site plugins + JSON-LD fallback, language detection, skill tagging.
  • EditingEditJobDialog.tsx, JobDetailsDialog.tsx (1400 lines).
  • Stages — free-text Status canonicalized by JobPipeline.Normalize, preserving unknown values (a good, non-destructive design).
  • Follow-upsFollowUpReminderHostedService, RulesEngine (auto-ghosting), AI drafts, send.
  • CommunicationCorrespondence per application; Gmail/IMAP/Graph import with review queue.

Target: Add Job → Import from URL → Extract → Verify → Choose/Create company → Add CV → Generate CV → Cover letter → Portfolio/files → Application tracking

Gap analysis — the wizard already matches the target almost exactly. Two things are missing, and one is fatal:

  1. Fatal: the destination stage does not exist. The wizard walks the user through preparing an application, then must save it as Applied. There is no Saved / Interested / Preparing stage (JobPipeline.cs:24-31 — stages are Applied, Waiting, Interview, Offer, Rejected, Ghosted). The wizard's whole premise — prepare before applying — has nowhere to land. Users must either lie about having applied or not use the wizard as intended.
  2. "Generate CV if needed" is absent from the wizard. Generation exists (POST /jobapplications/{id}/generate-tailored-cv-draft) but only after the job exists, from the job detail view. The wizard's CV step is an upload field only.
  3. Import is preview-only. JobImportController exposes exactly one endpoint (/preview). Persistence goes through the generic create path. This is fine, but it means no import history, no re-import, no dedup at import time (dedup exists separately at /duplicate-check).

Company selection is handled — CompaniesController + reusable Company entity, matching the documented Company → Job → Application intent at the company level.

3.3 Career workflow

Current:

  • ProfileProfilePage.tsx (1368 lines): personal details, avatar with cropping, CV upload, structured profile editing, 2FA/session cards.
  • CV — upload → CvUploadArtifactCvExtractionRun (OCR/parse, versioned with parser/normalizer/prompt versions — a genuinely good audit design) → StructuredCvProfile JSON → applied to ApplicationUser.ProfileCvStructureJson.
  • AI — rewrite-section, rewrite-preview, improve, parse, reprocess, rebuild, export-pdf.

Target: Profile → Master Career Profile → Master CV → CV Builder → Tailored CVs → Cover Letters

Gaps:

  • Master Career Profile is a JSON blob on the Identity user (Models/ApplicationUser.cs:9). It works and is defensible for a single-writer app, but: skills are not queryable, there is no per-section version history at the DB level, and two concurrent editors clobber each other wholesale. The StructuredCvProfile shape covers Summary, Jobs, Education, Certifications, Projects, Skills, Languages, Interests, OtherSections — the guide additionally names Achievements/Awards, Publications, Organisations, References, which have no home beyond generic OtherSections.
  • Master CV vs Career Profile are conflated. The glossary is explicit that "The career profile is NOT a CV" and Master CV is a generated representation. In code there is one blob and no generated-master concept.
  • CV Builder — missing entirely (§10).
  • Tailored CVs — this part works well. TailoredCvDraft is a separate entity per application, and the master is never auto-overwritten. The single most important documented invariant is correctly implemented.
  • CV Variants (Software Engineer CV / Management CV) — not modelled. Only per-application tailored drafts exist.

4. Architecture overview

4.1 Backend

  • Framework: ASP.NET Core net9.0, EF Core 9, ASP.NET Identity.
  • Projects — the layout is unusual and must be understood before touching it:
Project Role
JobTrackerApi/ Web host onlyProgram.cs, appsettings, migrations, Dockerfile. Its csproj excludes its own Controllers/** and Services/**.
JobTrackerBackend/ "Transitional shared-backend" library that link-compiles ../Data, ../Models, ../JobTrackerApi/Controllers, ../JobTrackerApi/Services via <Compile Include>. Exists so tests can reference controllers without the web host.
JobTrackerApi.Tests/ xUnit, 36 test classes.
Models/, Data/ (repo root) The real EF models and JobTrackerContext.

The files live in one place and compile from another. Any tooling that assumes csproj-adjacent source will mislead you.

  • Controllers (19): god-object problem is real and violates the guide's own rule —

    Controller Lines
    JobApplicationsController 2313
    ProfileCvController 2249
    GmailController 1023
    AuthController 879

    JobApplicationsController alone exposes 38 endpoints.

  • Services (~40): notable — StartupInitializationExtensions (1356 lines, a second god object), SummarizerService (671), GmailOAuthService (655), JobApplicationHelpers (622), MicrosoftGraphOAuthService (507), CvTemplateRenderer (448).

  • Authentication: "smart" policy scheme routes by token issuer — Google ID tokens → google handler, else → local JWT (symmetric key, issuer/audience validated). Cookie sessions (jobtracker_auth, HttpOnly, SameSite=Lax) with CSRF double-submit enforcement on mutating requests. Auth:Require=true in prod sets authorize-all fallback. Local tokens must carry a subject claim — hardened after finding M013-2.

  • Database: SQLite default; MariaDB/MySQL switchable via Database:Provider. 10 migrations.

  • Background jobs: RulesHostedService, FollowUpReminderHostedService, DailyExportHostedService, JobEnrichmentHostedService, DatabaseBackupHostedService, CvProcessingQueue, SummarizerProbeHostedService.

  • External integrations: Google OAuth/Gmail API, Microsoft Graph, IMAP, SMTP, LibreTranslate (optional), job sites (Finn/NAV/LinkedIn/Jobbnorge), Playwright (PDF).

  • AI integration: not in-process. The backend HTTP-calls a FastAPI sidecar (tools/summarizer/app.py, 866 lines) exposing /health, /cv/normalize, /cv/classify-block, /cv/rewrite, /summarize, /extract-text. The sidecar picks a provider from a process-wide env var AI_PROVIDER ∈ {ollama, gemini, groq}.

    The documented AI architecture does not exist. docs/00-ai-context.md describes a provider interface fanning out to OpenAI / Gemini / Claude / Ollama, admin-controlled, with users never locked to one model. Reality: one env var, one provider per deployment, no OpenAI, no Claude, no admin control, no per-user selection.

4.2 Frontend

  • Framework: Next.js 16 + React 19 + TypeScript 5.9 + MUI 7.
  • Routing — three stacks coexist. Next.js App Router (app/layout.tsx, app/page.tsx) is a shell that mounts a client-side react-router-dom v6 app. App.tsx uses both createBrowserRouter (public routes) and a nested <Routes> inside a catch-all Shell (authenticated routes). Meanwhile react-scripts 5.0.1 is still a dependency, used solely as the test runner ("test": "react-scripts test"). This is a CSR lift-and-shift: none of Next's SSR/routing/data value is realized, and the app pays for three toolchains. A known dev-only 404 on deep links follows directly from this.
  • Components (~34): largest are JobDetailsDialog (1400), JobTable (786), Correspondence (732), DashboardView (666), AddJobModal (618).
  • State management: none. No Redux/Zustand/React Query. Local useState + axios per component, with a hand-rolled refreshToken counter threaded through props. Two workspace-cache hooks exist (components/job-workspace/useWorkspaceTabCache.ts, useJobWorkspaceBaseData.ts). For an app of this size, the absence of a server-cache layer is the main driver of the large components.
  • Styling: MUI sx + a custom theme.ts (439 lines), light/dark. Consistent, but design tokens live in component-level sx props (e.g. the identical boxShadow: "0px 8px 24px -12px rgba(15,23,42,0.12)" is repeated inline across pages).
  • Major pages: Dashboard, Jobs, Kanban, Reminders, Companies, Correspondence (+review), Profile, Career, Settings (+connected accounts), Admin (audit/users/system), Trash.
  • i18n: custom provider, EN + NB, 2130-line translation file.
  • Design system: exists as a theme, not as a component library. There is no Storybook, no primitives layer.

4.3 Database

Entities (16 DbSets + Identity): Company, JobApplication, Correspondence, GmailConnection, GmailReviewDecision, MicrosoftGraphConnection, ImapConnection, Attachment, RuleSettings, UserRuleSettings, SystemEmailSettings, JobEvent, CvUploadArtifact, CvExtractionRun, TailoredCvDraft, TwoFactorRecoveryCode, TrustedDevice, UserSession.

Relationships: Company 1—* JobApplication 1—* {Correspondence, Attachment, JobEvent}, JobApplication 1—1 TailoredCvDraft, ApplicationUser 1—* {CvUploadArtifact, CvExtractionRun}.

Data duplication / debt:

  • JobApplication conflates Job and Application — 43 members mixing opportunity data (JobTitle, Description, JobUrl, Salary*, Deadline, Tags) with application data (Status, DateApplied, ResponseReceived, FollowUpAt). Applying twice to the same reposted role duplicates the whole description. This is issue #1.
  • Has* denormalized flags (HasResume, HasCoverLetter, HasPortfolio, HasOtherAttachment) duplicate what Attachments already says; there is a dedicated AttachmentFlagsRecomputeTests suite to keep them honest — a test that exists only because of the duplication.
  • Tags is a JSON array in a string column — not queryable; /tags and /tag-trends must scan.
  • CV text stored three times: CvExtractionRun.RawExtractedText, .NormalizedText, .StructuredProfileJson, plus ApplicationUser.ProfileCvText and .ProfileCvStructureJson. Deliberate (audit trail), but unbounded — no retention policy on extraction runs.
  • AvatarImageDataUrl — base64 image in a DB column on the hot /auth/me path.
  • Status is free-text — canonicalization is application-layer only; the DB accepts anything.

Migrations: only 10, with a SyncModelSnapshot migration — indicating the model drifted from migrations and was reconciled in bulk rather than incrementally.

4.4 Infrastructure

  • Docker: 4 services — backend, frontend (nginx), ai-service, ollama (opt-in via bundled-ollama profile, GPU). External jobtracker_shared network.
  • Deployment: Gitea Actions → SSH → deploy/deploy.sh → Docker Compose on the prod host.
  • CI/CD: .gitea/workflows/ci-deploy.yml. Builds backend, runs all backend tests, runs the whole frontend suite, builds frontend. Notably the file carries a comment forbidding re-introducing the old test whitelist ("the previous whitelist silently skipped new suites and let two regressions reach main") — that debt is paid. The workflow is heavily defended with retries against a flaky self-hosted runner (dotnet install, npm ci SIGSEGV, build OOM). There is no GitHub Actions workflow — do not look in .github/.
  • Environment variables: extensive, .env correctly gitignored, .env.example present.
  • External services: Google, Microsoft, Ollama/Gemini/Groq, SMTP, LibreTranslate.

5. Feature inventory

5.1 Working

Feature Purpose Location Problems Recommendation
Job/application CRUD Core tracking Controllers/JobApplicationsController.cs 2313 lines, 38 endpoints Split by concern (CRUD / AI / analytics / drafts)
Status pipeline Lifecycle Services/JobPipeline.cs Starts at Applied Add pre-application stages — issue #1
Kanban board Visual pipeline components/KanbanBoard.tsx (285) Keep
Job table + saved views List/filter components/JobTable.tsx (786) Large Keep; extract later
Companies Reusable orgs Controllers/CompaniesController.cs No people/contacts entity Add contacts (competitors have it)
Job import from URL Reduce entry Services/JobImport/ + 4 plugins Preview-only; NO-market only Keep; add browser extension later
Add-job wizard Guided flow components/AddJobModal.tsx Lands in Applied Fix once #1 lands
Gmail import + review queue Auto-tracking Controllers/GmailController.cs, views/GmailReviewPage.tsx 1023-line controller Protect — best differentiator
IMAP / MS Graph mail Auto-tracking Services/ImapService.cs, MicrosoftGraphOAuthService.cs Keep
Correspondence log Comms tracking Models/Correspondence.cs, components/Correspondence.tsx Keep
Follow-up reminders Never forget Services/FollowUpReminderHostedService.cs Keep
Rules engine (auto-ghost) Automation Services/RulesEngine.cs Keep
Attachments Files per app Controllers/AttachmentsController.cs Denormalized Has* flags Keep; drop flags later
CV upload → OCR → parse Master CV Controllers/ProfileCvController.cs, sidecar 2249-line controller Split
Structured CV profile Source of truth Models/StructuredCvProfile.cs JSON blob; missing awards/publications/refs Relational later
Extraction run audit Traceability Models/CvExtraction.cs No retention policy Add TTL
Tailored CV drafts Per-job CV Models/TailoredCvDraft.cs Correct — master never overwritten
CV PDF export Deliverable Services/PlaywrightCvPdfExporter.cs Keep
Match score / candidate fit Job↔CV fit Services/JobCvMatchService.cs Keep — top paid feature elsewhere
Interview prep Prep /jobapplications/{id}/interview-prep Not surfaced as a hub Give it a screen
Focus plan / readiness Guidance JobApplicationsController Keep
AI cover letters / follow-up drafts Reduce effort JobApplicationsController Keep
Application package generation Bundle /generate-application-package Keep
Analytics Insight Services/AnalyticsService.cs, StageAnalytics.cs Basic Expand (funnel, time-in-stage)
Auth: password/Google/MS Access Controllers/AuthController.cs 879 lines; registration off Split; open signup for SaaS
2FA + recovery codes Security Controllers/TwoFactorController.cs Keep
Trusted devices UX Services/TrustedDeviceService.cs Keep
Session management Security Controllers/SessionsController.cs Keep
Rate limiting Abuse Program.cs:373 (login 10, email 5, 2FA 5) No captcha Add captcha before public signup
Multi-tenancy Isolation Data/JobTrackerContext.cs global filters Correct — deny-on-null
Admin (users/audit/system) Ops Controllers/Admin*.cs Keep
DB backup + daily export Durability Services/DatabaseBackup*.cs Keep
i18n EN/NB Reach src/i18n/ Keep
Dark mode UX src/theme.ts Keep

5.2 Partial

Feature State Location Problem Recommendation
Career Workspace Shell only views/CareerWorkspacePage.tsx (36 lines) Tab wrapper around ProfilePage Make it a real workspace
CV Builder Inert views/ProfilePage.tsx:231 careerView never read — both tabs identical Delete tab or build it
CV themes 5 hardcoded Services/CvTemplateRenderer.cs:22 C# string HTML: ats-minimal, harvard, auckland, edinburgh, monarch, fjord. No user customisation Data-driven theme model
Onboarding 2-item checklist components/OnboardingChecklist.tsx Not a flow; infers CV from text field Real guided flow
Registration Endpoint only AuthController.cs:135 403 by default; no /register route Needed for SaaS
Portfolio Attachment slot AddJobModal step 4 No model, no page Model it
Analytics Basic stats AnalyticsService.cs No funnel/response-rate/time-in-stage Expand
Profile vs Career Duplicated /profile and /career → same component Two doors, one room Separate concerns

5.3 Planned (documented, zero code)

Feature Documented in Reality
Job Discovery / search docs/jobs/job-search.md, guide Phase 4 No code. Grep for JobSearch|discovery in backend → nothing
Public CV (/cv/{guid}) docs/00-ai-context.md, docs/career/public-profile.md No code. No route, no IsPublic, no slug
SaaS: billing/subscriptions/quotas docs/product/business-model.md (52 bytes), guide No code. No Stripe, no plan, no quota, no usage tracking
Multi-provider AI w/ admin control docs/00-ai-context.md Single env var; no OpenAI/Claude; no admin UI
CAPTCHA docs/00-ai-context.md No code
Passkeys docs/00-ai-context.md No code
CV variants (SWE CV / Mgmt CV) docs/01-glossary.md No code — only per-app drafts
Interview prep hub Guide Endpoint exists; no screen
Calendar / ICS docs/_archive/PRODUCT_RESEARCH.md No code
Awards / Publications / Organisations / References docs/00-ai-context.md Not in StructuredCvProfile; only generic OtherSections

6. UI/UX review

Note on mockups: the discovery brief says "compare against existing mockups". No mockups exist in this repo. Prior sessions recorded a mockup set at F:\Pictures\website\jobtracker\newnot verified in this audit (out of scope, drive not read). Any redesign work must confirm that source first.

Page-by-page

Page Assessment
Dashboard (DashboardView.tsx, 666) Matches vision reasonably. Stats + reminders + onboarding. Problem: 666 lines, no data layer.
Navigation (layout/AppShell.tsx, 499) Mostly clean. Problem: /profile and /career are two entries rendering one component — violates "one obvious place".
Authentication (LoginPage.tsx, 260) Solid and complete. Problem: no dedicated signup screen; sign-up hidden inside login.
Settings (SettingsView.tsx) Recently improved — connected accounts split out (d7d7e70). Good.
Jobs (JobTable.tsx 786 / KanbanBoard.tsx 285) Strong. Saved views, columns, tags. Matches vision.
Profile (ProfilePage.tsx, 1368) Needs redesign. 1368 lines doing profile + CV + AI + 2FA + sessions, forked by careerOnly. This is the worst file in the frontend.
CV Builder (CareerWorkspacePage.tsx, 36) Missing. Tab exists, gated on hasMasterCv, and does nothing when enabled.
Upload flows (ProfilePage, AddJobModal) Work. Async CV processing via CvProcessingQueue is right.

Requiring redesign

  1. ProfilePage — split profile / career / security.
  2. CareerWorkspacePage — currently a facade.
  3. Onboarding — checklist → guided flow.
  4. JobDetailsDialog (1400) — a dialog carrying an entire workspace.

Matching current vision

Dashboard, Jobs table, Kanban, Settings, Correspondence inbox + Gmail review, Login.

Missing screens

Signup, CV Builder (content + customise + preview + export), Public CV, Job Discovery, Interview Prep hub, Portfolio, Billing/plan.


7. FlowCV comparison

Analysed the local downloads at D:\FlowCV (Resume _ FlowCV.html, _customize.html, _overview.html). Text extracted from markup; no code, markup, assets, or branding copied.

FlowCV's structure

  • Top-level tabs: Overview | Content | Customize | AI Tools, with Download always present.

  • Overview: "My Resumes" list. "Your first resume is free forever. Need more than one resume? Upgrade your plan" — the free/paid line is drawn at resume count.

  • Content: section-based editor, drag-and-drop entries (with documented keyboard drag affordances — accessible), "Add Entry" / "Add Content".

  • Customize: a deep, fully data-driven control panel:

    Group Controls
    Document Settings Language, Date Format, Page Format (A4)
    Templates "Update your entire resume design with one click", browse
    Layout Columns (One/Two/Mix), Header Position (Top/Left/Right), section order, page breaks, column width (44%/56%)
    Font Size Base (10pt) + deltas: Full Name +14pt, Section Headings +2pt, Entry Header +0pt
    Spacing Line height 1.3, space between elements, L/R margin 18mm, T/B margin 16mm
    Entries Structure (Full Width/Columns), Date & Location position (Right/Below Title)
    Headings Capitalization (Capitalize/Uppercase), icons (None/Outline/Filled)
    Font Body font (e.g. Zilla Slab), Name font (same/different)
    Colors Area (Full Page/Column/Border), mode (Single/Multi/Image), accent applied per-element (Name, Job title, Headings, Header icons, Dots/bars, Dates, Entry subtitle, Link icons)
    Header Text alignment, details arrangement (Icon/Bullet/Bar), 7 icon styles
    Photo / Links / Footer / Sections
  • Preview: live, continuous, beside the editor.

  • Export: persistent Download button — never a mode you enter.

  • Notably: FlowCV also ships a Job Tracker. It is not a pure CV tool; it is a direct competitor converging on Jobjakt's territory from the CV side.

Jobjakt vs FlowCV

Dimension FlowCV Jobjakt
Content/design separation Total — content is data, design is config None — design is C# code
Templates Browse + one-click swap 6 IDs in a switch (CvTemplateRenderer.cs:22)
User customisation ~40 controls Zero
Preview Live, side-by-side Server round-trip → HTML/PDF
Export Always-available button POST /export-pdf via Playwright
Multi-resume Yes (paywalled at 2+) No variants; only per-app drafts
Job tracking Bolted on Core, and far deeper
AI grounded in real career data Generic "AI Tools" Yes — structured profile
Self-hosted / private No Yes

What Jobjakt must adapt (not copy)

  1. The theme system must become data, not code. This is the single highest-leverage lesson. CvTemplateRenderer renders by string-interpolating HTML inside a C# switch — it can never express "base font 10pt, headings +2pt, accent applies to Name and Dates but not Headings". Model themes as a CvTheme document (layout, columns, header position, font family/sizes as base+deltas, spacing, colour targets, icon style) and render one parameterized template from it. Five hardcoded renderers are five things to maintain and zero things a user can adjust.
  2. Overview | Content | Customize | Preview/Export is the right tab spine — and it maps onto Jobjakt's stated Content Tab → Customise Tab → Preview → Export.
  3. Live preview beside the editor, not behind a request.
  4. Drag-and-drop section ordering with keyboard support — FlowCV's accessibility affordances are worth matching.

What Jobjakt must NOT copy

  • The paywall shape. FlowCV gates at resume count — the exact "free tier caps at the point of seriousness" frustration documented in docs/_archive/PRODUCT_RESEARCH.md. Jobjakt's differentiator is privacy + self-hosting; gating CV count would surrender it.
  • CV-first framing. FlowCV is a CV tool that added a tracker. Jobjakt is a tracker; the guide is explicit — "Do not transform Jobjakt into a CV generator."
  • Control sprawl. ~40 customisation controls contradicts "avoid excessive configuration". Ship the ~12 that matter (template, columns, header position, accent, base font size, font family, spacing, margins, photo on/off, section order, date position, heading case).
  • Any code, markup, asset, or branding.

8. Competitor alignment

⚠️ CORRECTION (2026-07-17, during Phase 0)

The claim below — that Novoresume/Reactive Resume/ElegantCV are not analysed in this repo — is WRONG. It was true of main and of docs/_archive/, which is all this audit searched. It is not true of the repository.

A feature/career-workspace branch exists (local and on origin, 10 commits, unmerged, last touched 2026-07-12) carrying:

  • docs/cv-builder-competitor-deep-research.md (327 lines) — deep teardowns of Novoresume, Reactive Resume, FlowCV, Teal, Enhancv, Canva, Resume.io, Kickresume, with a feature matrix and a business-model analysis.
  • docs/cv-builder-product-teardown.md (321 lines)
  • docs/career-workspace-product-strategy.md (328 lines) — vision, positioning, four personas.
  • docs/career-workspace-implementation-roadmap.md (124 lines) — an ADR-grade plan with phases F0F5, F0F2 marked shipped.

Its conclusions independently reach the same findings as §7 and §10 of this report — "the winning editor model is structured-form + live preview, not canvas", client-side preview is a hard requirement, and themes must be declarative data. That is corroboration, not duplication.

The audit's method was too narrow: I searched the working tree and docs/, never git branch -a. See §13 and the Phase 0 report for the full consequences. Roadmap task 4.9 ("research Reactive Resume") is therefore already done — on that branch.

docs/research/ on main is unusable for this step. Every file is a stub; flowcv-analysis.md literally begins "TODO: Complete documentation." The archived docs/_archive/09-research/*.md are 0 bytes. The only competitor research reachable from main is docs/_archive/PRODUCT_RESEARCH.md (13 KB, sourced, dated 2026-07-02) — which covers the tracker market (Teal, Huntr, Simplify, Jobscan, OSS self-hosted), not the CV-builder market the brief asks about.

Therefore: Novoresume, Reactive Resume, and ElegantCV are not analysed in this repo at all. Superseded — see the correction above. They are analysed, on feature/career-workspace.

From FlowCV (verified) + PRODUCT_RESEARCH.md (archived, spot-checked):

What they do better:

  • Data-driven theme/customisation systems (FlowCV, Reactive Resume) — Jobjakt has none.
  • Live side-by-side preview.
  • Browser extension capture (Teal, Huntr, Simplify) — Jobjakt is server-side parse only.
  • Deep analytics: funnel, response rate, time-in-stage (Teal/Huntr premium) — Jobjakt has basic stats.
  • Contact/people CRM (Teal, Huntr) — Jobjakt is company-level only.
  • Interview scheduling + calendar (emerging) — Jobjakt has none.

What Jobjakt does better (protect these):

  • Inbox auto-tracking with a human review queue — Teal , Huntr , Simplify 🟡. This is ahead of the paid market.
  • Local-AI tailored CVs grounded in the user's real structured CV — directly answers the #1 competitor complaint ("AI slop / hallucinated skills").
  • Rules engine + auto-ghosting — richer than most.
  • Self-hosted privacy — the stated moat; every OSS alternative (JobSync, CareerSync, career-ops) is far less complete.
  • AI features free and local where competitors charge $2950/mo.

What should not be copied: count-based paywalls; autofill-at-scale / auto-apply (LazyApply, LoopCV — spray-and-pray contradicts "apply to more suitable jobs"); job-board ambitions (explicit non-goal); generic AI chat (explicit non-goal).


9. Architecture assessment

9.1 Career Workspace — Not supported

Requirement Verdict
Master career profile 🟡 Exists as a JSON blob on ApplicationUser. Works; not queryable; no section-level history; missing awards/publications/organisations/references
CV versions No variant model. TailoredCvDraft is per-application only. Nothing satisfies the glossary's "CV Variant"
CV themes Hardcoded C# renderers. No theme entity, no user customisation. Cannot be extended without a rewrite of CvTemplateRenderer
Public CV Zero code. No route, no IsPublic, no slug, no anonymous read path

Verdict: the current system does not support the Career Workspace as documented, and cannot be incrementally coaxed into it. The theme system in particular is a structural dead end — it is not a matter of adding templates.

9.2 AI — Largely supported

Requirement Verdict
CV generation Parse → structure → tailor → render → PDF, all working
Job matching JobCvMatchService, /match-score, /candidate-fit
Cover letters Generation + drafts
Interview preparation Endpoint exists — but no UI surface

AI is the healthiest supporting area. Grounding in the structured profile is the right architecture, and "AI never has final control" is genuinely upheld (drafts are separate entities; the master is never auto-written).

Caveat: the provider layer does not match its documentation (§4.1) and has no cost control, quota, or per-user selection. docs/ai/cost-control.md is 380 bytes of nothing; with AI_PROVIDER=gemini and an unauthenticated sidecar, there is no ceiling on spend.

9.3 SaaS — Not supported

Requirement Verdict
Multiple users Genuinely solid. OwnerUserId everywhere + deny-on-null global query filters + a dedicated authorization test suite. The hard part is done
Premium features No plan/tier/entitlement concept anywhere
Usage limits No quota, no usage tracking. AI is unmetered — the direct cost risk
Storage No limits. Attachments, CV artifacts, extraction runs, base64 avatars all unbounded

Also blocking: registration is off by default and has no UI; there is no CAPTCHA; there is no billing integration of any kind.

Verdict: multi-tenancy is ready; commercialisation is not started. That is the right order — but nothing should be built here until the core workflow (§9.1, issue #1) is fixed.


10. CV builder assessment

The CV Builder does not exist. This is the clearest finding in the audit.

Evidence:

  1. views/CareerWorkspacePage.tsx:25 renders <Tab value="builder" label="CV Builder" disabled={!hasMasterCv} />.
  2. :31 passes careerView={tab} into <ProfilePage careerOnly ... />.
  3. views/ProfilePage.tsx:231 destructures careerView = "master", and :235 types it — and the identifier appears nowhere else in the codebase. Verified: grep -rn "careerView" job-tracker-ui/src/ returns exactly three lines — the call site and the two declarations.
  4. Therefore selecting "CV Builder" re-renders identical content. The tab is a no-op that looks like a feature.

Both files (CareerWorkspacePage.tsx, ProfilePage.tsx) are uncommitted working-tree changes — this is in-progress work, not shipped deception. But as it stands the tab ships a promise the code does not keep.

What does exist: a template-driven rendererPOST /profile-cv/rewrite-preview, GET /profile-cv/templates, POST /profile-cv/export-pdf, and ProfilePage.tsx:970 ("Template-driven CV builder") / :1197 ("Choose a template and generate a live preview"). That is template selection plus a server round-trip, not a builder.

What is missing against docs/MASTER_IMPLEMENTATION_GUIDE.md:312 (Content Tab → Customise Tab → Preview → Export):

  • No Content tab (section add/remove/reorder, entry-level editing).
  • No Customise tab (nothing is customisable — no accent, no font, no layout, no photo toggle, no section config).
  • No live preview (server request required).
  • Export exists.

Structural blocker. CvTemplateRenderer.Render is:

templateId switch {
  "harvard"   => RenderHarvard(...),
  "auckland"  => RenderSidebar(..., roundedPhoto: false, curvedHeader: false),
  "edinburgh" => RenderSidebar(..., roundedPhoto: true,  curvedHeader: true),
  ...
}

Design decisions are C# method parameters and interpolated CSS strings. The guide requires themes supporting "Accent colours, Typography, Layout, Spacing, Photo options, Icons" — none of which can be expressed here without adding a boolean parameter per option and multiplying the switch. roundedPhoto/curvedHeader are already that pattern starting.

Recommendation: do not extend CvTemplateRenderer. Replace it with a CvTheme value object (layout, columns, header position, font family, base size + per-element deltas, spacing, margins, accent + application targets, icon style, photo settings) rendered by one parameterized template. Ship 35 themes as seeded theme documents, per the guide's "3-5 excellent themes". Delete the careerView prop or wire it — do not ship the inert tab.


11. Technical debt

Ranked by cost-to-carry:

  1. JobApplication conflates Job and Application — blocks the primary workflow. Everything else on this list is cosmetic by comparison.
  2. God controllersJobApplicationsController 2313 / 38 endpoints; ProfileCvController 2249; GmailController 1023; AuthController 879; StartupInitializationExtensions 1356. The guide's own "Coding Philosophy" forbids exactly this.
  3. Dead careerView prop — a shipped no-op tab.
  4. Hardcoded CV templates — dead-end for the entire Phase 4.
  5. Three frontend stacks — Next.js App Router shell + react-router v6 (×2 patterns: createBrowserRouter and nested <Routes>) + react-scripts as test runner. Pays three toolchain costs for one CSR app; causes the known dev-only deep-link 404.
  6. No frontend data layer — no server-cache library; hand-rolled refreshToken counters threaded via props. Root cause of the 6001400-line components.
  7. ProfilePage at 1368 lines forked by a boolean, serving two routes.
  8. Denormalized Has* flags — kept honest only by a dedicated test suite.
  9. Tags as a JSON string column — unqueryable; forces scans in /tags, /tag-trends.
  10. Unbounded CV text/artifact storage — no retention on CvExtractionRun; three copies of every CV.
  11. AvatarImageDataUrl — base64 blob in a DB column on the /auth/me hot path.
  12. Only 10 migrations + a SyncModelSnapshot — model drifted from migrations, reconciled in bulk.
  13. JobTrackerBackend link-compilation — source lives in one place, compiles from another. Self-described as "transitional". Surprising to every new contributor and every tool.
  14. Status free-text at the DB level — canonicalization is application-only.
  15. Documentation debt (see §13) — 139 stub files masquerading as documentation; 4 zero-byte ADRs.

12. Security review

Prior work is real and good: docs/_archive/SECURITY_REPORT.md plus M013 (adversarial), M014 (remediation verification), M015 (authorization replay). Findings were fixed, not just filed — LocalAuthIdentity subject-claim enforcement and deny-on-null query filters both trace to M013-2.

Strong

  • Multi-tenancy: global query filters CurrentUserId != null && OwnerUserId == CurrentUserIddeny-on-null, the correct default. Covered by JobApplicationsAuthorizationTests, OwnershipGuardTests.
  • CSRF: double-submit cookie+header on mutating requests.
  • Session cookie: HttpOnly, SameSite=Lax, Secure-configurable.
  • Auth fails closed when required but unconfigured.
  • Rate limiting: login 10/window, email 5, 2FA challenge 5.
  • 2FA: TOTP, encrypted secrets, recovery codes, trusted devices.
  • SSRF guard on IMAP — with a dedicated test (ImapServiceSsrfGuardTests).
  • OpenAPI dev-only.
  • Secrets: .env gitignored; DP keys and runtime exports untracked in 519c32e.

Open findings

Sev Finding Detail
High AI sidecar unauthenticated + published docker-compose.yml maps "8001:8001" to the host. tools/summarizer/app.py has no auth — no Depends, no API key, no token check (the only Authorization header in the file is outbound to Groq, line 480). Anyone reaching the host can call /cv/rewrite, /summarize, /extract-text — burning the Gemini/Groq key and running arbitrary text through the model. Recommendation: drop the host port mapping (expose: only — the backend reaches it on the compose network), and add a shared-secret header. Do not defer this if AI_PROVIDER=gemini or groq in production.
Medium DataProtection keys in git history Untracked in 519c32e, but git log --all -- JobTrackerApi/keys still returns commits — the keys remain recoverable from history. Flagged for rotation in the 2026-07-03 report; still open. Rotate.
Medium No AI cost ceiling No quota, no usage tracking, no per-user metering. Combined with the finding above, spend is unbounded.
Low No CAPTCHA Verified absent. Rate limiting only. Acceptable while registration is closed; required before opening signup.
Low Unbounded storage Attachments, CV artifacts, extraction runs, base64 avatars — no limits, no retention.
Low SameSite=Lax Appropriate for the OAuth redirect flows in use; noted, not a defect.

Not verified in this audit

Dependency CVEs (no npm audit / dotnet list package --vulnerable run — CI explicitly disables audit via npm_config_audit: 'false'). Recommend a scheduled scan.


13. Documentation assessment

This deserves its own section because it actively misleads.

Active docs (docs/, 139 files): the median file is ~480 bytes — a title and one generic sentence. Examples: docs/auth/login.md = 43 bytes. docs/product/business-model.md = 52 bytes. docs/cv-builder/themes.md = 44 bytes. docs/research/flowcv-analysis.md opens with "TODO: Complete documentation."

Archive (docs/_archive/, 344 KB): contains the genuinely good work — SYSTEM_OVERVIEW.md (20 KB, mermaid architecture, verified against a named commit), PRODUCT_RESEARCH.md (13 KB, sourced competitor analysis with links), SECURITY_REPORT.md, M013M015 assessments, performance/memory-leak analyses.

The real documentation was archived and replaced with stubs. docs/AI_SESSION_START.md instructs every future AI session to read docs/product/, docs/architecture/, docs/security/, docs/technical/ — all stubs. An assistant following its own instructions learns nothing true and confidently builds on fiction. This is the highest-leverage cheap fix in the report.

The four ADRs are 0 bytes: ADR-001-master-career-profile, ADR-002-job-application-model, ADR-003-cv-rendering, ADR-004-ai-provider-system. Their titles name the four most consequential decisions in the system — and every one is undocumented. ADR-002 in particular would have recorded whether the Job/Application conflation was deliberate.

Archive triage

Restore to active (verify first — noted deltas below):

File Status Deltas since 2026-07-02
SYSTEM_OVERVIEW.md Excellent — restore as docs/architecture/current.md Now stale on 5 points: (1) frontend is Next.js 16 + TS 5.9, not CRA + TS 4.9; (2) root Controller/ dead folder is removed; (3) CI runs the whole frontend suite — the whitelist is gone; (4) match-score now exists; (5) salary is now structured (SalaryMin/Max/Currency/Period). Everything else spot-checked accurate.
PRODUCT_RESEARCH.md Excellent — restore as docs/research/competitors.md Feature matrix stale on the same 2 points (match-score, salary). Covers the tracker market only — no Novoresume/Reactive Resume/ElegantCV.
SECURITY_REPORT.md + M013M015 Restore as docs/security/assessments/ Findings verified fixed; DP-key rotation still open.

Keep archived: s06-acceptance-run.md, s07-uat.md, jobbjakt-next-session.md, jobbjakt-cleanup-tracker.md, MERGE_REQUEST.md, gmail-correspondence-phase1.md — point-in-time session artifacts, correctly archived.

Delete: the 0-byte files (decisions/ADR-00{1,2,3,4}, 09-research/*, 10-development/known-issues.md). They imply content that has never existed.

Historical decisions worth preserving (recovered from archive + code, currently recorded nowhere active):

  1. JobTrackerBackend link-compilation exists so tests can reach controllers without the web host — deliberate, self-described "transitional".
  2. Status is free-text and canonicalized in the application layer specifically so custom user values are never destroyed (JobPipeline.cs docstring). Good decision; undocumented outside the source file.
  3. The CI test whitelist was removed after it "silently skipped new suites and let two regressions reach main". Recorded only as a code comment in the workflow.
  4. Ollama is intentionally not bundled by default (bundled-ollama profile) so deploys reuse a shared instance.
  5. AI_PROVIDER=gemini exists specifically "to offload a weak local GPU" in prod (compose comment).

Full task breakdown in docs/implementation-roadmap.md. Ordering rationale:

  1. Phase 1 — Critical fixes. The Job/Application split gates the guide's #2 priority workflow; nothing downstream is worth building first. The unauthenticated AI sidecar is a live security exposure. Documentation restoration is nearly free and prevents every future session from building on fiction.
  2. Phase 2 — UX. Onboarding + the Profile/Career split. Cheap, high-visibility.
  3. Phase 3 — Career Workspace. Requires the profile model to be real.
  4. Phase 4 — CV Builder. Requires Phase 3 + a data-driven theme system. The largest single piece of work in the plan.
  5. Phase 5 — AI. Mostly polish; the hard part is done.
  6. Phase 6 — Job Discovery. Greenfield; explicitly an enhancement.
  7. Phase 7 — SaaS. Last, per the guide's "do not over-engineer before needed".

15. Risks

Risk Likelihood Impact Mitigation
Job/Application split touches everything High High 38 endpoints, the Kanban, the table, the rules engine, and the wizard all assume one entity. Do it behind a migration + additive stage first (add Saved/Preparing to JobPipeline), then split the entity. Do not big-bang it.
CV Builder is scoped as a "tab" but is a subsystem High High Theme model + content editor + live preview + customise panel ≈ the largest item in the plan. The inert tab makes it look nearly done. It is not started.
Unauthenticated AI sidecar exploited / API key drained Medium High Remove host port mapping + add shared secret. Cheap; do it in Phase 1.
DP keys recoverable from git history Medium Medium Rotate. Open since 2026-07-03.
Docs mislead the next session into rebuilding what exists Certain High The add-job wizard, match scoring, and interview-prep already exist but read as "missing" from the stub docs. Restore SYSTEM_OVERVIEW.md first.
Norway-only import plugins vs SaaS ambition Medium Medium Finn/NAV/Jobbnorge are NO-specific. International SaaS needs a plugin strategy or extension-based capture. Unstated product decision.
Frontend rewrite temptation Medium High Three stacks invite a "clean rewrite". The guide forbids it. Consolidate incrementally: retire react-scripts as test runner, pick one router.
Unbounded AI/storage cost under multi-user Medium High No quota exists. Must land before opening registration.
Mockups unverified Medium Medium F:\Pictures\website\jobtracker\new referenced by prior sessions but not read here. Confirm before any redesign.
God controllers slow every change High Medium Split opportunistically while doing Phase 1, not as a standalone refactor.

Appendix — verification method

Known method gap (found 2026-07-17): this audit searched the working tree, main, and docs/_archive/. It never ran git branch -a. That missed feature/career-workspace — 10 unmerged commits containing the CV-builder competitor research, a Career Workspace product strategy, an F0F5 roadmap, and working code (CareerProfileService, CvVariant/CvVersion/TailoredApplication, interview-prep persistence) with tests. Two conclusions in this report (§8 "not analysed in this repo", and the §5.3 "planned, zero code" rows for CV variants) are wrong as a result; both are corrected in place. Any future audit of this repo must enumerate branches before concluding a feature does not exist.

Every other claim traces to a read of the code. Key verifications:

  • careerView dead: grep -rn "careerView" job-tracker-ui/src/ → 3 hits (1 call site, 2 declarations), 0 reads.
  • Pipeline stages: Services/JobPipeline.cs:24-31.
  • No job discovery: grep -rln "JobSearch\|discovery" JobTrackerApi/ --include=*.cs → 0.
  • No SaaS: grep -rlin "stripe\|subscription\|billing\|quota\|PlanTier" JobTrackerApi/ Models/ Data/ → 0.
  • No public CV: grep -rn "IsPublic\|PublicSlug\|public-cv" Controllers/ Models/ → 0.
  • No CAPTCHA: grep -rli "captcha" JobTrackerApi/ --include=*.cs → 0.
  • Sidecar auth: grep -n "Depends\|api_key\|Authorization" tools/summarizer/app.py → only outbound Groq header.
  • DP keys in history: git log --oneline --all -- JobTrackerApi/keys519c32e, 955cae6.
  • Registration default: Controllers/AuthController.cs:135_cfg.GetValue("Auth:AllowRegistration", false).
  • Doc sizes: wc -c across docs/**/*.md.

Not verified (out of read-only scope): mockups on F:\, dependency CVEs, production runtime configuration.