b176a44627
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>
639 lines
54 KiB
Markdown
639 lines
54 KiB
Markdown
# 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:**
|
||
- **Landing** — `views/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 setup** — `OnboardingChecklist.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 jobs** — `AddJobModal.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`.
|
||
- **Importing** — `POST /api/jobimport/preview` → `UniversalJobParser` + per-site plugins + JSON-LD fallback, language detection, skill tagging.
|
||
- **Editing** — `EditJobDialog.tsx`, `JobDetailsDialog.tsx` (1400 lines).
|
||
- **Stages** — free-text `Status` canonicalized by `JobPipeline.Normalize`, preserving unknown values (a good, non-destructive design).
|
||
- **Follow-ups** — `FollowUpReminderHostedService`, `RulesEngine` (auto-ghosting), AI drafts, send.
|
||
- **Communication** — `Correspondence` 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:**
|
||
- **Profile** — `ProfilePage.tsx` (1368 lines): personal details, avatar with cropping, CV upload, structured profile editing, 2FA/session cards.
|
||
- **CV** — upload → `CvUploadArtifact` → `CvExtractionRun` (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 only** — `Program.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\new` — **not 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 F0–F5, F0–F2 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 $29–50/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 *renderer* — `POST /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 3–5 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 controllers** — `JobApplicationsController` 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 600–1400-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 == CurrentUserId` — **deny-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`, M013–M015 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` + M013–M015 | 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).
|
||
|
||
---
|
||
|
||
## 14. Recommended roadmap
|
||
|
||
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 F0–F5 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/keys` → `519c32e`, `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.
|