diff --git a/docs/todo/work.md b/docs/todo/work.md index 0e47f7f..0c629c5 100644 --- a/docs/todo/work.md +++ b/docs/todo/work.md @@ -1,3 +1,42 @@ +# Active implementation checklist — 2026-08-24 + +## Current + +- [ ] Deploy the verified commits through the protected `main` workflow. + +## Next + +- [ ] Verify the live health/version endpoints and production UI after deployment. + +## Completed + +- [x] Revoke all sessions and trusted devices after administrator role mutations. +- [x] Isolate saved views, profile headlines and company caches by authenticated account. +- [x] Prevent stale resource requests from overwriting newer account/filter results. +- [x] Stream and cap remote job-import and CV summarizer inputs before parsing. +- [x] Apply workflow-readiness filters before pagination on the server. +- [x] Repair the CV builder unsaved-change navigation regression. +- [x] Restore the correspondence inbox to primary navigation and fix connected-account route metadata. +- [x] Bring recruiter status suggestions and durable strategy snapshots into the live application workspace. +- [x] Replace local-only notification toggles with a persisted per-user email reminder preference enforced by the worker. +- [x] Rebuild the CV editor as a responsive document studio with professional toolbar, autosave retry state, undo/redo and mobile edit/preview panes. +- [x] Add normalized colour, typography, spacing, page, date, language, skills and one/two-column layout controls shared by preview and PDF rendering. +- [x] Add real A4/Letter page stacks, overflow detection, fit/zoom/page navigation and long-content pagination safeguards. +- [x] Expand the structured master profile with GitHub/custom links and English/Norwegian labels/date rendering. +- [x] Upgrade CV management with a guided create flow, rendered template previews, useful job metadata, rename, duplicate, download and confirmed delete actions. +- [x] Add eight distinct data-driven templates, including ATS-safe, compact/two-column and photo-capable designs without duplicating renderer implementations. +- [x] Add explicit-review AI writing suggestions and one-click job-specific CV duplication/association. +- [x] Enforce server-side ownership for CV/job associations and revoke sessions after administrator role changes. +- [x] Make the standalone TypeScript check pass and remove obsolete React Router test flags. +- [x] Pass all 61 frontend suites (246 tests), standalone TypeScript, optimized production build, Python syntax, Compose config and diff checks. +- [x] Install .NET SDK 9.0.317 and configure the repository Git author from its established commit history. +- [x] Pass all 692 backend tests, 24 AI-service tests, 9 Playwright flows, dependency audits and all three production Docker image builds. +- [x] Reduce the frontend Docker build context from roughly 662 MB to about 10 KB by excluding generated artifacts. +- [x] Commit the release in three logical, fully verified batches. +- [x] Update CV architecture and verification notes with the rebuilt workflow and remaining environment gates. + +--- + Continue working on JobTracker using the completed audit under `docs/audits/`. This is now an implementation task. Read all audit reports, evidence, repository instructions, architecture documentation, and the current git status before changing code. diff --git a/job-tracker-ui/.dockerignore b/job-tracker-ui/.dockerignore index 3f8491a..8eccc99 100644 --- a/job-tracker-ui/.dockerignore +++ b/job-tracker-ui/.dockerignore @@ -1,5 +1,10 @@ node_modules build +.next +out +coverage +test-results +playwright-report .git .vscode npm-debug.log diff --git a/job-tracker-ui/e2e/job-discovery.spec.ts b/job-tracker-ui/e2e/job-discovery.spec.ts index 9abd507..905fb82 100644 --- a/job-tracker-ui/e2e/job-discovery.spec.ts +++ b/job-tracker-ui/e2e/job-discovery.spec.ts @@ -16,6 +16,7 @@ test.beforeEach(async ({ page }) => { }); test("discovery remains usable across widths and opens the reviewed import", async ({ page }) => { + test.setTimeout(60_000); await page.route("**/api/job-discovery/search**", async (route) => { await route.fulfill({ json: [ { diff --git a/job-tracker-ui/src/LandingPage.authRedirect.test.tsx b/job-tracker-ui/src/LandingPage.authRedirect.test.tsx index ba2dcb8..ed707f2 100644 --- a/job-tracker-ui/src/LandingPage.authRedirect.test.tsx +++ b/job-tracker-ui/src/LandingPage.authRedirect.test.tsx @@ -31,7 +31,7 @@ describe("LandingPage forwards deep-link intent to /login", () => { mockedApi.get.mockRejectedValueOnce(new Error("401")); // /auth/me: not signed in render( - + } /> } /> diff --git a/job-tracker-ui/src/application-workspace-overlay.test.tsx b/job-tracker-ui/src/application-workspace-overlay.test.tsx index 49edb94..ee784b9 100644 --- a/job-tracker-ui/src/application-workspace-overlay.test.tsx +++ b/job-tracker-ui/src/application-workspace-overlay.test.tsx @@ -232,6 +232,7 @@ test("hydrates list filters, sort and page from a shareable URL", async () => { companyId: 1, location: "Oslo", needsFollowUp: true, + readiness: "interview", includeDeleted: true, sortBy: "company", sortDir: "asc", diff --git a/job-tracker-ui/src/correspondence-inbox-page.test.tsx b/job-tracker-ui/src/correspondence-inbox-page.test.tsx index 1cb94ff..acbc360 100644 --- a/job-tracker-ui/src/correspondence-inbox-page.test.tsx +++ b/job-tracker-ui/src/correspondence-inbox-page.test.tsx @@ -27,7 +27,7 @@ function renderPage() { - + @@ -480,7 +480,7 @@ describe('CorrespondenceInboxPage', () => { - + } /> } /> diff --git a/job-tracker-ui/src/daily-control-loop.test.tsx b/job-tracker-ui/src/daily-control-loop.test.tsx index be3272c..5094c4b 100644 --- a/job-tracker-ui/src/daily-control-loop.test.tsx +++ b/job-tracker-ui/src/daily-control-loop.test.tsx @@ -163,7 +163,7 @@ function renderLoop(initialPath: string) { - + } /> diff --git a/job-tracker-ui/src/end-to-end-trust-loop.test.tsx b/job-tracker-ui/src/end-to-end-trust-loop.test.tsx index e416eca..4507431 100644 --- a/job-tracker-ui/src/end-to-end-trust-loop.test.tsx +++ b/job-tracker-ui/src/end-to-end-trust-loop.test.tsx @@ -65,7 +65,7 @@ function renderLoop(initialPath = '/jobs') { - + {}} columns={{ status: true, dateApplied: true, daysSince: true, jobUrl: false }} onColumnsChange={() => {}} mode="jobs" />} /> diff --git a/job-tracker-ui/src/gmail-review-page.test.tsx b/job-tracker-ui/src/gmail-review-page.test.tsx index 30db7d0..e8f6acb 100644 --- a/job-tracker-ui/src/gmail-review-page.test.tsx +++ b/job-tracker-ui/src/gmail-review-page.test.tsx @@ -25,7 +25,7 @@ function renderPage() { return render( - + diff --git a/job-tracker-ui/src/job-discovery.test.tsx b/job-tracker-ui/src/job-discovery.test.tsx index b491ae7..c537de7 100644 --- a/job-tracker-ui/src/job-discovery.test.tsx +++ b/job-tracker-ui/src/job-discovery.test.tsx @@ -12,7 +12,7 @@ afterEach(() => jest.clearAllMocks()); test("shows honest source metadata and offers the existing reviewed save flow", async () => { mockedApi.get.mockResolvedValue({ data: [{ id: "abc", title: "Backend Developer", company: "Acme", location: "OSLO", modifiedAt: "2026-08-01T10:00:00Z", deadline: "2026-08-15T23:59:59Z", url: "https://arbeidsplassen.nav.no/stillinger/stilling/abc", source: "nav", sourceName: "NAV Arbeidsplassen", acquisitionType: "searched", retrievedAt: "2026-08-10T10:00:00Z", countryCode: "NO" }] } as any); - render(); + render(); fireEvent.change(screen.getByLabelText("Role or company"), { target: { value: "backend" } }); fireEvent.click(screen.getByRole("button", { name: "Search NAV" })); @@ -27,7 +27,7 @@ test("shows honest source metadata and offers the existing reviewed save flow", test("distinguishes the initial guidance from a completed empty search", async () => { mockedApi.get.mockResolvedValue({ data: [] } as any); - render(); + render(); expect(screen.getByText("Search recent active vacancies by role, company, or municipality.")).toBeInTheDocument(); fireEvent.click(screen.getByRole("button", { name: "Search NAV" })); @@ -38,7 +38,7 @@ test("distinguishes the initial guidance from a completed empty search", async ( test("retries the last submitted search after an error", async () => { mockedApi.get.mockRejectedValueOnce(new Error("offline")).mockResolvedValueOnce({ data: [] } as any); - render(); + render(); fireEvent.change(screen.getByLabelText("Role or company"), { target: { value: " utvikler " } }); fireEvent.change(screen.getByLabelText("Municipality"), { target: { value: " Oslo " } }); @@ -54,7 +54,7 @@ test("sorts returned vacancies by title", async () => { { id: "2", title: "Zoologist", url: "https://arbeidsplassen.nav.no/2", source: "nav", countryCode: "NO" }, { id: "1", title: "Analyst", url: "https://arbeidsplassen.nav.no/1", source: "nav", countryCode: "NO" }, ] } as any); - render(); + render(); fireEvent.click(screen.getByRole("button", { name: "Search NAV" })); await screen.findByText("2 vacancies found"); diff --git a/job-tracker-ui/src/landing-page.test.tsx b/job-tracker-ui/src/landing-page.test.tsx index dcd7632..12bac2b 100644 --- a/job-tracker-ui/src/landing-page.test.tsx +++ b/job-tracker-ui/src/landing-page.test.tsx @@ -21,7 +21,7 @@ beforeEach(() => { test("shows exactly Free and Pro without invented commercial terms", async () => { render( - + , ); diff --git a/job-tracker-ui/src/login-page.test.tsx b/job-tracker-ui/src/login-page.test.tsx index 4b8462b..ae29ed6 100644 --- a/job-tracker-ui/src/login-page.test.tsx +++ b/job-tracker-ui/src/login-page.test.tsx @@ -33,7 +33,7 @@ let consoleErrorSpy: jest.SpyInstance; function renderLoginPage(initialMode: "login" | "register" = "login", from?: string) { return render( - + diff --git a/job-tracker-ui/src/microsoft-legacy-relink-page.test.tsx b/job-tracker-ui/src/microsoft-legacy-relink-page.test.tsx index 49454ee..48ccbbb 100644 --- a/job-tracker-ui/src/microsoft-legacy-relink-page.test.tsx +++ b/job-tracker-ui/src/microsoft-legacy-relink-page.test.tsx @@ -27,7 +27,7 @@ test("legacy Microsoft recovery requires the emailed proof and a fresh matching window.history.pushState({}, "", "/microsoft-legacy-relink?userId=user-1&tenantId=tenant-1&objectId=object-1&token=recovery-token"); render( - + , ); diff --git a/job-tracker-ui/src/nginx-config.test.ts b/job-tracker-ui/src/nginx-config.test.ts index 2487214..23b8a21 100644 --- a/job-tracker-ui/src/nginx-config.test.ts +++ b/job-tracker-ui/src/nginx-config.test.ts @@ -4,5 +4,5 @@ import path from "node:path"; test("canonical-host health reaches the backend instead of the SPA fallback", () => { const config = fs.readFileSync(path.join(process.cwd(), "nginx.conf.template"), "utf8"); - expect(config).toMatch(/location = \/health\s*{[^}]*proxy_pass http:\/\/backend-web:8080\/health;/s); + expect(config).toMatch(/location = \/health\s*{[\s\S]*?proxy_pass http:\/\/backend-web:8080\/health;/); }); diff --git a/job-tracker-ui/src/onboarding-checklist.test.tsx b/job-tracker-ui/src/onboarding-checklist.test.tsx index 9f14793..0bb3fa8 100644 --- a/job-tracker-ui/src/onboarding-checklist.test.tsx +++ b/job-tracker-ui/src/onboarding-checklist.test.tsx @@ -14,7 +14,7 @@ jest.mock("./api", () => ({ const mockedApi = api as jest.Mocked; function renderChecklist() { - return render(); + return render(); } test("uses structured career data and connected inbox status for onboarding progress", async () => { diff --git a/job-tracker-ui/src/verify-email-page.test.tsx b/job-tracker-ui/src/verify-email-page.test.tsx index cc888a8..0f996a9 100644 --- a/job-tracker-ui/src/verify-email-page.test.tsx +++ b/job-tracker-ui/src/verify-email-page.test.tsx @@ -16,7 +16,7 @@ function renderVerifyEmailPage(search: string, emailChange = false) { const path = emailChange ? '/confirm-email-change' : '/verify-email'; window.history.pushState({}, '', `${path}${search}`); return render( - +