diff --git a/docs/PRODUCT_RESEARCH.md b/docs/PRODUCT_RESEARCH.md
new file mode 100644
index 0000000..b65faa0
--- /dev/null
+++ b/docs/PRODUCT_RESEARCH.md
@@ -0,0 +1,121 @@
+# PRODUCT_RESEARCH.md — Job Application Tracking Market (2026)
+
+> Phase 2 deliverable. Research conducted 2026-07-02 via web sources (linked throughout).
+> Purpose: position Jobbjakt against the market and rank the features worth building next.
+
+---
+
+## 1. Market landscape
+
+The market splits into five clusters:
+
+| Cluster | Representatives | Model |
+|---|---|---|
+| **Tracker-first + AI resume** | [Teal](https://www.tealhq.com/), [Huntr](https://huntr.co/pricing), JibberJobber | Freemium SaaS; premium $29–40/mo |
+| **Autofill / volume** | [Simplify](https://simplify.jobs/job-application-tracker) (autofill), [LazyApply](https://lazyapply.com/) ($99–999/yr), LoopCV (auto-apply) | Extension-centric |
+| **Matching + copilot** | [Jobright](https://jobright.ai/blog/teal-review-2026-walkthrough-alternatives-and-faqs/) | AI job matching, resume tailoring, autofill |
+| **Resume/ATS optimization** | [Jobscan](https://www.jobscan.co/) ($49.95/mo!), Resume Worded, Rezi | Match-score per job description |
+| **Self-hosted / privacy** | [JobSync](https://github.com/Gsync/jobsync), [CareerSync](https://github.com/Tomiwajin/CareerSync), [career-ops](https://career-ops.org/), various [GitHub projects](https://github.com/topics/job-application-tracker) | OSS, local-first, often Ollama-based |
+| **Email auto-tracking** | [Trackr](https://www.trackrjobs.com/), [G-Track](https://jobtrack-ai.com/gmail-job-tracker), Gmail [Chrome extensions](https://chromewebstore.google.com/detail/gmail-job-application-tra/lkpjngmdfncejiomkofogfdoppgifmkh) | Inbox scanning → status updates |
+
+### Competitor snapshots
+
+**Teal** — market leader for tracker+resume. Free: unlimited tracking, Chrome extension (50+ job boards), kanban (Saved/Applied/Interview/Offer/Rejected), 10 ATS templates, contact manager, ATS score (15 checks). Premium ($9/wk, $29/mo, [$79/qtr](https://www.tealhq.com/pricing)): keyword match scoring, AI bullets/cover letters, analytics. Cons reported: [billing-after-cancellation complaints, generic/hallucinating AI content, ATS failures on two-column templates](https://resumehog.com/blog/posts/teal-hq-review-april-2026-is-the-job-tracker-worth-your-time.html), [high-maintenance workflow, overwhelming UI, poor support](https://resumejudge.com/blog/tealhq-review/), no automation.
+
+**Huntr** — best visual kanban + CRM layer. Free: 100 tracked jobs cap, unlimited base resumes, basic scoring. [Pro $40/mo](https://huntr.co/pricing): AI tailored resumes, unlimited cover letters, advanced matching/insights. 4.9★ extension (clip from any site + autofill). Cons: [must rebuild resume inside their builder, plain templates, free plan stops being useful fast](https://resumejudge.com/blog/huntr-review/), online-only.
+
+**Simplify** — free autofill extension for 100+ ATS portals (Workday, Greenhouse, iCIMS), real-time keyword flagging, pipeline tracking. Execution-focused, light on CRM depth.
+
+**Jobscan** — per-job resume match score (1–100, 30+ checks, "aim ≥75%"), cover-letter optimization report. Expensive ($49.95/mo). This single feature is the most-cited reason people pay for job-search tools.
+
+**Email auto-trackers** (Trackr, G-Track, extensions) — scan Gmail, AI-classify (Applied/Next step/Rejected/Offer), auto-update statuses, apply labels. This is rapidly becoming table stakes; users love "zero manual data entry".
+
+**Self-hosted OSS** (JobSync, CareerSync, career-ops) — privacy pitch ("no cloud, no telemetry, no account"), Ollama/local-LLM parsing, but all are far less complete than Jobbjakt: mostly CRUD + basic AI, no CV pipeline, no correspondence CRM, no rules engine.
+
+### Standard vs premium features across the market
+
+- **Table stakes (free everywhere):** kanban board, status stages, notes, basic contact tracking, browser clipper, export.
+- **Premium (what people pay for):** per-job resume↔JD **match scoring with keyword gaps**, AI tailored resumes/cover letters, analytics (response rate, funnel conversion, time-in-stage), email/interview follow-up automation, autofill at scale.
+- **Emerging differentiators:** inbox auto-tracking, interview prep hubs (question banks, scheduling, calendar sync — cf. [interview scheduling tools](https://www.selectsoftwarereviews.com/buyer-guide/interview-scheduling-software)), job-match scoring against a profile, salary/offer comparison.
+
+### Recurring user frustrations (opportunities)
+
+1. **Privacy/data anxiety** — sensitive career data on VC-funded SaaS; [breach/misuse concerns](https://www.saashub.com/compare-job-tracker-by-teal-vs-huntr). Jobbjakt's core moat.
+2. **Paywall fatigue** — free tiers cap exactly at the point of seriousness (Huntr's 100 jobs, Teal's AI credits, Jobscan's 5 scans/mo).
+3. **AI slop** — hallucinated skills, misspelled names, generic bullets; users want AI grounded in *their* real CV (Jobbjakt's structured-CV grounding is the right architecture).
+4. **Manual data entry** — retyping jobs and statuses; solved by clippers + inbox scanning.
+5. **Vendor lock-in** — resumes trapped in proprietary builders (Huntr), hard exports.
+6. **Tool sprawl** — tracker + Jobscan + resume builder + calendar = 4 subscriptions; users want one hub.
+
+---
+
+## 2. Feature matrix — Jobbjakt vs market
+
+✅ has it · 🟡 partial · ❌ missing
+
+| Feature | Teal | Huntr | Simplify | OSS self-hosted | **Jobbjakt today** |
+|---|---|---|---|---|---|
+| Kanban pipeline | ✅ | ✅ | ✅ | 🟡 | 🟡 board view exists; status is free-text, no drag-drop canonical pipeline |
+| Job capture from URL | ✅ ext | ✅ ext | ✅ ext | 🟡 | 🟡 server-side parse (Finn/NAV/LinkedIn/Jobbnorge + JSON-LD); no extension/bookmarklet |
+| Inbox auto-tracking | ❌ | ❌ | 🟡 | 🟡 | ✅ **Gmail OAuth import + human review queue** (ahead of paid SaaS) |
+| Contacts/recruiter CRM | ✅ | ✅ | ❌ | ❌ | 🟡 company-level only, no people entities |
+| Resume/CV builder | ✅ | ✅ | 🟡 | ❌ | ✅ structured CV parse + templates + PDF export |
+| Per-job tailored resume (AI) | 💰 | 💰 | 💰 | ❌ | ✅ **local-AI tailored drafts** (privacy-unique) |
+| Resume↔JD match score + keyword gaps | 💰 | 💰 | 🟡 | ❌ | ❌ (handoff doc lists "missing-keyword analysis" as planned) |
+| AI cover letters / messages | 💰 | 💰 | 💰 | ❌ | ✅ free, local |
+| Follow-up reminders | ✅ | ✅ | 🟡 | ❌ | ✅ + rules engine (auto-ghost) — richer than most |
+| Analytics dashboard (funnel, response rate, time-in-stage) | 💰 | 💰 | 🟡 | 🟡 | 🟡 basic stats endpoint only |
+| Interview management (schedule, prep notes, calendar) | 🟡 | 🟡 | ❌ | ❌ | ❌ (only generic follow-up dates) |
+| Calendar integration (ICS/Google) | 🟡 | 🟡 | ❌ | ❌ | ❌ |
+| Salary/offer tracking & comparison | 🟡 | 🟡 | ❌ | ❌ | 🟡 salary text field only |
+| Autofill applications | ❌ | ✅ | ✅ | ❌ | ❌ (out of scope — needs extension) |
+| Multi-language (EN/NB) + translation | ❌ | ❌ | ❌ | ❌ | ✅ unique for Nordic market |
+| Self-hosted / data ownership | ❌ | ❌ | ❌ | ✅ | ✅ |
+| Mobile experience | ✅ apps | ✅ | ✅ | ❌ | 🟡 responsive-ish desktop web; no PWA |
+| Export/portability | 🟡 | 🟡 | 🟡 | ✅ | ✅ JSON/CSV + daily export |
+
+**Position:** Jobbjakt is already **ahead of every OSS competitor** and matches or beats paid SaaS on AI drafting, Gmail import, and data ownership. Its gaps versus paid SaaS are: match scoring, canonical pipeline/kanban UX, interview & calendar layer, analytics depth, capture friction (no extension), and contact-level CRM.
+
+---
+
+## 3. Market gap — what would make Jobbjakt significantly better than existing solutions
+
+> **"The private, self-hosted career hub: everything Teal+Huntr+Jobscan charge $70–90/mo for, powered by your own local AI, with your data never leaving your server."**
+
+No product today combines: serious tracker UX + inbox auto-tracking + local-LLM tailoring + match scoring + interview hub, self-hosted. Jobbjakt is uniquely ~60% of the way there.
+
+---
+
+## 4. Ranked feature ideas (value × effort)
+
+Effort: S (<1 day) · M (1–3 days) · L (1–2 wk) · XL (>2 wk). Grounded in the Phase 1 codebase map.
+
+| # | Feature | User impact | Effort | Notes |
+|---|---|---|---|---|
+| 1 | **CV↔job match score + keyword gap analysis** (per job: score, missing keywords, section coverage; reuse structured CV JSON + existing Ollama path) | ★★★★★ — the #1 paid feature in the market, free & local here | M–L | Backend has all inputs already; add endpoint + UI panel in job workspace |
+| 2 | **Canonical pipeline + drag-drop kanban** (status enum/ordering, custom stages per user, drive board/badges from it) | ★★★★★ — core daily UX; free-text status blocks analytics too | M–L | Already on README wish list; needs migration for status normalization |
+| 3 | **Analytics dashboard v2** (funnel conversion, response rate, time-in-stage, weekly activity, source effectiveness) | ★★★★ — retention feature; needs #2 for clean stages | M | Data all exists in `JobEvent` history |
+| 4 | **Interview hub** (interview entity: rounds, type, scheduled time, prep notes, outcome; ICS feed/export + reminders) | ★★★★ — biggest functional gap vs SaaS | L | New entity + timeline integration; ICS is cheap, Google Calendar sync later |
+| 5 | **Bookmarklet / minimal browser capture** (one-click "save to Jobbjakt" using existing `jobimport/preview`) | ★★★★ — kills the biggest friction (manual entry); full extension can wait | S–M | Server parsing already exists; a bookmarklet or share-target PWA is days not weeks |
+| 6 | **Contacts (people) CRM** (recruiter/hiring-manager entities linked to companies/jobs/correspondence) | ★★★ | M | Natural extension of company recruiter fields |
+| 7 | **PWA pass** (installable, mobile nav polish, share-target for job URLs) | ★★★ — mobile is where users check status | M | CRA supports PWA manifest; pairs with #5 |
+| 8 | **Salary/offer tracker** (structured salary min/max/currency, offer comparison view) | ★★ | S–M | Currently a free-text field |
+| 9 | **Smarter inbox** (extend existing Gmail review with AI status suggestions: "this looks like a rejection → move to Rejected?") | ★★★★ — compounds an existing unique strength | M | Classification via existing Ollama service |
+| 10 | **Web push / digest notifications** (beyond SMTP) | ★★ | M | Needs service worker (pairs with #7) |
+
+Deliberately **not** recommended: auto-apply bots (ToS/ethics/quality problems, LazyApply-style tools are poorly reviewed), building a full Chrome-store extension now (high maintenance; bookmarklet first), multi-provider cloud AI (undermines the privacy moat — keep local-first with optional cloud later).
+
+## 5. Recommended implementation order (input to Phase 3 roadmap)
+
+1. **Match score + keyword gaps** (#1) — flagship differentiator, builds on freshest code (structured CV).
+2. **Canonical pipeline + kanban** (#2) — unblocks analytics, fixes daily UX.
+3. **Analytics v2** (#3) — quick follow-on.
+4. **Bookmarklet capture** (#5) + **PWA** (#7) — friction killers.
+5. **Interview hub** (#4) — biggest new surface, schedule after the above land.
+6. Then #9, #6, #8, #10 by appetite.
+
+Engineering-health work (CI test whitelist, prod DB backups, god-controller decomposition) is tracked separately in `docs/SYSTEM_OVERVIEW.md` §15–17 and should interleave with feature work in Phase 3.
+
+---
+
+Sources: [Prentus tracker roundup](https://prentus.com/blog/we-found-the-5-best-job-tracker-tools-on-the-market) · [ApplyArc comparison](https://applyarc.com/compare/best-job-application-trackers) · [Teal pricing](https://www.tealhq.com/pricing) · [Teal reviews (ResumeHog)](https://resumehog.com/blog/posts/teal-hq-review-april-2026-is-the-job-tracker-worth-your-time.html) · [Teal cons (ResumeJudge)](https://resumejudge.com/blog/tealhq-review/) · [Huntr pricing](https://huntr.co/pricing) · [Huntr cons (ResumeJudge)](https://resumejudge.com/blog/huntr-review/) · [Huntr vs Teal](https://huntr.co/blog/huntr-vs-teal) · [Simplify tracker](https://simplify.jobs/job-application-tracker) · [Jobright review of Teal](https://jobright.ai/blog/teal-review-2026-walkthrough-alternatives-and-faqs/) · [LazyApply](https://lazyapply.com/) · [Auto-apply tools compared](https://blog.fastapply.co/auto-apply-jobs-tools-compared-2026) · [Jobscan](https://www.jobscan.co/) · [Jobscan pricing](https://onlineatschecker.com/blog/jobscan-pricing-2026-free-plan-worth-it) · [JobSync (OSS)](https://github.com/Gsync/jobsync) · [CareerSync (OSS)](https://github.com/Tomiwajin/CareerSync) · [career-ops](https://career-ops.org/) · [Trackr](https://www.trackrjobs.com/) · [G-Track](https://jobtrack-ai.com/gmail-job-tracker) · [Gmail tracker extension](https://chromewebstore.google.com/detail/gmail-job-application-tra/lkpjngmdfncejiomkofogfdoppgifmkh) · [Interview scheduling software guide](https://www.selectsoftwarereviews.com/buyer-guide/interview-scheduling-software) · [SaaSHub Teal vs Huntr](https://www.saashub.com/compare-job-tracker-by-teal-vs-huntr)
diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md
new file mode 100644
index 0000000..ea56622
--- /dev/null
+++ b/docs/ROADMAP.md
@@ -0,0 +1,75 @@
+# ROADMAP.md — Jobbjakt Product & Engineering Roadmap
+
+> Phase 3 deliverable (2026-07-02). Sources: `docs/SYSTEM_OVERVIEW.md` (Phase 1) and `docs/PRODUCT_RESEARCH.md` (Phase 2).
+> Scoring: Value/Complexity/Risk on ▲ high / ● medium / ▽ low. Effort: S <1 day · M 1–3 days · L 1–2 wk · XL >2 wk.
+
+**North star:** the private, self-hosted career hub — the tracker UX of Huntr, the tailoring/scoring of Teal+Jobscan, powered by local AI, with data that never leaves your server.
+
+---
+
+## Tier 0 — Quick Wins (do first; days, low risk, compounding payoff)
+
+| # | Item | Type | Value | Effort | Risk | Rationale |
+|---|---|---|---|---|---|---|
+| Q1 | **CI: run the full frontend test suite** (replace the hand-maintained 10-file whitelist with the whole suite; fix/quarantine any flaky test explicitly) | eng | ▲ | S | ▽ | New tests currently silently skipped in CI; already caused a gap once |
+| Q2 | **Automated production DB backup** (scheduled SQLite `VACUUM INTO`/copy to `exports/` with retention; document restore) | eng | ▲ | S–M | ▽ | Prod currently has *no working automated backup* (backup endpoint is Windows-DPAPI-only, prod is Linux) |
+| Q3 | **Repo hygiene** (delete dead root `Controller/`; remove `temp_job.json`, `temp_post_job.py`; gitignore `JobTrackerApi/CvArtifacts/`, `bin_build/`, stray artifacts; commit pending WIP fixes on a branch) | eng | ● | S | ▽ | Removes footguns before refactors; working tree currently dirty |
+| Q4 | **Swagger/OpenAPI** (Swashbuckle or built-in OpenAPI, dev-only exposure) | eng | ● | S | ▽ | README endpoint list already drifts; prerequisite for a generated TS client later |
+| Q5 | **Structured salary fields** (min/max/currency/period alongside the free-text field, backfill-friendly) | product | ● | S–M | ▽ | Cheap now, prerequisite for offer comparison + analytics later |
+
+## Tier 1 — High Value (the differentiators; next 2–4 weeks of feature work)
+
+| # | Item | Value | Effort | Risk | Notes |
+|---|---|---|---|---|---|
+| H1 | **CV↔Job match score + keyword gap analysis** — per-job score, missing keywords, section coverage; reuse `ProfileCvStructureJson` + existing Ollama path; panel in job workspace | ▲▲ | M–L | ● | The market's #1 paid feature (Jobscan $50/mo), free & local here. Flagship differentiator |
+| H2 | **Canonical pipeline + drag-drop kanban** — status enum + ordering + per-user custom stages; migration normalizing existing free-text statuses; board becomes drag-drop | ▲▲ | M–L | ● | Fixes daily UX; unblocks H3; the riskiest part is the status migration (needs careful mapping + tests) |
+| H3 | **Analytics dashboard v2** — funnel conversion, response rate, time-in-stage, weekly activity, source effectiveness (data already in `JobEvent`) | ▲ | M | ▽ | Depends on H2 for clean stages |
+| H4 | **Gmail AI status suggestions** — extend the existing review queue: classify incoming mail (rejection/interview/offer) via local AI and suggest status moves, human-confirmed | ▲ | M | ● | Compounds an existing unique strength; keep human-in-the-loop |
+
+## Tier 2 — Medium Value (after Tier 1)
+
+| # | Item | Value | Effort | Risk |
+|---|---|---|---|---|
+| M1 | **Bookmarklet / PWA share-target capture** — one-click save-to-Jobbjakt reusing `jobimport/preview` | ▲ | S–M | ▽ |
+| M2 | **PWA pass** — manifest, installability, mobile nav polish | ● | M | ▽ |
+| M3 | **Interview hub** — interview entity (round, type, time, prep notes, outcome), timeline integration, ICS export + reminders | ▲ | L | ● |
+| M4 | **Contacts (people) CRM** — recruiter/hiring-manager entities linked to companies/jobs/correspondence | ● | M | ▽ |
+| M5 | **Durable CV processing queue** — DB-backed queue replacing in-memory (jobs survive restart) | ● | M | ● |
+| M6 | **ProblemDetails + validation consistency** across API | ● | M | ▽ |
+
+## Tier 3 — Long-Term Improvements (structural; interleave carefully)
+
+| # | Item | Value | Effort | Risk |
+|---|---|---|---|---|
+| L1 | **Decompose god controllers** (`JobApplicationsController` 151 KB, `ProfileCvController` 117 KB, `GmailController` 60 KB) into feature services; extract AI prompt construction behind interfaces. Strictly behavior-preserving, test-first, one slice per PR | ▲ (maintainability) | XL | ▲ |
+| L2 | **Finish the project-layout migration** — physically move linked `Models/`/`Data/`/controller/service files into real projects, retire glob-include `JobTrackerBackend` | ● | L | ● |
+| L3 | **Vite migration** (CRA/react-scripts is EOL; 4 GB-heap builds) | ● | L | ● |
+| L4 | **OpenAPI-generated TypeScript client** replacing hand-written `api.ts` surface | ● | M–L | ● |
+| L5 | **Staging environment / deploy gate** (compose profile or second host; smoke test before prod) | ▲ (ops) | L | ● |
+
+## Tier 4 — Future Ideas (not scheduled)
+
+- Full browser extension (Chrome/Firefox store) with autofill.
+- Web push notifications + weekly digest.
+- Company research assistant (local AI summarizing company info).
+- Offer comparison & salary analytics dashboards.
+- Job feed matching from saved searches (Finn/NAV polling).
+- Native mobile wrappers; CalDAV/Google Calendar two-way sync.
+- Multi-instance/scale-out readiness (distributed cache/queue).
+
+---
+
+## Recommended execution sequence (Phase 4+)
+
+Interleaving product and engineering so debt never blocks features:
+
+1. **Wave 0 (hygiene):** Q3 → Q1 → Q2 → Q4 → Q5 (each a small conventional commit on a feature branch; Q1/Q2 are the two items with real operational risk today)
+2. **Wave 1 (flagship):** H1 match scoring (design doc → backend endpoint → UI panel → tests)
+3. **Wave 2 (core UX):** H2 canonical pipeline/kanban, then H3 analytics
+4. **Wave 3:** H4 Gmail suggestions, M1 bookmarklet, M2 PWA
+5. **Wave 4:** M3 interview hub, M4 contacts, M5 durable queue
+6. **Continuous:** L1 controller decomposition proceeds opportunistically — whenever a wave touches a god-controller area, extract that slice first (M6 rides along); L2–L5 scheduled after Wave 3 checkpoint.
+
+Phases 5–10 of the mission (bug hunt, security audit, performance, refactoring, testing, docs) run after or between waves as checkpoints; Phase 11 rules apply throughout (feature branches, conventional commits, full test suite before commit, no auto-merge to main).
+
+**Explicitly deprioritized:** auto-apply automation (quality/ToS problems), cloud AI providers (undermines privacy moat), Chrome-store extension before the bookmarklet proves demand.
diff --git a/docs/SYSTEM_OVERVIEW.md b/docs/SYSTEM_OVERVIEW.md
new file mode 100644
index 0000000..6661dec
--- /dev/null
+++ b/docs/SYSTEM_OVERVIEW.md
@@ -0,0 +1,293 @@
+# Jobbjakt (Job Tracker) — System Overview
+
+> Phase 1 deliverable: full-system map produced before any code changes.
+> Last updated: 2026-07-02. Verified against commit `eea327e1` plus local working-tree changes.
+
+---
+
+## 1. What the product is
+
+Jobbjakt is a self-hosted, multi-user job application tracking platform with heavy AI assistance:
+
+- Track job applications end-to-end (status pipeline, follow-ups, deadlines, salary, tags, notes).
+- Company/recruiter CRM (pipeline stage, contact dates, recruiter details).
+- Correspondence log per application, including **Gmail OAuth import with review workflow**.
+- Attachments per application with purpose metadata and AI-inclusion toggles.
+- **CV platform**: upload → OCR/text extraction → structured CV parsing (Ollama-assisted block classification) → per-job tailored CV drafts → templated PDF export via Playwright.
+- AI drafts: cover letters, recruiter messages, follow-up drafts, job description summaries, translation (LibreTranslate optional).
+- Rules engine (auto-ghosting, follow-up "needs attention"), reminder emails, daily JSON export, history/event trail, encrypted backup (Windows/DPAPI).
+- Admin surface: user management, audit log, system readiness page.
+- Deployed to production at `https://jobs.cesnimda.uk` via Gitea Actions → SSH → Docker Compose.
+
+---
+
+## 2. Architecture overview
+
+```mermaid
+flowchart LR
+ subgraph Client
+ UI[React 19 SPA
MUI 7, react-router 6
CRA/react-scripts]
+ end
+
+ subgraph Frontend container
+ NGINX[nginx 1.29-alpine
serves build + proxies /api]
+ end
+
+ subgraph Backend container
+ API[ASP.NET Core net9.0 API
JobTrackerApi host]
+ BG[Hosted services:
Rules, FollowUpReminder,
DailyExport, JobEnrichment,
SummarizerProbe, CvProcessing]
+ DB[(SQLite default
or MariaDB/MySQL)]
+ FS[/Data root:
Attachments, CvArtifacts,
exports, DP keys/]
+ end
+
+ subgraph AI stack
+ AISVC[FastAPI ai-service :8001
distilbart summarizer,
OCR pytesseract/PyMuPDF,
docx/pdf extraction]
+ OLLAMA[Ollama :11434
qwen2.5:7b
CV classification + rewrite]
+ end
+
+ EXT1[Google OAuth / Gmail API]
+ EXT2[Job sites: Finn, NAV,
LinkedIn, Jobbnorge]
+ EXT3[SMTP - Gmail app password]
+ EXT4[LibreTranslate optional]
+
+ UI --> NGINX --> API
+ API --> DB
+ API --> FS
+ API --> AISVC --> OLLAMA
+ API --> EXT1
+ API --> EXT2
+ API --> EXT3
+ API --> EXT4
+ BG --> DB
+```
+
+### Solution layout (unusual — read this first)
+
+| Project | Role |
+|---|---|
+| `JobTrackerApi/` | Web **host** only: `Program.cs`, appsettings, migrations, Dockerfile. Its csproj **excludes** `Controllers/**` and `Services/**` from its own compilation. |
+| `JobTrackerBackend/` | "Transitional shared-backend" **library** that compiles, via `` links, the files physically located in `../Data`, `../Models`, `../JobTrackerApi/Controllers`, `../JobTrackerApi/Services`. Exists so tests can reference controllers/services without the web-entry project. |
+| `JobTrackerApi.Tests/` | xUnit test project (~20 test classes incl. authorization/hostile-fixture tests). |
+| `Models/`, `Data/` (repo root) | The *real* EF models and `JobTrackerContext`, compiled into JobTrackerBackend. |
+| `Controller/` (repo root) | **Legacy stub controllers (~1 KB each) — dead code**, not referenced by any csproj. |
+| `job-tracker-ui/` | React SPA. |
+| `tools/summarizer/` | FastAPI AI service (own Dockerfile, pytest tests). |
+| `deploy/`, `.gitea/workflows/` | Prod deploy script + CI/CD pipeline. |
+| `docs/` | Session handoffs, security assessments (M013–M015), UAT notes. |
+
+---
+
+## 3. Technology stack
+
+**Backend**: ASP.NET Core net9.0, EF Core 9 (SQLite default; Pomelo MySQL/MariaDB switchable via `Database:Provider`), ASP.NET Identity Core (users/roles), JWT bearer auth (local + Google policy scheme), built-in RateLimiter, DataProtection (file-system keys), Playwright (CV PDF export).
+
+**Frontend**: React 19, TypeScript 4.9, MUI 7 (+ x-data-grid, x-date-pickers, lab), axios, react-router-dom 6, @tanstack/react-table, CRA `react-scripts` 5 (build needs `--max-old-space-size=4096`), i18n EN + NB (custom provider), Jest/RTL tests.
+
+**AI**: FastAPI + transformers (`sshleifer/distilbart-cnn-12-6`) for summaries; pytesseract/PyMuPDF/pypdf/python-docx for extraction/OCR; Ollama (`qwen2.5:7b`) for CV block classification and rewrite paths; TTL cache.
+
+**Infra**: Docker Compose (4 services: backend, frontend/nginx, ai-service, ollama w/ GPU), Gitea Actions CI (build + backend tests + selected frontend tests + frontend build) → SSH deploy → `deploy/deploy.sh` on the prod host, external `jobtracker_shared` network.
+
+---
+
+## 4. Authentication & authorization
+
+- **Smart policy scheme**: inspects the bearer token issuer — Google-issued ID tokens (`accounts.google.com`) route to the `google` JWT handler (validated against `Auth:GoogleClientId`); everything else routes to `local` JWT (symmetric key `Auth:JwtKey`, issuer/audience validated, 2-min clock skew).
+- **Cookie session support**: local handler also reads the session cookie (`AuthSessionOptions.SessionCookieName`); **CSRF double-submit** middleware enforces cookie+header match for all mutating requests when a session cookie is present (login/register/reset/csrf endpoints exempt).
+- `Auth:Require=true` sets a fallback authorize-all policy (prod compose sets it). Dev without a JWT key generates an ephemeral key + warning; **fails closed** if auth required but no key.
+- Local tokens **must** carry a subject claim (`LocalAuthIdentity`), enforced in `OnTokenValidated` — hardened after finding M013-2.
+- **Multi-tenancy**: every tenant entity carries `OwnerUserId`; `JobTrackerContext` applies global query filters `CurrentUserId != null && OwnerUserId == CurrentUserId` (deny-on-null). Correspondence/JobEvents/CV entities filter through their parent's owner.
+- Roles via ASP.NET Identity: admin-only controllers (`UsersController`, `AdminAuditController`, `AdminSystemController`).
+- Password policy: min 8, digit + lowercase required. Password reset via emailed token (SMTP required). Registration disabled by default.
+- Rate limiting: `auth-login` (10/5 min/IP) and `auth-email` (5/15 min/IP) fixed-window policies.
+
+---
+
+## 5. Database schema (EF Core, 8 migrations)
+
+```mermaid
+erDiagram
+ ApplicationUser ||--o{ Company : owns
+ ApplicationUser ||--o{ JobApplication : owns
+ ApplicationUser ||--o| UserRuleSettings : has
+ ApplicationUser ||--o{ GmailConnection : has
+ ApplicationUser ||--o{ CvUploadArtifact : owns
+ ApplicationUser ||--o{ CvExtractionRun : owns
+ Company ||--o{ JobApplication : "has jobs"
+ JobApplication ||--o{ Correspondence : messages
+ JobApplication ||--o{ Attachment : attachments
+ JobApplication ||--o{ JobEvent : events
+ JobApplication ||--o| TailoredCvDraft : "1:1 draft"
+ CvUploadArtifact ||--o{ CvExtractionRun : "source of"
+ ApplicationUser ||--o{ GmailReviewDecision : decides
+```
+
+Key notes:
+
+- `ApplicationUser` (IdentityUser) also stores profile CV text, **structured CV JSON** (`ProfileCvStructureJson`), avatar data-URL, Google link info, current CV artifact/run pointers.
+- `JobApplication`: status string (default "Applied"), soft delete (`IsDeleted`/`DeletedAt`), tags as JSON string, imported description + translation, persisted `ShortSummary`, tailored CV text, reminder bookkeeping. Cascade deletes to messages/attachments/events/draft.
+- `RuleSettings` (global, seeded Id=1) + per-user `UserRuleSettings`.
+- `SystemEmailSettings`: DB-stored SMTP override (resolved by `EmailSettingsResolver`).
+- Indexes: `OwnerUserId` on Company/JobApplication/GmailConnection; composite `(OwnerUserId, UploadedAtUtc)`, `(OwnerUserId, StartedAtUtc)`, unique `(OwnerUserId, JobApplicationId)` on draft, unique `(OwnerUserId, GmailAddress)`.
+- SQLite file lives at `DataRoot/jobtracker.db` (WAL mode); migrations applied automatically at startup (`StartupInitializationExtensions`, 62 KB — also seeds admin, creates Identity tables where `dotnet ef` unavailable, ignores `PendingModelChangesWarning`).
+
+---
+
+## 6. API surface (all under `/api`, ~15 controllers)
+
+| Controller | Highlights |
+|---|---|
+| `JobApplicationsController` (**151 KB!**) | CRUD, paging/filtering/sorting, board, reminders, stats, history, unified timeline, status/follow-up PATCH, soft delete/restore, **plus** AI surface: application package material, follow-up drafts, cover-letter/recruiter drafts ("Maria" drafts), workflow signals. |
+| `ProfileCvController` (**117 KB**) | CV upload artifacts, extraction runs, structure parsing, rebuild/improve, tailored CV generation via Ollama rewrite, template rendering + Playwright PDF preview/export, benchmark corpus harness. |
+| `GmailController` (**60 KB**) | OAuth connect/callback, sync, message review queue, import decisions, job matching. |
+| `AuthController` (22 KB) | login/register/me/config, Google exchange, password reset request/reset, session cookie + CSRF endpoints. |
+| `CompaniesController` | CRUD, idempotent create by name, recruiter/pipeline fields. |
+| `CorrespondenceController` | per-job messages CRUD. |
+| `AttachmentsController` | multipart upload to disk, download, rename, delete, purpose/AI-inclusion metadata. |
+| `RulesController` | global + per-user rule settings, clamped. |
+| `ExportController` | JSON/CSV export. |
+| `BackupController` | DPAPI-encrypted backup (Windows only). |
+| `JobImportController` | URL preview via plugin parsers (SSRF-hardened). |
+| `UsersController`, `AdminAuditController`, `AdminSystemController` | admin: user/role management, audit trail, system readiness (DB/Gmail/AI). |
+| `ClientErrorsController` | frontend error intake → logs. |
+
+No OpenAPI/Swagger is wired up; the README is the de-facto API doc (already drifting).
+
+---
+
+## 7. Background services (6 hosted services)
+
+| Service | Function |
+|---|---|
+| `RulesHostedService` → `RulesEngine` | periodic auto-transitions (e.g., → Ghosted) from rule settings |
+| `FollowUpReminderHostedService` | reminder emails for due/upcoming follow-ups (dedup via `LastReminderEmailSentAt`) |
+| `DailyExportHostedService` | daily JSON export at configured local hour |
+| `JobEnrichmentHostedService` | backfills summaries/enrichment for jobs |
+| `SummarizerProbeHostedService` | probes AI service readiness |
+| `CvProcessingHostedService` + `CvProcessingQueue` | in-memory queue for CV extraction/processing jobs |
+
+All state is in-process (`IMemoryCache`, in-memory queue) — single-instance assumption; no distributed locks; queue contents lost on restart.
+
+---
+
+## 8. AI pipeline (data flow)
+
+1. **Job import**: URL → plugin parse (Finn/NAV/LinkedIn/Jobbnorge or universal JSON-LD parser) → optional LibreTranslate → language detect + skill tagging → preview → user accepts → stored on `JobApplication`.
+2. **Summaries**: API → `SummarizerService` (31 KB) → FastAPI `/summarize` (distilbart, TTL-cached, GPU-if-available) → persisted `ShortSummary`.
+3. **CV ingest**: upload (PDF/DOCX/image ≤ 8 MB) → FastAPI extract/OCR → block classification (Ollama-assisted, `CvAiClassifier`/`CvAiNormalizer`) → `ProfileCvStructureJson` on user.
+4. **Tailoring**: job description + structured CV sections → Ollama rewrite path (recent commits: clamped lengths, hardened diagnostics) → `TailoredCvDraft` (JSON blocks) → `CvTemplateRenderer` (25 KB, template carousel) → Playwright → PDF.
+5. **Drafts**: cover letter / recruiter message / follow-up drafts generated per job with attachment-aware context selection.
+
+Degradation: if AI service or Ollama is down, core tracking still works (probe service + "AI is not a deploy gate" in CI).
+
+---
+
+## 9. Email
+
+- `SmtpEmailSender` with `EmailSettingsResolver`: config from env/appsettings **or** DB-stored `SystemEmailSettings` (admin-editable).
+- Uses Gmail SMTP + app password in prod. Flows: password reset, follow-up reminders. `App:PublicBaseUrl` builds links.
+
+---
+
+## 10. Configuration & secrets
+
+- `.env` (git-ignored) → docker-compose env → ASP.NET config. `.env.example` documents the shape. Real secrets currently present in local `.env` (JWT key, admin password, SMTP app password, Google client secret).
+- `appsettings.Development.json` contains only `CHANGE_ME_*` placeholders (good).
+- Key knobs: `Database:Provider`, `ConnectionStrings:JobTracker`, `Data:Root`, `Cors:Origins`, `Ai:BaseUrl`, `Auth:*`, `Email:*`, `Exports:*`, `Translation:*`, `App:PublicBaseUrl`, `HttpsRedirection:*` (TLS terminated at reverse proxy; HSTS/redirect off in-container).
+- `ProductionConfigTests.cs` exists to guard prod config shape.
+
+---
+
+## 11. Build, CI/CD, deployment
+
+- **CI** (`.gitea/workflows/ci-deploy.yml`): on PR + push-to-main → build backend (Release), run backend tests, `npm ci`, run an **explicit whitelist of 10 frontend test files** (not the whole suite), build frontend.
+- **Deploy** (push to main only): SSH to prod host → `git reset --hard ` in `/opt/job-tracker/app` → `deploy/deploy.sh` (docker compose build/up with retry/cache-prune fallbacks) → verify containers; AI service health is non-blocking.
+- Frontend Dockerfile: node build stage → nginx 1.29-alpine (working-tree bump from 1.27 pending commit); nginx proxies `/api` to backend.
+- No staging environment; deploys go straight to prod after CI.
+
+---
+
+## 12. Testing strategy
+
+- **Backend**: xUnit integration-style tests via `TestHostFactory`; notable coverage: authorization (`JobApplicationsAuthorizationTests`, `OwnershipGuardTests`, hostile fixture DB project), auth/system, Gmail, CV corpus harness, summarizer, SQLite migration helper, production config.
+- **Frontend**: ~20 Jest/RTL test files (workspace flows, Gmail review, login, admin, attachments, drafts, trust-loop e2e-ish component tests). CI runs only the whitelisted subset.
+- **AI service**: pytest (`tools/summarizer/tests/test_app.py`).
+- No true end-to-end browser tests; no load/perf tests.
+
+---
+
+## 13. Logging & error handling
+
+- Console/debug logging; custom middleware logs every request (method, path, status, ms, traceId, sub claim). Unhandled exceptions logged then rethrown (500).
+- Client errors POSTed to `/api/client-errors` and logged server-side; React `ErrorBoundary` + route error page in UI.
+- No structured sink (Seq/OTLP), no log rotation policy in-app (container stdout), no correlation to frontend errorIds beyond log text, no ProblemDetails standardization.
+
+---
+
+## 14. Security posture (current)
+
+Strong points (much already hardened via M013–M015 adversarial assessments in `docs/security-assessments/`):
+
+- SSRF on job import **fixed & retested** (DNS resolution check, private/loopback/link-local rejection, redirects disabled).
+- Subjectless-JWT / owner-filter bypass **fixed & retested** (fail-closed identity, deny-on-null query filters).
+- Cross-user job history leak fixed (`81196374`); authorization replay findings recorded (M015).
+- CSRF double-submit for cookie sessions; CORS allowlist; rate-limited login/email endpoints; ephemeral JWT key refused when auth required; Identity password hashing (PBKDF2); DataProtection keys persisted outside repo runtime path.
+
+Open questions / watch areas (to verify in Phase 6):
+
+- `AllowCredentials()` combined with configurable `Cors:Origins="*"` wildcard mode (SetIsOriginAllowed(true) + credentials) — dangerous if ever enabled.
+- Attachment upload: file-type/size limits, path handling, content-type on download need re-audit.
+- Avatar stored as data-URL on user record (size/XSS considerations).
+- Gmail OAuth token storage encryption at rest; scopes; audit of `GmailController` (60 KB).
+- Global rate limiting only on 2 auth policies — AI/expensive endpoints unthrottled.
+- Backup endpoint Windows-only DPAPI — silently unavailable on Linux prod.
+- Dependency freshness (axios, react-scripts 5/CRA is deprecated upstream; transformers/torch pinning).
+- Secrets present in local `.env` (expected, git-ignored) — confirm no history leaks.
+
+---
+
+## 15. Technical debt report
+
+1. **God controllers**: `JobApplicationsController` (151 KB), `ProfileCvController` (117 KB), `GmailController` (60 KB), `StartupInitializationExtensions` (62 KB). Massive single files mixing HTTP, business logic, AI prompt construction, and persistence. Highest-leverage refactor target — but high risk, needs test cover first.
+2. **Transitional project layout**: `JobTrackerBackend` compiles files it doesn't own via glob includes; root `Models/`/`Data/` folders; **dead** root `Controller/` folder; `JobTrackerBackend/bin`+`obj` artifacts and `JobTrackerApi/jobtracker.db` + `bin_build/`, `CvArtifacts/`, `exports/`, `keys/` polluting the repo/working tree. `.gitignore` needs review.
+3. **CI runs a hand-maintained subset** of frontend tests — new test files silently not run (already bit them once; `profile-page.test.tsx` had to be added manually).
+4. **CRA/react-scripts 5** is EOL-ish, slow builds (needs 4 GB heap), TS 4.9. Vite migration is the obvious path (medium effort).
+5. **Naming drift**: `Summarizer*` vs `AiService*`; "Jobbjakt" vs "Job Tracker" branding split; EN/NB translation consistency flagged in handoff doc.
+6. No OpenAPI; README endpoint list already drifts from code (e.g., Gmail/profile/admin endpoints missing there).
+7. In-memory queue/cache single-instance coupling undocumented.
+8. Root-level clutter: `temp_job.json`, `temp_post_job.py`, `todo jobtracker.txt`, `test/`, `tmp/`, `vendor/`, `.venv/`.
+9. `DaysSince` compares `DateTime.UtcNow` with `.Days` truncation — timezone/UX edge cases; status is a free string, no canonical pipeline enum (README itself lists this as a wanted improvement).
+10. Windows-only backup path.
+
+---
+
+## 16. Areas of concern
+
+- **Single point of data**: SQLite in a Docker volume; backups are manual/Windows-only; no automated off-host backup.
+- **Deploy risk**: `git reset --hard` + straight-to-prod with no staging and non-exhaustive CI test coverage.
+- **AI coupling**: prompt logic buried in controllers makes model/provider changes and testing hard.
+- **Restart data loss**: queued CV processing jobs are lost on restart (in-memory queue).
+- **Uncommitted working tree**: 3 modified files (Dockerfile nginx bump, `useViewResource` stale-closure fix, handoff doc) + untracked `scripts/start-ollama-cv.ps1` and a stray `JobTrackerApi/CvArtifacts/` data folder.
+
+---
+
+## 17. Opportunities for improvement (input to Phase 2/3)
+
+Product (initial hypotheses, to be validated by market research):
+
+- Canonical pipeline model + customizable Kanban stages (already on README wish list).
+- Interview scheduling/prep hub (calendar integration, prep notes, question banks).
+- Salary/offer comparison and analytics dashboards (funnel conversion, response rates, time-in-stage).
+- Browser extension / bookmarklet for one-click job capture (plugins already exist server-side).
+- Saved searches/views, full-text search, date-range and tag filters.
+- Notifications beyond email (web push, digest).
+- Contact-level recruiter CRM (people, not just companies).
+- Mobile-friendly PWA pass.
+
+Engineering:
+
+- Swagger/OpenAPI + generated TS client; ProblemDetails everywhere.
+- Split god controllers into feature services; move AI prompting behind interfaces.
+- Run full frontend test suite in CI (`npm test -- --watchAll=false` without whitelist) once flaky tests are addressed; add `dotnet format`/eslint gates.
+- Vite migration; dependency refresh.
+- Durable job queue (DB-backed) for CV processing; automated DB backup job.
+- Repo hygiene: delete dead `Controller/`, ignore build artifacts, remove committed DB files.