feat: complete release readiness work

- consolidate API ownership and remove dead vendor code

- add Stripe billing, learning paths, and public CV hardening

- add migration, recovery, security, audit, and browser gates
This commit is contained in:
cesnimda
2026-07-31 16:54:16 +02:00
parent a23c3dfc97
commit ce76046a29
1634 changed files with 6889 additions and 135429 deletions
+1 -1
View File
@@ -51,7 +51,7 @@ master profile text ────────────┘ │
## History model
`AiInteraction` (`Models/AiInteraction.cs`) is **append-only** — one row per generation, never
`AiInteraction` (`JobTrackerApi/Models/AiInteraction.cs`) is **append-only** — one row per generation, never
overwritten. This is deliberately distinct from `AiWorkspaceNote` (a one-row-per-type *cache* for
candidate-fit/focus-plan). History gives the user restore/reuse (re-surface a past result), compare
(view two side by side), copy, and delete. `ResultJson` is `{ text, meta? }`; `Provider` records which
+7 -2
View File
@@ -93,6 +93,11 @@ Seeding is idempotent per `(JobApplicationId, SystemKey)` — enforced by a uniq
never duplicates. Custom items have a `NULL` `SystemKey`; both SQLite and MariaDB treat NULLs as
distinct in a unique index, so a user can add as many as they like.
The deterministic match-score endpoint also synchronises `learning:{hash}` system items from its
current missing skills. They form the first job-specific learning path without another table or an
external course catalogue. Manual learned/dismissed decisions persist; only recommendations that
were auto-completed because a gap disappeared reopen when the same gap returns.
Deleting a **system** item dismisses it (a hard delete would be undone by the next seed); deleting a
**custom** item removes the row. Dismissed items leave the progress denominator entirely.
@@ -378,6 +383,6 @@ belongs to the AI cache — a reminder that the two systems are genuinely differ
6. ✅ Completion and product readiness — ownership audit, security review, full local verification
(Phase 5.6). See `docs/phase-5-completion-report.md`.
**Phase 5 is feature-complete locally.** It is not deployed: CI is red for a documented environmental
reason and deployment is gated on it. See the completion report's *Remaining risks*.
**Phase 5 is feature-complete locally.** Current release and deployment dependencies are tracked in
`BLOCKERS.md`; the completion report is a historical snapshot.
7. Cover letter workflow. 8. Documents. 9. Interview preparation. 10. Dashboard improvements.
+2 -2
View File
@@ -10,7 +10,7 @@
> ## Implementation status (2026-07-18) — SHIPPED
>
> The model below is implemented on `main` and verified against the live database:
> - **Entities** (`Models/CareerEntities.cs`): `CareerExperience`, `CareerEducation`, `CareerSkill`,
> - **Entities** (`JobTrackerApi/Models/CareerEntities.cs`): `CareerExperience`, `CareerEducation`, `CareerSkill`,
> `CareerProject`, `CareerCertification`, `CareerLanguage` — relational children of `CareerProfile`;
> long tail in `CareerProfile.LongTailJson`. Migration `AddCareerProfileRelationalChildren`
> (applied cleanly on the real dev DB and the running container).
@@ -40,7 +40,7 @@ The Career Workspace foundation (`992f89e`) stores the whole profile as **one JS
`CareerProfileService.SaveVersionAsync` (dual-write). One row per user.
- `CareerProfileVersion` — append-only history: one row per save, with a `Source` discriminator.
The blob shape (`Models/StructuredCvProfile.cs`) already has structured items for **Jobs, Education,
The blob shape (`JobTrackerApi/Models/StructuredCvProfile.cs`) already has structured items for **Jobs, Education,
Certifications, Projects, Languages**, plus `Skills`/`Summary`/`Interests` (string lists) and
`OtherSections` (title + items). `CareerProfileService` already assigns **stable item IDs** to
Jobs/Education/Certifications/Projects and normalizes free-text dates to `YYYY-MM`.
+36 -51
View File
@@ -1,10 +1,11 @@
# Jobjakt — Current Architecture
> **This document describes the system as it actually is.** Every claim was verified against code.
> Last verified: 2026-07-17 (Phase 0). Supersedes the archived `docs/_archive/SYSTEM_OVERVIEW.md` (2026-07-02).
> Last verified: 2026-07-31. Supersedes the archived `docs/_archive/SYSTEM_OVERVIEW.md` (2026-07-02).
>
> **Rule:** if this document and the code disagree, the code wins — and this document is a bug. Fix it.
> Do not trust other files under `docs/` over this one; most are stubs.
> Topic documentation is maintained alongside this file; dated completion and review reports remain
> historical snapshots. When documentation conflicts, prefer this file and the code.
---
@@ -69,22 +70,19 @@ flowchart LR
BG --> DB
```
### Solution layout (unusual — read this first)
### Solution layout
| 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 link-compiles, via `<Compile Include>`, files physically located in `../Data`, `../Models`, `../JobTrackerApi/Controllers`, `../JobTrackerApi/Services`. Exists so tests can reference controllers/services without the web host. |
| `JobTrackerApi/` | ASP.NET Core host plus its controllers, services, EF models, `JobTrackerContext`, migrations, and Dockerfile. |
| `JobTrackerApi.Tests/` | xUnit, 36 test files incl. authorization + hostile-fixture suites. |
| `Models/`, `Data/` (repo root) | The *real* EF models and `JobTrackerContext`. |
| `job-tracker-ui/` | React SPA inside a Next.js shell. |
| `tools/summarizer/` | FastAPI AI service (own Dockerfile, pytest tests). |
| `tools/hostile-fixture-db/` | Security test fixture generator. |
| `deploy/`, `.gitea/workflows/` | Prod deploy script + CI/CD. |
**Source lives in one place and compiles from another.** Any tool assuming csproj-adjacent source will mislead you. Check `JobTrackerBackend.csproj` before adding files or projects.
> Corrected 2026-07-17: the dead root `Controller/` (singular) folder described in the archived overview **no longer exists** — removed in `519c32e`.
> Corrected 2026-07-31: the transitional `JobTrackerBackend` link-compilation project and root
> `Models/`/`Data/` directories were retired. Source now compiles from the project that owns it.
---
@@ -94,7 +92,7 @@ flowchart LR
**Frontend:** **Next.js 16** + React 19 + **TypeScript 5.9** + MUI 7 (+ x-data-grid, x-date-pickers, lab), axios, react-router-dom 6, @tanstack/react-table, i18n EN + NB (custom provider), Jest/RTL.
> Corrected 2026-07-17: the archived overview said "CRA/react-scripts 5, TypeScript 4.9". The CRA→Next.js migration has happened. See §4 for what that migration did and did not do.
> Corrected 2026-07-31: the CRA migration is complete; direct Jest/Babel configuration replaced `react-scripts`.
**AI:** FastAPI + transformers (`sshleifer/distilbart-cnn-12-6`) for summaries; pytesseract/PyMuPDF/pypdf/python-docx for extraction/OCR; **one** generation provider selected by the `AI_PROVIDER` env var ∈ {`ollama` (default, `qwen2.5:7b`), `gemini`, `groq`}; TTL cache.
@@ -104,17 +102,16 @@ flowchart LR
## 4. Frontend architecture
**Three toolchains coexist. This is the single most confusing thing about the frontend.**
**Two routing layers coexist intentionally:**
1. **Next.js 16 App Router** (`app/layout.tsx`, `app/page.tsx`) — a thin shell that mounts a client-side app. The CRA→Next migration was a **CSR lift-and-shift**: no SSR, no server components, no Next routing, no data fetching. Next is effectively a build tool here. Static export → nginx.
2. **react-router-dom v6** — does the actual routing, in **two different patterns inside one file** (`src/App.tsx`): `createBrowserRouter` for public routes (`/`, `/login`, `/forgot-password`, `/reset-password`, `/verify-email`) and a nested `<Routes>` inside a catch-all `Shell` for authenticated routes.
3. **react-scripts 5.0.1** — still a dependency, used **only** as the test runner (`"test": "react-scripts test"`).
Known consequence: a **dev-only 404 on deep links** follows directly from the Next shell + client router combination.
Development leaves static-export mode disabled so deep links reach the client router; production exports one shell and nginx falls back to `index.html` for unknown paths.
**Routes** (`src/App.tsx`): public — `/`, `/login`, `/forgot-password`, `/reset-password`, `/verify-email`. Authenticated — `/dashboard`, `/jobs`, `/reminders`, `/kanban`, `/companies`, `/correspondence`, `/correspondence/review`, `/profile`, `/career`, `/trash`, `/settings`, `/settings/connected-accounts`, `/admin/{audit,users,system}`.
> **No `/register` route exists.** Sign-up is folded into `LoginPage.tsx`, and the endpoint is disabled by default (§5).
> `/register` reuses the hardened auth form. Submission remains disabled until registration is enabled by production configuration (§5).
**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`). This is the root cause of the oversized components below.
@@ -138,7 +135,7 @@ the reference model for how account identity and the master career profile relat
| `/career` | `views/CareerWorkspacePage.tsx``views/CareerProfilePage.tsx` (~1293 lines) | The **master career profile** — the single editable source of truth |
`CareerWorkspacePage` is a thin shell (heading + source-of-truth notice) around `CareerProfilePage`.
The CV Builder is **not** built yet (Phase 4); `CareerProfilePage` is where it will live.
The CV Builder is a separate routed workspace and consumes the Career Profile as its source of truth.
### Request flow
@@ -209,7 +206,7 @@ null-leaves).
- **Sessions:** `UserSession` entity + `SessionsController` — list/revoke active sessions.
- Password policy: min 8, digit + lowercase. Reset via emailed token (SMTP required).
- **Registration is disabled by default** — `AuthController.cs:135` reads `Auth:AllowRegistration` defaulting to `false` and returns HTTP 403. There is no CAPTCHA anywhere.
- **Rate limiting (3 fixed-window policies):** `auth-login` 10/window, `auth-email` 5/window, `auth-2fa-challenge` 5/window. **AI and other expensive endpoints are unthrottled.**
- **Rate limiting:** login, auth email, 2FA challenge, and anonymous public-CV PDF export have dedicated fixed-window policies. AI usage is bounded by per-account monthly generation and token ceilings rather than request-window throttling.
---
@@ -246,12 +243,12 @@ erDiagram
- Salary is **structured**: `SalaryMin`, `SalaryMax`, `SalaryCurrency`, `SalaryPeriod` (plus a legacy free-text `Salary`).
- `Tags` is a **JSON array in a string column** — not queryable; `/tags` and `/tag-trends` must scan.
- Denormalized `HasResume`/`HasCoverLetter`/`HasPortfolio`/`HasOtherAttachment` flags duplicate `Attachments`; kept honest by `AttachmentFlagsRecomputeTests`.
- CV text is stored **three times** (`CvExtractionRun.RawExtractedText`, `.NormalizedText`, `.StructuredProfileJson`) plus twice on the user. Deliberate audit trail, but **no retention policy**.
- CV extraction runs retain the newest 20 completed runs per user; expired runs and unreferenced upload artifacts are pruned while the current artifact is preserved.
- `Status` is free-text at the DB level; canonicalized only in the application layer by `JobPipeline.Normalize` — deliberately, so custom user values are never destroyed.
- Indexes: `OwnerUserId` on Company/Job/JobApplication/GmailConnection; composites `(OwnerUserId, UploadedAtUtc)`, `(OwnerUserId, StartedAtUtc)`; unique `(OwnerUserId, JobApplicationId)` on draft, unique `(OwnerUserId, GmailAddress)`. EF auto-indexes FKs by convention. **Genuinely missing:** owner-prefixed composites `(OwnerUserId, IsDeleted, Status)` and `(OwnerUserId, FollowUpAt)`.
- SQLite at `DataRoot/jobtracker.db` (WAL); migrations applied at startup by `StartupInitializationExtensions` (1356 lines — also seeds admin, creates Identity tables where `dotnet ef` is unavailable, ignores `PendingModelChangesWarning`).
- Indexes include owner-prefixed list/board/reminder composites `(OwnerUserId, IsDeleted)`, `(OwnerUserId, IsDeleted, Status)`, and `(OwnerUserId, FollowUpAt)`, plus CV and provider-specific indexes.
- SQLite lives at `DataRoot/jobtracker.db` (WAL); migrations and legacy-schema reconciliation run at startup through `StartupInitializationExtensions`.
> Corrected 2026-07-17: prior session notes recorded the EF model snapshot as **broken/empty**. It was **resynced** in `20260711181039_SyncModelSnapshot` — the snapshot now covers the full model and incremental `dotnet ef migrations add` works normally. Note `dotnet ef` still needs the `Design` package temporarily added to `JobTrackerApi` (the `MigrationsAssembly`), since it lives in `JobTrackerBackend` with `PrivateAssets=all`.
> Corrected 2026-07-31: the model snapshot is current and the API directly carries the EF Design package and `JobTrackerContext`; no temporary project edit is required for `dotnet ef`.
---
@@ -294,10 +291,10 @@ erDiagram
| `DailyExportHostedService` | Daily JSON export at a configured local hour |
| `JobEnrichmentHostedService` | Backfills summaries/enrichment |
| `SummarizerProbeHostedService` | Probes AI service readiness |
| `CvProcessingHostedService` + `CvProcessingQueue` | In-memory queue for CV extraction |
| `CvProcessingHostedService` + `CvProcessingQueue` | Process-local wake-up queue for CV extraction; queued/running database work is recovered at startup |
| `DatabaseBackupHostedService``DatabaseBackupRunner` | Automated DB backup (`VACUUM INTO`, server-derived path) |
**All state is in-process** (`IMemoryCache`, in-memory queue) — single-instance assumption, no distributed locks, **queued CV jobs are lost on restart**.
Caches and worker coordination are process-local. CV work itself is durable and recovered after restart, but the worker remains a deliberate single-instance design without database leasing.
---
@@ -361,15 +358,15 @@ Inbound: `GmailOAuthService` (655), `MicrosoftGraphOAuthService` (507), `ImapSer
- **Backend:** xUnit integration-style via `TestHostFactory`. 36 test files. Notable: `JobApplicationsAuthorizationTests`, `OwnershipGuardTests`, `ImapServiceSsrfGuardTests`, `ProductionConfigTests`, `AttachmentFlagsRecomputeTests`, `CvCorpusHarnessTests`, `SqliteMigrationHelperTests`, `JobPipelineTests`, plus a `tools/hostile-fixture-db` project.
- **Frontend:** ~20 Jest/RTL files — **all run in CI**.
- **AI service:** pytest (`tools/summarizer/tests/`).
- **Gaps:** no true end-to-end browser tests; no load/perf tests; **no dependency CVE scanning** (CI explicitly sets `npm_config_audit: 'false'`).
- **Browser smoke:** Playwright drives login/session cookies, saved-job creation, Career Workspace routing, and anonymous public-CV rendering/PDF download against isolated API/SQLite and Next.js processes. CI installs Chromium and runs all four flows.
- **Gap:** no load/performance suite. CI reports NuGet transitive vulnerabilities and production npm audit findings.
---
## 14. Logging & error handling
Console/debug logging; middleware logs every request (method, path, status, ms, traceId, sub claim). Unhandled exceptions logged then rethrown (500). Client errors POST to `/api/client-errors`. React `ErrorBoundary` + route error page.
No structured sink (Seq/OTLP), no in-app log rotation, no ProblemDetails standardization.
Development uses simple console/debug logging; production emits structured JSON. Middleware records method, path, status, duration, trace ID, and subject. Unhandled errors return Problem Details with the same trace ID. Client errors POST to `/api/client-errors`; the frontend has an `ErrorBoundary` and route error page.
Compose bounds each container's local logs to three 10 MB files. There is still no external sink (Seq/OTLP) or cross-host aggregation.
---
@@ -385,17 +382,17 @@ No structured sink (Seq/OTLP), no in-app log rotation, no ProblemDetails standar
- 2FA + recovery codes + trusted devices + session revocation.
- **AI sidecar: backend-only.** Unpublished, on a private two-member network, and token-authenticated (§16). Verified against the running stack, not just configured.
**Open findings** (detail in `docs/application-discovery-report.md` §12 and `docs/phase-0-foundation-report.md`):
**Findings status** (detail in `docs/application-discovery-report.md` §12 and `docs/phase-0-foundation-report.md`):
| Sev | Finding | Status |
|---|---|---|
| Medium | DataProtection keys recoverable from git history (`519c32e`, `955cae6`) | **Open — rotation required, needs an operator** |
| Medium | **CORS: `Cors:Origins="*"` triggers `SetIsOriginAllowed(_ => true)` + `AllowCredentials()`** (`Program.cs:96-102`) — reflected-origin with cookies = session theft from any site. Not currently active (compose never sets `Cors__Origins`, so it defaults to `localhost:3000`), but it is one config value away. | **Open — landmine** |
| Medium | AI cost ceiling | Metering shipped in Phase 5; enforce quotas before open registration (Phase 7). |
| Low | No CAPTCHA (rate limiting only) | Open — blocks public signup |
| Low | Unbounded storage: attachments, CV artifacts, extraction runs, base64 avatars | Open |
| Medium | Wildcard credentialed CORS configuration | **Closed — startup rejects it** |
| Medium | AI cost ceiling | **Closed — monthly generation/token limits are enforced by plan** |
| Low | Public-registration abuse control | Implemented with Turnstile; production keys/configuration still required |
| Low | Unbounded storage | **Closed — attachment quotas, extraction/artifact pruning, file-backed avatars, and PDF-export retention are implemented** |
| Low | Backup / DPAPI is Windows-oriented — verify behaviour on Linux prod | Unverified |
| Low | No dependency CVE scanning in CI | Open |
| Low | No dependency CVE scanning in CI | **Closed — NuGet and production npm audit reporting are in CI** |
---
@@ -417,20 +414,9 @@ Full record: `docs/phase-0-foundation-report.md`. What changed architecturally:
---
## 17. Known debt (ranked)
## 17. Known debt
1. **`JobApplication` still carries opportunity columns** — the `Job` split is started but not completed. Reads/writes still use the legacy columns.
2. **God controllers**`JobApplicationsController` 2313/38 endpoints, `ProfileCvController` 2249, `StartupInitializationExtensions` 1356, `GmailController` 1023, `AuthController` 879.
3. **Hardcoded CV templates** — dead end for the CV Builder.
4. **Three frontend toolchains** — Next shell + react-router (×2 patterns) + react-scripts test runner.
5. **No frontend data layer** — root cause of the 6001400-line components.
6. **`ProfilePage` (1368 lines) serves two routes** behind a boolean; the `careerView` prop it accepts is **never read** (the "CV Builder" tab is inert).
7. **Denormalized `Has*` flags**; **`Tags` as a JSON string column**; **unbounded CV storage**; **base64 avatars in a DB column**.
8. **`JobTrackerBackend` link-compilation** — self-described "transitional".
9. **In-memory queue/cache** — single-instance coupling; restart loses queued CV jobs.
10. **No OpenAPI in prod, no ProblemDetails, no structured logging sink.**
11. **Root-level clutter**: `temp_job.json`, `temp_post_job.py`, `todo jobtracker.txt`, `test/`, `tmp/`, `vendor/`, `docs.7z`, `CV_Changes.md`, `SMART_GMAIL_PROGRESS.md`.
12. **Norway-only import plugins** (Finn/NAV/Jobbnorge). Product decision 2026-07-17: **Norway first, but no hardcoding Norway** — market must become a data dimension, not an assumption.
The live, prioritized ledger is `docs/architecture/technical-debt.md`. The principal remaining items are the legacy `JobApplication` opportunity columns, single-replica worker coordination, and optional cross-host log aggregation. Large orchestration files are refactored only when a cohesive behavior change provides a safe seam.
---
@@ -438,9 +424,8 @@ Full record: `docs/phase-0-foundation-report.md`. What changed architecturally:
Recorded nowhere else in active docs:
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). Deliberate; do not "fix" it with a DB enum.
3. The CI frontend-test whitelist was removed after it "silently skipped new suites and let two regressions reach main."
4. Ollama is intentionally not bundled by default so deploys reuse a shared instance.
5. `AI_PROVIDER=gemini` exists to offload a weak local GPU in prod.
6. `TailoredCvDraft` is a separate entity specifically to guarantee the master CV is never auto-modified.
1. `Status` is free-text and canonicalized in the application layer **specifically so custom user values are never destroyed** (`JobPipeline.cs` docstring). Deliberate; do not "fix" it with a DB enum.
2. The CI frontend-test whitelist was removed after it "silently skipped new suites and let two regressions reach main."
3. Ollama is intentionally not bundled by default so deploys reuse a shared instance.
4. `AI_PROVIDER=gemini` exists to offload a weak local GPU in prod.
5. `TailoredCvDraft` is a separate entity specifically to guarantee the master CV is never auto-modified.
+7 -2
View File
@@ -25,8 +25,8 @@ The master profile is never written by the builder. A variant references career
## Variant model
`CvVariant` (`Models/CvVariant.cs`) + `CvVariantVersion` (autosave history). The whole lens lives in one
`SettingsJson` blob (`CvVariantSettings`, `Models/CvVariantSettings.cs`) because it is edited and saved
`CvVariant` (`JobTrackerApi/Models/CvVariant.cs`) + `CvVariantVersion` (autosave history). The whole lens lives in one
`SettingsJson` blob (`CvVariantSettings`, `JobTrackerApi/Models/CvVariantSettings.cs`) because it is edited and saved
as a unit — never queried field-by-field. A variant stores:
- `ThemeId` + overrides: accent, heading/body font, density, page size, photo/icons/page-numbers.
@@ -56,6 +56,11 @@ Anonymous (`/api/public-cv/{slug}`, `PublicCvController`): serves a public varia
tenant filter — there is no current user). Unknown/private slug → 404
(`CvBuilderTests.Private_variant_is_not_served_publicly`).
Recruiters can download the same public render as PDF through `GET /api/public-cv/{slug}/pdf`.
The endpoint applies the identical public/private slug check; unknown, revoked, or private links return
404 without invoking the exporter. Anonymous PDF generation is limited to three requests per minute
per public link because it launches Chromium. `PublicCvPage` exposes it as a native **Download PDF** link.
## Builder workflow (frontend)
`/career/builder` lists variants (`CvBuilderPage`); the editor (`CvBuilderEditor`) is three tabs —
+2 -2
View File
@@ -7,7 +7,7 @@
**A theme is data, not code.** There is exactly one renderer — `ThemedCvRenderer`
(`JobTrackerApi/Services/ThemedCvRenderer.cs`) — with a single render path. Every visual difference
between themes is expressed by the fields of a `CvTheme` record (`Models/CvTheme.cs`). Adding a theme
between themes is expressed by the fields of a `CvTheme` record (`JobTrackerApi/Models/CvTheme.cs`). Adding a theme
never touches the renderer.
This replaces the previous approach (`CvTemplateRenderer`, one hand-written HTML method per template),
@@ -30,7 +30,7 @@ section keys move to the sidebar for the two-column layouts.
## Adding a theme
1. Append one `CvTheme { … }` to `CvThemeCatalog.Themes` (`Models/CvTheme.cs`). Only override the
1. Append one `CvTheme { … }` to `CvThemeCatalog.Themes` (`JobTrackerApi/Models/CvTheme.cs`). Only override the
fields that differ from the defaults.
2. Nothing else. It appears in `GET /api/cv/themes`, the Customize tab picker, and renders.
+14 -1
View File
@@ -1,3 +1,16 @@
# deployment
TODO: Complete documentation.
Production runs the frontend/nginx, ASP.NET API, AI sidecar, and configured database through Docker
Compose. The backend is not published directly; nginx proxies `/api`. `deploy/deploy.sh` validates
configuration, takes and verifies a provider-appropriate backup before replacement, builds/restarts the
stack, and performs health checks.
Production compose enables `Proxy:TrustForwardedHeaders` because nginx is the sole ingress, allowing
HTTPS scheme and client-IP rate limits to use one trusted forwarded hop. The development override
publishes the API directly and disables forwarded-header trust.
Each service rotates local Docker logs at 10 MB and retains three files. Add a central sink only if
cross-host search or longer retention becomes necessary.
Use `deploy/README.md`, `deploy/first-production-deployment.md`, and
`docs/deployment/backup-restore.md` as the operational runbooks.
+9 -1
View File
@@ -1,3 +1,11 @@
# future
TODO: Complete documentation.
Architecture evolves incrementally rather than through a rewrite. Current priorities are reducing large
controller/component orchestration boundaries, replacing the transitional link-compiled backend project,
adding durable background work if multi-instance deployment becomes necessary, and introducing browser
end-to-end coverage for critical user journeys.
Job-specific learning recommendations reuse deterministic match gaps and checklist state. Portfolio
content stays in public CVs. Broader course integrations or separate portfolio hosting remain out of
scope until concrete requirements justify new storage or service boundaries. Active items are tracked
in `docs/architecture/technical-debt.md` and `BLOCKERS.md`.
+48 -48
View File
@@ -1,56 +1,56 @@
# Technical Debt Management
# Technical debt
## Purpose
Last reconciled: 2026-07-31
Track known issues.
This ledger contains verified engineering debt only. Product ideas belong in the roadmaps and
operator/external dependencies belong in `BLOCKERS.md`.
---
## Resolved in the 2026-07-31 debt pass
# Categories
- Removed the 232 MB unused `vendor/saasable-ui-main` snapshot.
- Retired the link-compiling `JobTrackerBackend` project. The API now owns its controllers,
services, models, and `JobTrackerContext`; tests and tools reference the API directly.
- Added standard Problem Details responses with trace IDs and structured JSON production logs.
- Made trusted reverse-proxy headers explicit so rate limits see the real client address in the
production nginx topology.
- Made queued CV processing recover queued/running database work after restart and bypass the
request-only tenant filter with explicit owner checks.
- Pruned expired CV extraction runs and orphaned upload artifacts while preserving the current CV.
- Added configurable PDF export retention (`CvExports:RetainDays`, default 30 days).
- Corrected the remaining CRA-era environment-variable names after the Next.js migration.
- Removed React Router and `act(...)` warnings from the frontend tests.
- Replaced placeholder documentation stubs with concise, code-linked guidance.
- Removed stale locals and reconciled old TODO claims against the implemented code.
- Added a CI-gated Playwright smoke suite for login/session cookies, saved-job creation, Career
Workspace routing, and anonymous public-CV rendering/PDF download.
- Updated Axios and Next.js and overrode Next's vulnerable bundled Sharp/PostCSS versions; the only
remaining npm findings are the non-applicable/mitigated React Router items in `BLOCKERS.md`.
- Pinned SQLitePCLRaw 2.1.12 so the native SQLite runtime is no longer in the high-severity
CVE-2025-6965 range; the NuGet transitive vulnerability audit is clean.
- Made fresh SQLite startup reconcile schema-owned columns between historical EF migrations, so
strict SQLite identifier handling no longer breaks the later table rebuild.
- Updated Jest and Playwright, pinned the patched transitive brace-expansion package, and promoted
the full npm high-severity audit to a blocking CI gate.
- Corrected Chromium PDF export argument handling, bounded hung exports, and verified the returned
public artifact is a real PDF in the browser smoke suite.
- Removed unfinished Portfolio/Notes workspace navigation promises; existing project, attachment,
and application-note surfaces remain authoritative, and stale section links fall back to Overview.
## Architecture
## Remaining engineering debt
Examples:
| Priority | Debt | Current decision / trigger |
|---|---|---|
| P1 | `JobApplication` still duplicates opportunity data now owned by `Job`. | Keep the compatibility dual-write until a production-data backfill and restore rehearsal prove every application has a valid `JobId`; then remove the legacy columns in one migration. |
| P1 | Background workers assume one API instance. Restart recovery is durable, but there is no row lease for concurrent workers. | Add database leasing only before deploying more than one backend replica. |
| P2 | Production log aggregation is still deployment-owned; Compose now bounds each container's local logs to 3 × 10 MB. | Add an OTLP/Seq sink only before multi-host operation or when incident-response needs exceed `docker logs`. |
| P3 | `Tags` remains a JSON string and attachment-purpose booleans remain compatibility columns. | Normalize tags only when server-side tag querying becomes slow. Remove attachment flags only with an API/schema compatibility release; recomputation tests currently prevent drift. |
| P3 | Checklist reads seed and synchronize system-generated items, so the GET is intentionally non-cacheable. | Split read/write paths only if workspace read volume or caching makes the current idempotent behavior measurably costly. |
| P3 | Several orchestration files are large (`JobApplicationsController`, `StartupInitializationExtensions`, `JobDetailsDialog`). | File length alone is not a defect. Extract a cohesive slice only when the next behavior change touches it; avoid a standalone rewrite. |
- Large services.
- Tight coupling.
## Deliberate non-debt
---
## Frontend
Examples:
- Duplicate components.
- Complex state.
---
## Backend
Examples:
- Large controllers.
- Missing abstractions.
---
## Testing
Examples:
- Missing coverage.
---
# Rules
Do not fix everything immediately.
Prioritise:
Impact.
Risk.
User value.
- Free-text pipeline status is intentional so custom values are not destroyed.
- Next.js is the static build shell and React Router owns client navigation; changing routers has no
demonstrated user benefit.
- The CV queue is intentionally process-local for the current single-replica deployment.
- User-local untracked files at the repository root were not removed or modified.