From 682a3028bb0bb014e74aa6bb2e5c7a7b341b8e60 Mon Sep 17 00:00:00 2001 From: cesnimda Date: Fri, 11 Sep 2026 14:22:41 +0200 Subject: [PATCH] docs: consolidate repository guides and preserve audit history --- .env.example | 116 +++-- .gitignore | 5 + BLOCKERS.md | 8 +- .../ApiAuthorizationPostureTests.cs | 2 +- .../Controllers/AiWorkspaceController.cs | 2 +- .../ApplicationAssetsController.cs | 2 +- .../ApplicationChecklistController.cs | 2 +- .../ApplicationIntelligenceController.cs | 2 +- .../ApplicationWorkspaceController.cs | 2 +- .../Controllers/CareerProfileController.cs | 2 +- .../Controllers/CompaniesController.cs | 2 +- .../Controllers/CorrespondenceController.cs | 2 +- .../Controllers/CvVariantController.cs | 2 +- .../Controllers/InterviewPrepController.cs | 2 +- .../Controllers/JobApplicationsController.cs | 4 +- .../Controllers/JobImportController.cs | 3 +- .../Controllers/PublicCvController.cs | 2 +- JobTrackerApi/Controllers/RulesController.cs | 2 +- JobTrackerApi/Data/JobTrackerContext.cs | 14 +- ...2917_AddCareerProfileRelationalChildren.cs | 2 +- .../20260719120954_AddCoverLetterVersions.cs | 2 +- .../20260719145044_AddInterviewPrepItems.cs | 2 +- JobTrackerApi/Models/AiInteraction.cs | 2 +- .../Models/ApplicationChecklistItem.cs | 2 +- JobTrackerApi/Models/CareerEntities.cs | 2 +- JobTrackerApi/Models/CareerProfile.cs | 2 +- JobTrackerApi/Models/CvTheme.cs | 2 +- JobTrackerApi/Models/CvVariant.cs | 2 +- JobTrackerApi/Program.cs | 2 +- .../Services/ApplicationAssetsService.cs | 2 +- .../Services/ApplicationChecklistService.cs | 2 +- .../ApplicationIntelligenceService.cs | 2 +- .../Services/ApplicationTimelineService.cs | 2 +- .../Services/ApplicationWorkspaceService.cs | 2 +- JobTrackerApi/Services/BuildMetadata.cs | 2 +- JobTrackerApi/Services/CareerProfileMapper.cs | 2 +- .../Services/CareerProfileService.cs | 2 +- .../Services/EmailProviders/IEmailProvider.cs | 2 +- .../Services/InterviewPrepService.cs | 2 +- JobTrackerApi/Services/JobLifecycleEvents.cs | 2 +- .../StartupInitializationExtensions.cs | 4 +- JobTrackerApi/Services/ThemedCvRenderer.cs | 2 +- README.md | 313 +------------ deploy/README.md | 351 +------------- deploy/deploy.sh | 2 +- docs/README.md | 35 ++ .../pre-docs-cleanup-2026-09-11/BLOCKERS.md | 43 ++ .../pre-docs-cleanup-2026-09-11/CV_Changes.md | 0 .../pre-docs-cleanup-2026-09-11/INVENTORY.md | 29 ++ .../pre-docs-cleanup-2026-09-11/README.md | 7 + .../SMART_GMAIL_PROGRESS.md | 0 .../browser-extension/README.md | 20 + .../deploy/README.md | 350 ++++++++++++++ .../docs}/00-ai-context.md | 0 .../docs}/01-glossary.md | 0 .../docs}/02-product-principles.md | 0 .../docs}/03-design-principles.md | 0 .../docs}/AI_SESSION_START.md | 0 .../docs}/CHANGELOG_AI_WORK.md | 0 .../docs}/MASTER_IMPLEMENTATION_GUIDE.md | 0 .../docs}/ai/ai-principles.md | 0 .../docs}/ai/cost-control.md | 0 .../docs}/ai/cover-letter-ai.md | 0 .../docs}/ai/cv-ai.md | 0 .../docs}/ai/future-ai.md | 0 .../docs}/ai/jobs-ai.md | 0 .../docs}/ai/model-selection.md | 0 .../docs}/ai/overview.md | 0 .../docs}/ai/privacy.md | 0 .../docs/ai/prompts.md | 51 +++ .../docs/ai/providers.md | 66 +++ .../docs/ai/safety.md | 8 + .../docs}/ai/security.md | 0 .../docs}/ai/workload-inventory.md | 0 .../docs}/application-discovery-report.md | 0 .../docs}/architecture/ai-architecture.md | 0 .../docs}/architecture/ai-career-assistant.md | 0 .../docs}/architecture/ai-privacy.md | 0 .../docs}/architecture/api-design.md | 0 .../architecture/application-workspace.md | 0 .../docs}/architecture/attachment-storage.md | 0 .../docs}/architecture/authentication.md | 0 .../docs/architecture/backend.md | 75 +++ .../docs}/architecture/background-jobs.md | 0 .../docs}/architecture/background-workers.md | 0 .../docs}/architecture/career-architecture.md | 0 .../architecture/career-profile-model.md | 0 .../docs/architecture/current.md | 431 +++++++++++++++++ .../docs}/architecture/cv-builder.md | 0 .../docs}/architecture/cv-rendering.md | 0 .../docs}/architecture/cv-theme-engine.md | 0 .../docs}/architecture/database.md | 0 .../docs}/architecture/deployment.md | 0 .../docs}/architecture/durable-operations.md | 0 .../docs}/architecture/file-storage.md | 0 .../docs/architecture/frontend.md | 64 +++ .../docs}/architecture/future.md | 0 .../docs}/architecture/migrations.md | 0 .../docs}/architecture/notifications.md | 0 .../docs/architecture/overview.md | 93 ++++ .../docs}/architecture/principles.md | 0 .../docs}/architecture/technical-debt.md | 0 .../docs}/audits/2026-09-07/README.md | 0 .../docs}/audits/2026-09-07/coverage.md | 0 .../docs}/audits/2026-09-07/findings.md | 0 .../audits/2026-09-07/product-operations.md | 0 .../docs}/audits/2026-09-07/resilience.md | 0 .../docs}/audits/2026-09-07/source-index.md | 0 .../docs}/audits/2026-09-07/threat-data.md | 0 .../audits/2026-09-07/validation/README.md | 228 +++++++++ .../2026-09-07/validation/b01-completion.md | 0 .../2026-09-07/validation/b02-completion.md | 0 .../2026-09-07/validation/b03-completion.md | 0 .../2026-09-07/validation/b04-completion.md | 0 .../2026-09-07/validation/b05-completion.md | 0 .../2026-09-07/validation/b06-completion.md | 404 ++++++++++++++++ .../audits/2026-09-07/validation/evidence.md | 0 .../evidence/b02-browser-config.ts.txt | 0 .../evidence/b06-generated-nginx.conf.txt | 0 .../evidence/b06-runtime-public-probe.py.txt | 0 .../evidence/b06-topology-blocked.txt | 0 .../validation/implementation-queue.md | 132 ++++++ .../docs}/audits/2026-09-07/verification.md | 0 .../docs}/audits/audit-progress.md | 0 .../docs}/audits/audit-remediation-backlog.md | 0 .../audits/evidence/accessibility-evidence.md | 0 .../docs}/audits/evidence/ai-001/README.md | 0 .../docs}/audits/evidence/ai-002/README.md | 0 .../docs}/audits/evidence/ai-003/README.md | 0 .../docs}/audits/evidence/ai-004/README.md | 0 .../docs}/audits/evidence/browser-evidence.md | 0 .../audits/evidence/career-001/README.md | 0 .../audits/evidence/career-002/README.md | 0 .../core-001-runtime/synthetic-attachment.txt | 0 .../audits/evidence/dependency-evidence.md | 0 .../docs}/audits/evidence/mail-001/README.md | 0 .../docs}/audits/evidence/pol-001/README.md | 0 .../docs}/audits/evidence/pol-002/README.md | 0 .../docs}/audits/evidence/prod-002/README.md | 0 .../docs}/audits/evidence/qa-001/README.md | 0 .../audits/evidence/repository-inventory.md | 0 .../docs}/audits/evidence/runtime-evidence.md | 0 .../audits/evidence/two-user-isolation.md | 0 .../docs}/audits/evidence/ux-001/README.md | 0 .../docs}/audits/evidence/ux-002/README.md | 0 .../docs}/audits/full-application-audit.md | 0 .../docs}/audits/security-threat-model.md | 0 .../docs}/audits/user-journey-audit.md | 0 .../docs}/audits/verification-log.md | 0 .../docs}/auth/2fa.md | 0 .../docs}/auth/login.md | 0 .../docs}/auth/oauth.md | 0 .../docs}/auth/overview.md | 0 .../docs}/auth/signup.md | 0 .../career-workspace-branch-assessment.md | 0 ...career-workspace-implementation-roadmap.md | 0 .../career-workspace-product-strategy.md | 0 .../docs}/career-workspace-ux-refactor.md | 0 .../docs}/career/career-profile.md | 0 .../docs}/career/cover-letters.md | 0 .../docs}/career/cv-ai.md | 0 .../docs}/career/cv-rendering.md | 0 .../docs}/career/cv-tailoring.md | 0 .../docs}/career/cv-themes.md | 0 .../docs}/career/documents.md | 0 .../docs}/career/education.md | 0 .../docs}/career/experience.md | 0 .../docs}/career/future-features.md | 0 .../docs}/career/master-profile.md | 0 .../docs}/career/overview.md | 0 .../docs}/career/portfolio.md | 0 .../docs}/career/projects.md | 0 .../docs}/career/public-profile.md | 0 .../docs}/career/skills.md | 0 .../docs}/career/versioning.md | 0 .../docs}/cover-letters/ai.md | 0 .../docs}/cover-letters/builder.md | 0 .../docs}/cover-letters/overview.md | 0 .../cv-builder-competitor-deep-research.md | 0 .../docs}/cv-builder-product-teardown.md | 0 .../docs}/cv-builder/ai.md | 0 .../docs}/cv-builder/content.md | 0 .../docs}/cv-builder/customisation.md | 0 .../docs}/cv-builder/exports.md | 0 .../docs}/cv-builder/overview.md | 0 .../docs}/cv-builder/rendering.md | 0 .../docs}/cv-builder/themes.md | 0 .../docs}/cv-builder/workflow.md | 0 .../ADR-002-job-application-model.md | 113 +++++ .../decisions/ADR-004-ai-provider-system.md | 23 + .../docs}/deferred-inputs.md | 0 .../docs}/deployment/backup-restore.md | 0 .../docs}/deployment/manual-smoke-test.md | 0 .../docs/documentation-rules.md | 9 + .../docs}/implementation-roadmap.md | 0 .../infrastructure/database-ownership.md | 0 .../infrastructure/runner-investigation.md | 0 .../docs}/jobs/adding-jobs.md | 0 .../docs}/jobs/analytics.md | 0 .../docs}/jobs/applications.md | 0 .../docs}/jobs/communication-tracking.md | 0 .../docs}/jobs/company-management.md | 0 .../docs}/jobs/followups.md | 0 .../docs}/jobs/future-features.md | 0 .../docs}/jobs/job-import.md | 0 .../docs}/jobs/job-lifecycle.md | 0 .../docs}/jobs/job-search.md | 0 .../docs}/jobs/overview.md | 0 .../docs}/operations/calendar-integrations.md | 0 .../data-protection-key-rotation.md | 0 .../docs}/operations/google-drive-backups.md | 0 .../operations/job-opportunity-cutover.md | 0 .../production-backup-verification.md | 0 .../docs}/operations/stripe-activation.md | 0 .../stripe-billing-setup-walkthrough.md | 0 .../docs}/phase-0-foundation-report.md | 0 .../docs}/phase-2-1-cv-extraction-review.md | 0 .../docs}/phase-5-completion-report.md | 0 .../post-audit-ux-reliability-program.md | 0 .../docs}/product/business-model.md | 0 .../docs}/product/goals.md | 0 .../docs}/product/mission.md | 0 .../docs}/product/non-goals.md | 0 .../docs}/product/personas.md | 0 .../docs}/product/roadmap.md | 0 .../docs}/product/vision.md | 0 .../docs}/production-readiness-review.md | 0 .../production/ollama-model-benchmark.md | 0 .../production-ai-hardware-assessment.md | 0 .../production-ai-rollout-and-rollback.md | 0 .../production/production-ai-validation.md | 0 .../docs}/release-candidate-review.md | 0 .../docs}/release-checklist.md | 0 .../docs}/release-final-report.md | 0 .../docs}/research/competitors.md | 0 .../docs}/research/cv-builder-patterns.md | 0 .../docs}/research/elegantcv-analysis.md | 0 .../docs}/research/feature-comparison.md | 0 .../docs}/research/flowcv-analysis.md | 0 .../docs}/research/job-platform-analysis.md | 0 .../docs}/research/jobscan.md | 0 .../docs}/research/market-positioning.md | 0 .../docs}/research/novoresume-analysis.md | 0 .../docs}/research/overview.md | 0 .../research/reactive-resume-analysis.md | 0 .../docs}/research/recommendations.md | 0 .../docs}/research/saas-analysis.md | 0 .../docs}/security/ai-security.md | 0 .../docs}/security/api-security.md | 0 .../docs/security/authentication.md | 84 ++++ .../docs/security/authorization.md | 43 ++ .../docs/security/file-security.md | 56 +++ .../docs}/security/input-validation.md | 0 .../docs}/security/overview.md | 0 .../docs}/security/privacy.md | 0 .../docs}/security/saas-security.md | 0 .../docs}/security/supply-chain-policy.md | 0 .../security/two-factor-authentication.md | 0 .../docs}/security/uploads.md | 0 .../docs}/security/user-data.md | 0 .../docs}/security/validation.md | 0 .../docs}/technical/coding-standards.md | 0 .../docs}/technical/deployment.md | 0 .../docs}/technical/docker.md | 0 .../docs}/technical/environment.md | 0 .../docs}/technical/git-workflow.md | 0 .../docs}/technical/logging.md | 0 .../docs}/technical/monitoring.md | 0 .../docs}/technical/overview.md | 0 .../docs}/technical/performance.md | 0 .../docs}/technical/testing.md | 0 .../docs}/technical/troubleshooting.md | 0 .../docs}/todo/ollama.md | 0 .../docs}/todo/work.md | 2 + .../docs}/ux/accessibility.md | 0 .../docs}/ux/dashboard.md | 0 .../docs}/ux/design-system.md | 0 .../docs}/ux/empty-states.md | 0 .../docs}/ux/errors.md | 0 .../docs}/ux/loading-states.md | 0 .../docs}/ux/navigation.md | 0 .../docs}/ux/notifications.md | 0 .../docs}/ux/onboarding.md | 0 .../docs}/ux/profile.md | 0 .../docs}/ux/responsive.md | 0 .../docs}/ux/settings.md | 0 .../docs}/ux/user-flows.md | 0 .../verification/ai-001-durable-ai-queue.md | 0 .../verification/ai-002-provider-routing.md | 0 .../ai-003-strategy-snapshot-queue.md | 0 .../ai-004-cv-processing-queue.md | 0 .../verification/application-action-matrix.md | 0 .../verification/bg-001-tenant-workers.md | 0 .../career-001-career-workspace.md | 0 .../verification/career-002-cv-builder.md | 0 .../core-001-sqlite-provider-parity.md | 0 .../verification/core-002-route-uniqueness.md | 0 .../dep-001-frontend-advisories.md | 0 .../verification/jobs-001-job-discovery.md | 0 .../jobs-002-application-workspace.md | 0 .../verification/jt-017-dotnet-provenance.md | 0 .../jt-018-developer-documentation.md | 0 .../verification/jt-019-schema-ownership.md | 0 .../verification/mail-001-job-email-hub.md | 0 .../ops-001a-durable-operations.md | 0 .../verification/ops-001b-notifications.md | 0 .../verification/ops-001c-operation-ui.md | 0 .../pol-001-free-pro-entitlements.md | 0 .../docs}/verification/pol-002-ai-privacy.md | 0 .../verification/prod-002-ai-evaluation.md | 0 .../verification/product-001-honest-plans.md | 0 .../verification/qa-001-job-term-quality.md | 0 .../verification/sec-001-canonical-origin.md | 0 .../verification/sec-002-ingress-compose.md | 0 .../verification/sec-003-microsoft-tenant.md | 0 .../sec-004-microsoft-identity.md | 0 .../sec-005a-session-revocation.md | 0 .../verification/sec-005b-email-ownership.md | 0 .../sec-006-parser-dependencies.md | 0 .../verification/sec-007-parser-isolation.md | 0 .../sec-008-attachment-consistency.md | 0 .../verification/sec-009-account-lifecycle.md | 0 .../ux-001-unified-authentication.md | 0 .../ux-002-deterministic-theme-state.md | 0 .../docs}/verification/ux-003-kanban-theme.md | 0 .../ver-001-complete-regression.md | 0 .../docs}/work-programmes/decisions.md | 0 .../docs}/work-programmes/master-progress.md | 0 .../docs}/work-programmes/master-work-plan.md | 0 .../docs}/work-programmes/session-handoff.md | 0 .../job-tracker-ui/README.md | 64 +++ .../tools/summarizer/README.md | 94 ++++ docs/ai/README.md | 13 + docs/ai/architecture.md | 7 + docs/ai/document-processing.md | 5 + docs/ai/prompts.md | 52 +-- docs/ai/providers.md | 67 +-- docs/ai/safety.md | 9 +- docs/architecture/backend.md | 76 +-- docs/architecture/current.md | 432 +----------------- docs/architecture/data-flow.md | 17 + docs/architecture/frontend.md | 65 +-- docs/architecture/integrations.md | 16 + docs/architecture/overview.md | 115 ++--- docs/audits/2026-09-07/validation/README.md | 229 +--------- .../2026-09-07/validation/b06-completion.md | 405 +--------------- .../validation/implementation-queue.md | 133 +----- docs/backend/README.md | 10 + docs/backend/api.md | 18 + docs/backend/background-jobs.md | 5 + docs/backend/error-handling.md | 5 + docs/career/cv-builder | 0 docs/configuration/README.md | 5 + docs/configuration/environment-variables.md | 114 +++++ docs/configuration/environments.md | 13 + docs/database/README.md | 10 + docs/database/backups.md | 5 + docs/database/migrations.md | 39 ++ docs/database/schema.md | 7 + .../ADR-002-job-application-model.md | 8 +- docs/decisions/README.md | 3 + docs/development/README.md | 9 + docs/development/conventions.md | 5 + docs/development/getting-started.md | 17 + docs/development/troubleshooting.md | 12 + docs/documentation-rules.md | 17 +- docs/frontend/README.md | 13 + docs/frontend/localisation-accessibility.md | 5 + docs/frontend/navigation.md | 5 + docs/frontend/ui-system.md | 5 + docs/infrastructure/README.md | 11 + docs/infrastructure/backups.md | 13 + docs/infrastructure/deployment.md | 19 + docs/infrastructure/disaster-recovery.md | 5 + docs/infrastructure/docker.md | 5 + docs/infrastructure/monitoring.md | 5 + docs/infrastructure/stripe-activation.md | 35 ++ docs/product/README.md | 11 + docs/product/constraints.md | 9 + docs/product/features.md | 8 + docs/product/terminology.md | 11 + docs/redesign/README.md | 24 +- docs/roadmap/README.md | 9 + docs/roadmap/known-issues.md | 15 + docs/roadmap/technical-debt.md | 16 + docs/roadmap/todo.md | 25 + docs/security/README.md | 11 + docs/security/authentication.md | 83 +--- docs/security/authorization.md | 44 +- docs/security/file-security.md | 57 +-- docs/security/security-notes.md | 16 + docs/security/sessions.md | 5 + docs/testing/README.md | 8 + docs/testing/backend.md | 9 + docs/testing/browser.md | 5 + docs/testing/frontend.md | 13 + docs/testing/security.md | 5 + job-tracker-ui/README.md | 56 +-- .../src/components/ApplicationAssets.tsx | 2 +- .../src/components/ApplicationChecklist.tsx | 2 +- .../components/ApplicationIntelligence.tsx | 2 +- .../src/components/InterviewPrep.tsx | 2 +- .../src/views/ApplicationWorkspacePage.tsx | 2 +- tools/summarizer/README.md | 95 +--- 404 files changed, 3402 insertions(+), 2621 deletions(-) create mode 100644 docs/README.md create mode 100644 docs/_archive/pre-docs-cleanup-2026-09-11/BLOCKERS.md rename CV_Changes.md => docs/_archive/pre-docs-cleanup-2026-09-11/CV_Changes.md (100%) create mode 100644 docs/_archive/pre-docs-cleanup-2026-09-11/INVENTORY.md create mode 100644 docs/_archive/pre-docs-cleanup-2026-09-11/README.md rename SMART_GMAIL_PROGRESS.md => docs/_archive/pre-docs-cleanup-2026-09-11/SMART_GMAIL_PROGRESS.md (100%) create mode 100644 docs/_archive/pre-docs-cleanup-2026-09-11/browser-extension/README.md create mode 100644 docs/_archive/pre-docs-cleanup-2026-09-11/deploy/README.md rename docs/{ => _archive/pre-docs-cleanup-2026-09-11/docs}/00-ai-context.md (100%) rename docs/{ => _archive/pre-docs-cleanup-2026-09-11/docs}/01-glossary.md (100%) rename docs/{ => _archive/pre-docs-cleanup-2026-09-11/docs}/02-product-principles.md (100%) rename docs/{ => _archive/pre-docs-cleanup-2026-09-11/docs}/03-design-principles.md (100%) rename docs/{ => _archive/pre-docs-cleanup-2026-09-11/docs}/AI_SESSION_START.md (100%) rename docs/{ => _archive/pre-docs-cleanup-2026-09-11/docs}/CHANGELOG_AI_WORK.md (100%) rename docs/{ => _archive/pre-docs-cleanup-2026-09-11/docs}/MASTER_IMPLEMENTATION_GUIDE.md (100%) rename docs/{ => _archive/pre-docs-cleanup-2026-09-11/docs}/ai/ai-principles.md (100%) rename docs/{ => _archive/pre-docs-cleanup-2026-09-11/docs}/ai/cost-control.md (100%) rename docs/{ => _archive/pre-docs-cleanup-2026-09-11/docs}/ai/cover-letter-ai.md (100%) rename docs/{ => _archive/pre-docs-cleanup-2026-09-11/docs}/ai/cv-ai.md (100%) rename docs/{ => _archive/pre-docs-cleanup-2026-09-11/docs}/ai/future-ai.md (100%) rename docs/{ => _archive/pre-docs-cleanup-2026-09-11/docs}/ai/jobs-ai.md (100%) rename docs/{ => _archive/pre-docs-cleanup-2026-09-11/docs}/ai/model-selection.md (100%) rename docs/{ => _archive/pre-docs-cleanup-2026-09-11/docs}/ai/overview.md (100%) rename docs/{ => _archive/pre-docs-cleanup-2026-09-11/docs}/ai/privacy.md (100%) create mode 100644 docs/_archive/pre-docs-cleanup-2026-09-11/docs/ai/prompts.md create mode 100644 docs/_archive/pre-docs-cleanup-2026-09-11/docs/ai/providers.md create mode 100644 docs/_archive/pre-docs-cleanup-2026-09-11/docs/ai/safety.md rename docs/{ => _archive/pre-docs-cleanup-2026-09-11/docs}/ai/security.md (100%) rename docs/{ => _archive/pre-docs-cleanup-2026-09-11/docs}/ai/workload-inventory.md (100%) rename docs/{ => _archive/pre-docs-cleanup-2026-09-11/docs}/application-discovery-report.md (100%) rename docs/{ => _archive/pre-docs-cleanup-2026-09-11/docs}/architecture/ai-architecture.md (100%) rename docs/{ => _archive/pre-docs-cleanup-2026-09-11/docs}/architecture/ai-career-assistant.md (100%) rename docs/{ => _archive/pre-docs-cleanup-2026-09-11/docs}/architecture/ai-privacy.md (100%) rename docs/{ => _archive/pre-docs-cleanup-2026-09-11/docs}/architecture/api-design.md (100%) rename docs/{ => _archive/pre-docs-cleanup-2026-09-11/docs}/architecture/application-workspace.md (100%) rename docs/{ => _archive/pre-docs-cleanup-2026-09-11/docs}/architecture/attachment-storage.md (100%) rename docs/{ => _archive/pre-docs-cleanup-2026-09-11/docs}/architecture/authentication.md (100%) create mode 100644 docs/_archive/pre-docs-cleanup-2026-09-11/docs/architecture/backend.md rename docs/{ => _archive/pre-docs-cleanup-2026-09-11/docs}/architecture/background-jobs.md (100%) rename docs/{ => _archive/pre-docs-cleanup-2026-09-11/docs}/architecture/background-workers.md (100%) rename docs/{ => _archive/pre-docs-cleanup-2026-09-11/docs}/architecture/career-architecture.md (100%) rename docs/{ => _archive/pre-docs-cleanup-2026-09-11/docs}/architecture/career-profile-model.md (100%) create mode 100644 docs/_archive/pre-docs-cleanup-2026-09-11/docs/architecture/current.md rename docs/{ => _archive/pre-docs-cleanup-2026-09-11/docs}/architecture/cv-builder.md (100%) rename docs/{ => _archive/pre-docs-cleanup-2026-09-11/docs}/architecture/cv-rendering.md (100%) rename docs/{ => _archive/pre-docs-cleanup-2026-09-11/docs}/architecture/cv-theme-engine.md (100%) rename docs/{ => _archive/pre-docs-cleanup-2026-09-11/docs}/architecture/database.md (100%) rename docs/{ => _archive/pre-docs-cleanup-2026-09-11/docs}/architecture/deployment.md (100%) rename docs/{ => _archive/pre-docs-cleanup-2026-09-11/docs}/architecture/durable-operations.md (100%) rename docs/{ => _archive/pre-docs-cleanup-2026-09-11/docs}/architecture/file-storage.md (100%) create mode 100644 docs/_archive/pre-docs-cleanup-2026-09-11/docs/architecture/frontend.md rename docs/{ => _archive/pre-docs-cleanup-2026-09-11/docs}/architecture/future.md (100%) rename docs/{ => _archive/pre-docs-cleanup-2026-09-11/docs}/architecture/migrations.md (100%) rename docs/{ => _archive/pre-docs-cleanup-2026-09-11/docs}/architecture/notifications.md (100%) create mode 100644 docs/_archive/pre-docs-cleanup-2026-09-11/docs/architecture/overview.md rename docs/{ => _archive/pre-docs-cleanup-2026-09-11/docs}/architecture/principles.md (100%) rename docs/{ => _archive/pre-docs-cleanup-2026-09-11/docs}/architecture/technical-debt.md (100%) rename docs/{ => _archive/pre-docs-cleanup-2026-09-11/docs}/audits/2026-09-07/README.md (100%) rename docs/{ => _archive/pre-docs-cleanup-2026-09-11/docs}/audits/2026-09-07/coverage.md (100%) rename docs/{ => _archive/pre-docs-cleanup-2026-09-11/docs}/audits/2026-09-07/findings.md (100%) rename docs/{ => _archive/pre-docs-cleanup-2026-09-11/docs}/audits/2026-09-07/product-operations.md (100%) rename docs/{ => _archive/pre-docs-cleanup-2026-09-11/docs}/audits/2026-09-07/resilience.md (100%) rename docs/{ => _archive/pre-docs-cleanup-2026-09-11/docs}/audits/2026-09-07/source-index.md (100%) rename docs/{ => _archive/pre-docs-cleanup-2026-09-11/docs}/audits/2026-09-07/threat-data.md (100%) create mode 100644 docs/_archive/pre-docs-cleanup-2026-09-11/docs/audits/2026-09-07/validation/README.md rename docs/{ => _archive/pre-docs-cleanup-2026-09-11/docs}/audits/2026-09-07/validation/b01-completion.md (100%) rename docs/{ => _archive/pre-docs-cleanup-2026-09-11/docs}/audits/2026-09-07/validation/b02-completion.md (100%) rename docs/{ => _archive/pre-docs-cleanup-2026-09-11/docs}/audits/2026-09-07/validation/b03-completion.md (100%) rename docs/{ => _archive/pre-docs-cleanup-2026-09-11/docs}/audits/2026-09-07/validation/b04-completion.md (100%) rename docs/{ => _archive/pre-docs-cleanup-2026-09-11/docs}/audits/2026-09-07/validation/b05-completion.md (100%) create mode 100644 docs/_archive/pre-docs-cleanup-2026-09-11/docs/audits/2026-09-07/validation/b06-completion.md rename docs/{ => _archive/pre-docs-cleanup-2026-09-11/docs}/audits/2026-09-07/validation/evidence.md (100%) rename docs/{ => _archive/pre-docs-cleanup-2026-09-11/docs}/audits/2026-09-07/validation/evidence/b02-browser-config.ts.txt (100%) rename docs/{ => _archive/pre-docs-cleanup-2026-09-11/docs}/audits/2026-09-07/validation/evidence/b06-generated-nginx.conf.txt (100%) rename docs/{ => _archive/pre-docs-cleanup-2026-09-11/docs}/audits/2026-09-07/validation/evidence/b06-runtime-public-probe.py.txt (100%) rename docs/{ => _archive/pre-docs-cleanup-2026-09-11/docs}/audits/2026-09-07/validation/evidence/b06-topology-blocked.txt (100%) create mode 100644 docs/_archive/pre-docs-cleanup-2026-09-11/docs/audits/2026-09-07/validation/implementation-queue.md rename docs/{ => _archive/pre-docs-cleanup-2026-09-11/docs}/audits/2026-09-07/verification.md (100%) rename docs/{ => _archive/pre-docs-cleanup-2026-09-11/docs}/audits/audit-progress.md (100%) rename docs/{ => _archive/pre-docs-cleanup-2026-09-11/docs}/audits/audit-remediation-backlog.md (100%) rename docs/{ => _archive/pre-docs-cleanup-2026-09-11/docs}/audits/evidence/accessibility-evidence.md (100%) rename docs/{ => _archive/pre-docs-cleanup-2026-09-11/docs}/audits/evidence/ai-001/README.md (100%) rename docs/{ => _archive/pre-docs-cleanup-2026-09-11/docs}/audits/evidence/ai-002/README.md (100%) rename docs/{ => _archive/pre-docs-cleanup-2026-09-11/docs}/audits/evidence/ai-003/README.md (100%) rename docs/{ => _archive/pre-docs-cleanup-2026-09-11/docs}/audits/evidence/ai-004/README.md (100%) rename docs/{ => _archive/pre-docs-cleanup-2026-09-11/docs}/audits/evidence/browser-evidence.md (100%) rename docs/{ => _archive/pre-docs-cleanup-2026-09-11/docs}/audits/evidence/career-001/README.md (100%) rename docs/{ => _archive/pre-docs-cleanup-2026-09-11/docs}/audits/evidence/career-002/README.md (100%) rename docs/{ => _archive/pre-docs-cleanup-2026-09-11/docs}/audits/evidence/core-001-runtime/synthetic-attachment.txt (100%) rename docs/{ => _archive/pre-docs-cleanup-2026-09-11/docs}/audits/evidence/dependency-evidence.md (100%) rename docs/{ => _archive/pre-docs-cleanup-2026-09-11/docs}/audits/evidence/mail-001/README.md (100%) rename docs/{ => _archive/pre-docs-cleanup-2026-09-11/docs}/audits/evidence/pol-001/README.md (100%) rename docs/{ => _archive/pre-docs-cleanup-2026-09-11/docs}/audits/evidence/pol-002/README.md (100%) rename docs/{ => _archive/pre-docs-cleanup-2026-09-11/docs}/audits/evidence/prod-002/README.md (100%) rename docs/{ => _archive/pre-docs-cleanup-2026-09-11/docs}/audits/evidence/qa-001/README.md (100%) rename docs/{ => _archive/pre-docs-cleanup-2026-09-11/docs}/audits/evidence/repository-inventory.md (100%) rename docs/{ => _archive/pre-docs-cleanup-2026-09-11/docs}/audits/evidence/runtime-evidence.md (100%) rename docs/{ => _archive/pre-docs-cleanup-2026-09-11/docs}/audits/evidence/two-user-isolation.md (100%) rename docs/{ => _archive/pre-docs-cleanup-2026-09-11/docs}/audits/evidence/ux-001/README.md (100%) rename docs/{ => _archive/pre-docs-cleanup-2026-09-11/docs}/audits/evidence/ux-002/README.md (100%) rename docs/{ => _archive/pre-docs-cleanup-2026-09-11/docs}/audits/full-application-audit.md (100%) rename docs/{ => _archive/pre-docs-cleanup-2026-09-11/docs}/audits/security-threat-model.md (100%) rename docs/{ => _archive/pre-docs-cleanup-2026-09-11/docs}/audits/user-journey-audit.md (100%) rename docs/{ => _archive/pre-docs-cleanup-2026-09-11/docs}/audits/verification-log.md (100%) rename docs/{ => _archive/pre-docs-cleanup-2026-09-11/docs}/auth/2fa.md (100%) rename docs/{ => _archive/pre-docs-cleanup-2026-09-11/docs}/auth/login.md (100%) rename docs/{ => _archive/pre-docs-cleanup-2026-09-11/docs}/auth/oauth.md (100%) rename docs/{ => _archive/pre-docs-cleanup-2026-09-11/docs}/auth/overview.md (100%) rename docs/{ => _archive/pre-docs-cleanup-2026-09-11/docs}/auth/signup.md (100%) rename docs/{ => _archive/pre-docs-cleanup-2026-09-11/docs}/career-workspace-branch-assessment.md (100%) rename docs/{ => _archive/pre-docs-cleanup-2026-09-11/docs}/career-workspace-implementation-roadmap.md (100%) rename docs/{ => _archive/pre-docs-cleanup-2026-09-11/docs}/career-workspace-product-strategy.md (100%) rename docs/{ => _archive/pre-docs-cleanup-2026-09-11/docs}/career-workspace-ux-refactor.md (100%) rename docs/{ => _archive/pre-docs-cleanup-2026-09-11/docs}/career/career-profile.md (100%) rename docs/{ => _archive/pre-docs-cleanup-2026-09-11/docs}/career/cover-letters.md (100%) rename docs/{ => _archive/pre-docs-cleanup-2026-09-11/docs}/career/cv-ai.md (100%) rename docs/{ => _archive/pre-docs-cleanup-2026-09-11/docs}/career/cv-rendering.md (100%) rename docs/{ => _archive/pre-docs-cleanup-2026-09-11/docs}/career/cv-tailoring.md (100%) rename docs/{ => _archive/pre-docs-cleanup-2026-09-11/docs}/career/cv-themes.md (100%) rename docs/{ => _archive/pre-docs-cleanup-2026-09-11/docs}/career/documents.md (100%) rename docs/{ => _archive/pre-docs-cleanup-2026-09-11/docs}/career/education.md (100%) rename docs/{ => _archive/pre-docs-cleanup-2026-09-11/docs}/career/experience.md (100%) rename docs/{ => _archive/pre-docs-cleanup-2026-09-11/docs}/career/future-features.md (100%) rename docs/{ => _archive/pre-docs-cleanup-2026-09-11/docs}/career/master-profile.md (100%) rename docs/{ => _archive/pre-docs-cleanup-2026-09-11/docs}/career/overview.md (100%) rename docs/{ => _archive/pre-docs-cleanup-2026-09-11/docs}/career/portfolio.md (100%) rename docs/{ => _archive/pre-docs-cleanup-2026-09-11/docs}/career/projects.md (100%) rename docs/{ => _archive/pre-docs-cleanup-2026-09-11/docs}/career/public-profile.md (100%) rename docs/{ => _archive/pre-docs-cleanup-2026-09-11/docs}/career/skills.md (100%) rename docs/{ => _archive/pre-docs-cleanup-2026-09-11/docs}/career/versioning.md (100%) rename docs/{ => _archive/pre-docs-cleanup-2026-09-11/docs}/cover-letters/ai.md (100%) rename docs/{ => _archive/pre-docs-cleanup-2026-09-11/docs}/cover-letters/builder.md (100%) rename docs/{ => _archive/pre-docs-cleanup-2026-09-11/docs}/cover-letters/overview.md (100%) rename docs/{ => _archive/pre-docs-cleanup-2026-09-11/docs}/cv-builder-competitor-deep-research.md (100%) rename docs/{ => _archive/pre-docs-cleanup-2026-09-11/docs}/cv-builder-product-teardown.md (100%) rename docs/{ => _archive/pre-docs-cleanup-2026-09-11/docs}/cv-builder/ai.md (100%) rename docs/{ => _archive/pre-docs-cleanup-2026-09-11/docs}/cv-builder/content.md (100%) rename docs/{ => _archive/pre-docs-cleanup-2026-09-11/docs}/cv-builder/customisation.md (100%) rename docs/{ => _archive/pre-docs-cleanup-2026-09-11/docs}/cv-builder/exports.md (100%) rename docs/{ => _archive/pre-docs-cleanup-2026-09-11/docs}/cv-builder/overview.md (100%) rename docs/{ => _archive/pre-docs-cleanup-2026-09-11/docs}/cv-builder/rendering.md (100%) rename docs/{ => _archive/pre-docs-cleanup-2026-09-11/docs}/cv-builder/themes.md (100%) rename docs/{ => _archive/pre-docs-cleanup-2026-09-11/docs}/cv-builder/workflow.md (100%) create mode 100644 docs/_archive/pre-docs-cleanup-2026-09-11/docs/decisions/ADR-002-job-application-model.md create mode 100644 docs/_archive/pre-docs-cleanup-2026-09-11/docs/decisions/ADR-004-ai-provider-system.md rename docs/{ => _archive/pre-docs-cleanup-2026-09-11/docs}/deferred-inputs.md (100%) rename docs/{ => _archive/pre-docs-cleanup-2026-09-11/docs}/deployment/backup-restore.md (100%) rename docs/{ => _archive/pre-docs-cleanup-2026-09-11/docs}/deployment/manual-smoke-test.md (100%) create mode 100644 docs/_archive/pre-docs-cleanup-2026-09-11/docs/documentation-rules.md rename docs/{ => _archive/pre-docs-cleanup-2026-09-11/docs}/implementation-roadmap.md (100%) rename docs/{ => _archive/pre-docs-cleanup-2026-09-11/docs}/infrastructure/database-ownership.md (100%) rename docs/{ => _archive/pre-docs-cleanup-2026-09-11/docs}/infrastructure/runner-investigation.md (100%) rename docs/{ => _archive/pre-docs-cleanup-2026-09-11/docs}/jobs/adding-jobs.md (100%) rename docs/{ => _archive/pre-docs-cleanup-2026-09-11/docs}/jobs/analytics.md (100%) rename docs/{ => _archive/pre-docs-cleanup-2026-09-11/docs}/jobs/applications.md (100%) rename docs/{ => _archive/pre-docs-cleanup-2026-09-11/docs}/jobs/communication-tracking.md (100%) rename docs/{ => _archive/pre-docs-cleanup-2026-09-11/docs}/jobs/company-management.md (100%) rename docs/{ => _archive/pre-docs-cleanup-2026-09-11/docs}/jobs/followups.md (100%) rename docs/{ => _archive/pre-docs-cleanup-2026-09-11/docs}/jobs/future-features.md (100%) rename docs/{ => _archive/pre-docs-cleanup-2026-09-11/docs}/jobs/job-import.md (100%) rename docs/{ => _archive/pre-docs-cleanup-2026-09-11/docs}/jobs/job-lifecycle.md (100%) rename docs/{ => _archive/pre-docs-cleanup-2026-09-11/docs}/jobs/job-search.md (100%) rename docs/{ => _archive/pre-docs-cleanup-2026-09-11/docs}/jobs/overview.md (100%) rename docs/{ => _archive/pre-docs-cleanup-2026-09-11/docs}/operations/calendar-integrations.md (100%) rename docs/{ => _archive/pre-docs-cleanup-2026-09-11/docs}/operations/data-protection-key-rotation.md (100%) rename docs/{ => _archive/pre-docs-cleanup-2026-09-11/docs}/operations/google-drive-backups.md (100%) rename docs/{ => _archive/pre-docs-cleanup-2026-09-11/docs}/operations/job-opportunity-cutover.md (100%) rename docs/{ => _archive/pre-docs-cleanup-2026-09-11/docs}/operations/production-backup-verification.md (100%) rename docs/{ => _archive/pre-docs-cleanup-2026-09-11/docs}/operations/stripe-activation.md (100%) rename docs/{ => _archive/pre-docs-cleanup-2026-09-11/docs}/operations/stripe-billing-setup-walkthrough.md (100%) rename docs/{ => _archive/pre-docs-cleanup-2026-09-11/docs}/phase-0-foundation-report.md (100%) rename docs/{ => _archive/pre-docs-cleanup-2026-09-11/docs}/phase-2-1-cv-extraction-review.md (100%) rename docs/{ => _archive/pre-docs-cleanup-2026-09-11/docs}/phase-5-completion-report.md (100%) rename docs/{ => _archive/pre-docs-cleanup-2026-09-11/docs}/plans/post-audit-ux-reliability-program.md (100%) rename docs/{ => _archive/pre-docs-cleanup-2026-09-11/docs}/product/business-model.md (100%) rename docs/{ => _archive/pre-docs-cleanup-2026-09-11/docs}/product/goals.md (100%) rename docs/{ => _archive/pre-docs-cleanup-2026-09-11/docs}/product/mission.md (100%) rename docs/{ => _archive/pre-docs-cleanup-2026-09-11/docs}/product/non-goals.md (100%) rename docs/{ => _archive/pre-docs-cleanup-2026-09-11/docs}/product/personas.md (100%) rename docs/{ => _archive/pre-docs-cleanup-2026-09-11/docs}/product/roadmap.md (100%) rename docs/{ => _archive/pre-docs-cleanup-2026-09-11/docs}/product/vision.md (100%) rename docs/{ => _archive/pre-docs-cleanup-2026-09-11/docs}/production-readiness-review.md (100%) rename docs/{ => _archive/pre-docs-cleanup-2026-09-11/docs}/production/ollama-model-benchmark.md (100%) rename docs/{ => _archive/pre-docs-cleanup-2026-09-11/docs}/production/production-ai-hardware-assessment.md (100%) rename docs/{ => _archive/pre-docs-cleanup-2026-09-11/docs}/production/production-ai-rollout-and-rollback.md (100%) rename docs/{ => _archive/pre-docs-cleanup-2026-09-11/docs}/production/production-ai-validation.md (100%) rename docs/{ => _archive/pre-docs-cleanup-2026-09-11/docs}/release-candidate-review.md (100%) rename docs/{ => _archive/pre-docs-cleanup-2026-09-11/docs}/release-checklist.md (100%) rename docs/{ => _archive/pre-docs-cleanup-2026-09-11/docs}/release-final-report.md (100%) rename docs/{ => _archive/pre-docs-cleanup-2026-09-11/docs}/research/competitors.md (100%) rename docs/{ => _archive/pre-docs-cleanup-2026-09-11/docs}/research/cv-builder-patterns.md (100%) rename docs/{ => _archive/pre-docs-cleanup-2026-09-11/docs}/research/elegantcv-analysis.md (100%) rename docs/{ => _archive/pre-docs-cleanup-2026-09-11/docs}/research/feature-comparison.md (100%) rename docs/{ => _archive/pre-docs-cleanup-2026-09-11/docs}/research/flowcv-analysis.md (100%) rename docs/{ => _archive/pre-docs-cleanup-2026-09-11/docs}/research/job-platform-analysis.md (100%) rename docs/{ => _archive/pre-docs-cleanup-2026-09-11/docs}/research/jobscan.md (100%) rename docs/{ => _archive/pre-docs-cleanup-2026-09-11/docs}/research/market-positioning.md (100%) rename docs/{ => _archive/pre-docs-cleanup-2026-09-11/docs}/research/novoresume-analysis.md (100%) rename docs/{ => _archive/pre-docs-cleanup-2026-09-11/docs}/research/overview.md (100%) rename docs/{ => _archive/pre-docs-cleanup-2026-09-11/docs}/research/reactive-resume-analysis.md (100%) rename docs/{ => _archive/pre-docs-cleanup-2026-09-11/docs}/research/recommendations.md (100%) rename docs/{ => _archive/pre-docs-cleanup-2026-09-11/docs}/research/saas-analysis.md (100%) rename docs/{ => _archive/pre-docs-cleanup-2026-09-11/docs}/security/ai-security.md (100%) rename docs/{ => _archive/pre-docs-cleanup-2026-09-11/docs}/security/api-security.md (100%) create mode 100644 docs/_archive/pre-docs-cleanup-2026-09-11/docs/security/authentication.md create mode 100644 docs/_archive/pre-docs-cleanup-2026-09-11/docs/security/authorization.md create mode 100644 docs/_archive/pre-docs-cleanup-2026-09-11/docs/security/file-security.md rename docs/{ => _archive/pre-docs-cleanup-2026-09-11/docs}/security/input-validation.md (100%) rename docs/{ => _archive/pre-docs-cleanup-2026-09-11/docs}/security/overview.md (100%) rename docs/{ => _archive/pre-docs-cleanup-2026-09-11/docs}/security/privacy.md (100%) rename docs/{ => _archive/pre-docs-cleanup-2026-09-11/docs}/security/saas-security.md (100%) rename docs/{ => _archive/pre-docs-cleanup-2026-09-11/docs}/security/supply-chain-policy.md (100%) rename docs/{ => _archive/pre-docs-cleanup-2026-09-11/docs}/security/two-factor-authentication.md (100%) rename docs/{ => _archive/pre-docs-cleanup-2026-09-11/docs}/security/uploads.md (100%) rename docs/{ => _archive/pre-docs-cleanup-2026-09-11/docs}/security/user-data.md (100%) rename docs/{ => _archive/pre-docs-cleanup-2026-09-11/docs}/security/validation.md (100%) rename docs/{ => _archive/pre-docs-cleanup-2026-09-11/docs}/technical/coding-standards.md (100%) rename docs/{ => _archive/pre-docs-cleanup-2026-09-11/docs}/technical/deployment.md (100%) rename docs/{ => _archive/pre-docs-cleanup-2026-09-11/docs}/technical/docker.md (100%) rename docs/{ => _archive/pre-docs-cleanup-2026-09-11/docs}/technical/environment.md (100%) rename docs/{ => _archive/pre-docs-cleanup-2026-09-11/docs}/technical/git-workflow.md (100%) rename docs/{ => _archive/pre-docs-cleanup-2026-09-11/docs}/technical/logging.md (100%) rename docs/{ => _archive/pre-docs-cleanup-2026-09-11/docs}/technical/monitoring.md (100%) rename docs/{ => _archive/pre-docs-cleanup-2026-09-11/docs}/technical/overview.md (100%) rename docs/{ => _archive/pre-docs-cleanup-2026-09-11/docs}/technical/performance.md (100%) rename docs/{ => _archive/pre-docs-cleanup-2026-09-11/docs}/technical/testing.md (100%) rename docs/{ => _archive/pre-docs-cleanup-2026-09-11/docs}/technical/troubleshooting.md (100%) rename docs/{ => _archive/pre-docs-cleanup-2026-09-11/docs}/todo/ollama.md (100%) rename docs/{ => _archive/pre-docs-cleanup-2026-09-11/docs}/todo/work.md (99%) rename docs/{ => _archive/pre-docs-cleanup-2026-09-11/docs}/ux/accessibility.md (100%) rename docs/{ => _archive/pre-docs-cleanup-2026-09-11/docs}/ux/dashboard.md (100%) rename docs/{ => _archive/pre-docs-cleanup-2026-09-11/docs}/ux/design-system.md (100%) rename docs/{ => _archive/pre-docs-cleanup-2026-09-11/docs}/ux/empty-states.md (100%) rename docs/{ => _archive/pre-docs-cleanup-2026-09-11/docs}/ux/errors.md (100%) rename docs/{ => _archive/pre-docs-cleanup-2026-09-11/docs}/ux/loading-states.md (100%) rename docs/{ => _archive/pre-docs-cleanup-2026-09-11/docs}/ux/navigation.md (100%) rename docs/{ => _archive/pre-docs-cleanup-2026-09-11/docs}/ux/notifications.md (100%) rename docs/{ => _archive/pre-docs-cleanup-2026-09-11/docs}/ux/onboarding.md (100%) rename docs/{ => _archive/pre-docs-cleanup-2026-09-11/docs}/ux/profile.md (100%) rename docs/{ => _archive/pre-docs-cleanup-2026-09-11/docs}/ux/responsive.md (100%) rename docs/{ => _archive/pre-docs-cleanup-2026-09-11/docs}/ux/settings.md (100%) rename docs/{ => _archive/pre-docs-cleanup-2026-09-11/docs}/ux/user-flows.md (100%) rename docs/{ => _archive/pre-docs-cleanup-2026-09-11/docs}/verification/ai-001-durable-ai-queue.md (100%) rename docs/{ => _archive/pre-docs-cleanup-2026-09-11/docs}/verification/ai-002-provider-routing.md (100%) rename docs/{ => _archive/pre-docs-cleanup-2026-09-11/docs}/verification/ai-003-strategy-snapshot-queue.md (100%) rename docs/{ => _archive/pre-docs-cleanup-2026-09-11/docs}/verification/ai-004-cv-processing-queue.md (100%) rename docs/{ => _archive/pre-docs-cleanup-2026-09-11/docs}/verification/application-action-matrix.md (100%) rename docs/{ => _archive/pre-docs-cleanup-2026-09-11/docs}/verification/bg-001-tenant-workers.md (100%) rename docs/{ => _archive/pre-docs-cleanup-2026-09-11/docs}/verification/career-001-career-workspace.md (100%) rename docs/{ => _archive/pre-docs-cleanup-2026-09-11/docs}/verification/career-002-cv-builder.md (100%) rename docs/{ => _archive/pre-docs-cleanup-2026-09-11/docs}/verification/core-001-sqlite-provider-parity.md (100%) rename docs/{ => _archive/pre-docs-cleanup-2026-09-11/docs}/verification/core-002-route-uniqueness.md (100%) rename docs/{ => _archive/pre-docs-cleanup-2026-09-11/docs}/verification/dep-001-frontend-advisories.md (100%) rename docs/{ => _archive/pre-docs-cleanup-2026-09-11/docs}/verification/jobs-001-job-discovery.md (100%) rename docs/{ => _archive/pre-docs-cleanup-2026-09-11/docs}/verification/jobs-002-application-workspace.md (100%) rename docs/{ => _archive/pre-docs-cleanup-2026-09-11/docs}/verification/jt-017-dotnet-provenance.md (100%) rename docs/{ => _archive/pre-docs-cleanup-2026-09-11/docs}/verification/jt-018-developer-documentation.md (100%) rename docs/{ => _archive/pre-docs-cleanup-2026-09-11/docs}/verification/jt-019-schema-ownership.md (100%) rename docs/{ => _archive/pre-docs-cleanup-2026-09-11/docs}/verification/mail-001-job-email-hub.md (100%) rename docs/{ => _archive/pre-docs-cleanup-2026-09-11/docs}/verification/ops-001a-durable-operations.md (100%) rename docs/{ => _archive/pre-docs-cleanup-2026-09-11/docs}/verification/ops-001b-notifications.md (100%) rename docs/{ => _archive/pre-docs-cleanup-2026-09-11/docs}/verification/ops-001c-operation-ui.md (100%) rename docs/{ => _archive/pre-docs-cleanup-2026-09-11/docs}/verification/pol-001-free-pro-entitlements.md (100%) rename docs/{ => _archive/pre-docs-cleanup-2026-09-11/docs}/verification/pol-002-ai-privacy.md (100%) rename docs/{ => _archive/pre-docs-cleanup-2026-09-11/docs}/verification/prod-002-ai-evaluation.md (100%) rename docs/{ => _archive/pre-docs-cleanup-2026-09-11/docs}/verification/product-001-honest-plans.md (100%) rename docs/{ => _archive/pre-docs-cleanup-2026-09-11/docs}/verification/qa-001-job-term-quality.md (100%) rename docs/{ => _archive/pre-docs-cleanup-2026-09-11/docs}/verification/sec-001-canonical-origin.md (100%) rename docs/{ => _archive/pre-docs-cleanup-2026-09-11/docs}/verification/sec-002-ingress-compose.md (100%) rename docs/{ => _archive/pre-docs-cleanup-2026-09-11/docs}/verification/sec-003-microsoft-tenant.md (100%) rename docs/{ => _archive/pre-docs-cleanup-2026-09-11/docs}/verification/sec-004-microsoft-identity.md (100%) rename docs/{ => _archive/pre-docs-cleanup-2026-09-11/docs}/verification/sec-005a-session-revocation.md (100%) rename docs/{ => _archive/pre-docs-cleanup-2026-09-11/docs}/verification/sec-005b-email-ownership.md (100%) rename docs/{ => _archive/pre-docs-cleanup-2026-09-11/docs}/verification/sec-006-parser-dependencies.md (100%) rename docs/{ => _archive/pre-docs-cleanup-2026-09-11/docs}/verification/sec-007-parser-isolation.md (100%) rename docs/{ => _archive/pre-docs-cleanup-2026-09-11/docs}/verification/sec-008-attachment-consistency.md (100%) rename docs/{ => _archive/pre-docs-cleanup-2026-09-11/docs}/verification/sec-009-account-lifecycle.md (100%) rename docs/{ => _archive/pre-docs-cleanup-2026-09-11/docs}/verification/ux-001-unified-authentication.md (100%) rename docs/{ => _archive/pre-docs-cleanup-2026-09-11/docs}/verification/ux-002-deterministic-theme-state.md (100%) rename docs/{ => _archive/pre-docs-cleanup-2026-09-11/docs}/verification/ux-003-kanban-theme.md (100%) rename docs/{ => _archive/pre-docs-cleanup-2026-09-11/docs}/verification/ver-001-complete-regression.md (100%) rename docs/{ => _archive/pre-docs-cleanup-2026-09-11/docs}/work-programmes/decisions.md (100%) rename docs/{ => _archive/pre-docs-cleanup-2026-09-11/docs}/work-programmes/master-progress.md (100%) rename docs/{ => _archive/pre-docs-cleanup-2026-09-11/docs}/work-programmes/master-work-plan.md (100%) rename docs/{ => _archive/pre-docs-cleanup-2026-09-11/docs}/work-programmes/session-handoff.md (100%) create mode 100644 docs/_archive/pre-docs-cleanup-2026-09-11/job-tracker-ui/README.md create mode 100644 docs/_archive/pre-docs-cleanup-2026-09-11/tools/summarizer/README.md create mode 100644 docs/ai/README.md create mode 100644 docs/ai/architecture.md create mode 100644 docs/ai/document-processing.md create mode 100644 docs/architecture/data-flow.md create mode 100644 docs/architecture/integrations.md create mode 100644 docs/backend/README.md create mode 100644 docs/backend/api.md create mode 100644 docs/backend/background-jobs.md create mode 100644 docs/backend/error-handling.md delete mode 100644 docs/career/cv-builder create mode 100644 docs/configuration/README.md create mode 100644 docs/configuration/environment-variables.md create mode 100644 docs/configuration/environments.md create mode 100644 docs/database/README.md create mode 100644 docs/database/backups.md create mode 100644 docs/database/migrations.md create mode 100644 docs/database/schema.md create mode 100644 docs/decisions/README.md create mode 100644 docs/development/README.md create mode 100644 docs/development/conventions.md create mode 100644 docs/development/getting-started.md create mode 100644 docs/development/troubleshooting.md create mode 100644 docs/frontend/README.md create mode 100644 docs/frontend/localisation-accessibility.md create mode 100644 docs/frontend/navigation.md create mode 100644 docs/frontend/ui-system.md create mode 100644 docs/infrastructure/README.md create mode 100644 docs/infrastructure/backups.md create mode 100644 docs/infrastructure/deployment.md create mode 100644 docs/infrastructure/disaster-recovery.md create mode 100644 docs/infrastructure/docker.md create mode 100644 docs/infrastructure/monitoring.md create mode 100644 docs/infrastructure/stripe-activation.md create mode 100644 docs/product/README.md create mode 100644 docs/product/constraints.md create mode 100644 docs/product/features.md create mode 100644 docs/product/terminology.md create mode 100644 docs/roadmap/README.md create mode 100644 docs/roadmap/known-issues.md create mode 100644 docs/roadmap/technical-debt.md create mode 100644 docs/roadmap/todo.md create mode 100644 docs/security/README.md create mode 100644 docs/security/security-notes.md create mode 100644 docs/security/sessions.md create mode 100644 docs/testing/README.md create mode 100644 docs/testing/backend.md create mode 100644 docs/testing/browser.md create mode 100644 docs/testing/frontend.md create mode 100644 docs/testing/security.md diff --git a/.env.example b/.env.example index 1d88519..4b57a4f 100644 --- a/.env.example +++ b/.env.example @@ -1,70 +1,54 @@ -# Copy this file to `.env` (same folder as docker-compose.yml) and fill in values. -# -# Used by docker-compose.yml -# -# Database. deploy/deploy.sh REQUIRES DATABASE_PROVIDER to be set explicitly and -# refuses to deploy without it — it selects which backup to take, and guessing it -# wrong means backing up the wrong database. Use `mariadb` (or `mysql`) for a -# server deployment, `sqlite` for a single-file local one. +# Copy to .env beside docker-compose.yml. Never commit real secret values. +# Full descriptions: docs/configuration/environment-variables.md + +# ============================================================================= +# DATABASE +# ============================================================================= DATABASE_PROVIDER=sqlite -# Required when DATABASE_PROVIDER is mariadb/mysql. Ignored for sqlite, which -# stores its file in the jobtracker_data volume. -# The host resolves from INSIDE the backend container: 127.0.0.1 means the -# container, not the Docker host. +# Required for mariadb/mysql; ignored for sqlite. JOBTRACKER_CONNECTION_STRING= -AUTH_JWT_KEY=CHANGE_ME_LONG_RANDOM_SECRET +# ============================================================================= +# AUTHENTICATION, IDENTITY, BILLING +# ============================================================================= +AUTH_JWT_KEY= AUTH_ADMIN_EMAIL=admin@example.com -AUTH_ADMIN_PASSWORD=CHANGE_ME_STRONG_PASSWORD -# Public signup remains closed until explicitly enabled. Configure both Turnstile keys first. +AUTH_ADMIN_PASSWORD= AUTH_ALLOW_REGISTRATION=false -# Require local accounts to confirm ownership of their email address before signing in. AUTH_REQUIRE_EMAIL_VERIFICATION=true -# Destructive account deletion stays dark-launched until the retention and restore runbook is -# approved and rehearsed. The tombstone path is mounted separately by docker-compose.yml. ACCOUNT_DELETION_ENABLED=false TURNSTILE_SITE_KEY= TURNSTILE_SECRET_KEY= -# Optional hosted Stripe Checkout. Configure all three values and the customer portal before enabling billing. STRIPE_SECRET_KEY= STRIPE_PRICE_PREMIUM= STRIPE_WEBHOOK_SECRET= -AUTH_GOOGLE_CLIENT_ID=CHANGE_ME_GOOGLE_CLIENT_ID -# Optional: enables the "Continue with Microsoft" sign-in tab (separate from the -# MICROSOFT_CLIENT_ID below, which is for Outlook mail linking, not sign-in). +AUTH_GOOGLE_CLIENT_ID= AUTH_MICROSOFT_CLIENT_ID= -# Microsoft application sign-in tenant policy: tenant GUID, organizations, consumers, or common. -# This is separate from MICROSOFT_TENANT_ID, which configures Graph mailbox OAuth. AUTH_MICROSOFT_TENANT=common -GOOGLE_GMAIL_CLIENT_SECRET=CHANGE_ME_GOOGLE_OAUTH_CLIENT_SECRET -# Optional. If omitted, the backend uses https:///api/gmail/oauth/callback -MICROSOFT_CLIENT_ID=CHANGE_ME_MICROSOFT_CLIENT_ID -MICROSOFT_CLIENT_SECRET=CHANGE_ME_MICROSOFT_OAUTH_CLIENT_SECRET -# Optional. Defaults to "common" (personal + work/school accounts). + +# ============================================================================= +# MAIL PROVIDER CONNECTIONS +# ============================================================================= +GOOGLE_GMAIL_CLIENT_SECRET= +MICROSOFT_CLIENT_ID= +MICROSOFT_CLIENT_SECRET= MICROSOFT_TENANT_ID= -# Optional. If omitted, the backend uses https:///api/microsoft-graph/oauth/callback + +# ============================================================================= +# AI SERVICE AND PROVIDERS +# ============================================================================= AI_SERVICE_BASE_URL=http://ai-service:8001 -# REQUIRED. Shared secret the backend sends to ai-service on every call except /health. -# The stack refuses to start without it. Generate with: openssl rand -hex 32 +# Required for protected backend -> AI service calls. AI_SERVICE_TOKEN= -# Optional: enables hybrid CV block classification in the local AI service. OLLAMA_BASE_URL=http://ollama:11434 OLLAMA_MODEL=qwen2.5:7b - -# Optional external fallback provider for heavy /cv/* calls: ollama (none) | gemini | groq. -# Local Ollama is always attempted first unless the explicitly configured mode is -# external_only. External processing still requires the administrator gate, an -# allowed task, and the authenticated Pro user's opt-in. /summarize stays local. -# Keys are read from the environment only — never commit real keys. AI_PROVIDER=ollama EXTERNAL_AI_ENABLED=false AI_ROUTING_MODE=local_first EXTERNAL_AI_ALLOWED_TASKS=cv-normalize,cv-classify,cv-rewrite -# Per-request cost/privacy ceiling. Requests above this size remain local even after local failure. EXTERNAL_AI_MAX_PROMPT_CHARS=24000 LOCAL_AI_CIRCUIT_FAILURE_THRESHOLD=3 LOCAL_AI_CIRCUIT_OPEN_SECONDS=30 -# Isolated CV document decoder budgets. Increase only after benign-file measurement. PARSER_TIMEOUT_SECONDS=25 PARSER_CPU_SECONDS=20 PARSER_MEMORY_MIB=512 @@ -73,8 +57,6 @@ GEMINI_API_KEY= GEMINI_MODEL=gemini-2.0-flash GROQ_API_KEY= GROQ_MODEL=llama-3.3-70b-versatile - -# Durable AI operation worker. Keep false until handlers, monitoring and rollout gates are verified. WORKER_AI_OPERATIONS_ENABLED=false AI_QUEUE_WORKER_CONCURRENCY=1 AI_QUEUE_GLOBAL_CAPACITY=100 @@ -82,47 +64,57 @@ AI_QUEUE_PER_USER_CAPACITY=10 AI_QUEUE_DEADLINE_MINUTES=15 AI_QUEUE_OPERATION_TIMEOUT_SECONDS=300 -# Optional: only needed if you want the UI to call a non-default API base URL. -# In production the UI defaults to `/api`. +# ============================================================================= +# FRONTEND AND PUBLIC APPLICATION +# ============================================================================= +# Leave empty in production so nginx proxies same-origin /api requests. NEXT_PUBLIC_API_BASE_URL= - -# Used by docker-compose.yml (email / password resets / notifications) APP_PUBLIC_BASE_URL=https://jobs.cesnimda.uk -# Dedicated nginx-to-backend network. Confirm this CIDR does not overlap existing Docker networks. -WEB_PROXY_SUBNET=172.31.250.0/29 -# Required for production: space-separated observed Traefik socket-peer CIDRs at nginx. -# Prefer individual /32 or /128 addresses. Never guess Docker gateway/proxy addresses. -# Empty fails deployment/nginx startup; development explicitly uses direct mode. -INGRESS_PROXY_CIDRS= APP_VERSION= APP_COMMIT_SHA= APP_BUILD_STAMP= -# Optional. Use a narrowly scoped Gitea token that can dispatch Actions workflows. + +# ============================================================================= +# INGRESS AND MAINTENANCE +# ============================================================================= +WEB_PROXY_SUBNET=172.31.250.0/29 +# Space-separated trusted reverse-proxy peer CIDRs; required in production. +INGRESS_PROXY_CIDRS= MAINTENANCE_GITEA_BASE_URL=https://git.cesnimda.uk MAINTENANCE_GITEA_OWNER=cesnimda MAINTENANCE_GITEA_REPOSITORY=jobtrackingapp MAINTENANCE_GITEA_TOKEN= -# Complete recovery sets are created before deployment and nightly at 02:00 Europe/Oslo. + +# ============================================================================= +# BACKUPS +# ============================================================================= BACKUP_TIMEZONE=Europe/Oslo BACKUP_DAILY_RETENTION_DAYS=30 BACKUP_MONTHLY_RETENTION_COUNT=12 -# Optional encrypted rclone destination, for example gdrive-crypt:jobtracker. +# Optional encrypted rclone destination, e.g. gdrive-crypt:jobtracker. BACKUP_RCLONE_REMOTE= + +# ============================================================================= +# SMTP AND EMAIL WORKERS +# ============================================================================= EMAIL_ENABLED=false EMAIL_SMTP_HOST=smtp.gmail.com EMAIL_SMTP_PORT=587 -EMAIL_SMTP_USER=CHANGE_ME_GMAIL_ADDRESS -EMAIL_SMTP_PASSWORD=CHANGE_ME_GOOGLE_APP_PASSWORD -EMAIL_FROM=CHANGE_ME_GMAIL_ADDRESS +EMAIL_SMTP_USER= +EMAIL_SMTP_PASSWORD= +EMAIL_FROM= EMAIL_FROM_NAME=Jobbjakt EMAIL_FOLLOWUPREMINDERS_ENABLED=false EMAIL_FOLLOWUPREMINDERS_UPCOMINGDAYS=2 +EMAIL_SMTP_ENABLE_SSL=true +EMAIL_SMTP_TIMEOUT_MS=15000 + +# ============================================================================= +# APPLICATION WORKERS AND DISCOVERY +# ============================================================================= WORKER_RULES_ENABLED=false WORKER_FOLLOWUP_REMINDERS_ENABLED=false WORKER_SAVED_SEARCH_ALERTS_ENABLED=true SAVED_SEARCH_INTERVAL_HOURS=6 WORKER_DAILY_EXPORT_ENABLED=false WORKER_JOB_ENRICHMENT_ENABLED=false - -EMAIL_SMTP_ENABLE_SSL=true -EMAIL_SMTP_TIMEOUT_MS=15000 diff --git a/.gitignore b/.gitignore index ffa2ec9..8390585 100644 --- a/.gitignore +++ b/.gitignore @@ -69,6 +69,11 @@ website_details.md docker-compose.override.yml docs.7z todo.md +!docs/roadmap/todo.md +# The documentation snapshot can contain local dependency/cache README copies. +# Preserve those locally without vendoring generated environments into the archive. +docs/_archive/**/.venv*/ +docs/_archive/**/.pytest_cache/ # Private local test files vendor/ diff --git a/BLOCKERS.md b/BLOCKERS.md index b85bc6c..74e894e 100644 --- a/BLOCKERS.md +++ b/BLOCKERS.md @@ -9,7 +9,7 @@ Updated: 2026-09-01 - **Required:** Configure the Pro recurring price, enable the Stripe customer portal, register `/api/billing/webhook` for `customer.subscription.created`, `customer.subscription.updated`, and `customer.subscription.deleted`, then supply `STRIPE_SECRET_KEY`, the recurring `price_...` value in the legacy-named `STRIPE_PRICE_PREMIUM` setting, and `STRIPE_WEBHOOK_SECRET` through the deployment environment. Do not place secret values in source control or chat. - **Recommended:** One monthly Pro price first; add annual billing only after the monthly flow is operating. - **Current access check:** Production has test-mode secret and webhook values, but `STRIPE_PRICE_PREMIUM` currently contains a `prod_...` Product ID. Checkout requires the recurring `price_...` Price ID; the server now treats the wrong identifier type as disabled rather than calling Stripe. The publishable key is not used by hosted Checkout. Local fake-gateway coverage proves active → expired → canceled/replayed role transitions without losing non-AI data (V-185). -- **Runbook:** Follow `docs/operations/stripe-activation.md`, completing test mode before creating or installing live-mode values. +- **Runbook:** Follow `docs/infrastructure/stripe-activation.md`, completing test mode before creating or installing live-mode values. ## Public registration verification @@ -23,9 +23,9 @@ Updated: 2026-09-01 - **Blocked:** Authenticated production smoke tests, backup restore verification against real data, OAuth-provider checks, and rollback rehearsal. - **Why:** The authorised deployment is complete, but authenticated smoke, port/network closure, complete backup restore, provider checks and failure-injection rollback require credentials and/or separately authorised production mutations. -- **Required:** Confirm the admin-only version badge matches version 286 / commit `ea7d9f3`, then run the authenticated application workspace, Career, CV, attachment, email-verification and rollback checks from `docs/release-candidate-review.md` and `docs/release-checklist.md`. +- **Required:** Confirm the admin-only version badge matches version 286 / commit `ea7d9f3`, then run the authenticated application workspace, Career, CV, attachment, email-verification and rollback checks from `docs/infrastructure/deployment.md` and `docs/infrastructure/disaster-recovery.md`. - **Recommended:** Verify backup/restore before deployment, then exercise login, existing application counts, Career Workspace, public CV refresh/download, AI, and attachments in order. -- **Current status:** Gitea run 705 (application version 286, commit `ea7d9f3`) completed both the complete test job and remote deployment successfully on 2026-09-01. Public `/health`, `/ready`, and `/api/auth/config` returned HTTP 200 after replacement; liveness reported version 286 and readiness reported `ready`. This proves the proxy/frontend, API and database readiness path, but not authenticated application behavior, provider delivery or restore/rollback behavior. Read-only PROD-001 inventory previously found the JobTracker Ollama and frontend published on all host interfaces, the newest observed gzip-valid MariaDB backup dated 2026-08-02, no observed scheduled JobTracker backup, and no owner-file/key/tombstone recovery bundle. Those remaining gates still require operator-controlled verification; see `docs/production/production-ai-hardware-assessment.md`. +- **Current status:** Gitea run 705 (application version 286, commit `ea7d9f3`) completed both the complete test job and remote deployment successfully on 2026-09-01. Public `/health`, `/ready`, and `/api/auth/config` returned HTTP 200 after replacement; liveness reported version 286 and readiness reported `ready`. This proves the proxy/frontend, API and database readiness path, but not authenticated application behavior, provider delivery or restore/rollback behavior. Read-only PROD-001 inventory previously found the JobTracker Ollama and frontend published on all host interfaces, the newest observed gzip-valid MariaDB backup dated 2026-08-02, no observed scheduled JobTracker backup, and no owner-file/key/tombstone recovery bundle. Those remaining gates still require operator-controlled verification; see the current [deployment](docs/infrastructure/deployment.md), [backup](docs/infrastructure/backups.md), and [disaster-recovery](docs/infrastructure/disaster-recovery.md) guides; the detailed historical inventory remains in the pre-cleanup archive. - **Repository-side rollback status:** Core images are now tagged by commit, the prior running backend/frontend images are retained, and any failure after replacement triggers an automatic core rollback while preserving the failed CI status. Compose configuration validates locally. A real rollback rehearsal remains blocked because the local Linux Docker daemon is unavailable and production mutation is not authorised; run the failure-injection rehearsal on a disposable Linux Docker host before relying on it in production. ## Account deletion retention and restore policy @@ -39,5 +39,5 @@ Updated: 2026-09-01 - **Blocked:** Removing the opportunity columns duplicated between `JobApplication` and `Job`. - **Why:** The compatibility dual-write protects existing production rows and older clients. The release branch now backfills missing opportunities on startup, synchronizes both creation paths, and leaves all legacy columns intact. Dropping columns still requires production validation and an observation release. -- **Required:** After deployment, run the read-only report in `docs/operations/job-opportunity-cutover.md` against production and a restored backup, then confirm that backward API compatibility is no longer required. +- **Required:** After deployment, run the read-only report in `docs/database/migrations.md#production-cutover-validation` against production and a restored backup, then confirm that backward API compatibility is no longer required. - **Recommended:** Use an expand/contract release: first stop legacy reads after a verified backfill, observe one release, then drop the duplicate columns in the following migration. diff --git a/JobTrackerApi.Tests/ApiAuthorizationPostureTests.cs b/JobTrackerApi.Tests/ApiAuthorizationPostureTests.cs index 96a227f..4221a4d 100644 --- a/JobTrackerApi.Tests/ApiAuthorizationPostureTests.cs +++ b/JobTrackerApi.Tests/ApiAuthorizationPostureTests.cs @@ -10,7 +10,7 @@ namespace JobTrackerApi.Tests; // The point is that a NEW controller cannot ship unprotected by accident. Authentication used to rely // on the Auth:Require fallback policy, which defaults to false — a deployment that lost that flag // would have served tenant data anonymously. Every controller must now declare its intent. -// docs/production-readiness-review.md. +// docs/security/security-notes.md. public sealed class ApiAuthorizationPostureTests { // Endpoints that are anonymous ON PURPOSE. Adding to this list is a deliberate security decision. diff --git a/JobTrackerApi/Controllers/AiWorkspaceController.cs b/JobTrackerApi/Controllers/AiWorkspaceController.cs index c4b567d..1350f82 100644 --- a/JobTrackerApi/Controllers/AiWorkspaceController.cs +++ b/JobTrackerApi/Controllers/AiWorkspaceController.cs @@ -9,7 +9,7 @@ using Microsoft.EntityFrameworkCore; namespace JobTrackerApi.Controllers; // Phase 5 — the AI Workspace for one job application. Every module runs through ISummarizerService and -// is stored as append-only history; nothing is applied automatically. docs/architecture/ai-career-assistant.md. +// is stored as append-only history; nothing is applied automatically. docs/ai/architecture.md. [ApiController] [Route("api/jobapplications/{jobId:int}/ai")] [Authorize(AuthenticationSchemes = "local")] diff --git a/JobTrackerApi/Controllers/ApplicationAssetsController.cs b/JobTrackerApi/Controllers/ApplicationAssetsController.cs index fc74f3b..57ced8c 100644 --- a/JobTrackerApi/Controllers/ApplicationAssetsController.cs +++ b/JobTrackerApi/Controllers/ApplicationAssetsController.cs @@ -12,7 +12,7 @@ namespace JobTrackerApi.Controllers; // /api/cv (CvVariantController), and documents stay on /api/attachments. These routes only cover what // is genuinely application-scoped — which variant this application uses, what to tailor, and the // cover letter with its history. Every route is tenant-scoped and returns 404 for another user's -// application. docs/architecture/application-workspace.md. +// application. docs/architecture/overview.md. [ApiController] [Route("api/jobapplications/{jobId:int}")] [Authorize(AuthenticationSchemes = "local")] diff --git a/JobTrackerApi/Controllers/ApplicationChecklistController.cs b/JobTrackerApi/Controllers/ApplicationChecklistController.cs index 1a24c1b..b6fcb0e 100644 --- a/JobTrackerApi/Controllers/ApplicationChecklistController.cs +++ b/JobTrackerApi/Controllers/ApplicationChecklistController.cs @@ -8,7 +8,7 @@ namespace JobTrackerApi.Controllers; // Phase 5 Milestone 2 — the application checklist. System items seed themselves on first read from the // existing readiness signals; the user owns everything after that. -// docs/architecture/application-workspace.md. +// docs/architecture/overview.md. [ApiController] [Route("api/jobapplications/{jobId:int}/checklist")] [Authorize(AuthenticationSchemes = "local")] diff --git a/JobTrackerApi/Controllers/ApplicationIntelligenceController.cs b/JobTrackerApi/Controllers/ApplicationIntelligenceController.cs index 4d1b7ae..5ee813e 100644 --- a/JobTrackerApi/Controllers/ApplicationIntelligenceController.cs +++ b/JobTrackerApi/Controllers/ApplicationIntelligenceController.cs @@ -11,7 +11,7 @@ namespace JobTrackerApi.Controllers; // // None of them write anything. The AI narrative for analysis and match stays on the existing // /api/jobapplications/{id}/ai routes, which are suggestion-only and versioned by AiInteraction. -// docs/architecture/application-workspace.md. +// docs/architecture/overview.md. [ApiController] [Route("api/jobapplications/{jobId:int}")] [Authorize(AuthenticationSchemes = "local")] diff --git a/JobTrackerApi/Controllers/ApplicationWorkspaceController.cs b/JobTrackerApi/Controllers/ApplicationWorkspaceController.cs index 55ad699..36c2959 100644 --- a/JobTrackerApi/Controllers/ApplicationWorkspaceController.cs +++ b/JobTrackerApi/Controllers/ApplicationWorkspaceController.cs @@ -8,7 +8,7 @@ namespace JobTrackerApi.Controllers; // Phase 5 Milestone 1 — one aggregate read for the Application Workspace overview, so the page loads // from a single call instead of fanning out. Read-only; owns no data. -// docs/architecture/application-workspace.md. +// docs/architecture/overview.md. [ApiController] [Route("api/jobapplications/{jobId:int}/workspace")] [Authorize(AuthenticationSchemes = "local")] diff --git a/JobTrackerApi/Controllers/CareerProfileController.cs b/JobTrackerApi/Controllers/CareerProfileController.cs index 012c6e5..be7cc1c 100644 --- a/JobTrackerApi/Controllers/CareerProfileController.cs +++ b/JobTrackerApi/Controllers/CareerProfileController.cs @@ -11,7 +11,7 @@ namespace JobTrackerApi.Controllers; // here. The relational children (via CareerProfileService) are the source of truth; the // ApplicationUser.ProfileCvStructureJson blob is kept in sync as a derived projection so the legacy // read paths (CV rendering, tailoring, match-score) keep working. See -// docs/architecture/career-profile-model.md. +// docs/architecture/overview.md. [ApiController] [Route("api/career/profile")] [Authorize] diff --git a/JobTrackerApi/Controllers/CompaniesController.cs b/JobTrackerApi/Controllers/CompaniesController.cs index df83bef..7ade445 100644 --- a/JobTrackerApi/Controllers/CompaniesController.cs +++ b/JobTrackerApi/Controllers/CompaniesController.cs @@ -10,7 +10,7 @@ namespace JobTrackerApi.Controllers [ApiController] // Explicitly authorized. These endpoints are all tenant-scoped user data, so they must not // depend on the Auth:Require fallback policy being switched on: a deployment that lost that flag - // would otherwise serve them anonymously. docs/production-readiness-review.md. + // would otherwise serve them anonymously. docs/security/security-notes.md. [Route("api/companies")] [Authorize(AuthenticationSchemes = "local")] public class CompaniesController : ControllerBase diff --git a/JobTrackerApi/Controllers/CorrespondenceController.cs b/JobTrackerApi/Controllers/CorrespondenceController.cs index 56c64ea..1e08def 100644 --- a/JobTrackerApi/Controllers/CorrespondenceController.cs +++ b/JobTrackerApi/Controllers/CorrespondenceController.cs @@ -11,7 +11,7 @@ namespace JobTrackerApi.Controllers [ApiController] // Explicitly authorized. These endpoints are all tenant-scoped user data, so they must not // depend on the Auth:Require fallback policy being switched on: a deployment that lost that flag - // would otherwise serve them anonymously. docs/production-readiness-review.md. + // would otherwise serve them anonymously. docs/security/security-notes.md. [Route("api/correspondence")] [Authorize(AuthenticationSchemes = "local")] public class CorrespondenceController : ControllerBase diff --git a/JobTrackerApi/Controllers/CvVariantController.cs b/JobTrackerApi/Controllers/CvVariantController.cs index b67f509..0113af0 100644 --- a/JobTrackerApi/Controllers/CvVariantController.cs +++ b/JobTrackerApi/Controllers/CvVariantController.cs @@ -8,7 +8,7 @@ namespace JobTrackerApi.Controllers; // Phase 4 — Career Workspace Builder API. Every endpoint reads the master profile through // ICareerProfileService and renders through the data-driven theme engine; none of them store career -// data. docs/architecture/cv-builder.md. +// data. docs/architecture/overview.md. [ApiController] [Route("api/cv")] [Authorize(AuthenticationSchemes = "local")] diff --git a/JobTrackerApi/Controllers/InterviewPrepController.cs b/JobTrackerApi/Controllers/InterviewPrepController.cs index 9833a04..0115347 100644 --- a/JobTrackerApi/Controllers/InterviewPrepController.cs +++ b/JobTrackerApi/Controllers/InterviewPrepController.cs @@ -11,7 +11,7 @@ namespace JobTrackerApi.Controllers; // Prep content is the user's; AI suggestions come from the existing /api/jobapplications/{id}/ai // routes and only reach here once the user accepts one (POST with source "ai"). Communication stays // entirely on the existing Correspondence routes — there is no messaging endpoint here. -// docs/architecture/application-workspace.md. +// docs/architecture/overview.md. [ApiController] [Route("api/jobapplications/{jobId:int}")] [Authorize(AuthenticationSchemes = "local")] diff --git a/JobTrackerApi/Controllers/JobApplicationsController.cs b/JobTrackerApi/Controllers/JobApplicationsController.cs index 69bd4e2..1231ba6 100644 --- a/JobTrackerApi/Controllers/JobApplicationsController.cs +++ b/JobTrackerApi/Controllers/JobApplicationsController.cs @@ -20,7 +20,7 @@ namespace JobTrackerApi.Controllers [ApiController] // Explicitly authorized. These endpoints are all tenant-scoped user data, so they must not // depend on the Auth:Require fallback policy being switched on: a deployment that lost that flag - // would otherwise serve them anonymously. docs/production-readiness-review.md. + // would otherwise serve them anonymously. docs/security/security-notes.md. [Route("api/jobapplications")] [Authorize(AuthenticationSchemes = "local")] public class JobApplicationsController : ControllerBase @@ -1356,7 +1356,7 @@ Candidate CV/profile: // application checklist is the one workflow surface; readiness projects it into the score / // completed / missing / reminders health view the dialog and dashboard already consume, so // the two can never disagree. The DTO shape is unchanged on purpose. - // docs/architecture/application-workspace.md. + // docs/architecture/overview.md. var checklist = job.OwnerUserId is null ? null : await _checklist.GetAsync(job.OwnerUserId, id, cancellationToken); diff --git a/JobTrackerApi/Controllers/JobImportController.cs b/JobTrackerApi/Controllers/JobImportController.cs index 03e9045..cc546e3 100644 --- a/JobTrackerApi/Controllers/JobImportController.cs +++ b/JobTrackerApi/Controllers/JobImportController.cs @@ -7,7 +7,7 @@ namespace JobTrackerApi.Controllers; [ApiController] // Explicitly authorized. These endpoints are all tenant-scoped user data, so they must not // depend on the Auth:Require fallback policy being switched on: a deployment that lost that flag -// would otherwise serve them anonymously. docs/production-readiness-review.md. +// would otherwise serve them anonymously. docs/security/security-notes.md. [Route("api/jobimport")] [Authorize(AuthenticationSchemes = "local")] public sealed class JobImportController : ControllerBase @@ -29,4 +29,3 @@ public sealed class JobImportController : ControllerBase return Ok(result); } } - diff --git a/JobTrackerApi/Controllers/PublicCvController.cs b/JobTrackerApi/Controllers/PublicCvController.cs index 42ef278..cb3e976 100644 --- a/JobTrackerApi/Controllers/PublicCvController.cs +++ b/JobTrackerApi/Controllers/PublicCvController.cs @@ -8,7 +8,7 @@ using Microsoft.AspNetCore.RateLimiting; namespace JobTrackerApi.Controllers; // Anonymous read-only surface for public CVs: /cv/{slug} in the SPA fetches this. Only variants the -// owner has explicitly toggled public are served; noindex by default. docs/architecture/cv-builder.md. +// owner has explicitly toggled public are served; noindex by default. docs/architecture/overview.md. [ApiController] [Route("api/public-cv")] [AllowAnonymous] diff --git a/JobTrackerApi/Controllers/RulesController.cs b/JobTrackerApi/Controllers/RulesController.cs index 6b2ba2c..d7d8025 100644 --- a/JobTrackerApi/Controllers/RulesController.cs +++ b/JobTrackerApi/Controllers/RulesController.cs @@ -9,7 +9,7 @@ namespace JobTrackerApi.Controllers [ApiController] // Explicitly authorized. These endpoints are all tenant-scoped user data, so they must not // depend on the Auth:Require fallback policy being switched on: a deployment that lost that flag - // would otherwise serve them anonymously. docs/production-readiness-review.md. + // would otherwise serve them anonymously. docs/security/security-notes.md. [Route("api/rules")] [Authorize(AuthenticationSchemes = "local")] public class RulesController : ControllerBase diff --git a/JobTrackerApi/Data/JobTrackerContext.cs b/JobTrackerApi/Data/JobTrackerContext.cs index a6824fe..f158df9 100644 --- a/JobTrackerApi/Data/JobTrackerContext.cs +++ b/JobTrackerApi/Data/JobTrackerContext.cs @@ -385,7 +385,7 @@ namespace JobTrackerApi.Data modelBuilder.Entity() .HasIndex(x => x.UserId); - // Career Workspace foundation (docs/career-workspace-implementation-roadmap.md Phase F1). + // Career Workspace foundation (docs/roadmap/todo.md). // One CareerProfile per user for now -- see roadmap "Not now: multiple profiles per user". modelBuilder.Entity() .HasQueryFilter(x => CurrentUserId != null && x.OwnerUserId == CurrentUserId); @@ -412,7 +412,7 @@ namespace JobTrackerApi.Data .OnDelete(DeleteBehavior.Cascade); // Phase 3: relational children of CareerProfile (the editable master profile). - // docs/architecture/career-profile-model.md. Same deny-on-null tenant filter as everything + // docs/architecture/overview.md. Same deny-on-null tenant filter as everything // else; cascade-delete with the parent; indexed by (OwnerUserId, CareerProfileId, SortOrder) // for the ordered per-profile reads the /career editor does. ConfigureCareerChild(modelBuilder); @@ -470,7 +470,7 @@ namespace JobTrackerApi.Data .OnDelete(DeleteBehavior.Cascade); // Phase 4: CV variants (a lens over the master profile) + their autosave history. - // docs/architecture/cv-builder.md. Same deny-on-null tenant filter as everything else. A + // docs/architecture/overview.md. Same deny-on-null tenant filter as everything else. A // variant references a job application (reference-not-ownership, SetNull on delete) and never // owns career data. PublicSlug is globally unique so /cv/{slug} can resolve it anonymously. modelBuilder.Entity() @@ -504,7 +504,7 @@ namespace JobTrackerApi.Data // Phase 5: append-only AI interaction history per job application. Same deny-on-null tenant // filter; indexed for the per-job history read; cascades with the application. - // docs/architecture/ai-career-assistant.md. + // docs/ai/architecture.md. modelBuilder.Entity() .HasQueryFilter(x => CurrentUserId != null && x.OwnerUserId == CurrentUserId); // varchar (not longtext) for the indexed columns — see the CvVariant note above. @@ -532,7 +532,7 @@ namespace JobTrackerApi.Data // Phase 5 Milestone 2: the application checklist — a workflow guidance layer over the existing // readiness signals, not a second store of truth. Same deny-on-null tenant filter; cascades with - // the application. docs/architecture/application-workspace.md. + // the application. docs/architecture/overview.md. modelBuilder.Entity() .HasQueryFilter(x => CurrentUserId != null && x.OwnerUserId == CurrentUserId); // varchar (not longtext) for the indexed columns — see the CvVariant note above. @@ -557,7 +557,7 @@ namespace JobTrackerApi.Data // Phase 5.4: append-only cover letter history. JobApplication.CoverLetterText stays the // current text; this makes every previous state recoverable. - // docs/architecture/application-workspace.md. + // docs/architecture/overview.md. modelBuilder.Entity() .HasQueryFilter(x => CurrentUserId != null && x.OwnerUserId == CurrentUserId); // varchar (not longtext) for the indexed columns — see the CvVariant note above. @@ -600,7 +600,7 @@ namespace JobTrackerApi.Data .OnDelete(DeleteBehavior.Cascade); // Phase 5.5: user-owned interview preparation. Unlike InterviewPrepNote (an AI cache), nothing - // regenerates this. docs/architecture/application-workspace.md. + // regenerates this. docs/architecture/overview.md. modelBuilder.Entity() .HasQueryFilter(x => CurrentUserId != null && x.OwnerUserId == CurrentUserId); // varchar (not longtext) for the indexed columns — see the CvVariant note above. diff --git a/JobTrackerApi/Migrations/20260717222917_AddCareerProfileRelationalChildren.cs b/JobTrackerApi/Migrations/20260717222917_AddCareerProfileRelationalChildren.cs index 23f5f45..0ea4d50 100644 --- a/JobTrackerApi/Migrations/20260717222917_AddCareerProfileRelationalChildren.cs +++ b/JobTrackerApi/Migrations/20260717222917_AddCareerProfileRelationalChildren.cs @@ -14,7 +14,7 @@ namespace JobTrackerApi.Migrations // The six CareerProfile child tables were initially provisioned by the reconciler. Explicit // ownership moved to 20260830133000_AdoptCareerProfileSchema; this historical migration stays // recorded so existing chains remain compatible. - // docs/infrastructure/database-ownership.md. + // docs/database/migrations.md. /// protected override void Up(MigrationBuilder migrationBuilder) { diff --git a/JobTrackerApi/Migrations/20260719120954_AddCoverLetterVersions.cs b/JobTrackerApi/Migrations/20260719120954_AddCoverLetterVersions.cs index 20ed56f..2a3f42c 100644 --- a/JobTrackerApi/Migrations/20260719120954_AddCoverLetterVersions.cs +++ b/JobTrackerApi/Migrations/20260719120954_AddCoverLetterVersions.cs @@ -14,7 +14,7 @@ namespace JobTrackerApi.Migrations // CoverLetterVersions is reconciler-owned and provisioned by StartupInitializationExtensions, // which carries correct DDL per provider and guards the create on JobApplications existing. // This migration exists only to keep the model snapshot in sync. - // docs/infrastructure/database-ownership.md. + // docs/database/migrations.md. /// protected override void Up(MigrationBuilder migrationBuilder) { diff --git a/JobTrackerApi/Migrations/20260719145044_AddInterviewPrepItems.cs b/JobTrackerApi/Migrations/20260719145044_AddInterviewPrepItems.cs index 8a6bfa8..ecd1a78 100644 --- a/JobTrackerApi/Migrations/20260719145044_AddInterviewPrepItems.cs +++ b/JobTrackerApi/Migrations/20260719145044_AddInterviewPrepItems.cs @@ -13,7 +13,7 @@ namespace JobTrackerApi.Migrations // // InterviewPrepItems is reconciler-owned and provisioned by StartupInitializationExtensions, // which carries correct DDL per provider and guards the create on JobApplications existing. - // docs/infrastructure/database-ownership.md. + // docs/database/migrations.md. /// protected override void Up(MigrationBuilder migrationBuilder) { diff --git a/JobTrackerApi/Models/AiInteraction.cs b/JobTrackerApi/Models/AiInteraction.cs index 8210119..1ef6ff9 100644 --- a/JobTrackerApi/Models/AiInteraction.cs +++ b/JobTrackerApi/Models/AiInteraction.cs @@ -4,7 +4,7 @@ namespace JobTrackerApi.Models; // Unlike AiWorkspaceNote (one row per (owner, job, type), overwritten on regenerate — a cache), this // keeps EVERY generation so the user can restore, compare, reuse, or delete past outputs. Suggestion // only: an interaction never mutates the master profile, a CV variant, or the application itself. -// docs/architecture/ai-career-assistant.md. +// docs/ai/architecture.md. public sealed class AiInteraction { public int Id { get; set; } diff --git a/JobTrackerApi/Models/ApplicationChecklistItem.cs b/JobTrackerApi/Models/ApplicationChecklistItem.cs index 2585e9b..94b6722 100644 --- a/JobTrackerApi/Models/ApplicationChecklistItem.cs +++ b/JobTrackerApi/Models/ApplicationChecklistItem.cs @@ -9,7 +9,7 @@ namespace JobTrackerApi.Models; // System items carry a stable SystemKey and (usually) an AutoSignal: the same readiness signal the // /readiness endpoint already computes. When the signal is satisfied the item auto-completes, so the // checklist and readiness can never disagree — readiness became the calculation, this is the surface. -// docs/architecture/application-workspace.md. +// docs/architecture/overview.md. public sealed class ApplicationChecklistItem { public int Id { get; set; } diff --git a/JobTrackerApi/Models/CareerEntities.cs b/JobTrackerApi/Models/CareerEntities.cs index 04e9f0b..eb054ab 100644 --- a/JobTrackerApi/Models/CareerEntities.cs +++ b/JobTrackerApi/Models/CareerEntities.cs @@ -4,7 +4,7 @@ using System.Text.Json; namespace JobTrackerApi.Models; // Phase 3: relational children of CareerProfile — the editable master career profile. -// See docs/architecture/career-profile-model.md. +// See docs/architecture/overview.md. // // Design notes shared by all child entities: // - OwnerUserId is denormalized so the tenant global query filter applies directly (same pattern diff --git a/JobTrackerApi/Models/CareerProfile.cs b/JobTrackerApi/Models/CareerProfile.cs index dd0b493..68c2ef5 100644 --- a/JobTrackerApi/Models/CareerProfile.cs +++ b/JobTrackerApi/Models/CareerProfile.cs @@ -12,7 +12,7 @@ public sealed class CareerProfile // Phase 3: ProfileJson is now a DERIVED projection of the relational children + LongTailJson, // serialized into the StructuredCvProfile shape and kept in sync on every save so the legacy // read paths (CV rendering, tailoring, match-score) keep working unchanged. It is no longer an - // independently editable source. See docs/architecture/career-profile-model.md §4. + // independently editable source. See docs/architecture/overview.md. public string ProfileJson { get; set; } = string.Empty; // The editable long tail that is not worth a relational table: contact, summary, interests, diff --git a/JobTrackerApi/Models/CvTheme.cs b/JobTrackerApi/Models/CvTheme.cs index ae6ab30..acdf132 100644 --- a/JobTrackerApi/Models/CvTheme.cs +++ b/JobTrackerApi/Models/CvTheme.cs @@ -3,7 +3,7 @@ namespace JobTrackerApi.Models; // A theme is DATA, not code. The renderer (ThemedCvRenderer) has one render path; every visual // difference between themes is expressed by the fields below. Adding a theme = adding one CvTheme to // CvThemeCatalog — no renderer changes. This is the core Phase 4 architectural rule -// ("Do NOT hardcode HTML templates"). docs/architecture/cv-theme-engine.md. +// ("Do NOT hardcode HTML templates"). docs/architecture/frontend.md. public sealed class CvTheme { public string Id { get; init; } = string.Empty; diff --git a/JobTrackerApi/Models/CvVariant.cs b/JobTrackerApi/Models/CvVariant.cs index 000ba31..9d52f00 100644 --- a/JobTrackerApi/Models/CvVariant.cs +++ b/JobTrackerApi/Models/CvVariant.cs @@ -6,7 +6,7 @@ namespace JobTrackerApi.Models; // sections show, in what order, with what per-item overrides, rendered through a chosen theme with // layout/colour/font settings. The career data is never duplicated here — the variant stores only // selections + overrides keyed by the master profile's stable ItemKeys, plus builder preferences. -// docs/architecture/cv-builder.md. +// docs/architecture/overview.md. public sealed class CvVariant { public int Id { get; set; } diff --git a/JobTrackerApi/Program.cs b/JobTrackerApi/Program.cs index 7a984d1..3b99803 100644 --- a/JobTrackerApi/Program.cs +++ b/JobTrackerApi/Program.cs @@ -664,7 +664,7 @@ app.MapControllers(); // this process up and serving?" and nothing else. It does NOT touch the database — a health check // that queried MariaDB would restart a perfectly healthy backend whenever the database blipped, and // would also be a free unauthenticated way to probe database availability. -// docs/production-readiness-review.md. +// docs/security/security-notes.md. // Version comes from configuration (App:Version), which is how docker-compose supplies it as // App__Version. Reading the APP_VERSION environment variable directly meant /health always // reported "unknown" in a container. Falls back to the assembly version for local development. diff --git a/JobTrackerApi/Services/ApplicationAssetsService.cs b/JobTrackerApi/Services/ApplicationAssetsService.cs index fa75580..9babc59 100644 --- a/JobTrackerApi/Services/ApplicationAssetsService.cs +++ b/JobTrackerApi/Services/ApplicationAssetsService.cs @@ -14,7 +14,7 @@ namespace JobTrackerApi.Services; // // The flow is strictly one-directional — CareerProfile -> CvVariant -> application output. Nothing // here writes upward: no method touches CareerProfile or its children. -// docs/architecture/application-workspace.md. +// docs/architecture/overview.md. public sealed record ApplicationCvDto( int? AttachedVariantId, string? AttachedVariantName, diff --git a/JobTrackerApi/Services/ApplicationChecklistService.cs b/JobTrackerApi/Services/ApplicationChecklistService.cs index 2f47371..691b330 100644 --- a/JobTrackerApi/Services/ApplicationChecklistService.cs +++ b/JobTrackerApi/Services/ApplicationChecklistService.cs @@ -12,7 +12,7 @@ namespace JobTrackerApi.Services; // /readiness endpoint computes, and are re-synced on every read, so "readiness says X is missing" and // "the checklist says X is pending" cannot drift apart. Readiness keeps its API contract and becomes // the calculation; the checklist is what the user actually works from and edits. -// docs/architecture/application-workspace.md. +// docs/architecture/overview.md. public sealed record ChecklistItemDto( int Id, string? SystemKey, diff --git a/JobTrackerApi/Services/ApplicationIntelligenceService.cs b/JobTrackerApi/Services/ApplicationIntelligenceService.cs index 35e8003..4958bd4 100644 --- a/JobTrackerApi/Services/ApplicationIntelligenceService.cs +++ b/JobTrackerApi/Services/ApplicationIntelligenceService.cs @@ -18,7 +18,7 @@ namespace JobTrackerApi.Services; // The AI narrative is deliberately NOT here: it stays in AiWorkspaceService's "job-analysis" and // "career-match" modules, which are suggestion-only, append-only (AiInteraction is the version // history), and require the user to act on them. Nothing in this file writes to the CareerProfile, -// a CvVariant, or the JobApplication. docs/architecture/application-workspace.md. +// a CvVariant, or the JobApplication. docs/architecture/overview.md. public sealed record JobAnalysisDto( string? Role, string? Company, diff --git a/JobTrackerApi/Services/ApplicationTimelineService.cs b/JobTrackerApi/Services/ApplicationTimelineService.cs index fb9ec6a..1796964 100644 --- a/JobTrackerApi/Services/ApplicationTimelineService.cs +++ b/JobTrackerApi/Services/ApplicationTimelineService.cs @@ -10,7 +10,7 @@ namespace JobTrackerApi.Services; // writes nothing, stores nothing, and adds no table. It turns rows like // ("StatusChanged", "Applied", "Interview") into a sentence, tags each event with a category and // whether it is a milestone, and groups the result by day so the workspace can render a real -// timeline instead of a flat list. docs/architecture/application-workspace.md. +// timeline instead of a flat list. docs/architecture/overview.md. public sealed record TimelineEventDto( int Id, string Type, diff --git a/JobTrackerApi/Services/ApplicationWorkspaceService.cs b/JobTrackerApi/Services/ApplicationWorkspaceService.cs index 6252243..d408dfd 100644 --- a/JobTrackerApi/Services/ApplicationWorkspaceService.cs +++ b/JobTrackerApi/Services/ApplicationWorkspaceService.cs @@ -9,7 +9,7 @@ namespace JobTrackerApi.Services; // This is an AGGREGATE READ ONLY. It owns no data and duplicates none: the CV comes from the Phase 4 // CvVariant lens, documents from Attachments, activity from JobEvent, AI history from AiInteraction, // stage semantics from JobPipeline. Nothing here writes, and no career data is copied. -// docs/architecture/application-workspace.md. +// docs/architecture/overview.md. public sealed record WorkspaceCvDto(int? VariantId, string? VariantName, string? ThemeId, bool HasTailoredCvText, DateTimeOffset? UpdatedAtUtc); public sealed record WorkspaceActivityDto(string Type, string? Detail, DateTime At); public sealed record WorkspaceNextStepDto(string Key, string Label, string Reason, string? Section); diff --git a/JobTrackerApi/Services/BuildMetadata.cs b/JobTrackerApi/Services/BuildMetadata.cs index a2f5a24..87a11e4 100644 --- a/JobTrackerApi/Services/BuildMetadata.cs +++ b/JobTrackerApi/Services/BuildMetadata.cs @@ -9,7 +9,7 @@ namespace JobTrackerApi.Services; // the anonymous /health endpoint. /health used to read the APP_VERSION *environment // variable* directly, which compose never sets under that name — it passes App__Version, // binding to the App:Version configuration key. So /health always reported "unknown". -// See docs/release-candidate-review.md (N2). +// See docs/infrastructure/deployment.md for the health/version verification contract. public static class BuildMetadata { // Ignore unresolved shell/compose placeholders that would otherwise leak into the diff --git a/JobTrackerApi/Services/CareerProfileMapper.cs b/JobTrackerApi/Services/CareerProfileMapper.cs index 0cf5593..d7cdbe2 100644 --- a/JobTrackerApi/Services/CareerProfileMapper.cs +++ b/JobTrackerApi/Services/CareerProfileMapper.cs @@ -6,7 +6,7 @@ namespace JobTrackerApi.Services; // Phase 3: two-way projection between the relational CareerProfile children and the // StructuredCvProfile shape (the derived blob + the frontend/AI edit unit). -// See docs/architecture/career-profile-model.md. +// See docs/architecture/overview.md. // // Relational holds: Experience, Education, Skill, Project, Certification, Language. // LongTailJson holds the rest of StructuredCvProfile: Contact, Summary, Interests, OtherSections, diff --git a/JobTrackerApi/Services/CareerProfileService.cs b/JobTrackerApi/Services/CareerProfileService.cs index a26fd53..fecefc3 100644 --- a/JobTrackerApi/Services/CareerProfileService.cs +++ b/JobTrackerApi/Services/CareerProfileService.cs @@ -5,7 +5,7 @@ using Microsoft.EntityFrameworkCore; namespace JobTrackerApi.Services; -// Career Workspace foundation (see docs/career-workspace-implementation-roadmap.md, Phase F1). +// Career Workspace foundation (see docs/roadmap/todo.md). // Bounded to the profile/CV domain -- job tracking is untouched. Every existing read path still // goes through ApplicationUser.ProfileCvStructureJson (dual-write window); this service is the // single place stable item IDs and normalized dates get assigned, and where profile history is diff --git a/JobTrackerApi/Services/EmailProviders/IEmailProvider.cs b/JobTrackerApi/Services/EmailProviders/IEmailProvider.cs index a46c163..f2d0487 100644 --- a/JobTrackerApi/Services/EmailProviders/IEmailProvider.cs +++ b/JobTrackerApi/Services/EmailProviders/IEmailProvider.cs @@ -3,7 +3,7 @@ namespace JobTrackerApi.Services.EmailProviders /// /// Provider-neutral email operations so job correspondence can be sourced from Gmail, /// Microsoft Graph, generic IMAP, or manual/free-text entry behind a single seam. - /// See docs/remaster/PRODUCT_DIRECTION.md (multi-provider email). Gmail is the first + /// See docs/architecture/integrations.md (multi-provider email). Gmail is the first /// implementation (); the controller migration and additional /// providers land in follow-up slices. /// diff --git a/JobTrackerApi/Services/InterviewPrepService.cs b/JobTrackerApi/Services/InterviewPrepService.cs index ab1b518..5d4abdb 100644 --- a/JobTrackerApi/Services/InterviewPrepService.cs +++ b/JobTrackerApi/Services/InterviewPrepService.cs @@ -13,7 +13,7 @@ namespace JobTrackerApi.Services; // // Follow-ups reuse what already exists: JobApplication.FollowUpAt for the date (RulesEngine and the // reminder hosted service already act on it) and ApplicationChecklistItem for the task. No second -// reminder system. docs/architecture/application-workspace.md. +// reminder system. docs/architecture/overview.md. public sealed record InterviewPrepItemDto( int Id, string Category, diff --git a/JobTrackerApi/Services/JobLifecycleEvents.cs b/JobTrackerApi/Services/JobLifecycleEvents.cs index cf81ea6..3c0a666 100644 --- a/JobTrackerApi/Services/JobLifecycleEvents.cs +++ b/JobTrackerApi/Services/JobLifecycleEvents.cs @@ -11,7 +11,7 @@ namespace JobTrackerApi.Services; // // Events are derived from the TRANSITION, not from the resulting state, so one user action produces // at most one lifecycle event and re-saving an unchanged status produces none. -// docs/architecture/application-workspace.md. +// docs/architecture/overview.md. public static class JobLifecycleEvents { /// diff --git a/JobTrackerApi/Services/StartupInitializationExtensions.cs b/JobTrackerApi/Services/StartupInitializationExtensions.cs index 862f722..3b060b5 100644 --- a/JobTrackerApi/Services/StartupInitializationExtensions.cs +++ b/JobTrackerApi/Services/StartupInitializationExtensions.cs @@ -121,7 +121,7 @@ public static class StartupInitializationExtensions // migration-owned tables do not exist yet. Repairing an absent table is not this pass's job -- // it is skipped, Migrate() creates it, and the post-Migrate pass then finds it and reconciles. // Without this guard a clean MariaDB install died on "Table 'CareerProfiles' doesn't exist". - // See docs/infrastructure/database-ownership.md. + // See docs/database/migrations.md. private static void EnsureMySqlIndex(DbConnection c, string table, string indexName, string columnsSql, bool unique = false) { if (!HasMySqlTable(c, table)) return; @@ -276,7 +276,7 @@ public static class StartupInitializationExtensions // the migration-created tables and finishes compatibility repair. // // Every statement in here is existence-guarded, so running it twice is a no-op scan on an - // already-correct database. docs/infrastructure/database-ownership.md. + // already-correct database. docs/database/migrations.md. void ReconcileSchema() { if (useSqliteBootstrap) diff --git a/JobTrackerApi/Services/ThemedCvRenderer.cs b/JobTrackerApi/Services/ThemedCvRenderer.cs index 6acbc14..05d1352 100644 --- a/JobTrackerApi/Services/ThemedCvRenderer.cs +++ b/JobTrackerApi/Services/ThemedCvRenderer.cs @@ -15,7 +15,7 @@ public interface IThemedCvRenderer // ONE render path for every theme. The theme (data) drives layout, palette, typography and spacing; // the variant settings apply a few runtime overrides (accent, fonts, density, page size, photo, -// icons, page numbers). Adding a theme never touches this file. docs/architecture/cv-theme-engine.md. +// icons, page numbers). Adding a theme never touches this file. docs/architecture/frontend.md. public sealed class ThemedCvRenderer : IThemedCvRenderer { public ThemedCvRenderResult Render(CvRenderModel model, CvTheme theme, CvVariantSettings settings) diff --git a/README.md b/README.md index 07a90e1..1239382 100644 --- a/README.md +++ b/README.md @@ -1,315 +1,52 @@ # Job Tracker -Job Tracker is a self-hosted career workspace built with a Next.js/React frontend and an ASP.NET Core API. SQLite is the local default; MariaDB/MySQL is the supported server-database option. +Job Tracker (Jobjakt) is a self-hosted workspace for managing job applications, companies, correspondence, follow-ups, attachments, career profiles, CVs, and optional local/private AI assistance. -## Features (high level) +## Stack -- Track job applications (status, applied date, notes, tags, follow-up dates, deadlines, salary, links, etc.) -- Share opt-in public CV links with recruiter-friendly PDF download -- Company management (location/source, recruiter details, pipeline stage, next contact date) -- Correspondence log per application (email/messages with subject/channel/date) -- Attachments per application (upload, list, download, rename, delete) -- Reminders + follow-up “needs attention” logic driven by configurable rules -- History/event trail per application (created, status changes, follow-up set, delete/restore) -- Export jobs to JSON/CSV + daily scheduled JSON export -- Optional “job import” preview from supported job sites (plugins) + optional translation to English -- Quick-capture bookmarklet (Settings) + installable PWA with a mobile share-target: both open `/?add=` to pre-fill Add Job from any posting - - Note: no offline service-worker cache is bundled by design (the app is deployed frequently; an aggressive cache would risk serving stale builds). The manifest provides installability and share-to-capture without it. -- Optional local AI service for short/full descriptions -- Optional Google sign-in (Google ID tokens) to protect the API -- Standard Problem Details responses with trace IDs for unhandled API errors +- Next.js 16 / React 19 / TypeScript / MUI frontend +- ASP.NET Core 9 / EF Core 9 API +- SQLite by default; MariaDB/MySQL for server deployments +- FastAPI document/AI service with optional Ollama, Gemini, or Groq CV routing +- Docker Compose + nginx; Gitea Actions deployment -## Architecture +## Quick start -- `job-tracker-ui/`: statically exported Next.js 16 / React 19 app (runs on `http://localhost:3000` in dev) -- `JobTrackerApi/`: ASP.NET Core API (defaults to `http://localhost:5202`) -- Database: SQLite defaults to `JobTrackerApi/jobtracker.db`; MariaDB/MySQL is selected with `Database:Provider` -- Attachments: stored on disk under `DataRoot/Attachments//...` -- Optional local AI service: `tools/summarizer/` (FastAPI) used by the API via `Ai:BaseUrl` +Prerequisites: .NET SDK 9, Node.js 24/npm, and Docker Compose for the full stack. -## Quickstart (Docker) +For Docker, copy `.env.example` to `.env`, fill required values, then run: -This runs: frontend (nginx), backend API, the local AI service, and an Ollama container for hybrid CV block classification. - -1) Create a `.env` file next to `docker-compose.yml` (you can start from `.env.example`). - -```bash +```powershell docker compose -f docker-compose.yml -f docker-compose.dev.yml up --build ``` -- UI: `http://localhost:3000` -- API: available via the UI container reverse proxy at `http://localhost:3000/api/...` -- Persistent data: stored in the `jobtracker_data` Docker volume (mounted at `/data` in the API container) +Open `http://localhost:3000`. The API is available at `http://localhost:5202` in local process development. -## Local development +For local processes: -### Prereqs - -- .NET SDK `9.x` (API targets `net9.0`) -- Node.js 24 and npm (matching CI) -- (Optional) Python 3.12 if running the AI service without Docker - -### 1) Run the API - -```bash +```powershell cd JobTrackerApi dotnet restore --locked-mode dotnet run -``` -By default the API listens on `http://localhost:5202` (see `JobTrackerApi/Properties/launchSettings.json`). - -### Local preflight before browser/UAT - -Run the backend first from `JobTrackerApi/`, then use the preflight gate from the repo root: - -```bash -bash scripts/s06-preflight.sh -``` - -The preflight assumes the dev pairing used by `job-tracker-ui/src/api.ts` and `JobTrackerApi/appsettings.Development.json`: - -- UI origin: `http://localhost:3000` -- API base: `http://localhost:5202/api` - -It first checks the anonymous `GET /api/auth/config` endpoint, then probes `GET /api/admin/system` for DB/Gmail/AI readiness. -If auth is enabled and `/api/admin/system` returns `401/403`, export an admin bearer token first and rerun: - -```bash -export AUTH_TOKEN="" -bash scripts/s06-preflight.sh -``` - -To obtain a local admin token in dev, log in against the API with the seeded admin email/password from `JobTrackerApi/appsettings.Development.json` (or your environment override) via `POST /api/auth/login`, then export only the returned access token. The script never prints token values. Use `API_BASE` if your API is not on the default dev port. - -### Seed acceptance-ready data - -After preflight passes and you have a bearer token, seed one deterministic acceptance fixture for the `/jobs` → workspace → follow-up → dashboard/reminders rerun: - -```bash -export AUTH_TOKEN="" -bash scripts/s06-acceptance-data.sh -``` - -The script reuses `scripts/s06-preflight.sh`, creates or reuses the acceptance company/job, saves tailored package material, ensures one deterministic recruiter-thread correspondence entry, schedules follow-up readiness, and prints the seeded ids/readiness summary without echoing the token. - -If the placeholder development password no longer matches the local DB, use the real account for this environment or a bearer token from an already-authenticated local browser session. - -### 2) Run the UI - -```bash -cd job-tracker-ui +cd ..\job-tracker-ui npm ci npm run dev ``` -The UI defaults to calling `http://localhost:5202/api` when running on localhost (see `job-tracker-ui/src/api.ts`). +## Quality checks -### 3) Run the browser smoke suite - -```bash +```powershell +dotnet test JobTrackerApi.Tests/JobTrackerApi.Tests.csproj --no-restore cd job-tracker-ui -npx playwright install chromium -npm run test:e2e +npm run lint +npm test -- --runInBand +npm run typecheck +npm run build ``` -The suite uses `http://localhost:3300`, starts isolated API/SQLite and Next.js processes, then covers login, saved-job creation, -Career Workspace, and anonymous public-CV/PDF access. It is also a required CI gate. +Run browser tests with `npx playwright install chromium` once, followed by `npm run test:e2e` from `job-tracker-ui/`. -### 4) (Optional) Run the AI service +## Documentation -The API calls a local FastAPI service to generate summaries. If it’s not running, the app still works (summary generation may be empty / best-effort). - -With Docker (recommended): - -```bash -# One command for local Ollama startup + pull + AI-service restart -OLLAMA_MODEL=qwen2.5:7b ./scripts/start-ollama-cv.sh - -# Then start the rest of the app if needed -docker compose -f docker-compose.yml -f docker-compose.dev.yml up --build -d backend frontend -``` - -The first Ollama startup is usually quick, but the first model pull and first generation can take a while. After the model is cached in the `ollama_data` volume, later restarts are much faster. - -Or run directly from `tools/summarizer/` (see `tools/summarizer/README.md`). - -## Configuration - -### API settings (appsettings / env vars) - -Common keys: - -- `ConnectionStrings:JobTracker`: overrides SQLite location (otherwise uses `DataRoot/jobtracker.db`) -- `Data:Root`: folder for the SQLite DB + exports (defaults to API content root) -- `Data:AttachmentsRoot`: override attachments folder (defaults to `/Attachments`) -- `CvExports:RetainDays`: generated PDF retention in days (default `30`, clamped to `1`–`365`) -- `Cors:Origins`: list of allowed origins (defaults to `http://localhost:3000`; wildcard origins are rejected because requests use credentials) -- `Ai:BaseUrl`: AI service base URL (default `http://127.0.0.1:8001`) -- `Workers:RulesEnabled`, `Workers:FollowUpRemindersEnabled`, `Workers:DailyExportEnabled`, `Workers:JobEnrichmentEnabled`: explicit worker kill switches, all default `false`; do not enable before the prerequisites in `docs/architecture/background-workers.md` -- `Exports:DailyEnabled`: legacy daily-export setting; both it and `Workers:DailyExportEnabled` must be true -- `Exports:DailyFolder`: export destination (relative to `Data:Root` if not absolute) -- `Exports:DailyHourLocal`: local hour (0–23) when the daily export runs -- `Backups:Enabled`: enable/disable the automated SQLite backup job (default `true`) -- `Backups:HourLocal`: local hour (0–23) when the daily database backup runs (default `3`) -- `Backups:RetainCount`: how many backup files to keep in `/backups` (default `14`) - - Backups use SQLite `VACUUM INTO` (consistent snapshot, safe with WAL). A catch-up backup runs at startup when none exists from the last 24 h. For MySQL/MariaDB configure external backups instead (see `deploy/MARIADB.md`). -- `Auth:GoogleClientId`: if set, enables JWT bearer validation for Google ID tokens -- `Auth:JwtKey`: secret used to sign local JWTs for username/password login (set via env var `Auth__JwtKey`) -- `Auth:JwtIssuer`: JWT issuer (default `JobTrackerApi`) -- `Auth:JwtAudience`: JWT audience (default `job-tracker-ui`) -- `Auth:JwtExpiresMinutes`: access token lifetime in minutes (default `720`) -- `Auth:AdminEmail` / `Auth:AdminPassword`: optional seed admin user (created on startup if missing) -- `Auth:AllowRegistration`: allow self-service registration via `POST /api/auth/register` (default `false`) -- `Auth:Require`: if `true`, all endpoints require auth (except endpoints explicitly marked anonymous) -- `Stripe:SecretKey`, `Stripe:PricePremium`, `Stripe:WebhookSecret`: enable hosted Premium checkout, - the customer portal, and signed subscription webhooks when all are configured -- `Translation:Provider`: `none` (default) or `libretranslate` -- `Translation:LibreTranslate:BaseUrl`: base URL for LibreTranslate (only if provider enabled) -- `Translation:LibreTranslate:ApiKey`: optional API key for LibreTranslate -- `App:PublicBaseUrl`: the single external origin used for email links, OAuth callbacks, billing redirects, secure cookies, and production Host validation (Production requires HTTPS; local development defaults to `http://localhost:3000`) -- `Auth:MicrosoftTenant`: Microsoft application sign-in policy (`common`, `organizations`, `consumers`, or one tenant GUID); required in Production when Microsoft sign-in is enabled and distinct from Graph's `Microsoft:TenantId` -- `Email:Enabled`: enable SMTP sending (`true`/`false`) -- `Email:SmtpHost`: SMTP host (for Gmail: `smtp.gmail.com`) -- `Email:SmtpPort`: SMTP port (for Gmail: `587`) -- `Email:SmtpUser`: SMTP username (often your Gmail address) -- `Email:SmtpPassword`: SMTP password (for Gmail: use an App Password) -- `Email:From`: from address (default: `Email:SmtpUser`) -- `Email:FromName`: from name (default: `Jobbjakt`) -- `Email:FollowUpReminders:Enabled`: enable scheduled follow-up reminder emails -- `Email:FollowUpReminders:UpcomingDays`: how far ahead reminder emails look for upcoming follow-up dates (default `2`) - -### UI settings - -- `NEXT_PUBLIC_API_BASE_URL`: override the API base URL (example: `http://localhost:5202/api`) - -The complete environment template is `.env.example`. Production requirements and restore/rollback procedures are maintained in `deploy/README.md`; current component boundaries and supported providers are documented in `docs/architecture/current.md`. - -## API endpoint reference - -Base URL in local dev: `http://localhost:5202` (all routes are under `/api/...`). - -Authentication: - -- If `Auth:GoogleClientId` is configured, most endpoints require `Authorization: Bearer `. -- If it’s not configured, endpoints are effectively anonymous. - -### Job applications (`/api/jobapplications`) - -- `GET /api/jobapplications` - - Query: `page`, `pageSize` (15/20/25), `q`, `status`, `companyId`, `location`, `needsFollowUp`, `includeDeleted`, `deletedOnly`, `sortBy`, `sortDir` - - Returns a paged list of `JobApplicationDto` (includes computed follow-up flags and short summary). -- `GET /api/jobapplications/{id}` - - Returns a single `JobApplicationDto` (includes computed follow-up flags and a “full” summary on demand). -- `GET /api/jobapplications/board?includeDeleted=false` - - Returns all job applications (intended for a board/overview view). -- `GET /api/jobapplications/reminders?upcomingDays=7` - - Returns jobs that need follow-up / are in key statuses and have upcoming follow-up dates. -- `POST /api/jobapplications` - - Body: `CreateJobApplicationRequest` (job title, company id, status, notes/description, follow-up fields, tags, attachments flags, etc.) - - Creates a job application and a `JobEvent` of type `Created`. -- `PUT /api/jobapplications/{id}` - - Body: `UpdateJobApplicationRequest` - - Updates an application; records a `StatusChanged` event if the status changed. -- `PATCH /api/jobapplications/{id}/status` - - Body: `{ "status": "..." }` - - Updates only status; records `StatusChanged` if it changed. The status is normalized against the canonical pipeline (casing + known synonyms like `Interviewing`→`Interview`); unrecognized values are preserved as custom statuses. -- `GET /api/jobapplications/pipeline` - - Returns the canonical ordered pipeline stages (`Applied, Waiting, Interview, Offer, Rejected, Ghosted`) with display order and category (`Active`/`Success`/`Closed`). The UI renders board columns and status dropdowns from this single source of truth. -- `PATCH /api/jobapplications/{id}/followup` - - Body: `{ "followUpAt": "2026-03-13T12:00:00Z" }` (or `null`) - - Sets/clears follow-up date; records a `FollowUpSet` event. -- `GET /api/jobapplications/{id}/history` - - Returns `JobEvent` history for the job. -- `GET /api/jobapplications/{id}/timeline` - - Returns a unified timeline combining job events, correspondence, and attachments. -- `GET /api/jobapplications/stats` - - Returns totals, counts by status, applied-last-30-days, and average days since applied. -- `GET /api/jobapplications/{id}/match-score` - - Deterministic CV↔job keyword-coverage score (0–100) with matched/missing keywords and per-CV-section coverage. Missing skills also seed a checklist-backed job-specific learning path, so users can mark them learned or dismiss them. No AI calls: results are instant and reproducible. Requires profile CV text/structure and a job description. (The AI narrative equivalent is `GET /api/jobapplications/{id}/candidate-fit`.) -- `GET /api/jobapplications/{id}/status-suggestion` - - Deterministic status suggestion derived from the job's most recent inbound message (interview invite / offer / rejection). Returns a forward-only suggestion (`hasSuggestion`, `suggestedStatus`, `signal`, …) or `hasSuggestion: false`. Applying it is a normal `PATCH .../status` — always user-confirmed. -- `DELETE /api/jobapplications/{id}` - - Soft-deletes an application (`IsDeleted=true`); records a `Deleted` event. -- `POST /api/jobapplications/{id}/restore` - - Restores a soft-deleted application; records a `Restored` event. - -### Companies (`/api/companies`) - -- `GET /api/companies`: list companies -- `GET /api/companies/{id}`: company by id -- `POST /api/companies`: create (idempotent by name; returns existing if already present) - - Body: `{ "name": "...", "location": "...?", "source": "...?" }` -- `PUT /api/companies/{id}`: update - - Body includes recruiter details and `pipelineStage`, `lastContactedAt`, `nextContactAt` - -### Correspondence (`/api/correspondence`) - -- `GET /api/correspondence/{jobId}`: list messages for a job (ordered by date) -- `POST /api/correspondence`: create message - - Body: `{ "jobApplicationId": 1, "from": "...", "content": "...", "subject": "...?", "channel": "...?", "date": "..."? }` - -### Attachments (`/api/attachments`) - -- `GET /api/attachments/{jobId}`: list attachments for a job -- `GET /api/attachments/download/{id}`: download an attachment by attachment id -- `POST /api/attachments`: upload files (multipart/form-data) - - Form fields: `jobId` (int), `files` (one or more) -- `PATCH /api/attachments/{id}`: rename attachment - - Body: `{ "fileName": "NewName.pdf" }` -- `DELETE /api/attachments/{id}`: delete attachment record + best-effort delete file on disk - -### Rules (`/api/rules`) - -- `GET /api/rules`: get rule settings (auto-creates defaults on first request) -- `PUT /api/rules`: update rule settings (values are clamped to sane bounds) - -### Export (`/api/export`) - -- `GET /api/export/jobs?format=json|csv&includeDeleted=false` - - Downloads a file (`job-tracker-export-YYYY-MM-DD.json` or `.csv`). - -### Backup (`/api/backup`) - -- `POST /api/backup/encrypted` - - Returns an encrypted backup file (`.jtbackup`). - - Note: only implemented on Windows in this build (uses ASP.NET Data Protection / DPAPI). - -### Job import (`/api/jobimport`) - -- `POST /api/jobimport/preview` - - Body: `{ "url": "https://..." }` - - Returns a parsed preview payload (`JobImportResult`), if a matching plugin can parse it. - -### Client error reporting (`/api/client-errors`) - -- `POST /api/client-errors` - - Body: `{ errorId?, message?, stack?, componentStack?, url?, userAgent?, at? }` - - Logs frontend errors into the API logs (best-effort). - -### Authentication (`/api/auth`) - -- `GET /api/auth/config`: returns auth feature flags for the UI -- `POST /api/auth/login`: local email/password login (returns a signed JWT) -- `POST /api/auth/register`: local registration (only if enabled via `Auth:AllowRegistration=true`) -- `GET /api/auth/me`: returns current user/claims summary for the UI -- `POST /api/auth/request-password-reset`: sends reset email (requires SMTP enabled) -- `POST /api/auth/reset-password`: resets password using emailed token - -### Users (`/api/users`) (admin-only) - -- `GET /api/users`: list users + roles -- `POST /api/users`: create a user (and optionally roles) -- `PUT /api/users/{id}/roles`: replace roles for a user -- `DELETE /api/users/{id}`: delete user -- `POST /api/users/{id}/send-password-reset`: send a reset email to the user - -## Notes for contributors - -- The API applies EF Core migrations on startup for the configured SQLite database. -- The background rules engine may automatically transition jobs to `Ghosted` based on rule settings. -- In Docker, the UI proxies `/api/*` to the backend service (see `job-tracker-ui/nginx.conf`). - -## Project status and planned work - -This README documents released behaviour, not the roadmap. Validated findings and implementation status live in `docs/audits/audit-remediation-backlog.md` and `docs/work-programmes/master-progress.md`; archived phase documents are historical evidence and may describe superseded behaviour. +Start at [docs/README.md](docs/README.md) for architecture, configuration, frontend/backend guides, AI, database, security, infrastructure, testing, and remaining work. Production/external blockers remain in [BLOCKERS.md](BLOCKERS.md). diff --git a/deploy/README.md b/deploy/README.md index 272df8c..3d1d5f8 100644 --- a/deploy/README.md +++ b/deploy/README.md @@ -1,350 +1,5 @@ -# Production deployment notes +# Deployment entry point -## Gitea Actions -This repo includes `.gitea/workflows/ci-deploy.yml` for: -- backend build -- backend tests -- frontend tests -- frontend production build -- deployment to Ubuntu after successful tests on `main` +The deployment script and Gitea workflow live in this directory and `.gitea/workflows/`. The current deployment, health/readiness, rollback, and secret-handling guidance is in the repository [infrastructure guide](../docs/infrastructure/README.md), especially [deployment](../docs/infrastructure/deployment.md) and [disaster recovery](../docs/infrastructure/disaster-recovery.md). -### Required secrets in Gitea -- `PROD_HOST` -- `PROD_USER` -- `PROD_SSH_KEY` - -## Ubuntu server setup -Recommended app path: -- `/opt/job-tracker/app` - -Persistent runtime secrets path: -- `/opt/job-tracker/shared/.env` - -Requirements: -- Docker Engine -- Docker Compose plugin -- reverse proxy in front (Nginx, Caddy, or Traefik) -- shared env file present on server in `/opt/job-tracker/shared/.env` -- network connectivity from the backend container to your `mariadb` container/service - -The deploy script will automatically create a symlink from: -- `/opt/job-tracker/shared/.env` - -to: -- `/opt/job-tracker/app/.env` - -This keeps secrets outside the uploaded repo checkout so they are not wiped by CI deploys. - -### Frontend API base URL -The production frontend already proxies `/api` to the backend container via Nginx. - -Recommended default: -- leave `NEXT_PUBLIC_API_BASE_URL` unset/empty in production - -Only set `NEXT_PUBLIC_API_BASE_URL` if the UI must call a different external API origin on purpose. - -## Example production `.env` -```env -DATABASE_PROVIDER=mariadb -JOBTRACKER_CONNECTION_STRING=server=mariadb;port=3306;database=jobtracker;user=jobtracker;password=REPLACE_ME -AUTH_JWT_KEY=replace_with_long_random_secret -AUTH_ADMIN_EMAIL=you@example.com -AUTH_ADMIN_PASSWORD=replace_with_strong_password -AUTH_REQUIRE_EMAIL_VERIFICATION=true -APP_PUBLIC_BASE_URL=https://your-domain.example -WEB_PROXY_SUBNET=172.31.250.0/29 -STRIPE_SECRET_KEY=sk_live_... -STRIPE_PRICE_PREMIUM=price_... -STRIPE_WEBHOOK_SECRET=whsec_... -AI_SERVICE_BASE_URL=http://ai-service:8001 -OLLAMA_BASE_URL=http://ollama:11434 -OLLAMA_MODEL=qwen2.5:7b -EMAIL_FOLLOWUPREMINDERS_ENABLED=true -EMAIL_FOLLOWUPREMINDERS_UPCOMINGDAYS=2 -# Optional backward-compatible alias if older config still references the previous name: -SUMMARIZER_BASE_URL=http://ai-service:8001 -``` - -## Supported databases - -The application supports only these provider values: - -- `sqlite` — local development and small single-instance deployments; persist and back up the data volume. -- `mariadb` or `mysql` — server deployments through the Pomelo EF Core provider. The production example uses MariaDB. - -PostgreSQL is not implemented. Do not configure or recommend it without first adding an EF Core provider, migrations, startup validation, backup/restore support, and a provider test matrix. - -## Deployment flow - -Production automation always selects `docker-compose.yml` explicitly. Local development must add -`docker-compose.dev.yml`; never add that file to a production command. The effective base configuration -publishes **frontend 3000 → 80** on all host interfaces for the recorded Traefik host-port dependency. -Backend, ai-service and bundled Ollama have no host-port bindings. CORS and Docker service names are -not network isolation. The backend also joins the operator-managed `jobtracker_shared` network for -existing service dependencies; its membership and other host services need operator inspection. - -The external Traefik configuration is operator-owned and is not stored here. Before deployment it -must route only the exact host from `APP_PUBLIC_BASE_URL` to the recorded frontend host port 3000, -terminate TLS, and replace `X-Forwarded-For` with one client IP and `X-Forwarded-Proto=https`. -Do not migrate to container routing or bind port 3000 to loopback without verifying where Traefik runs. -Host firewall/Docker forwarding rules must restrict ingress appropriately; their existence is unproven. - -Nginx requires `INGRESS_PROXY_CIDRS` (space-separated observed Traefik peer CIDRs, preferably /32 or -/128), rejects other peers before serving application routes, rejects missing scheme/IP and forwarding -chains, and emits normalized forwarding metadata. It rejects non-canonical Hosts except the default -server's liveness-only `/health`. The backend trusts only `WEB_PROXY_SUBNET` and one hop. Never trust a -NAT/gateway address shared by both Traefik and untrusted callers. If peer identity cannot distinguish -them, stop rollout and establish a verified host binding/firewall or dedicated proxy route first. - -Deployment requires Python 3 (standard library) and Docker Compose's JSON config support. The new -`scripts/validate-ingress.py` preflight checks the **rendered** production configuration before normal -deployment backup/build/replacement; backup-only runs remain independent of the new ingress setting. -CI runs `python3 scripts/test-ingress.py` without needing a Docker engine. Empty/missing peer values -fail safely; do not copy synthetic addresses from tests into production. Runtime/container verification -is still pending locally due to Docker Desktop failure. See the [B06 report and operator checks](../docs/audits/2026-09-07/validation/b06-completion.md) -before deploying this change. No production verification is implied by configuration tests. -1. push to `main` -2. Gitea Actions runs tests -3. if green, workflow uploads repo to server -4. `deploy/deploy.sh` links `/opt/job-tracker/shared/.env` into the repo checkout, then explicitly runs `docker compose -f docker-compose.yml build` and `up -d` -5. if `OLLAMA_MODEL` is set, the deploy script waits for Ollama, pulls the configured model if missing, then restarts `ai-service` so hybrid CV classification can use it -6. workflow checks service status after deployment - -## Post-deploy verification you should also do manually the first time -- confirm reverse proxy routes to the frontend correctly -- confirm API auth/login works with production config -- confirm backend can connect to MariaDB -- confirm AI service container is reachable from backend -- confirm reminder and admin/system pages load -- verify follow-up reminder emails are enabled only when intended and that links open the correct job/tab - ---- - -# Backups, restore and rollback - -**Database restore and application rollback are two different operations.** A bad deploy usually needs -only the rollback. Restore the database only if the data itself is wrong or lost — it discards -everything written since the dump. - -## Environment loading - -`deploy/deploy.sh` **loads `/opt/job-tracker/shared/.env` into its own shell** before it decides -anything. The symlink it creates in the checkout is for docker compose, which reads `.env` itself; -the script needs the values too, to pick the right backup and to check its own configuration. - -- Parsed line by line, not `source`d — a compose `.env` is not a shell script, so an unquoted value - containing spaces would execute as a command. -- **Variables already set in the environment win**, so CI-provided `APP_VERSION`, `APP_COMMIT_SHA` - and `APP_BUILD_STAMP` still override the file. -- No value is ever echoed. Error messages name variables, never their contents. - -This was added on 2026-07-19. Before it, the script read an empty environment: `DATABASE_PROVIDER` -fell back to `sqlite` on a MariaDB host, so the deploy tarred the data volume, printed -`Backup verified`, and continued with no database dump at all. See -`docs/release-candidate-review.md` (B1). - -## Required production variables - -`validate_deploy_config` runs **before** the backup, and therefore before anything is built, stopped -or replaced. A missing variable aborts the deploy while the running stack is still untouched. - -| Variable | Required | Why it is checked here | -|---|---|---| -| `DATABASE_PROVIDER` | **Always** — no default | Selects the backup. Guessing it wrong backs up the wrong database and reports success | -| `JOBTRACKER_CONNECTION_STRING` | When provider is `mariadb`/`mysql` | Without it there is no way to dump the database | -| `AI_SERVICE_TOKEN` | Always | `docker-compose.yml` declares it with `:?`; missing it kills the stack *after* the images are built | -| `AUTH_JWT_KEY` | Always | Compose sets `Auth__Require=true`, and the backend throws at startup on a blank key — after the containers have been replaced | -| `APP_PUBLIC_BASE_URL` | Always | Canonical HTTPS origin for links, OAuth callbacks, billing redirects, secure cookies, Host validation, and the public smoke check | -| `AUTH_MICROSOFT_TENANT` | When `AUTH_MICROSOFT_CLIENT_ID` is set | Exact Microsoft application sign-in account mode; distinct from the Graph mailbox tenant | -| `WEB_PROXY_SUBNET` | Always | Dedicated nginx-to-backend CIDR trusted for exactly one forwarded hop; must not overlap another Docker network | - -### Microsoft sign-in migration gate - -Before enabling `AUTH_MICROSOFT_CLIENT_ID` with the canonical identity release, take the normal -backup and record counts only. Do not print subjects or email addresses: - -```sql -SELECT COUNT(*) AS legacy_links -FROM AspNetUsers -WHERE MicrosoftSubject IS NOT NULL OR MicrosoftEmail IS NOT NULL; - -SELECT COUNT(*) AS legacy_without_alternate_credential -FROM AspNetUsers -WHERE (MicrosoftSubject IS NOT NULL OR MicrosoftEmail IS NOT NULL) - AND PasswordHash IS NULL - AND GoogleSubject IS NULL; - -SELECT COUNT(*) AS duplicate_legacy_subject_groups -FROM ( - SELECT MicrosoftSubject - FROM AspNetUsers - WHERE MicrosoftSubject IS NOT NULL - GROUP BY MicrosoftSubject HAVING COUNT(*) > 1 -) duplicate_subjects; - -SELECT COUNT(*) AS duplicate_legacy_email_groups -FROM ( - SELECT MicrosoftEmail - FROM AspNetUsers - WHERE MicrosoftEmail IS NOT NULL - GROUP BY MicrosoftEmail HAVING COUNT(*) > 1 -) duplicate_emails; -``` - -Apply `20260802212509_AddCanonicalMicrosoftIdentity` before deploying code that queries the two new -columns. The migration does not backfill legacy rows and adds a unique nullable composite index. -Keep Microsoft sign-in disabled if the inventory or migration fails. Roll back the application -binary while leaving the additive columns in place; never roll back to email auto-linking. - -`DATABASE_PROVIDER` deliberately has **no default**. An unset value used to mean "sqlite"; it now -means "stop and tell me". - -## Backup creation - -`deploy/deploy.sh` takes a backup **before** it builds, stops or replaces anything, and **aborts the -deploy if the backup fails**. Nothing else in the deploy runs without a restore point. - -- **Location:** `/opt/job-tracker/backups` — override with `BACKUP_DIR`. If an earlier root-run deploy owns the directory, the deploy script repairs its ownership through Docker before writing. -- **Selection:** driven solely by `DATABASE_PROVIDER`, which must be set. - - `mariadb` / `mysql` → SQL dump, `jobtracker--.sql.gz`, e.g. - `jobtracker-jobtracker-20260719T153759Z.sql.gz` - - MariaDB deployments also archive `jobtracker_data`, which contains attachments, imported CVs, - generated exports and Data Protection keys. - - `sqlite` → data volume archive, `jobtracker-sqlite-.tar.gz` - - every deployment archives the separate account-deletion tombstone volume when it exists; - restore it independently and never replace it with an older application-data snapshot - - anything else → the deploy stops -- **The filename tells you which path ran.** If you expect a MariaDB deploy and find a - `jobtracker-sqlite-*.tar.gz`, the environment is wrong — that is the exact failure this check exists - to make visible. -- **Naming:** the UTC timestamp makes every file unique, so a deploy never overwrites an earlier backup. -- **Credentials** come from `JOBTRACKER_CONNECTION_STRING` and are passed via `MYSQL_PWD`, never on the - command line, so they cannot appear in the process list or the deploy log. -- **Compression:** gzip. A small database compresses to a few KB. -- **SQLite volume resolution:** compose prefixes volume names with the project name, so the script - resolves `_jobtracker_data` and **fails if that volume does not exist**. Naming the bare - volume would silently create an empty one and back *that* up. - -### Verification — each provider gets the check that proves its own format - -A backup that exists but is empty, truncated, or the wrong *kind* is worse than none, because it looks -like a restore point. - -| Provider | Checks | -|---|---| -| MariaDB | non-empty; valid gzip; contains `CREATE TABLE`; contains the `Dump completed` trailer that `mariadb-dump` writes last, so a dump that died partway through is rejected | -| SQLite | non-empty; valid gzip; the archive actually contains `jobtracker.db` | -| Recovery volumes | non-empty valid gzip archives; a missing required application-data volume aborts the deploy | - -A failed check deletes the file rather than leaving something that looks like a backup. - -### Taking one by hand - -```bash -BACKUP_DIR=/opt/job-tracker/backups -mkdir -p "$BACKUP_DIR" -MYSQL_PWD='' mariadb-dump \ - --host=127.0.0.1 --port=3306 --user= \ - --single-transaction --routines --events --quick \ - jobtracker | gzip -c > "$BACKUP_DIR/jobtracker-manual-$(date -u +%Y%m%dT%H%M%SZ).sql.gz" -``` - -### Retention - -Verified top-level recovery sets are retained for 30 days and the first complete set in each month is -copied into `monthly/`; the newest 12 monthly sets are retained. Configure -`BACKUP_DAILY_RETENTION_DAYS` and `BACKUP_MONTHLY_RETENTION_COUNT` to change those positive integer -limits. Paths outside the fixed backup directory and monthly naming contract are never pruned. - -Set `BACKUP_RCLONE_REMOTE` to an encrypted rclone remote to copy each current recovery set off-host. -See [`docs/operations/google-drive-backups.md`](../docs/operations/google-drive-backups.md). - -## Restoring the database - -Tested end-to-end against a clean MariaDB 11 container: dump taken from a seeded database, restored -into an empty one, rows verified identical. - -```bash -# 1. Stop the application so nothing writes during the restore. -docker compose stop backend - -# 2. Restore. This REPLACES the current contents of the named database. -gzip -dc /opt/job-tracker/backups/jobtracker-jobtracker-20260719T153759Z.sql.gz \ - | MYSQL_PWD='' mariadb --host=127.0.0.1 --port=3306 --user= jobtracker - -# 3. Verify before starting anything. -MYSQL_PWD='' mariadb --host=127.0.0.1 --port=3306 --user= jobtracker \ - -e "SELECT COUNT(*) AS applications FROM JobApplications;" - -# 4. Start again. -docker compose start backend -``` - -Restoring a **SQLite** deployment instead: - -```bash -docker compose stop backend -docker run --rm -v jobtracker_data:/data -v /opt/job-tracker/backups:/backup \ - -e ARCHIVE_NAME=jobtracker-sqlite-20260719T153941Z.tar.gz \ - alpine:3 sh -c 'rm -rf /data/* && tar xzf "/backup/$ARCHIVE_NAME" -C /data' -docker compose start backend -``` - -## Restoring application containers (rollback) - -This is the usual fix for a bad deploy, and it **does not touch the database**. - -Normal deployments tag backend and frontend images with `APP_COMMIT_SHA` and retain the images from -the currently running containers as `jobtracker-backend:previous` and `jobtracker-frontend:previous`. -If any step fails after core replacement—including public liveness, readiness, or auth-configuration -checks—`deploy.sh` automatically recreates both core services from those retained images and still -returns a failed status so CI reports the rejected release. The previous images are replaced only at -the start of the next deployment. - -Use the manual procedure below if automatic restoration itself fails or an older release is required. - -```bash -cd /opt/job-tracker/app # the deployment checkout -git log --oneline -5 # find the last good commit -git checkout -deploy/deploy.sh -``` - -`deploy.sh` takes a fresh backup first, so rolling back is itself protected. - -**Why a code rollback is safe here:** every Phase 4/5 migration is a no-op — the startup reconciler -owns those tables — so reverting the code never leaves migration history ahead of the schema. The -reconciler is additive and never drops a table holding rows, so the older code simply ignores the -newer tables. - -**What a code rollback does not undo:** rows users created in the newer tables stay. That is usually -what you want. If you additionally restore the database, those rows are lost — so restore only when -the data is the problem. - -## Choosing between them - -| Symptom | Action | -|---|---| -| New version starts but behaves wrong | Rollback the code. Leave the database. | -| Backend will not start; schema looks wrong | Rollback the code, then restore only if it still fails. | -| Data is missing or corrupted | Restore the database from the most recent good dump. | -| Deploy aborted before starting | Nothing to undo — the backup ran before any change. | - -## Health checks - -`backend` and `frontend` both report container health, so `docker compose ps` shows real state rather -than merely "running". - -- **Backend:** `curl -fsS http://127.0.0.1:8080/health`. Anonymous, and deliberately **does not touch - the database** — a health check that queried MariaDB would restart a healthy backend whenever the - database blipped. `start_period` is 90s to cover first-boot schema reconciliation. -- **API/database readiness:** `curl -fsS https:///ready`. This checks that the API can reach its - configured database and returns only `ready` or `unavailable`; detailed diagnostics remain admin-only. - Deployment validation requires this probe, while Docker restart policy continues to use liveness. -- **Frontend:** `wget` against nginx on port 80. -- `frontend` waits for `backend` to be *healthy*, not merely started, because nginx proxies `/api` to - it and refuses to start if the upstream cannot be resolved. - -A backend that cannot reach its database during startup exits and is reported `unhealthy`. A dependency -failure after startup leaves the process alive but makes `/ready` return 503, so deployment validation -cannot mistake a live-but-unready stack for a successful release. +Keep the server `.env` outside the repository. Do not run production deployment, restore, migration, or credential rotation as part of local documentation work. diff --git a/deploy/deploy.sh b/deploy/deploy.sh index 7349fe1..49e142a 100644 --- a/deploy/deploy.sh +++ b/deploy/deploy.sh @@ -26,7 +26,7 @@ fi # own decisions — which backup to take, which variables are missing — ran against # an empty environment until this existed, so DATABASE_PROVIDER defaulted to # sqlite and a MariaDB host silently got a volume tar instead of a dump, with a -# "Backup verified" line to match. See docs/release-candidate-review.md (B1). +# "Backup verified" line to match. See docs/infrastructure/deployment.md. # # Parsed line by line rather than sourced: a compose .env is not a shell script, # so an unquoted value containing spaces would execute as a command under `.`. diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 0000000..c1406a8 --- /dev/null +++ b/docs/README.md @@ -0,0 +1,35 @@ +# Job Tracker documentation + +This is the entry point for the current system documentation. Job Tracker (also branded Jobjakt in some UI and deployment text) is a self-hosted job-application workspace: it combines job tracking, company context, correspondence, documents, follow-up automation, and privacy-conscious AI assistance. + +## System at a glance + +- `job-tracker-ui/` is a statically exported Next.js 16 shell containing the React 19, TypeScript, MUI, and React Router application. +- `JobTrackerApi/` is an ASP.NET Core 9 API using EF Core 9, ASP.NET Identity, JWT/cookie authentication, SQLite by default, and MariaDB/MySQL as the server option. +- `tools/summarizer/` is a FastAPI service for local summarisation and bounded document extraction/OCR. CV generation can route through local Ollama or an explicitly enabled Gemini/Groq provider. +- Docker Compose runs the API, nginx frontend, AI service, and optional bundled Ollama. Gitea Actions builds and deploys the stack. + +## Documentation map + +| Area | Start here | +|---|---| +| Architecture | [Architecture overview](architecture/overview.md) | +| Frontend and UI | [Frontend guide](frontend/README.md) | +| Backend and API | [Backend guide](backend/README.md) · [API reference](backend/api.md) | +| AI | [AI guide](ai/README.md) | +| Database | [Database guide](database/README.md) | +| Authentication and security | [Security guide](security/README.md) | +| Configuration | [Configuration guide](configuration/README.md) · [Environment variables](configuration/environment-variables.md) | +| Docker and deployment | [Infrastructure guide](infrastructure/README.md) | +| Tests and quality gates | [Testing guide](testing/README.md) | +| Local development | [Development guide](development/README.md) | +| Product behavior | [Product guide](product/README.md) | +| Remaining work | [Roadmap](roadmap/README.md) | +| Decisions | [Architecture decisions](decisions/README.md) | +| Documentation maintenance | [Documentation rules](documentation-rules.md) | + +## Authority and archive + +The current source code and the documents linked above are authoritative. `BLOCKERS.md` remains the short operator/external-dependency handoff because repository instructions refer to it directly. Historical audits, plans, reports, and the pre-cleanup snapshot are under [`_archive/`](./_archive/); they are evidence, not current behavior. + +When documentation and code disagree, code wins and the relevant document should be corrected in the same change. diff --git a/docs/_archive/pre-docs-cleanup-2026-09-11/BLOCKERS.md b/docs/_archive/pre-docs-cleanup-2026-09-11/BLOCKERS.md new file mode 100644 index 0000000..b85bc6c --- /dev/null +++ b/docs/_archive/pre-docs-cleanup-2026-09-11/BLOCKERS.md @@ -0,0 +1,43 @@ +# Blockers + +Updated: 2026-09-01 + +## Stripe billing + +- **Blocked:** Activating roadmap item 7.5 in production. +- **Why:** Hosted Checkout, customer-portal sessions, signed subscription webhooks, persisted billing state, Pro-role provisioning and a mock lifecycle regression are implemented. The Stripe recurring price, portal, webhook registration and production credentials must be configured outside the repository. +- **Required:** Configure the Pro recurring price, enable the Stripe customer portal, register `/api/billing/webhook` for `customer.subscription.created`, `customer.subscription.updated`, and `customer.subscription.deleted`, then supply `STRIPE_SECRET_KEY`, the recurring `price_...` value in the legacy-named `STRIPE_PRICE_PREMIUM` setting, and `STRIPE_WEBHOOK_SECRET` through the deployment environment. Do not place secret values in source control or chat. +- **Recommended:** One monthly Pro price first; add annual billing only after the monthly flow is operating. +- **Current access check:** Production has test-mode secret and webhook values, but `STRIPE_PRICE_PREMIUM` currently contains a `prod_...` Product ID. Checkout requires the recurring `price_...` Price ID; the server now treats the wrong identifier type as disabled rather than calling Stripe. The publishable key is not used by hosted Checkout. Local fake-gateway coverage proves active → expired → canceled/replayed role transitions without losing non-AI data (V-185). +- **Runbook:** Follow `docs/operations/stripe-activation.md`, completing test mode before creating or installing live-mode values. + +## Public registration verification + +- **Blocked:** Completing a real-browser production signup check. +- **Why:** The 2026-07-31 anonymous production check confirms `allowRegistration=true`, `turnstileEnabled=true`, and Google sign-in enabled. Completing Turnstile and creating a disposable account requires an interactive production browser session. +- **Required:** Register one disposable account through Turnstile, verify email/sign-in/rate-limit behavior, then remove the account if it is not needed. +- **Recommended:** Monitor Turnstile and rate-limit failures during the first public rollout; keep email verification required. +- **Current status:** Production returns `allowRegistration=true`, `turnstileEnabled=true`, `googleEnabled=true`, and `microsoftEnabled=false`. A registration request without a Turnstile token is rejected with HTTP 400. SMTP is configured and enabled. The operator reports `AUTH_REQUIRE_EMAIL_VERIFICATION` is now enabled; the disposable interactive signup is still required to prove the deployed behavior end to end. + +## Production verification + +- **Blocked:** Authenticated production smoke tests, backup restore verification against real data, OAuth-provider checks, and rollback rehearsal. +- **Why:** The authorised deployment is complete, but authenticated smoke, port/network closure, complete backup restore, provider checks and failure-injection rollback require credentials and/or separately authorised production mutations. +- **Required:** Confirm the admin-only version badge matches version 286 / commit `ea7d9f3`, then run the authenticated application workspace, Career, CV, attachment, email-verification and rollback checks from `docs/release-candidate-review.md` and `docs/release-checklist.md`. +- **Recommended:** Verify backup/restore before deployment, then exercise login, existing application counts, Career Workspace, public CV refresh/download, AI, and attachments in order. +- **Current status:** Gitea run 705 (application version 286, commit `ea7d9f3`) completed both the complete test job and remote deployment successfully on 2026-09-01. Public `/health`, `/ready`, and `/api/auth/config` returned HTTP 200 after replacement; liveness reported version 286 and readiness reported `ready`. This proves the proxy/frontend, API and database readiness path, but not authenticated application behavior, provider delivery or restore/rollback behavior. Read-only PROD-001 inventory previously found the JobTracker Ollama and frontend published on all host interfaces, the newest observed gzip-valid MariaDB backup dated 2026-08-02, no observed scheduled JobTracker backup, and no owner-file/key/tombstone recovery bundle. Those remaining gates still require operator-controlled verification; see `docs/production/production-ai-hardware-assessment.md`. +- **Repository-side rollback status:** Core images are now tagged by commit, the prior running backend/frontend images are retained, and any failure after replacement triggers an automatic core rollback while preserving the failed CI status. Compose configuration validates locally. A real rollback rehearsal remains blocked because the local Linux Docker daemon is unavailable and production mutation is not authorised; run the failure-injection rehearsal on a disposable Linux Docker host before relying on it in production. + +## Account deletion retention and restore policy + +- **Blocked:** Enabling SEC-009 self-service deletion in production and declaring backup erasure complete. +- **Why:** The readable export and idempotent live-data deletion coordinator are implemented behind an explicit disabled gate. Repository code cannot truthfully choose legal retention periods, backup expiry, provider obligations or the tombstone lifetime needed to prevent restoration from resurrecting an erased account. Production inventory confirms current backups are database-only and no protected tombstone volume is deployed yet; the release branch now defines the separate volume and retryable authenticated sidecar-cache purge. +- **Required:** Decide retention periods for operational backups, audit/security records and deletion tombstones; identify any legal hold/export obligations; approve the restore behavior for deleted identities. +- **Recommended:** Keep production self-service and admin deletion disabled. Decide retention, deploy/protect the configured tombstone volume, build a complete DB/files/keys backup set, then use a disposable account to prove remote-provider cleanup, sidecar purge across restart and restored-backup tombstone replay before staged activation. + +## Legacy job/application column cutover + +- **Blocked:** Removing the opportunity columns duplicated between `JobApplication` and `Job`. +- **Why:** The compatibility dual-write protects existing production rows and older clients. The release branch now backfills missing opportunities on startup, synchronizes both creation paths, and leaves all legacy columns intact. Dropping columns still requires production validation and an observation release. +- **Required:** After deployment, run the read-only report in `docs/operations/job-opportunity-cutover.md` against production and a restored backup, then confirm that backward API compatibility is no longer required. +- **Recommended:** Use an expand/contract release: first stop legacy reads after a verified backfill, observe one release, then drop the duplicate columns in the following migration. diff --git a/CV_Changes.md b/docs/_archive/pre-docs-cleanup-2026-09-11/CV_Changes.md similarity index 100% rename from CV_Changes.md rename to docs/_archive/pre-docs-cleanup-2026-09-11/CV_Changes.md diff --git a/docs/_archive/pre-docs-cleanup-2026-09-11/INVENTORY.md b/docs/_archive/pre-docs-cleanup-2026-09-11/INVENTORY.md new file mode 100644 index 0000000..1c15631 --- /dev/null +++ b/docs/_archive/pre-docs-cleanup-2026-09-11/INVENTORY.md @@ -0,0 +1,29 @@ +# Documentation cleanup inventory + +Inventory date: 2026-09-11. Scope: active repository Markdown/text documentation, README files, root notes, audit/report material, and configuration documentation. The pre-existing `docs/_archive/` was treated as historical and was not re-audited as active documentation. + +## Counts + +| Scope | Count | Disposition | +|---|---:|---| +| Active `docs/` Markdown/text files before cleanup | 454 | Classified below; current material consolidated, dated material preserved in this backup or the existing archive | +| Existing historical archive Markdown/text files | 127 | Retained in place | +| Root/component documentation and notes copied into backup | included | Root clutter consolidated or removed; component READMEs rewritten where they are useful entry points | +| Active Markdown/text entries removed after verification | 269 | Superseded, duplicated, temporary, or merged into current guides | + +## Classification policy + +| Classification | Representative paths | Result | +|---|---|---| +| KEEP | `docs/redesign/**` current UI work-in-progress; `docs/decisions/ADR-*.md`; `BLOCKERS.md` | Retained because these are active implementation evidence, durable decisions, or operator handoff material | +| MERGE/REWRITE | `docs/architecture/**`, `docs/ai/**`, `docs/security/**`, `docs/technical/**`, `docs/ux/**`, `docs/career/**`, `docs/jobs/**`, root `README.md` | Current facts moved into focused guides; duplicates and stubs removed | +| ARCHIVE | `docs/audits/**`, `docs/verification/**`, release reports, research, phase reports, work-programme logs | Human-readable originals preserved under this pre-cleanup snapshot; generated audit artifacts were not copied into this backup | +| DELETE AFTER BACKUP | root TODO/progress/portfolio notes, duplicate topic stubs, completed status diaries, obsolete CRA-era setup notes | Useful TODOs, decisions, security items, and operational gates were migrated to current roadmap/security/infrastructure docs | + +## Preservation checks + +- Open P4-02, P2-D01, P3-O01/P3-O02, B06/F04, CV Builder follow-up, Stripe, production verification, account deletion, and schema cutover work is represented in `docs/roadmap/` or `BLOCKERS.md`. +- Current technical debt is represented in `docs/roadmap/technical-debt.md`. +- Current security implementation and unverified gates are represented in `docs/security/security-notes.md`. +- Environment variable names and local-only/obsolete names are represented in `docs/configuration/environment-variables.md`. +- This inventory and the original files are evidence of the pre-cleanup state; they are not authoritative current documentation. diff --git a/docs/_archive/pre-docs-cleanup-2026-09-11/README.md b/docs/_archive/pre-docs-cleanup-2026-09-11/README.md new file mode 100644 index 0000000..258a8b6 --- /dev/null +++ b/docs/_archive/pre-docs-cleanup-2026-09-11/README.md @@ -0,0 +1,7 @@ +# Pre-documentation cleanup backup + +This directory preserves documentation from before the repository-wide documentation consolidation on 2026-09-11. + +It contains active documentation and documentation-like root files that were inventoried before being rewritten, merged, moved, or removed. The archive exists to make the cleanup recoverable and to preserve historical TODOs, decisions, audit evidence, deployment notes, and other context. + +Use the current documentation tree under `docs/` as the authoritative source. This backup is a snapshot of the pre-cleanup state and must not be treated as current system documentation. diff --git a/SMART_GMAIL_PROGRESS.md b/docs/_archive/pre-docs-cleanup-2026-09-11/SMART_GMAIL_PROGRESS.md similarity index 100% rename from SMART_GMAIL_PROGRESS.md rename to docs/_archive/pre-docs-cleanup-2026-09-11/SMART_GMAIL_PROGRESS.md diff --git a/docs/_archive/pre-docs-cleanup-2026-09-11/browser-extension/README.md b/docs/_archive/pre-docs-cleanup-2026-09-11/browser-extension/README.md new file mode 100644 index 0000000..3ae9c64 --- /dev/null +++ b/docs/_archive/pre-docs-cleanup-2026-09-11/browser-extension/README.md @@ -0,0 +1,20 @@ +# Jobjakt Quick Capture + +The Manifest V3 extension opens the current page or selected link in Jobjakt's existing reviewed import flow. It does not scrape page content, store credentials, or submit an application automatically. + +## Install locally + +1. Open `chrome://extensions` (or the equivalent extensions page in Edge). +2. Enable developer mode. +3. Choose **Load unpacked** and select this `browser-extension` folder. +4. Open a vacancy and click the Jobjakt extension, or use **Save vacancy to Jobjakt** from the page/link context menu. + +The Jobjakt add-job dialog opens with the vacancy URL and runs the same server-side preview, validation, duplicate checking, and user review used by normal URL imports. + +## Test + +```sh +node --test browser-extension/tests/*.test.mjs +``` + +No build or package installation is required. Zip the folder contents for store submission after adding store artwork and completing the browser-store privacy declarations. diff --git a/docs/_archive/pre-docs-cleanup-2026-09-11/deploy/README.md b/docs/_archive/pre-docs-cleanup-2026-09-11/deploy/README.md new file mode 100644 index 0000000..272df8c --- /dev/null +++ b/docs/_archive/pre-docs-cleanup-2026-09-11/deploy/README.md @@ -0,0 +1,350 @@ +# Production deployment notes + +## Gitea Actions +This repo includes `.gitea/workflows/ci-deploy.yml` for: +- backend build +- backend tests +- frontend tests +- frontend production build +- deployment to Ubuntu after successful tests on `main` + +### Required secrets in Gitea +- `PROD_HOST` +- `PROD_USER` +- `PROD_SSH_KEY` + +## Ubuntu server setup +Recommended app path: +- `/opt/job-tracker/app` + +Persistent runtime secrets path: +- `/opt/job-tracker/shared/.env` + +Requirements: +- Docker Engine +- Docker Compose plugin +- reverse proxy in front (Nginx, Caddy, or Traefik) +- shared env file present on server in `/opt/job-tracker/shared/.env` +- network connectivity from the backend container to your `mariadb` container/service + +The deploy script will automatically create a symlink from: +- `/opt/job-tracker/shared/.env` + +to: +- `/opt/job-tracker/app/.env` + +This keeps secrets outside the uploaded repo checkout so they are not wiped by CI deploys. + +### Frontend API base URL +The production frontend already proxies `/api` to the backend container via Nginx. + +Recommended default: +- leave `NEXT_PUBLIC_API_BASE_URL` unset/empty in production + +Only set `NEXT_PUBLIC_API_BASE_URL` if the UI must call a different external API origin on purpose. + +## Example production `.env` +```env +DATABASE_PROVIDER=mariadb +JOBTRACKER_CONNECTION_STRING=server=mariadb;port=3306;database=jobtracker;user=jobtracker;password=REPLACE_ME +AUTH_JWT_KEY=replace_with_long_random_secret +AUTH_ADMIN_EMAIL=you@example.com +AUTH_ADMIN_PASSWORD=replace_with_strong_password +AUTH_REQUIRE_EMAIL_VERIFICATION=true +APP_PUBLIC_BASE_URL=https://your-domain.example +WEB_PROXY_SUBNET=172.31.250.0/29 +STRIPE_SECRET_KEY=sk_live_... +STRIPE_PRICE_PREMIUM=price_... +STRIPE_WEBHOOK_SECRET=whsec_... +AI_SERVICE_BASE_URL=http://ai-service:8001 +OLLAMA_BASE_URL=http://ollama:11434 +OLLAMA_MODEL=qwen2.5:7b +EMAIL_FOLLOWUPREMINDERS_ENABLED=true +EMAIL_FOLLOWUPREMINDERS_UPCOMINGDAYS=2 +# Optional backward-compatible alias if older config still references the previous name: +SUMMARIZER_BASE_URL=http://ai-service:8001 +``` + +## Supported databases + +The application supports only these provider values: + +- `sqlite` — local development and small single-instance deployments; persist and back up the data volume. +- `mariadb` or `mysql` — server deployments through the Pomelo EF Core provider. The production example uses MariaDB. + +PostgreSQL is not implemented. Do not configure or recommend it without first adding an EF Core provider, migrations, startup validation, backup/restore support, and a provider test matrix. + +## Deployment flow + +Production automation always selects `docker-compose.yml` explicitly. Local development must add +`docker-compose.dev.yml`; never add that file to a production command. The effective base configuration +publishes **frontend 3000 → 80** on all host interfaces for the recorded Traefik host-port dependency. +Backend, ai-service and bundled Ollama have no host-port bindings. CORS and Docker service names are +not network isolation. The backend also joins the operator-managed `jobtracker_shared` network for +existing service dependencies; its membership and other host services need operator inspection. + +The external Traefik configuration is operator-owned and is not stored here. Before deployment it +must route only the exact host from `APP_PUBLIC_BASE_URL` to the recorded frontend host port 3000, +terminate TLS, and replace `X-Forwarded-For` with one client IP and `X-Forwarded-Proto=https`. +Do not migrate to container routing or bind port 3000 to loopback without verifying where Traefik runs. +Host firewall/Docker forwarding rules must restrict ingress appropriately; their existence is unproven. + +Nginx requires `INGRESS_PROXY_CIDRS` (space-separated observed Traefik peer CIDRs, preferably /32 or +/128), rejects other peers before serving application routes, rejects missing scheme/IP and forwarding +chains, and emits normalized forwarding metadata. It rejects non-canonical Hosts except the default +server's liveness-only `/health`. The backend trusts only `WEB_PROXY_SUBNET` and one hop. Never trust a +NAT/gateway address shared by both Traefik and untrusted callers. If peer identity cannot distinguish +them, stop rollout and establish a verified host binding/firewall or dedicated proxy route first. + +Deployment requires Python 3 (standard library) and Docker Compose's JSON config support. The new +`scripts/validate-ingress.py` preflight checks the **rendered** production configuration before normal +deployment backup/build/replacement; backup-only runs remain independent of the new ingress setting. +CI runs `python3 scripts/test-ingress.py` without needing a Docker engine. Empty/missing peer values +fail safely; do not copy synthetic addresses from tests into production. Runtime/container verification +is still pending locally due to Docker Desktop failure. See the [B06 report and operator checks](../docs/audits/2026-09-07/validation/b06-completion.md) +before deploying this change. No production verification is implied by configuration tests. +1. push to `main` +2. Gitea Actions runs tests +3. if green, workflow uploads repo to server +4. `deploy/deploy.sh` links `/opt/job-tracker/shared/.env` into the repo checkout, then explicitly runs `docker compose -f docker-compose.yml build` and `up -d` +5. if `OLLAMA_MODEL` is set, the deploy script waits for Ollama, pulls the configured model if missing, then restarts `ai-service` so hybrid CV classification can use it +6. workflow checks service status after deployment + +## Post-deploy verification you should also do manually the first time +- confirm reverse proxy routes to the frontend correctly +- confirm API auth/login works with production config +- confirm backend can connect to MariaDB +- confirm AI service container is reachable from backend +- confirm reminder and admin/system pages load +- verify follow-up reminder emails are enabled only when intended and that links open the correct job/tab + +--- + +# Backups, restore and rollback + +**Database restore and application rollback are two different operations.** A bad deploy usually needs +only the rollback. Restore the database only if the data itself is wrong or lost — it discards +everything written since the dump. + +## Environment loading + +`deploy/deploy.sh` **loads `/opt/job-tracker/shared/.env` into its own shell** before it decides +anything. The symlink it creates in the checkout is for docker compose, which reads `.env` itself; +the script needs the values too, to pick the right backup and to check its own configuration. + +- Parsed line by line, not `source`d — a compose `.env` is not a shell script, so an unquoted value + containing spaces would execute as a command. +- **Variables already set in the environment win**, so CI-provided `APP_VERSION`, `APP_COMMIT_SHA` + and `APP_BUILD_STAMP` still override the file. +- No value is ever echoed. Error messages name variables, never their contents. + +This was added on 2026-07-19. Before it, the script read an empty environment: `DATABASE_PROVIDER` +fell back to `sqlite` on a MariaDB host, so the deploy tarred the data volume, printed +`Backup verified`, and continued with no database dump at all. See +`docs/release-candidate-review.md` (B1). + +## Required production variables + +`validate_deploy_config` runs **before** the backup, and therefore before anything is built, stopped +or replaced. A missing variable aborts the deploy while the running stack is still untouched. + +| Variable | Required | Why it is checked here | +|---|---|---| +| `DATABASE_PROVIDER` | **Always** — no default | Selects the backup. Guessing it wrong backs up the wrong database and reports success | +| `JOBTRACKER_CONNECTION_STRING` | When provider is `mariadb`/`mysql` | Without it there is no way to dump the database | +| `AI_SERVICE_TOKEN` | Always | `docker-compose.yml` declares it with `:?`; missing it kills the stack *after* the images are built | +| `AUTH_JWT_KEY` | Always | Compose sets `Auth__Require=true`, and the backend throws at startup on a blank key — after the containers have been replaced | +| `APP_PUBLIC_BASE_URL` | Always | Canonical HTTPS origin for links, OAuth callbacks, billing redirects, secure cookies, Host validation, and the public smoke check | +| `AUTH_MICROSOFT_TENANT` | When `AUTH_MICROSOFT_CLIENT_ID` is set | Exact Microsoft application sign-in account mode; distinct from the Graph mailbox tenant | +| `WEB_PROXY_SUBNET` | Always | Dedicated nginx-to-backend CIDR trusted for exactly one forwarded hop; must not overlap another Docker network | + +### Microsoft sign-in migration gate + +Before enabling `AUTH_MICROSOFT_CLIENT_ID` with the canonical identity release, take the normal +backup and record counts only. Do not print subjects or email addresses: + +```sql +SELECT COUNT(*) AS legacy_links +FROM AspNetUsers +WHERE MicrosoftSubject IS NOT NULL OR MicrosoftEmail IS NOT NULL; + +SELECT COUNT(*) AS legacy_without_alternate_credential +FROM AspNetUsers +WHERE (MicrosoftSubject IS NOT NULL OR MicrosoftEmail IS NOT NULL) + AND PasswordHash IS NULL + AND GoogleSubject IS NULL; + +SELECT COUNT(*) AS duplicate_legacy_subject_groups +FROM ( + SELECT MicrosoftSubject + FROM AspNetUsers + WHERE MicrosoftSubject IS NOT NULL + GROUP BY MicrosoftSubject HAVING COUNT(*) > 1 +) duplicate_subjects; + +SELECT COUNT(*) AS duplicate_legacy_email_groups +FROM ( + SELECT MicrosoftEmail + FROM AspNetUsers + WHERE MicrosoftEmail IS NOT NULL + GROUP BY MicrosoftEmail HAVING COUNT(*) > 1 +) duplicate_emails; +``` + +Apply `20260802212509_AddCanonicalMicrosoftIdentity` before deploying code that queries the two new +columns. The migration does not backfill legacy rows and adds a unique nullable composite index. +Keep Microsoft sign-in disabled if the inventory or migration fails. Roll back the application +binary while leaving the additive columns in place; never roll back to email auto-linking. + +`DATABASE_PROVIDER` deliberately has **no default**. An unset value used to mean "sqlite"; it now +means "stop and tell me". + +## Backup creation + +`deploy/deploy.sh` takes a backup **before** it builds, stops or replaces anything, and **aborts the +deploy if the backup fails**. Nothing else in the deploy runs without a restore point. + +- **Location:** `/opt/job-tracker/backups` — override with `BACKUP_DIR`. If an earlier root-run deploy owns the directory, the deploy script repairs its ownership through Docker before writing. +- **Selection:** driven solely by `DATABASE_PROVIDER`, which must be set. + - `mariadb` / `mysql` → SQL dump, `jobtracker--.sql.gz`, e.g. + `jobtracker-jobtracker-20260719T153759Z.sql.gz` + - MariaDB deployments also archive `jobtracker_data`, which contains attachments, imported CVs, + generated exports and Data Protection keys. + - `sqlite` → data volume archive, `jobtracker-sqlite-.tar.gz` + - every deployment archives the separate account-deletion tombstone volume when it exists; + restore it independently and never replace it with an older application-data snapshot + - anything else → the deploy stops +- **The filename tells you which path ran.** If you expect a MariaDB deploy and find a + `jobtracker-sqlite-*.tar.gz`, the environment is wrong — that is the exact failure this check exists + to make visible. +- **Naming:** the UTC timestamp makes every file unique, so a deploy never overwrites an earlier backup. +- **Credentials** come from `JOBTRACKER_CONNECTION_STRING` and are passed via `MYSQL_PWD`, never on the + command line, so they cannot appear in the process list or the deploy log. +- **Compression:** gzip. A small database compresses to a few KB. +- **SQLite volume resolution:** compose prefixes volume names with the project name, so the script + resolves `_jobtracker_data` and **fails if that volume does not exist**. Naming the bare + volume would silently create an empty one and back *that* up. + +### Verification — each provider gets the check that proves its own format + +A backup that exists but is empty, truncated, or the wrong *kind* is worse than none, because it looks +like a restore point. + +| Provider | Checks | +|---|---| +| MariaDB | non-empty; valid gzip; contains `CREATE TABLE`; contains the `Dump completed` trailer that `mariadb-dump` writes last, so a dump that died partway through is rejected | +| SQLite | non-empty; valid gzip; the archive actually contains `jobtracker.db` | +| Recovery volumes | non-empty valid gzip archives; a missing required application-data volume aborts the deploy | + +A failed check deletes the file rather than leaving something that looks like a backup. + +### Taking one by hand + +```bash +BACKUP_DIR=/opt/job-tracker/backups +mkdir -p "$BACKUP_DIR" +MYSQL_PWD='' mariadb-dump \ + --host=127.0.0.1 --port=3306 --user= \ + --single-transaction --routines --events --quick \ + jobtracker | gzip -c > "$BACKUP_DIR/jobtracker-manual-$(date -u +%Y%m%dT%H%M%SZ).sql.gz" +``` + +### Retention + +Verified top-level recovery sets are retained for 30 days and the first complete set in each month is +copied into `monthly/`; the newest 12 monthly sets are retained. Configure +`BACKUP_DAILY_RETENTION_DAYS` and `BACKUP_MONTHLY_RETENTION_COUNT` to change those positive integer +limits. Paths outside the fixed backup directory and monthly naming contract are never pruned. + +Set `BACKUP_RCLONE_REMOTE` to an encrypted rclone remote to copy each current recovery set off-host. +See [`docs/operations/google-drive-backups.md`](../docs/operations/google-drive-backups.md). + +## Restoring the database + +Tested end-to-end against a clean MariaDB 11 container: dump taken from a seeded database, restored +into an empty one, rows verified identical. + +```bash +# 1. Stop the application so nothing writes during the restore. +docker compose stop backend + +# 2. Restore. This REPLACES the current contents of the named database. +gzip -dc /opt/job-tracker/backups/jobtracker-jobtracker-20260719T153759Z.sql.gz \ + | MYSQL_PWD='' mariadb --host=127.0.0.1 --port=3306 --user= jobtracker + +# 3. Verify before starting anything. +MYSQL_PWD='' mariadb --host=127.0.0.1 --port=3306 --user= jobtracker \ + -e "SELECT COUNT(*) AS applications FROM JobApplications;" + +# 4. Start again. +docker compose start backend +``` + +Restoring a **SQLite** deployment instead: + +```bash +docker compose stop backend +docker run --rm -v jobtracker_data:/data -v /opt/job-tracker/backups:/backup \ + -e ARCHIVE_NAME=jobtracker-sqlite-20260719T153941Z.tar.gz \ + alpine:3 sh -c 'rm -rf /data/* && tar xzf "/backup/$ARCHIVE_NAME" -C /data' +docker compose start backend +``` + +## Restoring application containers (rollback) + +This is the usual fix for a bad deploy, and it **does not touch the database**. + +Normal deployments tag backend and frontend images with `APP_COMMIT_SHA` and retain the images from +the currently running containers as `jobtracker-backend:previous` and `jobtracker-frontend:previous`. +If any step fails after core replacement—including public liveness, readiness, or auth-configuration +checks—`deploy.sh` automatically recreates both core services from those retained images and still +returns a failed status so CI reports the rejected release. The previous images are replaced only at +the start of the next deployment. + +Use the manual procedure below if automatic restoration itself fails or an older release is required. + +```bash +cd /opt/job-tracker/app # the deployment checkout +git log --oneline -5 # find the last good commit +git checkout +deploy/deploy.sh +``` + +`deploy.sh` takes a fresh backup first, so rolling back is itself protected. + +**Why a code rollback is safe here:** every Phase 4/5 migration is a no-op — the startup reconciler +owns those tables — so reverting the code never leaves migration history ahead of the schema. The +reconciler is additive and never drops a table holding rows, so the older code simply ignores the +newer tables. + +**What a code rollback does not undo:** rows users created in the newer tables stay. That is usually +what you want. If you additionally restore the database, those rows are lost — so restore only when +the data is the problem. + +## Choosing between them + +| Symptom | Action | +|---|---| +| New version starts but behaves wrong | Rollback the code. Leave the database. | +| Backend will not start; schema looks wrong | Rollback the code, then restore only if it still fails. | +| Data is missing or corrupted | Restore the database from the most recent good dump. | +| Deploy aborted before starting | Nothing to undo — the backup ran before any change. | + +## Health checks + +`backend` and `frontend` both report container health, so `docker compose ps` shows real state rather +than merely "running". + +- **Backend:** `curl -fsS http://127.0.0.1:8080/health`. Anonymous, and deliberately **does not touch + the database** — a health check that queried MariaDB would restart a healthy backend whenever the + database blipped. `start_period` is 90s to cover first-boot schema reconciliation. +- **API/database readiness:** `curl -fsS https:///ready`. This checks that the API can reach its + configured database and returns only `ready` or `unavailable`; detailed diagnostics remain admin-only. + Deployment validation requires this probe, while Docker restart policy continues to use liveness. +- **Frontend:** `wget` against nginx on port 80. +- `frontend` waits for `backend` to be *healthy*, not merely started, because nginx proxies `/api` to + it and refuses to start if the upstream cannot be resolved. + +A backend that cannot reach its database during startup exits and is reported `unhealthy`. A dependency +failure after startup leaves the process alive but makes `/ready` return 503, so deployment validation +cannot mistake a live-but-unready stack for a successful release. diff --git a/docs/00-ai-context.md b/docs/_archive/pre-docs-cleanup-2026-09-11/docs/00-ai-context.md similarity index 100% rename from docs/00-ai-context.md rename to docs/_archive/pre-docs-cleanup-2026-09-11/docs/00-ai-context.md diff --git a/docs/01-glossary.md b/docs/_archive/pre-docs-cleanup-2026-09-11/docs/01-glossary.md similarity index 100% rename from docs/01-glossary.md rename to docs/_archive/pre-docs-cleanup-2026-09-11/docs/01-glossary.md diff --git a/docs/02-product-principles.md b/docs/_archive/pre-docs-cleanup-2026-09-11/docs/02-product-principles.md similarity index 100% rename from docs/02-product-principles.md rename to docs/_archive/pre-docs-cleanup-2026-09-11/docs/02-product-principles.md diff --git a/docs/03-design-principles.md b/docs/_archive/pre-docs-cleanup-2026-09-11/docs/03-design-principles.md similarity index 100% rename from docs/03-design-principles.md rename to docs/_archive/pre-docs-cleanup-2026-09-11/docs/03-design-principles.md diff --git a/docs/AI_SESSION_START.md b/docs/_archive/pre-docs-cleanup-2026-09-11/docs/AI_SESSION_START.md similarity index 100% rename from docs/AI_SESSION_START.md rename to docs/_archive/pre-docs-cleanup-2026-09-11/docs/AI_SESSION_START.md diff --git a/docs/CHANGELOG_AI_WORK.md b/docs/_archive/pre-docs-cleanup-2026-09-11/docs/CHANGELOG_AI_WORK.md similarity index 100% rename from docs/CHANGELOG_AI_WORK.md rename to docs/_archive/pre-docs-cleanup-2026-09-11/docs/CHANGELOG_AI_WORK.md diff --git a/docs/MASTER_IMPLEMENTATION_GUIDE.md b/docs/_archive/pre-docs-cleanup-2026-09-11/docs/MASTER_IMPLEMENTATION_GUIDE.md similarity index 100% rename from docs/MASTER_IMPLEMENTATION_GUIDE.md rename to docs/_archive/pre-docs-cleanup-2026-09-11/docs/MASTER_IMPLEMENTATION_GUIDE.md diff --git a/docs/ai/ai-principles.md b/docs/_archive/pre-docs-cleanup-2026-09-11/docs/ai/ai-principles.md similarity index 100% rename from docs/ai/ai-principles.md rename to docs/_archive/pre-docs-cleanup-2026-09-11/docs/ai/ai-principles.md diff --git a/docs/ai/cost-control.md b/docs/_archive/pre-docs-cleanup-2026-09-11/docs/ai/cost-control.md similarity index 100% rename from docs/ai/cost-control.md rename to docs/_archive/pre-docs-cleanup-2026-09-11/docs/ai/cost-control.md diff --git a/docs/ai/cover-letter-ai.md b/docs/_archive/pre-docs-cleanup-2026-09-11/docs/ai/cover-letter-ai.md similarity index 100% rename from docs/ai/cover-letter-ai.md rename to docs/_archive/pre-docs-cleanup-2026-09-11/docs/ai/cover-letter-ai.md diff --git a/docs/ai/cv-ai.md b/docs/_archive/pre-docs-cleanup-2026-09-11/docs/ai/cv-ai.md similarity index 100% rename from docs/ai/cv-ai.md rename to docs/_archive/pre-docs-cleanup-2026-09-11/docs/ai/cv-ai.md diff --git a/docs/ai/future-ai.md b/docs/_archive/pre-docs-cleanup-2026-09-11/docs/ai/future-ai.md similarity index 100% rename from docs/ai/future-ai.md rename to docs/_archive/pre-docs-cleanup-2026-09-11/docs/ai/future-ai.md diff --git a/docs/ai/jobs-ai.md b/docs/_archive/pre-docs-cleanup-2026-09-11/docs/ai/jobs-ai.md similarity index 100% rename from docs/ai/jobs-ai.md rename to docs/_archive/pre-docs-cleanup-2026-09-11/docs/ai/jobs-ai.md diff --git a/docs/ai/model-selection.md b/docs/_archive/pre-docs-cleanup-2026-09-11/docs/ai/model-selection.md similarity index 100% rename from docs/ai/model-selection.md rename to docs/_archive/pre-docs-cleanup-2026-09-11/docs/ai/model-selection.md diff --git a/docs/ai/overview.md b/docs/_archive/pre-docs-cleanup-2026-09-11/docs/ai/overview.md similarity index 100% rename from docs/ai/overview.md rename to docs/_archive/pre-docs-cleanup-2026-09-11/docs/ai/overview.md diff --git a/docs/ai/privacy.md b/docs/_archive/pre-docs-cleanup-2026-09-11/docs/ai/privacy.md similarity index 100% rename from docs/ai/privacy.md rename to docs/_archive/pre-docs-cleanup-2026-09-11/docs/ai/privacy.md diff --git a/docs/_archive/pre-docs-cleanup-2026-09-11/docs/ai/prompts.md b/docs/_archive/pre-docs-cleanup-2026-09-11/docs/ai/prompts.md new file mode 100644 index 0000000..8b4bfeb --- /dev/null +++ b/docs/_archive/pre-docs-cleanup-2026-09-11/docs/ai/prompts.md @@ -0,0 +1,51 @@ +# AI Prompt Management + +## Purpose + +Centralise prompts. + +Avoid random prompts scattered throughout code. + +--- + +# Requirements + +Prompts should: + +- Be version controlled. +- Be documented. +- Have tests where possible. + +--- + +# Prompt Structure + +Every prompt should define: + +Purpose. + +Inputs. + +Expected output. + +Restrictions. + +--- + +# Example + +Task: + +Improve CV bullet point. + +Input: + +Existing bullet. + +Output: + +Improved bullet. + +Restrictions: + +Do not add unsupported achievements. \ No newline at end of file diff --git a/docs/_archive/pre-docs-cleanup-2026-09-11/docs/ai/providers.md b/docs/_archive/pre-docs-cleanup-2026-09-11/docs/ai/providers.md new file mode 100644 index 0000000..f1ffb64 --- /dev/null +++ b/docs/_archive/pre-docs-cleanup-2026-09-11/docs/ai/providers.md @@ -0,0 +1,66 @@ +# AI Providers + +## Purpose + +Support multiple AI providers. + +--- + +# Current/Future Providers + +Possible: + +- OpenAI. +- Anthropic. +- Google Gemini. +- Groq. +- Ollama. +- Other compatible APIs. + +--- + +# Provider Architecture + +Use abstraction: + +AI Provider Interface + +↓ + +Provider Implementation + +↓ + +Model + +--- + +# Requirements + +Users should eventually be able to: + +- Select provider. +- Select model. +- Configure API keys. + +--- + +# Admin Controls + +Admins should control: + +- Available providers. +- Allowed models. +- Usage limits. + +--- + +# SaaS Consideration + +Different plans may have: + +Free models. + +Premium models. + +BYO-key support. \ No newline at end of file diff --git a/docs/_archive/pre-docs-cleanup-2026-09-11/docs/ai/safety.md b/docs/_archive/pre-docs-cleanup-2026-09-11/docs/ai/safety.md new file mode 100644 index 0000000..48b2a20 --- /dev/null +++ b/docs/_archive/pre-docs-cleanup-2026-09-11/docs/ai/safety.md @@ -0,0 +1,8 @@ +# safety + +AI output is suggestion-only and cannot overwrite the Career Profile automatically. Prompts delimit +untrusted source text, require preservation of factual claims, and instruct providers not to invent +employers, titles, dates, or metrics. Attachments are included only when explicitly selected. + +Provider outages degrade AI features without blocking core tracking. Calls and tokens are metered and +limited by plan. See `docs/ai/security.md` and `docs/ai/privacy.md`. diff --git a/docs/ai/security.md b/docs/_archive/pre-docs-cleanup-2026-09-11/docs/ai/security.md similarity index 100% rename from docs/ai/security.md rename to docs/_archive/pre-docs-cleanup-2026-09-11/docs/ai/security.md diff --git a/docs/ai/workload-inventory.md b/docs/_archive/pre-docs-cleanup-2026-09-11/docs/ai/workload-inventory.md similarity index 100% rename from docs/ai/workload-inventory.md rename to docs/_archive/pre-docs-cleanup-2026-09-11/docs/ai/workload-inventory.md diff --git a/docs/application-discovery-report.md b/docs/_archive/pre-docs-cleanup-2026-09-11/docs/application-discovery-report.md similarity index 100% rename from docs/application-discovery-report.md rename to docs/_archive/pre-docs-cleanup-2026-09-11/docs/application-discovery-report.md diff --git a/docs/architecture/ai-architecture.md b/docs/_archive/pre-docs-cleanup-2026-09-11/docs/architecture/ai-architecture.md similarity index 100% rename from docs/architecture/ai-architecture.md rename to docs/_archive/pre-docs-cleanup-2026-09-11/docs/architecture/ai-architecture.md diff --git a/docs/architecture/ai-career-assistant.md b/docs/_archive/pre-docs-cleanup-2026-09-11/docs/architecture/ai-career-assistant.md similarity index 100% rename from docs/architecture/ai-career-assistant.md rename to docs/_archive/pre-docs-cleanup-2026-09-11/docs/architecture/ai-career-assistant.md diff --git a/docs/architecture/ai-privacy.md b/docs/_archive/pre-docs-cleanup-2026-09-11/docs/architecture/ai-privacy.md similarity index 100% rename from docs/architecture/ai-privacy.md rename to docs/_archive/pre-docs-cleanup-2026-09-11/docs/architecture/ai-privacy.md diff --git a/docs/architecture/api-design.md b/docs/_archive/pre-docs-cleanup-2026-09-11/docs/architecture/api-design.md similarity index 100% rename from docs/architecture/api-design.md rename to docs/_archive/pre-docs-cleanup-2026-09-11/docs/architecture/api-design.md diff --git a/docs/architecture/application-workspace.md b/docs/_archive/pre-docs-cleanup-2026-09-11/docs/architecture/application-workspace.md similarity index 100% rename from docs/architecture/application-workspace.md rename to docs/_archive/pre-docs-cleanup-2026-09-11/docs/architecture/application-workspace.md diff --git a/docs/architecture/attachment-storage.md b/docs/_archive/pre-docs-cleanup-2026-09-11/docs/architecture/attachment-storage.md similarity index 100% rename from docs/architecture/attachment-storage.md rename to docs/_archive/pre-docs-cleanup-2026-09-11/docs/architecture/attachment-storage.md diff --git a/docs/architecture/authentication.md b/docs/_archive/pre-docs-cleanup-2026-09-11/docs/architecture/authentication.md similarity index 100% rename from docs/architecture/authentication.md rename to docs/_archive/pre-docs-cleanup-2026-09-11/docs/architecture/authentication.md diff --git a/docs/_archive/pre-docs-cleanup-2026-09-11/docs/architecture/backend.md b/docs/_archive/pre-docs-cleanup-2026-09-11/docs/architecture/backend.md new file mode 100644 index 0000000..a9152c5 --- /dev/null +++ b/docs/_archive/pre-docs-cleanup-2026-09-11/docs/architecture/backend.md @@ -0,0 +1,75 @@ +# Backend Architecture + +## Purpose + +Define backend organisation. + +--- + +# Preferred Structure + +Example: + +Application + +├── Features + +├── Domain + +├── Infrastructure + +├── Services + +├── API + +--- + +# Responsibilities + +## API Layer + +Handles: + +- HTTP requests. +- Authentication. +- Validation. +- Responses. + +Should not contain business logic. + +--- + +## Application Layer + +Handles: + +- Workflows. +- Business operations. +- Coordination. + +--- + +## Domain Layer + +Contains: + +- Entities. +- Rules. +- Business concepts. + +--- + +## Infrastructure + +Handles: + +- Database. +- External APIs. +- File storage. +- AI providers. + +--- + +# Rule + +Controllers should remain thin. \ No newline at end of file diff --git a/docs/architecture/background-jobs.md b/docs/_archive/pre-docs-cleanup-2026-09-11/docs/architecture/background-jobs.md similarity index 100% rename from docs/architecture/background-jobs.md rename to docs/_archive/pre-docs-cleanup-2026-09-11/docs/architecture/background-jobs.md diff --git a/docs/architecture/background-workers.md b/docs/_archive/pre-docs-cleanup-2026-09-11/docs/architecture/background-workers.md similarity index 100% rename from docs/architecture/background-workers.md rename to docs/_archive/pre-docs-cleanup-2026-09-11/docs/architecture/background-workers.md diff --git a/docs/architecture/career-architecture.md b/docs/_archive/pre-docs-cleanup-2026-09-11/docs/architecture/career-architecture.md similarity index 100% rename from docs/architecture/career-architecture.md rename to docs/_archive/pre-docs-cleanup-2026-09-11/docs/architecture/career-architecture.md diff --git a/docs/architecture/career-profile-model.md b/docs/_archive/pre-docs-cleanup-2026-09-11/docs/architecture/career-profile-model.md similarity index 100% rename from docs/architecture/career-profile-model.md rename to docs/_archive/pre-docs-cleanup-2026-09-11/docs/architecture/career-profile-model.md diff --git a/docs/_archive/pre-docs-cleanup-2026-09-11/docs/architecture/current.md b/docs/_archive/pre-docs-cleanup-2026-09-11/docs/architecture/current.md new file mode 100644 index 0000000..7710de1 --- /dev/null +++ b/docs/_archive/pre-docs-cleanup-2026-09-11/docs/architecture/current.md @@ -0,0 +1,431 @@ +# Jobjakt — Current Architecture + +> **This document describes the system as it actually is.** Every claim was verified against code. +> 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. +> Topic documentation is maintained alongside this file; dated completion and review reports remain +> historical snapshots. When documentation conflicts, prefer this file and the code. + +--- + +## 1. What the product is + +Jobjakt is a self-hosted, multi-user job application tracking platform with local-AI career assistance: + +- Track jobs and applications end-to-end (pipeline stages, follow-ups, deadlines, salary, tags, notes). +- Company CRM (pipeline stage, contact dates, recruiter details) — company-level only, no people entities. +- Correspondence log per application: **Gmail OAuth import with a human review queue**, IMAP, Microsoft Graph. +- Attachments per application with purpose metadata and AI-inclusion toggles. +- CV platform: upload → OCR/extraction → structured parsing → per-job tailored CV drafts → templated PDF via Playwright. +- AI drafts: cover letters, recruiter messages, follow-up drafts, job summaries, match scoring, interview prep. +- Rules engine (auto-ghosting), reminder emails, daily JSON export, event trail, automated DB backup. +- Admin: user management, audit log, system readiness. +- Production: `https://jobs.cesnimda.uk` via Gitea Actions → SSH → Docker Compose. + +**Product hierarchy** (from `docs/MASTER_IMPLEMENTATION_GUIDE.md` — job tracking is the core; career tools support it): +Job Tracking → Applications → Workflow → Follow-ups → Communication, then Career Profile → Master CV → CV Builder → Cover Letters → Portfolio → Interview Prep, then Job Discovery. + +--- + +## 2. Architecture overview + +```mermaid +flowchart LR + subgraph Client + UI[React 19 SPA
MUI 7, react-router 6
Next.js 16 CSR shell] + end + + subgraph Frontend container + NGINX[nginx 1.29-alpine
serves static export + proxies /api] + end + + subgraph Backend container + API[ASP.NET Core net9.0
JobTrackerApi host] + BG[7 hosted services:
Rules, FollowUpReminder, DailyExport,
JobEnrichment, SummarizerProbe,
CvProcessing, DatabaseBackup] + DB[(SQLite default
or MariaDB/MySQL)] + FS[/Data root:
Attachments, CvArtifacts,
exports, DP keys/] + end + + subgraph AI stack + AISVC[FastAPI ai-service :8001
distilbart summarizer,
OCR, docx/pdf extraction] + PROV[Provider via AI_PROVIDER env:
ollama qwen2.5:7b / gemini / groq] + end + + EXT1[Google OAuth / Gmail API] + EXT2[Microsoft Graph / IMAP] + EXT3[Job sites: Finn, NAV,
LinkedIn, Jobbnorge] + EXT4[SMTP] + EXT5[LibreTranslate optional] + + UI --> NGINX --> API + API --> DB + API --> FS + API --> AISVC --> PROV + API --> EXT1 + API --> EXT2 + API --> EXT3 + API --> EXT4 + API --> EXT5 + BG --> DB +``` + +### Solution layout + +| Project | Role | +|---|---| +| `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. | +| `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. | + +> 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. + +--- + +## 3. Technology stack + +**Backend:** ASP.NET Core net9.0, EF Core 9 (SQLite default; Pomelo MySQL/MariaDB via `Database:Provider`), ASP.NET Identity Core, JWT bearer (smart policy scheme: local + Google), built-in RateLimiter, DataProtection (file-system keys), Playwright (PDF export). + +**Frontend:** **Next.js 16** + React 19 + **TypeScript 5.9** + MUI 7 (+ x-data-grid, x-date-pickers, lab), axios, react-router-dom 7, @tanstack/react-table, i18n EN + NB (custom provider), Jest/RTL. + +> 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. + +**Infra:** Docker Compose (backend, frontend/nginx 1.29-alpine, ai-service, ollama opt-in via `bundled-ollama` profile w/ GPU), Gitea Actions CI → SSH deploy → `deploy/deploy.sh`, external `jobtracker_shared` network. + +--- + +## 4. Frontend architecture + +**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 `` inside a catch-all `Shell` for authenticated routes. + +Development leaves static-export mode disabled and rewrites deep links to the root shell; production exports that 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}`. + +> `/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. + +**Styling:** MUI `sx` + custom `src/theme.ts` (439 lines), light/dark. Design tokens live inline in component `sx` props rather than in the theme (e.g. the same `boxShadow: "0px 8px 24px -12px rgba(15,23,42,0.12)"` is repeated across pages). There is no component primitives layer and no Storybook. + +**Oversized components** (refactor targets, in order): `JobDetailsDialog.tsx` (1400), `CareerProfilePage.tsx` (1293), `JobTable.tsx` (786), `Correspondence.tsx` (732), `DashboardView.tsx` (666), `AdminSystemPage.tsx` (623), `AddJobModal.tsx` (618). (`ProfilePage.tsx` was 1368; Phase 2.2 split it — see §4a.) + +--- + +## 4a. Profile / Career separation (Phase 2, 2026-07-17) + +`/profile` and `/career` were one 1368-line component (`ProfilePage`) forked by a `careerOnly` +boolean. Phase 2 scoped their saves; Phase 2.2 split them into two dedicated components. This is +the reference model for how account identity and the master career profile relate. + +### Components & routes + +| Route | Component | Owns | +|---|---|---| +| `/profile` | `views/ProfilePage.tsx` (~490 lines) | Account identity + security + preferences | +| `/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 a separate routed workspace and consumes the Career Profile as its source of truth. + +### Request flow + +``` +/profile → ProfilePage + load: GET /auth/me (account row only) + save: PUT /auth/profile { email, userName, firstName, lastName, displayName } + +/career → CareerWorkspacePage → CareerProfilePage + load: GET /auth/me + GET /profile-cv/runs (extraction history) + GET /jobapplications?… (for per-job CV tailoring context) + save: PUT /auth/profile { profileCvText, profileCvStructureJson } + (ProfileCvController paths additionally dual-write CareerProfileService — + CareerProfiles / CareerProfileVersions — see §9 / §16) +``` + +### Data ownership (the invariant) + +Both surfaces persist through the **same** endpoint, `PUT /auth/profile`, which does **partial +updates** (`AuthController.UpdateProfile`): a field is touched only if the request carries it — +`null`/omitted leaves it unchanged, `""` clears it, a value sets it. Email and UserName are never +cleared (login identifiers). + +| Field(s) on `ApplicationUser` | Owner (only surface that writes them) | +|---|---| +| `Email`, `UserName`, `FirstName`, `LastName`, `DisplayName`, `AvatarImageDataUrl`, password, TOTP/2FA, linked OAuth accounts | **/profile** | +| `ProfileCvText`, `ProfileCvStructureJson` (the master career profile) | **/career** | + +Because the endpoint is partial, `/profile` saving identity does **not** null the master profile, +and `/career` saving the profile does **not** null identity. This is enforced by tests +(`AuthAndSystemControllerTests`: identity-save-keeps-CV, career-save-keeps-identity, empty-clears, +null-leaves). + +### API responsibilities + +- `AuthController.UpdateProfile` (`PUT /auth/profile`) — partial update of the account row; the + single write path for both surfaces. Local accounts only. +- `AuthController` (`GET /auth/me`) — returns the whole account row; each surface reads the fields + it owns. +- `ProfileCvController` — CV ingest/parse/rewrite/export and the `CareerProfileService` dual-write. + Called from `/career`. +- `JobApplicationsController` — `/career` reads job list for tailoring context only. + +### Future extension points + +- **`CareerProfilePage` is the foundation for all future career outputs** (Phase 3/4): CV Builder, + tailored CVs, cover letters, portfolio, interview prep. They attach here, referencing the master + profile — never duplicating it (per `docs/MASTER_IMPLEMENTATION_GUIDE.md`). +- **Source-of-truth flip (F5):** today `ProfileCvStructureJson` is authoritative and + `CareerProfileService` mirrors it. A later phase makes `CareerProfiles`/`CareerProfileVersions` + authoritative; the `/career` save would then route through `CareerProfileService` rather than the + blob column. The partial-update endpoint and the ownership split above do not change. +- **Full decomposition (roadmap 2.2 residue):** `CareerProfilePage` is still large because it owns + the whole master-CV surface; the CV Builder work will extract sub-components from it. + +--- + +## 5. Authentication & authorization + +- **Smart policy scheme:** inspects the bearer token issuer — Google ID tokens (`accounts.google.com`) → `google` handler (validated against `Auth:GoogleClientId`); everything else → `local` JWT (symmetric `Auth:JwtKey`, issuer/audience validated, 2-min clock skew). +- **Cookie sessions:** local handler also reads `jobtracker_auth` (HttpOnly, SameSite=Lax, Secure-configurable, 30d when persistent). **CSRF double-submit** middleware enforces cookie+header match on all mutating requests when a session cookie is present (login/register/reset/csrf exempt). +- `Auth:Require=true` sets a fallback authorize-all policy (prod compose sets it). Dev without a JWT key generates an ephemeral key + warning; **fails closed** if auth is required but unconfigured. +- Local tokens **must** carry a subject claim (`LocalAuthIdentity`), enforced in `OnTokenValidated` — hardened after finding M013-2. +- **Multi-tenancy:** every tenant entity carries `OwnerUserId`; `JobTrackerContext` applies global query filters `CurrentUserId != null && OwnerUserId == CurrentUserId` (**deny-on-null**). Correspondence/JobEvent/CV entities filter through their parent's owner. Covered by `JobApplicationsAuthorizationTests`, `OwnershipGuardTests`. +- Roles via Identity: admin-only `UsersController`, `AdminAuditController`, `AdminSystemController`. +- **2FA:** TOTP (`Otp.NET`), encrypted secrets, QR enrolment (`QRCoder`), recovery codes, trusted devices (`jobtracker_td` cookie), pending-token flow. +- **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:** 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. + +--- + +## 6. Database + +EF Core, **11 migrations**. App DbSets + Identity tables. + +```mermaid +erDiagram + ApplicationUser ||--o{ Company : owns + ApplicationUser ||--o{ Job : owns + ApplicationUser ||--o{ JobApplication : owns + ApplicationUser ||--o| UserRuleSettings : has + ApplicationUser ||--o{ GmailConnection : has + ApplicationUser ||--o{ CvUploadArtifact : owns + ApplicationUser ||--o{ CvExtractionRun : owns + ApplicationUser ||--o{ UserSession : has + ApplicationUser ||--o{ TrustedDevice : has + Company ||--o{ Job : "posts" + Company ||--o{ JobApplication : "has jobs" + Job ||--o{ JobApplication : "applied to via" + JobApplication ||--o{ Correspondence : messages + JobApplication ||--o{ Attachment : attachments + JobApplication ||--o{ JobEvent : events + JobApplication ||--o| TailoredCvDraft : "1:1 draft" + CvUploadArtifact ||--o{ CvExtractionRun : "source of" +``` + +**Entities:** `Company`, **`Job`**, `JobApplication`, `Correspondence`, `GmailConnection`, `GmailReviewDecision`, `MicrosoftGraphConnection`, `ImapConnection`, `Attachment`, `RuleSettings`, `UserRuleSettings`, `SystemEmailSettings`, `JobEvent`, `CvUploadArtifact`, `CvExtractionRun`, `TailoredCvDraft`, `TwoFactorRecoveryCode`, `TrustedDevice`, `UserSession`. + +**Key notes:** +- `ApplicationUser` (IdentityUser) also stores `ProfileCvText`, **`ProfileCvStructureJson`** (the master career profile — a JSON blob, not relational), `AvatarImageDataUrl` (base64 in a column, on the `/auth/me` hot path), Google/Microsoft link info, TOTP secrets, current CV artifact/run pointers. +- **`Job` vs `JobApplication`** — `Job` is the opportunity (title, company, description, URL, salary, location, deadline, tags); `JobApplication` is the user's pursuit of it (status, dates, follow-ups, correspondence, attachments). `JobApplication.JobId` remains nullable for compatibility, but startup backfills missing links and persistence paths synchronize both copies. Legacy reads and columns remain until the production cutover report passes. See §16 and `docs/decisions/ADR-002-job-application-model.md`. +- 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 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 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-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`. + +--- + +## 7. API surface (19 controllers, all under `/api`) + +| Controller | Lines | Highlights | +|---|---|---| +| `JobApplicationsController` | **2394** | **37 endpoints.** CRUD, paging/filter/sort, board, reminders, stats, analytics, history, status/follow-up PATCH, soft delete/restore, duplicate-check, **plus** the AI surface: match-score, candidate-fit, focus-plan, generated interview-prep brief, readiness, tailored-CV draft/preview/export/generate, application-drafts, application-package, follow-up drafts + send, ai-metrics. The canonical timeline and editable interview board live in their focused controllers. | +| `ProfileCvController` | **2249** | CV upload artifacts, extraction runs, structure parsing, reprocess/rebuild/improve, rewrite-section, rewrite-preview, templates, Playwright PDF export, benchmark harness. | +| `GmailController` | **1023** | OAuth connect/callback, sync, review queue, import decisions, job matching. | +| `AuthController` | **879** | login/register/me/config, Google + Microsoft exchange and link/unlink, avatar, password change/reset, email verification, session cookie + CSRF. | +| `AdminSystemController` | 342 | System readiness (DB/Gmail/AI). | +| `TwoFactorController` | 341 | TOTP enrol/verify/disable, recovery codes. | +| `AttachmentsController` | 342 | Validated staged upload, managed-root download, metadata-only rename, quarantined delete, purpose/AI-inclusion metadata and restart reconciliation. See `attachment-storage.md`. | +| `UsersController` | 229 | Admin user/role management. | +| `AdminAuditController` | 219 | Audit trail. | +| `CorrespondenceController` | 185 | Per-job messages CRUD. | +| `CompaniesController` | 150 | CRUD, idempotent create-by-name, recruiter/pipeline fields. | +| `MicrosoftGraphController` | 150 | Outlook/M365 mail linking. | +| `SessionsController` | 104 | List/revoke sessions. | +| `ExportController` | 102 | JSON/CSV export. | +| `RulesController` | 101 | Global + per-user rule settings, clamped. | +| `ClientErrorsController` | 100 | Frontend error intake → logs. | +| `ImapController` | 96 | IMAP mail linking (SSRF-guarded). | +| `BackupController` | 89 | Manual backup trigger. | +| `JobImportController` | **27** | **One endpoint:** `POST /preview`. URL parse only — no persistence, no import history. | + +**God controllers are a top debt.** `JobApplicationsController` and `ProfileCvController` mix HTTP, business logic, AI prompt construction, and persistence. `docs/MASTER_IMPLEMENTATION_GUIDE.md` forbids exactly this ("Avoid: Massive controllers"). Refactor needs test cover first — the tests exist. + +**OpenAPI** is wired (`AddOpenApi` / `MapOpenApi`) but **dev-only** — guarded by `app.Environment.IsDevelopment()`, not exposed in production. + +--- + +## 8. Background services (7 hosted services) + +| Service | Function | +|---|---| +| `RulesHostedService` → `RulesEngine` | Periodic auto-transitions (e.g. → Ghosted) from rule settings | +| `FollowUpReminderHostedService` | Reminder emails for due/upcoming follow-ups (dedup via `LastReminderEmailSentAt`) | +| `DailyExportHostedService` | Daily JSON export at a configured local hour | +| `JobEnrichmentHostedService` | Backfills summaries/enrichment | +| `SummarizerProbeHostedService` | Probes AI service readiness | +| `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) | + +Rules, reminders, daily export and enrichment now enumerate owners explicitly, then re-enter normal tenant-filtered scopes. All four have deny-by-default worker switches and remain inactive until their notification/privacy/entitlement/operations prerequisites are ready; see `docs/architecture/background-workers.md`. 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. + +--- + +## 9. AI pipeline + +**Architecture:** the backend does **not** call any LLM in-process. It HTTP-calls a FastAPI sidecar (`tools/summarizer/app.py`) exposing `/health`, `/cv/normalize`, `/cv/classify-block`, `/cv/rewrite`, `/summarize`, `/extract-text`. The sidecar picks **one** provider from the process-wide `AI_PROVIDER` env var ∈ {`ollama`, `gemini`, `groq`}. + +> **Important — `docs/00-ai-context.md` is wrong about this.** It describes a provider interface fanning out to OpenAI/Gemini/Claude/Ollama, admin-controlled, with users never locked to one model. **None of that exists.** There is one env var, one provider per deployment, no OpenAI, no Claude, no admin control, no per-user selection. Product decision 2026-07-17: **the docs get fixed, the abstraction does not get built** — revisit only if a customer asks. + +**Data flow:** +1. **Job import:** URL → plugin parse (Finn/NAV/LinkedIn/Jobbnorge) or universal JSON-LD parser → optional LibreTranslate → language detect + skill tagging → preview → user accepts → persisted. +2. **Summaries:** `SummarizerService` → `/summarize` (distilbart, TTL-cached, GPU if available) → persisted `ShortSummary`. +3. **CV ingest:** upload (PDF/DOCX/image) → `/extract-text` (OCR) → block classification (`CvAiClassifier`/`CvAiNormalizer` via `/cv/classify-block`) → `StructuredCvProfile` → `ProfileCvStructureJson` on the user. +4. **Tailoring:** job description + structured CV → `/cv/rewrite` → `TailoredCvDraft` (separate entity, per application) → `CvTemplateRenderer` → Playwright → PDF. +5. **Drafts:** cover letter / recruiter message / follow-up per job, attachment-aware context selection. + +**Invariant that holds:** the master profile is **never** auto-overwritten. Tailored output lands in `TailoredCvDraft`, a separate entity. This is the most important documented rule and it is correctly implemented — do not break it. + +**Degradation:** if the AI service or provider is down, core tracking still works (probe service; AI is not a deploy gate). + +**CV templates are hardcoded** — `CvTemplateRenderer.Render` is a C# `switch` over 6 template IDs (`ats-minimal`, `harvard`, `auckland`, `edinburgh`, `monarch`, `fjord`), each a function interpolating HTML strings, with booleans like `roundedPhoto`/`curvedHeader`. **There is no theme model and nothing is user-customisable.** This is a structural dead end for the CV Builder — see `docs/application-discovery-report.md` §10. + +--- + +## 10. Email + +`SmtpEmailSender` + `EmailSettingsResolver`: config from env/appsettings **or** DB-stored `SystemEmailSettings` (admin-editable). Gmail SMTP + app password in prod. Flows: password reset, email verification, follow-up reminders. `App:PublicBaseUrl` is the canonical external origin for generated links, OAuth callbacks, billing redirects, secure cookies and production Host validation. + +Inbound: `GmailOAuthService` (655), `MicrosoftGraphOAuthService` (507), `ImapService` (345, SSRF-guarded). + +--- + +## 11. Configuration & secrets + +- `.env` (git-ignored) → docker-compose env → ASP.NET config. `.env.example` documents the shape. +- `appsettings.Development.json` holds only `CHANGE_ME_*` placeholders. +- Key knobs: `Database:Provider`, `ConnectionStrings:JobTracker`, `Data:Root`, `Cors:Origins`, `Ai:BaseUrl`, `Summarizer:BaseUrl`, **`Ai:ServiceToken`**, `Auth:*` (incl. `Auth:AllowRegistration`), `Email:*`, `Exports:*`, `App:*`, `HttpsRedirection:*` (TLS terminated at the reverse proxy). +- AI service knobs (compose): `AI_PROVIDER`, **`AI_SERVICE_TOKEN`**, `OLLAMA_BASE_URL`, `OLLAMA_MODEL`, `GEMINI_API_KEY`, `GROQ_API_KEY`. +- **`AI_SERVICE_TOKEN` is mandatory.** Both `Ai__ServiceToken` (backend) and `AI_SERVICE_TOKEN` (ai-service) use `${AI_SERVICE_TOKEN:?...}`, so `docker compose up` fails loudly rather than booting an unauthenticated AI service. Generate with `python -c "import secrets; print(secrets.token_hex(32))"`. Rotating it requires recreating **both** containers together — they must agree. +- Note both those compose entries are **quoted**: the `:?` error message contains a colon-space, which YAML would otherwise parse as a map (`services.backend.environment.[20]: unexpected type map[string]interface{}`). +- `ProductionConfigTests.cs` guards prod config shape. +- Ollama is **intentionally not bundled by default** (`bundled-ollama` compose profile) so deploys reuse a shared instance. `AI_PROVIDER=gemini` exists specifically to offload a weak local GPU in prod. + +--- + +## 12. Build, CI/CD, deployment + +- **CI is Gitea, not GitHub** — `.gitea/workflows/ci-deploy.yml`. **There is no `.github/` directory.** +- On PR + push-to-main: build backend (Release) → run **all** backend tests → `npm ci` → run the **whole** frontend suite → build frontend. + + > Corrected 2026-07-17: the archived overview said CI runs "an explicit whitelist of 10 frontend test files". **The whitelist is gone.** The workflow now runs `npm test -- --watchAll=false --runInBand` and carries a comment forbidding its return: the previous whitelist "silently skipped new suites and let two regressions reach main." + +- The workflow is heavily defended against a flaky self-hosted runner: dotnet install retry, `npm ci` SIGSEGV retry, frontend build OOM retry. +- **Deploy** (push to main only): SSH to prod → `git reset --hard ` in `/opt/job-tracker/app` → `deploy/deploy.sh` (compose build/up with retry + cache-prune fallbacks) → verify containers. AI health is non-blocking. +- **No staging environment.** Deploys go straight to prod after CI. + +--- + +## 13. Testing + +- **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/`). +- **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 + +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. + +--- + +## 15. Security posture + +**Verified strong:** +- Multi-tenancy via deny-on-null global query filters, with a dedicated authorization test suite. +- CSRF double-submit on mutating requests; HttpOnly SameSite=Lax session cookie. +- Auth fails closed when required but unconfigured; subjectless-JWT rejected (M013-2). +- SSRF on job import and IMAP fixed and retested (DNS resolution check, private/loopback/link-local rejection, redirects disabled). +- Rate-limited login/email/2FA endpoints; Identity PBKDF2 hashing. +- OpenAPI dev-only. `.env` git-ignored; DP keys and runtime exports untracked (`519c32e`). +- 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. + +**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`) | **Closed 2026-08-02 — production keys rotated by the operator** | +| 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 | **Closed — NuGet and production npm audit reporting are in CI** | + +--- + +## 16. Phase 0 changes (2026-07-17) + +Full record: `docs/phase-0-foundation-report.md`. What changed architecturally: + +- **AI sidecar secured — three layers, verified against the running stack (2026-07-17):** + 1. **No host port.** `ports: "8001:8001"` removed; `expose:` only. + 2. **Private network.** `ai-service` sits on a new `ai_internal` bridge and **nothing else**. It was removed from `default` (which the frontend shares) and from `shared_services` — the latter is `external: true` (`jobtracker_shared`), so any other compose stack on the host could join it and reach port 8001. `ai_internal` has exactly two members: `ai-service` and `backend`. It is **not** `internal: true`, because ai-service needs egress to Gemini/Groq. + 3. **Shared secret.** `X-Ai-Service-Token` required on every endpoint except `/health`, compared with `hmac.compare_digest`. Backend sends it via `Ai:ServiceToken`; sidecar reads `AI_SERVICE_TOKEN`. Unset = open (local dev/tests), but compose declares both with `:?` so the stack **refuses to start** without it. + + **Only the backend can reach the AI service.** Verified live: host → connection refused; frontend container → cannot even resolve `ai-service`; unauthenticated calls to `/summarize`, `/cv/rewrite`, `/extract-text` → 401; wrong token → 401; backend (172.23.0.3) with token → **200 OK**. + + > If you point `OLLAMA_BASE_URL` at an Ollama in **another** compose stack, address it by host IP (e.g. `http://:11435`) — `ai-service` can no longer resolve container names on `shared_services`, by design. The bundled `ollama` profile is on `ai_internal` and still works by name. +- **Pipeline expanded beyond `Applied`** — `JobPipeline` now models pre-application stages (`Saved`, `Interested`, `Preparing`) in a new `PipelineCategory.Prospect`, so a job can be tracked before it is applied to. `Saved` is the new default for wizard-created jobs; `Applied` remains the default for the legacy create path. +- **`DateApplied` is nullable** + `SavedAt` added — a saved job no longer carries a fabricated application date. +- **`Job` entity introduced** alongside `JobApplication`; linked rows are now created and backfilled, while reads remain on the legacy columns pending production cutover validation. + +--- + +## 17. Known debt + +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. + +--- + +## 18. Historical decisions worth knowing + +Recorded nowhere else in active docs: + +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. diff --git a/docs/architecture/cv-builder.md b/docs/_archive/pre-docs-cleanup-2026-09-11/docs/architecture/cv-builder.md similarity index 100% rename from docs/architecture/cv-builder.md rename to docs/_archive/pre-docs-cleanup-2026-09-11/docs/architecture/cv-builder.md diff --git a/docs/architecture/cv-rendering.md b/docs/_archive/pre-docs-cleanup-2026-09-11/docs/architecture/cv-rendering.md similarity index 100% rename from docs/architecture/cv-rendering.md rename to docs/_archive/pre-docs-cleanup-2026-09-11/docs/architecture/cv-rendering.md diff --git a/docs/architecture/cv-theme-engine.md b/docs/_archive/pre-docs-cleanup-2026-09-11/docs/architecture/cv-theme-engine.md similarity index 100% rename from docs/architecture/cv-theme-engine.md rename to docs/_archive/pre-docs-cleanup-2026-09-11/docs/architecture/cv-theme-engine.md diff --git a/docs/architecture/database.md b/docs/_archive/pre-docs-cleanup-2026-09-11/docs/architecture/database.md similarity index 100% rename from docs/architecture/database.md rename to docs/_archive/pre-docs-cleanup-2026-09-11/docs/architecture/database.md diff --git a/docs/architecture/deployment.md b/docs/_archive/pre-docs-cleanup-2026-09-11/docs/architecture/deployment.md similarity index 100% rename from docs/architecture/deployment.md rename to docs/_archive/pre-docs-cleanup-2026-09-11/docs/architecture/deployment.md diff --git a/docs/architecture/durable-operations.md b/docs/_archive/pre-docs-cleanup-2026-09-11/docs/architecture/durable-operations.md similarity index 100% rename from docs/architecture/durable-operations.md rename to docs/_archive/pre-docs-cleanup-2026-09-11/docs/architecture/durable-operations.md diff --git a/docs/architecture/file-storage.md b/docs/_archive/pre-docs-cleanup-2026-09-11/docs/architecture/file-storage.md similarity index 100% rename from docs/architecture/file-storage.md rename to docs/_archive/pre-docs-cleanup-2026-09-11/docs/architecture/file-storage.md diff --git a/docs/_archive/pre-docs-cleanup-2026-09-11/docs/architecture/frontend.md b/docs/_archive/pre-docs-cleanup-2026-09-11/docs/architecture/frontend.md new file mode 100644 index 0000000..e623a95 --- /dev/null +++ b/docs/_archive/pre-docs-cleanup-2026-09-11/docs/architecture/frontend.md @@ -0,0 +1,64 @@ +# Frontend Architecture + +## Goals + +The frontend should be: + +- Maintainable. +- Consistent. +- Easy to extend. + +--- + +# Structure + +Organise by feature. + +Example: + +features/ + +jobs/ + +career/ + +cv-builder/ + +settings/ + +auth/ + + +--- + +# Components + +Prefer: + +Reusable components. + +Small focused components. + +--- + +# Pages + +Pages should: + +- Compose components. +- Handle routing. +- Manage page-level state. + +--- + +# State + +Avoid unnecessary global state. + +Use: + +- Local state. +- Feature state. +- Server state. + +Where appropriate. \ No newline at end of file diff --git a/docs/architecture/future.md b/docs/_archive/pre-docs-cleanup-2026-09-11/docs/architecture/future.md similarity index 100% rename from docs/architecture/future.md rename to docs/_archive/pre-docs-cleanup-2026-09-11/docs/architecture/future.md diff --git a/docs/architecture/migrations.md b/docs/_archive/pre-docs-cleanup-2026-09-11/docs/architecture/migrations.md similarity index 100% rename from docs/architecture/migrations.md rename to docs/_archive/pre-docs-cleanup-2026-09-11/docs/architecture/migrations.md diff --git a/docs/architecture/notifications.md b/docs/_archive/pre-docs-cleanup-2026-09-11/docs/architecture/notifications.md similarity index 100% rename from docs/architecture/notifications.md rename to docs/_archive/pre-docs-cleanup-2026-09-11/docs/architecture/notifications.md diff --git a/docs/_archive/pre-docs-cleanup-2026-09-11/docs/architecture/overview.md b/docs/_archive/pre-docs-cleanup-2026-09-11/docs/architecture/overview.md new file mode 100644 index 0000000..9b24b57 --- /dev/null +++ b/docs/_archive/pre-docs-cleanup-2026-09-11/docs/architecture/overview.md @@ -0,0 +1,93 @@ +# Application Architecture Overview + +## Purpose + +Define the target architecture direction for Jobjakt. + +This document does not require an immediate rewrite. + +The goal is incremental improvement while keeping the application functional. + +--- + +# Product Architecture + +The application consists of: + +## Core Platform + +Job Tracking + +↓ + +Applications + +↓ + +Communication + +↓ + +Follow-ups + + +--- + +## Supporting Platform + +Career Workspace + +↓ + +Master Career Profile + +↓ + +CV Builder + +↓ + +Documents + + +--- + +## Intelligence Layer + +AI Services + +↓ + +Recommendations + +↓ + +Generation + +--- + +# Architecture Goals + +The system should be: + +- Maintainable. +- Testable. +- Extensible. +- Secure. +- SaaS-ready. + +--- + +# Evolution Strategy + +Prefer: + +- Refactoring. +- Extraction. +- Clear boundaries. +- Incremental migrations. + +Avoid: + +- Large rewrites. +- Replacing working systems unnecessarily. \ No newline at end of file diff --git a/docs/architecture/principles.md b/docs/_archive/pre-docs-cleanup-2026-09-11/docs/architecture/principles.md similarity index 100% rename from docs/architecture/principles.md rename to docs/_archive/pre-docs-cleanup-2026-09-11/docs/architecture/principles.md diff --git a/docs/architecture/technical-debt.md b/docs/_archive/pre-docs-cleanup-2026-09-11/docs/architecture/technical-debt.md similarity index 100% rename from docs/architecture/technical-debt.md rename to docs/_archive/pre-docs-cleanup-2026-09-11/docs/architecture/technical-debt.md diff --git a/docs/audits/2026-09-07/README.md b/docs/_archive/pre-docs-cleanup-2026-09-11/docs/audits/2026-09-07/README.md similarity index 100% rename from docs/audits/2026-09-07/README.md rename to docs/_archive/pre-docs-cleanup-2026-09-11/docs/audits/2026-09-07/README.md diff --git a/docs/audits/2026-09-07/coverage.md b/docs/_archive/pre-docs-cleanup-2026-09-11/docs/audits/2026-09-07/coverage.md similarity index 100% rename from docs/audits/2026-09-07/coverage.md rename to docs/_archive/pre-docs-cleanup-2026-09-11/docs/audits/2026-09-07/coverage.md diff --git a/docs/audits/2026-09-07/findings.md b/docs/_archive/pre-docs-cleanup-2026-09-11/docs/audits/2026-09-07/findings.md similarity index 100% rename from docs/audits/2026-09-07/findings.md rename to docs/_archive/pre-docs-cleanup-2026-09-11/docs/audits/2026-09-07/findings.md diff --git a/docs/audits/2026-09-07/product-operations.md b/docs/_archive/pre-docs-cleanup-2026-09-11/docs/audits/2026-09-07/product-operations.md similarity index 100% rename from docs/audits/2026-09-07/product-operations.md rename to docs/_archive/pre-docs-cleanup-2026-09-11/docs/audits/2026-09-07/product-operations.md diff --git a/docs/audits/2026-09-07/resilience.md b/docs/_archive/pre-docs-cleanup-2026-09-11/docs/audits/2026-09-07/resilience.md similarity index 100% rename from docs/audits/2026-09-07/resilience.md rename to docs/_archive/pre-docs-cleanup-2026-09-11/docs/audits/2026-09-07/resilience.md diff --git a/docs/audits/2026-09-07/source-index.md b/docs/_archive/pre-docs-cleanup-2026-09-11/docs/audits/2026-09-07/source-index.md similarity index 100% rename from docs/audits/2026-09-07/source-index.md rename to docs/_archive/pre-docs-cleanup-2026-09-11/docs/audits/2026-09-07/source-index.md diff --git a/docs/audits/2026-09-07/threat-data.md b/docs/_archive/pre-docs-cleanup-2026-09-11/docs/audits/2026-09-07/threat-data.md similarity index 100% rename from docs/audits/2026-09-07/threat-data.md rename to docs/_archive/pre-docs-cleanup-2026-09-11/docs/audits/2026-09-07/threat-data.md diff --git a/docs/_archive/pre-docs-cleanup-2026-09-11/docs/audits/2026-09-07/validation/README.md b/docs/_archive/pre-docs-cleanup-2026-09-11/docs/audits/2026-09-07/validation/README.md new file mode 100644 index 0000000..2d25ef3 --- /dev/null +++ b/docs/_archive/pre-docs-cleanup-2026-09-11/docs/audits/2026-09-07/validation/README.md @@ -0,0 +1,228 @@ +# Independent validation of the Jobjakt audit + +Validated on 2026-09-07 against application commit `7d641edc7bca866ccf06012897ba1d107c07e59f`. This is a verification and planning pass. No application behavior, schema, deployment configuration or production data was changed. The original audit is preserved. + +**Subsequent implementation status, 2026-09-09:** [B01 HTTP import enforcement](b01-completion.md), [B02 account/session isolation](b02-completion.md), [B03 shared PDF admission](b03-completion.md), [B04 authenticated rate partitioning](b04-completion.md) and [B05 spreadsheet-safe CSV](b05-completion.md) are complete locally. B05 verification passed 902 selected backend tests, 298 frontend tests, 18 browser tests, nine B04 HTTP scenarios and the unchanged B03 admission probe. Release builds and targeted lint pass; the existing R01a TypeScript failure remains. The historical analysis below describes the pre-fix baseline. F05 is resolved locally under its defined export policy; F08/V01 and F06 are resolved locally. F11 remains open for B07/IMAP and deployment evidence. F03 admission is resolved locally, with R04 containment/capacity evidence still open. B06 was subsequently authorized; see the separate progress and remaining gates below. + +**B06 runtime follow-up, 2026-09-09:** [Report and operator handoff](b06-completion.md#runtime-verification-follow-up-2026-09-09). **Implementation complete; full runtime/production verification incomplete; F04 open.** Fresh 166 focused backend, 3 Compose, 6 Production HTTP and 9 B04 HTTP checks pass. Public DNS/valid HTTPS are now observed; direct ports 3000/5202/8080 are unreachable from this workstation, without proof of the rejecting layer. HTTP redirects to unreachable HTTPS port 444. Docker's inference socket failure still blocks actual Compose/Traefik/nginx tests; production peers, backend client IP and host/provider firewall remain unverified. No SSH alias/user was found in permitted non-secret sources. B01–B05 preserved; B07 not started. + +**Verdict: Suitable for private beta only.** Keep participation controlled, with informed testers, an operator recovery path and paid checkout disabled until its gates pass. This is not approval to expose the current import/PDF surfaces to unrestricted registration. The evidence does not establish a universal tenant authorization failure that would make all external use inappropriate, but it does establish account-switch corruption, silent lost updates and insufficient abuse containment. Those prevent a public free beta. Paid use adds unresolved billing/deletion and operational proof requirements. + +**Confidence in the original audit: 7/10.** It finds substantial real problems, provides useful source locations, and often labels unverified conditions honestly. Its 416-row coverage inventory is a map of evidence and open questions, not 416 completed validations. Severity is too broad for CSV, calendar and privileged-support concerns; the account-switch consequence is understated; the import address filter has a simpler bypass than the reported DNS race. Its implementation order puts recovery too late and puts identity isolation too close to a general UI cleanup. + +- [Validated implementation queue, ticket sizes, dependencies and handoff](implementation-queue.md) +- [Evidence, commands, limitations and reproduction instructions](evidence.md) +- [Independent browser/API observations](evidence/observations.json) +- [Independent .NET boundary observations](evidence/boundary-results.json) + +## Audit confidence and method + +Read the original README, findings, coverage, verification, resilience, product-operations, threat-data, source-index, `break-jobjakt.mjs`, retained break results and relevant `docs/todo/work.md` items. Inspected the implementation and tests behind the high-priority claims, rather than accepting their descriptions. The pasted expanded audit request provides background; the current validation-only instruction controls this pass. + +Fresh verification: **149 backend tests passed** in two disjoint targeted runs (70 + 79); **13 interview frontend tests passed**; standalone TypeScript failed with the exact original error. Independent probes used synthetic SQLite databases, real local HTTP endpoints, two browser tabs, the real PDF exporter with three inert child executables, and a real .NET host with a one-shot database fault. Existing original full-suite/build results are historical evidence, not new executions. No live Stripe, SMTP, DNS rebinding, production host, production database, external website or cloud metadata endpoint was contacted. + +The original harness is useful observational evidence, not a regression gate: recording a failed expectation does not itself make its exit code fail. Its simultaneous HTTP saves do not force both EF contexts to load the same revision. Its repeated POST does not establish an advertised idempotency contract. Its combined overflow/axe result conflates layout and accessibility. The independent probes add a deterministic two-context schedule, a UTC calendar control, actual cross-account saving, and process/fault boundaries. These probes also collect observations; implementation tickets must turn the relevant failures into assertions. + +## Every original P0/P1 finding + +There are **no original P0s and 11 original P1s**, including conditional P1s. None is omitted below. Classifications distinguish a proven implementation defect from its unverified production impact. No new P0 is supported. + +### F01 — stale saves and revision history + +**Confirmed. Retain P1.** The HTTP stale save returned 200 and replaced the newer state. Two independently preloaded real SQLite contexts both saved revision 1; the final row was Writer B/revision 2 and history was **[1, 2, 2]**. This is recoverable only in part: settings snapshots exist, but variant names are not included in those snapshots, and duplicate revision numbers make restore identification ambiguous. + +The root cause is correct: [SaveAsync](../../../../JobTrackerApi/Services/CvVariantService.cs#L108) increments an ordinary integer after reading, [SaveRequest](../../../../JobTrackerApi/Controllers/CvVariantController.cs#L34) supplies no expected revision, and [the model/index](../../../../JobTrackerApi/Data/JobTrackerContext.cs#L498) does not make the update conditional. A database transaction alone will not reject a stale client. Career Profile child/history writes already have a transaction; their missing precondition and the later legacy user projection are separate remaining problems. Job full-document updates likewise have no client revision precondition. + +**Corrected fix:** define a small revision contract and implement it aggregate by aggregate. Atomically compare expected revision and increment on update; make history insertion part of that transaction. Reconcile existing duplicate labels without discarding snapshots before adding uniqueness. Retain the draft on 409/412; do not automatically retry it over newer content. `StructuredCvProfile.version` is a document schema version, not a concurrency revision: introduce a distinct field. Include restore and every mutation capable of changing that aggregate; do not assume one PUT covers status patches, admin undo or generated drafts. + +**Acceptance:** the proposed tests are appropriate but require deterministic barriers/two contexts and both supported relational providers, not an in-memory provider or two loosely timed requests. Assert one winner, no partial children/projection/history, a preserved losing draft, unambiguous restore, delete-versus-save behavior and an older open frontend receiving a safe refusal. Do not promise transparent compatibility for old unconditional writers while claiming lost updates are prevented. Queue: D01–D04. + +### F02 — billing and account deletion + +**Mostly confirmed. Retain P1 for paid operation; provider effects need runtime verification.** [Checkout](../../../../JobTrackerApi/Controllers/BillingController.cs#L61) and [the Stripe gateway](../../../../JobTrackerApi/Services/StripeBillingGateway.cs#L14) create sessions without a durable purchase-attempt identity. Two requests can create two sessions; that is not proof of two charges, since completion still matters. The webhook verifies the signature, fetches current subscription state and checks ownership/price: ordinary sequential replay is already handled better than an event-order-only design. Identity's concurrency stamp also means some races fail and retry rather than blindly overwrite. + +The remaining cause is split subscription/role updates plus an account model that does not reconcile all relevant subscriptions. An active subscription and a different cancelled subscription for the same account cannot be resolved reliably by treating each event's subscription as the entire account truth. [Deletion](../../../../JobTrackerApi/Services/AccountDeletionService.cs) removes identity/billing IDs without a billing cancellation stage. Deletion admission is **disabled by default**; do not describe an active paid deletion incident as observed. + +**Corrected fix:** purchase-attempt idempotency, one explicit entitlement derivation, atomic local subscription/role state and retryable reconciliation. Start with a guarded synchronous handler and provider retries if that meets the contract. A new event inbox, broker or general outbox is not intrinsically required; durable receipt is required before acknowledging work that will happen asynchronously. Reuse existing deletion stages, preserve billing identifiers until resolution, and initially reject/defer deletion when active or uncertain billing cannot be resolved. This containment can precede full cancellation automation. + +**Acceptance:** existing billing tests cover configured price, rejected product ID, signed sequential lifecycle/replay and bad signature; they do not prove concurrent purchase/deletion correctness. Add crash-between-local-writes, two subscriptions, delayed conflicting events, retry after an unknown checkout result, pending checkout versus deletion, unavailable provider and verified cancellation tests. Follow with an authorized Stripe sandbox run. Queue: P01–P03. + +### F03 — PDF host exhaustion + +**Remediation update, 2026-09-08:** [B03](b03-completion.md) closes shared admission and normal failure/cancellation lifecycle defects. Default one active render per API process, one per document owner, zero queue, validated configuration and safe 503/504 behavior now have permanent before/after and multi-route regression evidence. The original finding below remains historical; F03 is only partially resolved until R04 proves deployment containment and capacity. + +**Confirmed missing admission control. Retain P1 for an untrusted public audience.** The singleton [exporter](../../../../JobTrackerApi/Services/PlaywrightCvPdfExporter.cs#L37) starts one child process per call. Three calls through that actual implementation started three inert child processes while all were held open. A per-process 60-second timeout bounds duration, not total concurrency. The authenticated variant export has no PDF rate policy. Other PDF endpoints share this exporter, so endpoint-only patches leave bypasses. + +Root cause is shared resource admission and lifecycle, not a proven Chromium exploit. Root container execution and sandbox-disabled flags increase containment concerns but do not establish RCE. **Corrected fix:** a small configurable shared concurrency bound, bounded or zero waiting, cancellation/cleanup, owner burst limits and host resource limits. Preserve legitimate downloads and distinguish admission denial from renderer failure. Measure before splitting the renderer into another service; no durable queue is required for request-scoped exports. Per-process admission is sufficient only for the current process topology; account for additional API replicas explicitly. + +**Acceptance:** retain the proposed fake-process test, cover all exporter entry points, cancellation, timeout and partial output cleanup; then a small controlled host test measures RSS/process count with container limits. The three-process probe proves missing serialization, not an exhaustion threshold or safe production capacity. Queue: B03, R04. + +### F04 — forwarded headers and direct ingress + +**Needs runtime verification. Retain conditional P1.** [Compose](../../../../docker-compose.yml#L140) publishes nginx on host port 3000; [nginx](../../../../job-tracker-ui/nginx.conf.template#L64) forwards incoming forwarding headers while the backend trusts the nginx network. This is a real unsafe assumption if that path is externally reachable without the expected sanitizing proxy. A canonical Host header is not caller authentication. + +The original root cause and qualified recommendation are sound. **Do not assert that the deployed firewall is bypassed:** its rules/topology were not inspected. Verify both the direct and normal routes, IPv4/IPv6 and effective backend client IP. Then close the direct route or sanitize at the correct trusted hop. Loopback binding is appropriate only if the upstream proxy is local. Avoid a speculative proxy redesign. The proposed isolated topology test is necessary, followed by operator evidence from the actual host. Queue: B06; it gates exposed release, not local source work. + +**Subsequent B06 status:** [Peer enforcement/configuration changes](b06-completion.md) are implemented and native API regressions pass. F04 is **not resolved**: real nginx/container execution is blocked and actual host/proxy evidence is still required. Historical findings above remain preserved. + +### F05 — spreadsheet formula injection + +**Confirmed. Downgrade P1 → P2.** A harmless title `=1+1` remains a formula in CSV. [Esc](../../../../JobTrackerApi/Controllers/ExportController.cs#L78) performs CSV syntax quoting, which does not change spreadsheet cell interpretation. Opening attacker-influenced imported content in a spreadsheet is a plausible additional user step, but no command execution, credential disclosure or server compromise was demonstrated. The current evidence does not justify a blanket P1. + +The root cause and small fix are correct. Use one text-field policy, preserve JSON/raw portability, distinguish real numeric fields and verify leading whitespace/control characters and delimiters through a CSV parser. A huge generic export framework is unnecessary. The proposed acceptance needs a harmless manual spreadsheet check because parser round-trip alone does not establish spreadsheet interpretation. Fix early because it is cheap and protects a shipped export; it is not an independent reason to prohibit all external use. Queue: B05. + +**Subsequent resolution, 2026-09-09:** [B05 completion](b05-completion.md) preserves this historical finding and closes the reproduced export failure locally. Actual Excel before/after changes `=1+1` from an evaluated formula to apostrophe-prefixed literal text; all 13 text columns share the tested policy. Typed cells and stored/JSON data remain unchanged. UTF-8 BOM plus byte-preserving browser download fixes observed Norwegian decoding. LibreOffice runtime and arbitrary spreadsheet reprocessing are not claimed verified. + +### F10 — aggregate storage and resource growth + +**Mostly confirmed. Retain P1 for storage/admission exposure; split off P2 duplicate-intent and unmeasured scale work.** [Attachment admission](../../../../JobTrackerApi/Controllers/AttachmentsController.cs#L269) sums current rows before staging/writing; concurrent uploads can both pass. It omits generated exports/artifacts/package copies. [Account export](../../../../JobTrackerApi/Services/AccountDataExportService.cs#L42) streams the ZIP file but materializes collections and serializes full JSON byte arrays. It is incorrect to call the entire ZIP an in-memory buffer; peak per-table memory remains a real concern. + +No production disk exhaustion or 100k-row memory threshold was measured. Two job POSTs with the same unrecognized `Idempotency-Key` produced different IDs. That proves no implemented key contract, not an HTTP violation or a need for unique company/title jobs. Repeated applications can be legitimate. Idempotency belongs to a specific creation intent and does not replace quotas. + +**Corrected fix:** hard admission bounds first; reuse the existing [owned-file inventory](../../../../JobTrackerApi/Services/AccountOwnedFileInventory.cs) for accounting/reconciliation. Use a database-safe owner reservation, not a process-local quota lock. Include crash/release handling and generated categories. Stream/batch large exports when measurements require it. Do not silently prune user history to meet a new limit. Separate job creation retry semantics from this resource ticket and from F01. + +**Acceptance:** simultaneous near-limit reservations, file/DB failure cleanup, generated artifacts and multi-instance accounting; synthetic bounded-memory exports; existing over-quota accounts remain readable/exportable. Same-intent/different-payload/retry tests belong to D04. Do not require every query to be redesigned before safe initial bounds ship. Queue: D04–D07; PDF overlap belongs only to B03. + +### F11 — outbound URL boundary + +**Remediation status:** HTTP B01/V02 resolved in the working tree on 2026-09-08; [connection-time regression evidence and limitations](b01-completion.md). Overall F11 remains partially remediated because IMAP B07 is unstarted. The following finding/evidence is preserved from the original validation. + +**Confirmed boundary defect; retain P1. Production reachability is unverified.** In addition to the source-confirmed validation/connection DNS gap, the real [import validator](../../../../JobTrackerApi/Services/JobImport/JobImportService.cs#L199) accepts IPv4-mapped IPv6 representations of denied IPv4 addresses. A fake HTTP handler was invoked for mapped private, link-local and metadata addresses, while ordinary IPv4 representations were blocked. No network socket was opened by that probe. This bypass does not need changing DNS answers. + +The original explanation is incomplete: normalize mapped addresses before classification, and bind address validation to the address actually connected. The registered HTTP handler resolves separately after validation. IMAP also validates then calls `ConnectAsync(host, ...)`; it **already normalizes mapped IPv4** in its guard, so do not attribute the import-specific normalization bug to IMAP. It still needs connection-time enforcement. + +**Corrected fix:** protect the actual socket boundary while retaining hostname/TLS validation, redirect rejection, size limits and cancellation. A proven restrictive egress layer can satisfy the boundary, but none was verified here. Reuse a narrow address policy only where the policies are genuinely the same; do not build a universal network platform or disable TLS checks. The original proposed mixed/mapped/DNS/TLS acceptance matrix is good, but it was a proposed test, not proof that the existing mapped check worked. Queue: B01 HTTP, B07 IMAP. + +### F12 — backup/restore and rollback + +**Mostly confirmed. Retain P1 as a release/recovery gate.** The [daily runner](../../../../JobTrackerApi/Services/DatabaseBackupRunner.cs#L68) uses SQLite `VACUUM INTO`, a consistent database snapshot. The pre-deploy [live-volume tar](../../../../deploy/deploy.sh#L314) is not equivalent; concurrently changing DB/WAL/files have no demonstrated common recovery point. MariaDB's transactional SQL dump does not make separate file archives transactionally consistent. No corrupt backup was reproduced or alleged. + +The failure trap [restores previous images](../../../../deploy/deploy.sh#L638), not the old database schema. This is safe for some additive changes and unsafe for incompatible migrations. Do not disable every image rollback indiscriminately. **Corrected fix:** verified recovery sets, a deliberate file checkpoint/write pause, checksums and an isolated restore drill; attach schema compatibility to rollback decisions. Keep keys and deletion tombstones recoverable with appropriate independent custody. Reuse existing backup machinery. + +**Acceptance:** restore while the source has synthetic writes, validate integrity/ownership/files/decryption/tombstone replay, kill incomplete backups and prove compatibility with the previous binary. The original tests are sufficient in intent but unexecuted for deployment recovery. Run this **before the first new schema migration**, not after concurrency, billing and quota migrations. Queue: R02–R03. + +### F13 — privileged support, auditing and retention + +**Partially confirmed. Downgrade engineering finding P1 → P2.** [AdminAuditController](../../../../JobTrackerApi/Controllers/AdminAuditController.cs#L12) is explicitly Admin-authorized. Cross-owner activity and undo are deliberate admin capabilities; they are not a normal-user tenant escape. Its returned private event fields and absence of a dedicated security event ledger support minimization/audit work. Infrastructure root access and absence of a support/impersonation role are not vulnerabilities by themselves. + +The finding combines three different things: admin product permissions, accountable security mutations, and owner/legal retention decisions. **Corrected fix:** document the existing admin boundary, minimize fields where they are unnecessary, add safe actor/action/target/outcome events for high-impact changes, and use recent authentication where warranted. Do not invent impersonation, a role hierarchy or tamper-proof logging claims. A restricted support role is justified only by an actual support use case. + +**Acceptance:** unauthorized verbs remain denied, intentionally authorized admin verbs remain functional, privilege revocation takes effect, and audit metadata excludes arbitrary content/secrets. Off-host audit custody and policy approval require operator evidence; unit tests cannot certify them. Retention/deletion policy remains a public/paid operational prerequisite without converting every suggested admin improvement into P1. Queue: W04 and P03 policy dependency. + +### F20 — AI worker failure containment + +**Confirmed when enabled. Retain conditional P1.** A real host running the actual [worker](../../../../JobTrackerApi/Services/AiOperationQueue.cs#L254) with concurrency 1 stopped after a single injected database timeout. With concurrency 2, one loop failed but the host and combined task did not stop during the observation window: `Task.WhenAll` waits on the surviving loop. The audit misses this second mode, where capacity silently shrinks rather than immediately stopping the host. + +The cause is failure escaping the loop boundary, not simply the host exception policy. Compose defaults this worker **off**, so a default deployment outage is not established. **Corrected fix:** catch/classify at the loop, bounded retry/backoff, explicit degraded health and lease recovery. Do not globally ignore background-service exceptions or hide a dead worker behind a healthy API. + +**Acceptance:** one- and multi-worker tests, claim/owner/result persistence failures, cancellation, recovery and lease expiry, followed by bounded outage drills. The local probe confirms host/worker behavior, not endpoint availability during a deployed DB outage or eventual recovery. Keep the feature disabled until those acceptance tests pass. Queue: R05. + +### F21 — calendar/timezone + +**Mostly confirmed. Downgrade P1 → P2 for the currently implemented workflow.** Offset-bearing input lost offset information after SQLite reload; the reproduced spring value exported 09:00Z instead of 07:00Z, and the two autumn offsets collapsed. However, the **UTC input control preserved 07:00Z**. This is not evidence that every production UTC event shifts. Server timezone affects DateTime parsing; production timezone/provider behavior was not exercised. + +More fundamentally, the normal job editor and Interview Prep send **date-only follow-up values**. [Calendar conversion](../../../../JobTrackerApi/Controllers/ApplicationCalendarController.cs#L129) turns an unspecified date into a UTC timed event. The independent date-only control became midnightZ, which can display on a different local date. The root cause is a missing date-versus-instant contract, not a universal need for named-zone recurrence. + +**Corrected fix:** preserve the current date-only product semantics and emit all-day follow-up/deadline events in ICS and provider links. Specify UTC normalization for any genuinely timed API field. Do not infer historical timezones or shift stored dates globally. A timed appointment/recurrence feature needs its own product scope and evidence. Current impact is workflow correctness; P1 would need evidence that a promised timed scheduling flow is routinely losing important appointments. + +**Acceptance:** date-only all-day values and exclusive end dates, UTC control, offset cases, both providers and server timezone permutations. The original offset tests are valuable but insufficient without the date-only control. Queue: W01, with no broad timestamp migration assumed. + +## New findings and severity increases + +### V01 — account A's open form can overwrite account B (F08 raised to P1) + +**Remediation status:** B02/F08/V01 resolved in the working tree on 2026-09-08. [Server session precondition, frontend isolation, before/after regression evidence and rollout limits](b02-completion.md). The original finding below is preserved as historical evidence. + +**Confirmed with real UI and HTTP persistence.** Tab A loaded account A's Career Profile and edited its full name. Tab B used the actual sign-out menu and signed into B. Tab A remained mounted; clicking its Save changes button returned **200**. An independent API session read the A draft in **B's stored profile**, while A's stored profile remained unchanged. + +[auth.ts](../../../../job-tracker-ui/src/auth.ts) broadcasts same-window events; [App](../../../../job-tracker-ui/src/App.tsx#L248) does not synchronize the authentication boundary across tabs. [api.ts](../../../../job-tracker-ui/src/api.ts) uses shared current cookies/CSRF state. [CareerProfilePage](../../../../job-tracker-ui/src/views/CareerProfilePage.tsx#L855) sends its retained local form to an endpoint whose owner is implicit in those cookies. Backend ownership checks correctly resolve B; the client submits A's content as B. This is not a direct-object-reference authorization bypass, and it is more serious than private text staying visible after logout. + +**Required scope:** one account/session generation boundary; promptly clear/unmount private owner state on logout/account change, cancel or ignore late responses, and refuse dispatch from a form loaded under a different identity. Include a server-checked expected-owner/session precondition where needed to close the cookie-switch race; it can only reject a mismatch, never grant authority. Reading the latest localStorage identity at send time defeats the purpose: bind to the identity that loaded the form. Revision numbers alone do not solve this because A and B can have equal revisions. Tests must repeat this exact two-tab save and assert both accounts' stored profiles, not just navigation. Queue: B02. + +### V02 — mapped-address import bypass (F11 strengthened) + +The new address-normalization proof above removes DNS timing as a prerequisite for passing the application guard. Track it under F11/B01, not as a second duplicate SSRF implementation project. F20's multi-worker failure mode likewise extends its existing ticket. No additional independent P0/P1 is claimed merely because an area lacks complete test coverage. + +## Other highlighted findings and negative evidence + +| Area | Independent conclusion | +|---|---| +| F06 account/IP rate limiting | **Confirmed P2; resolved locally by [B04](b04-completion.md), 2026-09-08.** Historical behavior: A's two exports made B's first return 429 because rate limiting preceded authentication. Authentication and endpoint authorization now resolve the final trusted resource principal before account partitioning; permanent real HTTP tests prove A/B independence, shared same-account sessions, invalid/forged credential handling and preserved IP policies. Deployment proxy reachability remains B06. | +| F09 draft recovery | **Partially confirmed P2.** The original user accepted an unload warning and refreshed while offline. Lack of durable recovery is real, but this is not a violated guaranteed-save contract. Preserve current dirty-state warnings; do not introduce persistent private browser drafts without an ownership/retention decision. W05 follows identity and revision work. | +| F14 accessibility | **Confirmed P2, with a structural release acceptance gap.** Shared contrast defects and a responsive preview scrolling defect reproduce; see the focused review below. Axe's `serious` label is not automatically product severity P1. | +| F15 export/cache/CSP | Owner checks and recent-auth account export are present. Missing no-store headers are hardening evidence, not a demonstrated cross-user CDN cache disclosure. Review sensitive responses and preview-compatible CSP without conflating it with V01. W03. | +| F17 TypeScript | **Confirmed P2 CI gap**, test-only diagnostic; exact explanation below. R01. | +| F19 provider fallback | Source confirms silent SQLite fallback for a missing connection or unknown provider. Keep P2 with a release-startup gate: production misconfiguration should fail closed. No proof the deployed instance points at the wrong database. R01. | +| Tenant/document authorization | Fresh owner-boundary checks denied B's access to A's variant/preview/PDF. Version listing returns **200 with []**, which reveals no other-owner history and is not an IDOR. Acceptance should require no data/no effects rather than insist every safe empty listing be 404. Query filters and explicit owner checks remain important positive evidence. | +| Auth, admin, entitlement | Fresh authorization/session/role/identity tests passed. The Pro handler re-reads user roles/AI preference, rather than relying only on stale JWT entitlement claims. No newly demonstrated ordinary-user admin access or session-revocation bypass. Admin content access remains intentional and needs policy/minimization, not an automatic security rewrite. | +| AI and uploads | Privacy/ownership tests passed; AI output remains untrusted and must stay inside existing structured validation and review boundaries. Existing parser/file staging/type/size/owner checks constrain the obvious paths. No new cross-owner document download, executable upload or model-driven privileged action was demonstrated. Resource admission and URL boundaries remain the evidenced problems. | +| Migrations, secrets and deployment | Fresh migration-chain tests passed; no new destructive migration/data-drop defect was reproduced. They do not prove rolling back old binaries or restoring files/keys. The tracked-secret scanner and lock files are useful but do not certify Git history, deployed secrets or current dependency advisories. External/host verification remains open; no secrets or live environments were inspected. | + +## TypeScript and production release gates + +Exact fresh error: `src/interview-prep.test.tsx(120,10): error TS2571: Object is of type 'unknown'.` + +The [test](../../../../job-tracker-ui/src/interview-prep.test.tsx#L120) reads `.content` from `mockedApi.post.mock.calls[0][1]`; the generic Axios request body is `unknown` at that boundary. The test's earlier runtime matcher does not narrow that later expression for TypeScript. All 13 tests in the suite passed under Babel/Jest, which transpiles TypeScript without performing this static check. The installed Next `runTypeCheck.js` explicitly filters `.test`, `.spec`, `__tests__` and `__mocks__` diagnostics; Next build success is therefore consistent with standalone tsc failure. + +The [CI workflow](../../../../.gitea/workflows/ci-deploy.yml) runs frontend tests/lint/build/browser checks without an explicit standalone `tsc --noEmit` gate. This is a real configuration gap. The diagnostic is in a test and does not demonstrate a broken emitted production component. It would not alone justify an emergency production rollback or P1 incident, but the next planned release should have a clean explicit type gate. Correct the test's typed payload boundary narrowly; do not disable strictness, exclude all tests or use an unrelated framework migration. No fix was made in this pass. + +## Accessibility conclusions + +The [normal browser accessibility suite](../../../../job-tracker-ui/e2e/accessibility.spec.ts) checks `/`, `/login`, `/dashboard`, `/jobs`, `/kanban`, `/career`, `/settings` and `/admin/system`. It forces English and waits primarily for `body`/`main` visibility. It does not ensure loaded content or open interactive states, and does not systematically cover dark mode or Bokmål. + +Missing important coverage includes `/career/builder`, `/career/builder/:id`, actual profile/notes states, job detail/edit and interview interactions, public CV, registration/reset/second-factor flows, admin users/audit, menus/dialogs, errors/conflicts and mobile preview/edit transitions. A green suite is valid evidence for its sampled states, not accessibility certification for the product. + +The fresh loaded-builder probe found light-mode success chips at **3.29:1** (`#16a34a` on white), including Saved and ATS chips. Dark primary text appears at **3.50–3.66:1** on the two dark surfaces. [theme.ts](../../../../job-tracker-ui/src/theme.ts) reuses the accent as primary text across themes; outlined buttons/tabs/chips inherit these values. This proves a shared token/component relationship. Fix semantic foreground/background pairs and relevant MUI state overrides centrally, then verify existing surfaces; do not patch each page's color or add a new design-system framework. + +The preview's scroll region also fails `scrollable-region-focusable` in the mobile-to-desktop transition. It is not present in every initial viewport/theme sample. The offending overflow region contains the rendered page iframe and has no explicit focusable region contract; [CvBuilderEditor:480](../../../../job-tracker-ui/src/views/CvBuilderEditor.tsx#L480) is the corresponding preview container. Repair named region/focus/keyboard scrolling and verify actual behavior, rather than adding tabindex to every Box. The editor controls already include keyboard-capable template cards; do not label every custom card inaccessible without testing it. + +Before public release, manually complete the primary tracking/profile/builder flow using keyboard only and NVDA or Narrator, including preview scroll, tab order, focus return, error/conflict announcements and reauthentication. Test 200%/400% zoom and 320 CSS-pixel reflow, text spacing, Windows forced colors, reduced motion, touch/keyboard alternatives to drag-and-drop, and real mobile keyboard overlap. No screen-reader/manual accessibility certification was performed here. Structural keyboard and focus failures take precedence over polish. Queue: A01–A03. + +## Shared root causes, without a generic overhaul + +1. **Record preconditions:** CV, job and Career Profile updates lack a client/server revision contract. Share the convention and error semantics; keep aggregate-specific transactions and conflict UI. Do not confuse this with schema version numbers. +2. **Identity lifecycle:** cross-tab cookies, retained forms and late responses have no common account-generation boundary. Fix this before adding persistent drafts or rebuilding editors. It is not fixed by per-record revisions. +3. **Admission before side effects:** PDF launch, file quotas and owner rate policies do not consistently enforce limits at the boundary that consumes the resource. Reuse existing services, with a shared exporter bound and database-safe storage reservations. An idempotency key limits repeated intent; it is not a resource quota. +4. **Validated target versus connected target:** import and IMAP share a time-of-check/time-of-use boundary, while only import misses mapped-address normalization. Share only the proven common policy/connection principle. +5. **Split local/provider state:** billing/deletion and profile legacy projection need explicit commit/recovery boundaries. They do not all require a new event bus. The profile projection can usually join its transaction; external billing cannot. +6. **Shared visual semantics:** theme tokens create cross-page contrast failures; preview focus is a local structural component issue. Fix these before changing page layouts. +7. **Proof at the wrong layer:** helper tests do not prove middleware order, a green shell does not prove loaded accessibility, a tar file does not prove restore, and a green build does not prove the standalone TypeScript program. Add the missing boundary tests rather than expanding every suite indiscriminately. + +## Confirmed launch blockers and release levels + +For a **public free beta**, close V01/F08 identity corruption, F01 lost writes, F11 outbound target enforcement, F03 PDF admission and the concrete F10 storage admission gaps. Establish a verified recovery set/compatible rollback and confirm actual ingress/configuration safety. Resolve primary-flow structural accessibility failures and retain manual evidence. These are correctness/security/recovery gates; a broad UX overhaul is not required. + +For a **limited paid beta**, additionally prove F02 purchase/entitlement/deletion handling in sandbox, a workable retention/deletion/support policy, verified key/data recovery and functioning enabled premium features. F20 blocks enabling its worker until containment/recovery tests pass. Keeping paid deletion disabled is not a complete long-term paid-account policy. + +For a **public paid launch**, complete representative end-to-end paid journeys, accessibility/manual verification, support and recovery drills, measured capacity at the intended exposure, and production ingress/secrets/release evidence. Exact public host, Stripe, mail, off-site backup, advisory and operator controls remain unverified. Their absence from this checkout is a missing release proof, not proof that deployed controls do not exist. + +The rejected stronger verdict is public paid launch readiness today. Conversely, the evidence does not support demanding a new broker, renderer microservice, role platform, PWA, total page redesign or 100k-record optimization as prerequisites to a controlled beta. + +## Decisions Required From Connor + +These decisions can be supplied when the dependent ticket starts; none is needed to read or approve this validation. + +| Decision | Why the repository cannot decide it | Proposed default / dependency | +|---|---|---| +| Paid deletion timing | Whether deletion cancels paid service immediately or keeps access until period end changes provider calls, messaging and retention timing. | Immediate cancellation only on explicit delete confirmation; never erase billing identity before verified resolution. Decide before P03. | +| Recovery objectives and custody | Acceptable data-loss window, recovery time and who can recover encryption keys require an operator/business commitment. | Use 24-hour RPO and 4-hour RTO as proposed drill targets, not measured promises; approve or replace before R03. | +| Private device-local drafts | Persisting CV/profile content on shared devices is a privacy/product choice. | Keep memory-only drafts plus dirty-state warnings until an explicit opt-in, owner scope and expiry policy are approved. W05. | +| Version retention/over-quota promises | Pruning historical CV/profile versions changes users' restore expectations and paid-plan promises. | Preserve existing history; reject further growth with a recoverable explanation before silently pruning. Decide before D07 expands retention. | +| Operator content access and retention | Which support actions actually need private document/event content and how long account/export/deletion records remain are policy decisions. | Retain current admin-only boundary, minimize unnecessary content, add no impersonation role. W04/P03. | + +Do not ask whether follow-ups should retain their existing date-only behavior, whether a concurrency token is needed, or whether to add a type gate: the repository/evidence answer those engineering questions. New timed scheduling is a separate future product request. + +## Recommended implementation order and first handoff + +Use the exact ticket order and permitted parallel branches in [implementation-queue.md](implementation-queue.md). The essential order is: **contain security/identity/resource defects → establish release and recovery proof → introduce data-integrity migrations → correct paid lifecycle → accessibility foundations → workflow corrections → broader UX and measured operations**. Independent theme/type tests may run earlier; they do not authorize skipping a dependency. + +The original first handoff was **B01 only: HTTP import target enforcement and its regression tests**. B01–B05 are now complete locally; see the linked completion records above. **B06 verify and close ingress bypass path** was subsequently authorized; its implementation is present, with runtime/host gates still pending. Do not start B07. Completing B01 does not close F11's separately scoped IMAP/deployment work; B03 does not close F03's R04 containment/capacity work; B04 does not close B06's deployment ingress proof. + +## Stop conditions + +- Do not close a finding from source inspection alone when its acceptance requires a socket, two contexts, browser tabs, provider sandbox or restored database. +- Do not start schema-changing tickets until R02/R03 produce a restorable set and an explicit old/new binary compatibility decision. Never delete duplicate CV snapshots merely to make an index migration pass. +- Do not enable open registration/exposed costly features while their boundary/admission findings remain open. Keep AI workers and paid lifecycle operations gated until their own tests pass. +- Do not begin page redesign or persistent draft work while shared identity, revision or accessibility primitives it depends on are unresolved. +- Stop a rollout on owner-state contamination, silent conflict overwrite, unexpected entitlement divergence, unresolved billing before identity deletion, failed restore/integrity, unbounded resource admission, or primary-flow keyboard regression. Preserve evidence and existing data; use the rehearsed compatible rollback or forward repair. +- The validation itself authorized no implementation. Subsequent explicit instructions authorized B01–B05, now complete locally. B06 was subsequently authorized and implemented with verification gates still pending. Stop before B07 or any other ticket without a further instruction. diff --git a/docs/audits/2026-09-07/validation/b01-completion.md b/docs/_archive/pre-docs-cleanup-2026-09-11/docs/audits/2026-09-07/validation/b01-completion.md similarity index 100% rename from docs/audits/2026-09-07/validation/b01-completion.md rename to docs/_archive/pre-docs-cleanup-2026-09-11/docs/audits/2026-09-07/validation/b01-completion.md diff --git a/docs/audits/2026-09-07/validation/b02-completion.md b/docs/_archive/pre-docs-cleanup-2026-09-11/docs/audits/2026-09-07/validation/b02-completion.md similarity index 100% rename from docs/audits/2026-09-07/validation/b02-completion.md rename to docs/_archive/pre-docs-cleanup-2026-09-11/docs/audits/2026-09-07/validation/b02-completion.md diff --git a/docs/audits/2026-09-07/validation/b03-completion.md b/docs/_archive/pre-docs-cleanup-2026-09-11/docs/audits/2026-09-07/validation/b03-completion.md similarity index 100% rename from docs/audits/2026-09-07/validation/b03-completion.md rename to docs/_archive/pre-docs-cleanup-2026-09-11/docs/audits/2026-09-07/validation/b03-completion.md diff --git a/docs/audits/2026-09-07/validation/b04-completion.md b/docs/_archive/pre-docs-cleanup-2026-09-11/docs/audits/2026-09-07/validation/b04-completion.md similarity index 100% rename from docs/audits/2026-09-07/validation/b04-completion.md rename to docs/_archive/pre-docs-cleanup-2026-09-11/docs/audits/2026-09-07/validation/b04-completion.md diff --git a/docs/audits/2026-09-07/validation/b05-completion.md b/docs/_archive/pre-docs-cleanup-2026-09-11/docs/audits/2026-09-07/validation/b05-completion.md similarity index 100% rename from docs/audits/2026-09-07/validation/b05-completion.md rename to docs/_archive/pre-docs-cleanup-2026-09-11/docs/audits/2026-09-07/validation/b05-completion.md diff --git a/docs/_archive/pre-docs-cleanup-2026-09-11/docs/audits/2026-09-07/validation/b06-completion.md b/docs/_archive/pre-docs-cleanup-2026-09-11/docs/audits/2026-09-07/validation/b06-completion.md new file mode 100644 index 0000000..f58f2ee --- /dev/null +++ b/docs/_archive/pre-docs-cleanup-2026-09-11/docs/audits/2026-09-07/validation/b06-completion.md @@ -0,0 +1,404 @@ +# B06 — Ingress boundary implementation and verification record + +**Latest runtime follow-up:** [2026-09-09 observations and remaining gates](#runtime-verification-follow-up-2026-09-09). Public DNS/TLS and bounded direct-access observations are now available. Docker still cannot start; real container/Traefik verification remains blocked. **B06 implementation complete, but F04 remains open.** The initial implementation-pass record below is preserved chronologically; statements about no public requests/DNS/TLS apply to that earlier pass. + +Date: **2026-09-09**. **Repository/application changes implemented; full local acceptance and production verification are NOT complete.** B01–B05 regression coverage passes. B07 was not started. No deployment, host firewall change, schema change, external scan, image pull or Docker reset was performed. + +Two gates remain: (1) execute the real nginx/container topology tests with a working Docker engine and the intended image; (2) establish the deployed Traefik socket peers, forwarding configuration and host/provider network controls. **F04 remains open, conditional P1.** This file is a completion record for work performed, not a claim that the remaining gates passed. + +## Finding and independently established boundary + +The original [F04 validation](README.md#f04--forwarded-headers-and-direct-ingress) and [B04 deferral](b04-completion.md) remain historical evidence. The vulnerability is **not a production API host-port mapping**: rendered base Compose has no backend published port. It publishes frontend/nginx **host 3000 → container 80**, without a host-IP restriction. Nginx accepts the canonical Host and previously passed arbitrary incoming `X-Forwarded-For` / `X-Forwarded-Proto` to the API. The API legitimately trusts nginx's dedicated network, so bypassing the external sanitizing proxy could launder attacker-supplied headers through a trusted peer. A canonical Host is not proof of a trusted caller. + +Before changing application/configuration code, `docker compose --env-file .artifacts/b06/compose.env -f docker-compose.yml --profile bundled-ollama config --format json` confirmed this exposure using synthetic values. The new permanent effective-config test failed because production had no upstream-peer enforcement setting. Source tracing confirmed verbatim forwarding in all three nginx proxy locations. **No pre-fix live nginx exploit was reproduced in this environment**, because the Docker engine was unavailable. The optional `--baseline` topology probe can test HEAD's original nginx sources before these edits are committed. It is not a substitute for an immutable baseline after committing; original source excerpts/observations remain in the audit. + +Deployment evidence conflicts: `docs/architecture/deployment.md` asserted a firewall blocked port 3000; `deploy/README.md` claimed no frontend host mapping and container-direct Traefik routing; `sec-002-ingress-compose.md` retained a host-port compatibility dependency while also listing a contradictory “production ports absent” check. Current rendered configuration and deployment scripts take precedence. These claims are now corrected in active documentation. The old verification record is retained as historical evidence, not current proof. + +## Production-intended topology and actual configuration evidence + +```text +Internet → DNS/provider network/host firewall [unverified] + → operator-owned Traefik [TLS termination/routing/config unverified] + → Docker host TCP 3000 [current compatibility mapping] + → frontend nginx TCP 80 [new explicit socket-peer allowlist] + → dedicated internal web_proxy network + → ASP.NET API TCP 8080 [unpublished] +``` + +The repository establishes the expected contract, **not the observed current production topology**. No Traefik configuration, live `.env`, DNS, firewall rules or infrastructure credentials were available/used. A request for the real Traefik placement/observed peer addresses remains unanswered; “continue” was not treated as a network value or production approval. + +| Surface | Rendered production configuration | After B06 | Evidence level | +|---|---|---|---| +| Frontend nginx | TCP 3000 → 80, host IP unspecified; Docker normally publishes broadly, platform IPv6 behavior must be checked | Mapping preserved; application requests require configured Traefik socket peers, single client IP and HTTPS metadata | Mapping proven by Compose; nginx enforcement source/generation checked, runtime pending | +| API | `expose: 8080`, no `ports`; listens on container interfaces | Same exposure; explicit `ASPNETCORE_ENVIRONMENT=Production` | Proven in rendered configuration | +| `web_proxy` | Dedicated bridge, `internal: true`, configured `WEB_PROXY_SUBNET` (synthetic/default /29) | Only frontend/backend members permitted by preflight; API known network must match exactly | Proven in configuration; live membership pending | +| `jobtracker_shared` | External network; frontend and backend both join | Preserved for existing operator-managed service dependencies | Membership configured; actual other members/access need host inspection | +| Backend `default` / `ai_internal` | Backend also joins both; ai-service only `ai_internal` | Unchanged | Normal bridge networks are not air gaps or protection from a Docker administrator | +| AI 8001 / optional Ollama 11434 | No host mappings, including `bundled-ollama` profile | Unchanged; new preflight rejects publications | Proven in rendered configuration | +| Database | External MariaDB/MySQL or volume-backed SQLite; no DB service/port in this Compose | Unchanged | External DB listener/network requires operator verification | +| PDF renderer | API child process, no separate listening service | B03 admission unchanged | Source and regression tests | +| Development | Explicit `-f docker-compose.dev.yml`, frontend 3000, API 5202, optional Ollama 11434 | Explicit direct nginx mode, HTTP localhost, API forwarding trust disabled | Rendered and tested separately; cannot pass production preflight | + +Frontend and API are intended to share the canonical HTTPS origin; UI API base is `/api`. Preflight rejects a different production build-time API URL. Public 80/443 handling belongs to operator-owned Traefik and cannot be proved here. No cache/debug/control service is published by base Compose. `expose` is metadata, not a firewall. Host processes, attached containers, Docker administrators and routed/direct-container-IP configurations remain separate trust considerations. + +## Implementation and trust rules + +1. **Nginx original socket peer:** `INGRESS_MODE=proxy` is explicit in production. `INGRESS_PROXY_CIDRS` must list observed Traefik peers; there is no permissive default. The generated `geo $realip_remote_addr` gate checks the original socket peer, before allowing application locations. Untrusted peers receive **403**, including callers that know the canonical Host and forge every forwarding header. +2. **Normalized forwarding:** nginx `set_real_ip_from` uses that same explicit list; `real_ip_header X-Forwarded-For`, `real_ip_recursive off`. It sends `$remote_addr` as one normalized XFF value, canonical Host/port and canonical HTTPS scheme. It strips `X-Forwarded-Host` and RFC `Forwarded`; raw headers do not select an identity. All `/api/`, canonical `/health` and `/ready` proxy locations use this rule. Authentication/B02 headers pass through normally and retain their existing server checks. +3. **Malformed/extra metadata:** missing XFF, missing/nonexact `https`, or comma-separated XFF chains return **400** at nginx in proxy mode. An invalid single IP makes nginx realip retain the actual peer; only that peer's address is emitted, not the invalid supplied text. This safely charges the proxy peer rather than manufacturing a client partition; it also signals a broken upstream contract. This nginx fallback behavior still needs the runtime probe. ASP.NET's independent malformed/rightmost-hop behavior is tested and proven. Traefik must overwrite attacker headers, not append arbitrary chains. +4. **Development:** `INGRESS_MODE=direct` is permitted only with an HTTP canonical origin. It does not enable realip or trust incoming forwarding metadata; nginx forwards the actual socket IP and HTTP scheme. Production preflight rejects direct mode/Development environment. Local API-port convenience remains unchanged. +5. **Fail-safe configuration:** nginx rejects empty, broad/default-route or directive-shaped peer inputs and calls `nginx -t` before serving. The standard-library Python preflight validates the actual Compose JSON, requires narrow peer networks (private /24 or narrower IPv4; /64 or narrower IPv6; individual public proxy addresses), exact dedicated backend trust, same-origin HTTPS, Production environment, no host networking and no additional service publications. It rejects `DOTNET_ENVIRONMENT=Development` and automatic framework-forwarding enablement. Shell startup enforces CIDR characters/prefix bounds; nginx itself validates address syntax. Prefer individual /32 or /128 peers rather than trusting other containers in a subnet. +6. **API defence:** `ForwardedProxyConfiguration` still clears implicit known proxies/networks, trusts one configured hop, processes XFF/XFP only, and now rejects `/0`. Untrusted sockets—including mapped IPv4—cannot change effective IP/scheme. Existing `ExternalOrigin` still rejects noncanonical application Hosts in Production, allows only internal liveness exceptions, generates reset/verification/OAuth links from configuration and controls secure cookies. Forwarded Host is never enabled. `AllowedHosts=*` remains as a framework setting; the existing explicit canonical-host middleware supplies the actual Production host restriction. +7. **CORS correction:** Production previously defaulted to `http://localhost:3000` when no allowlist existed. It now defaults to the configured canonical origin; conflicting overrides/wildcards fail before listening. Credentialed requests remain allowed only for that origin. Development's configured allowlists remain usable. This is not a network-access control. + +B04 middleware ordering is unchanged: canonical host/proxy handling → existing CSRF → authentication → endpoint authorization → rate limiter → B02 session context → controller. Owner quotas remain principal-based; anonymous/security quotas use the effective socket/proxy client IP. B03 admission is still inside the shared exporter, independent of ingress or endpoint. B01 outbound transport and B05 CSV behavior were not changed. + +The host-port mapping is deliberately retained because the repository records a live dependency and does not reveal whether Traefik is host-local, containerized or remote. Blind loopback binding/removal could cause downtime. The new nginx peer gate is the repository's application-level closure for that compatibility listener. **If Docker/NAT makes trusted and untrusted traffic appear as the same peer, this gate is insufficient: do not allowlist that shared gateway.** Establish a verified binding/firewall or dedicated proxy route before rollout. This is a production stop condition, not a claim that a guessed CIDR fixes the host. + +## Headers, limits and health + +Nginx explicitly supplies `X-Content-Type-Options: nosniff`, `X-Frame-Options: DENY`, `Referrer-Policy: strict-origin-when-cross-origin` and restrictive camera/microphone/geolocation permissions on canonical responses. No new duplicate headers were added. Base API HTTPS redirection/HSTS are disabled because the external proxy owns TLS/redirects; actual Traefik HSTS/redirect/header policy is unverified. No proxy request-body or timeout tuning is configured in this nginx template: nginx defaults apply. B06 does not claim a special rate/WAF/body policy at Traefik. Existing application size/time limits and B03 admission remain; tuning broader limits is separate work. + +Container health requires no host API publication: backend curls `127.0.0.1:8080/health`; frontend checks `127.0.0.1:80/health`. The nginx default-host `/health` returns only static liveness and remains accessible even to untrusted peers. Canonical `/health` still proxies backend liveness; `/ready` remains canonical/proxy-gated. Default-host application paths are rejected. Health checks do not prove that the new trusted-proxy route is configured correctly, so the route smoke below is mandatory. + +## Permanent tests and local results + +| Check | Result / exact scope | +|---|---| +| `scripts/test-ingress.py` | **3 tests pass**, including real production/dev Compose rendering and 17 unsafe-config mutations; no daemon needed. Before config edits, safe-production test failed as expected. | +| `ForwardedIngressTests` plus existing origin/rate-key tests | **30 focused pass**; 13 new backend cases cover trusted/untrusted/mapped/IPv6 peers, scheme, host, malformed/rightmost chains and trust-all rejection. | +| `ingress-tests/production.spec.ts` | **6 real HTTP scenarios pass**: Production CORS/default/unsafe startup, Host/XFH, secure-cookie scheme independence, untrusted IP-limit forgery, trusted clients and excess-chain defence. | +| Broader backend | **915 pass**, no selected skips; includes B01–B05. One pre-existing external-DNS IMAP test excluded because internet access was not authorized. | +| Frontend | **298 tests / 67 suites pass**, including nginx-health, B02/B03 and B05 download regressions. | +| Browser | **18 pass**, no failures/flaky outcomes/skips; includes B02 switching and B05 actual download. | +| B04 HTTP / B03 admission probes | **9 / 1 pass** unchanged. | +| Build/static checks | API/test-project Release warnings-as-errors: **0 warnings/errors**. Next production build and targeted lint pass. Shell syntax checks, Python compilation and `git diff --check` pass. | +| Standalone TypeScript | Existing unrelated `src/interview-prep.test.tsx(120,10): error TS2571: Object is of type 'unknown'.` remains; exit **1**. No suppression/fix added. | +| Entrypoint generation | **9 local guard/render cases pass**, seven rejected configurations plus production and development generation. Temporary probe substitutes only fixture paths and disables `nginx -t`; explicitly **not nginx runtime proof**. | +| Real nginx/container topology | **BLOCKED before resource creation**. Docker Desktop engine pipe absent; attempted startup crashed on inaccessible `dockerInference` socket. No factory reset/repair, image pull or production request attempted. | + +The first native Production fixture run passed 2/4 but inherited the project's Development launch profile despite its environment setting. The fixture now uses `dotnet run --no-launch-profile`; all six final tests prove Production behavior. This was a fixture correction, not a relaxation of host/CORS assertions. Final results/hashes: [manifest](evidence/b06-test-results.json). Safe effective topology/guard observations: [observations](evidence/b06-observations.json). Raw local files remain in `.artifacts/b06`. + +The permanent `scripts/test-ingress-topology.py` uses only an already-installed nginx image, synthetic data, unique containers/networks and loopback-only ephemeral host publication. It exercises this repository's real nginx template/entrypoint and a header-echo backend, plus a simulated trusted proxy peer. It does **not** claim to run production Traefik or a containerized full API. API behavior is covered separately by the real native HTTP tests. The topology test asserts trusted IPv4/IPv6 forwarding, two clients, host/header normalization, unchanged B02 header transport, untrusted direct nginx denial, missing/bad scheme and chain rejection, safe malformed-IP fallback, no backend publication and denied edge-to-backend routing. It cleans up only resources it created. Its runtime result is still pending. + +### Commands run + +Repository root (PowerShell): + +```powershell +python scripts/test-ingress.py +docker compose --env-file .artifacts/b06/compose.env -f docker-compose.yml --profile bundled-ollama config --format json > .artifacts/b06/compose-after.json +Get-Content .artifacts/b06/compose-after.json -Raw | python scripts/validate-ingress.py +& C:/Users/Connor/.dotnet/dotnet.exe test JobTrackerApi.Tests/JobTrackerApi.Tests.csproj --configuration Release --no-restore --filter 'FullyQualifiedName~ForwardedIngressTests|FullyQualifiedName~ExternalOriginTests|FullyQualifiedName~RateLimitPartitionKeysTests' --logger 'trx;LogFileName=focused.trx' --results-directory .artifacts/b06 -- xUnit.parallelizeTestCollections=false xUnit.maxParallelThreads=1 +& C:/Users/Connor/.dotnet/dotnet.exe test JobTrackerApi.Tests/JobTrackerApi.Tests.csproj --configuration Release --no-restore --no-build --filter 'FullyQualifiedName!~ImapServiceSsrfGuardTests.ConnectAsync_rejects_unresolvable_host_without_leaking_dns_detail' --logger 'trx;LogFileName=backend.trx' --results-directory .artifacts/b06 -- xUnit.parallelizeTestCollections=false xUnit.maxParallelThreads=1 +& C:/Users/Connor/.dotnet/dotnet.exe build JobTrackerApi.Tests/JobTrackerApi.Tests.csproj --configuration Release --no-restore -warnaserror +& C:/Users/Connor/.cache/codex-runtimes/codex-primary-runtime/dependencies/native/git/usr/bin/sh.exe -n deploy/deploy.sh +& C:/Users/Connor/.cache/codex-runtimes/codex-primary-runtime/dependencies/native/git/usr/bin/sh.exe -n job-tracker-ui/configure-nginx-origin.sh +python -m py_compile scripts/validate-ingress.py scripts/test-ingress.py scripts/test-ingress-topology.py +python .artifacts/b06/render-probe.py +python scripts/test-ingress-topology.py # blocked: Docker engine unavailable +git diff --check +``` + +UI directory; API suites ran serially because all use port 5302: + +```powershell +$env:DOTNET_HOST_PATH='C:/Users/Connor/.dotnet/dotnet.exe' +$env:NEXT_TELEMETRY_DISABLED='1' +node node_modules/@playwright/test/cli.js test --config=playwright.ingress.config.ts --reporter=json > ../.artifacts/b06/ingress-api.json +node node_modules/@playwright/test/cli.js test --config=playwright.rate-limits.config.ts --reporter=json > ../.artifacts/b06/rate-limits.json +node node_modules/@playwright/test/cli.js test --config=playwright.pdf.config.ts --reporter=json > ../.artifacts/b06/pdf-admission.json +node node_modules/jest/bin/jest.js --runInBand --watchAll=false --json --outputFile=../.artifacts/b06/frontend.json +$env:PLAYWRIGHT_STATIC_EXPORT='true' +$env:PLAYWRIGHT_PREBUILT_EXPORT='false' +node node_modules/@playwright/test/cli.js test --config=../.artifacts/b06/playwright.offline.config.ts --reporter=json > ../.artifacts/b06/browser.json +$env:NEXT_PUBLIC_API_BASE_URL='/api' +node --max-old-space-size=4096 node_modules/next/dist/bin/next build +node node_modules/eslint/bin/eslint.js playwright.ingress.config.ts ingress-tests/production.spec.ts --max-warnings=0 +node node_modules/typescript/bin/tsc --noEmit --pretty false +``` + +The offline browser configuration was copied from the existing B02 fixture; external providers/workers were disabled. Its fresh test-targeted static build was subsequently replaced by the normal `/api` build. Neither was deployed. + +## Required deployment verification — exact safe procedure + +Run the container probe first on a **safe development/test Docker engine**, not the production host: + +```bash +python3 scripts/test-ingress.py +python3 scripts/test-ingress-topology.py --image nginx:1.29.8-alpine +# Image must already exist. The script never pulls it. Arrange any image acquisition separately. +``` + +On the documented Linux deployment host, use the existing checkout `/opt/job-tracker/app` and shared configuration `/opt/job-tracker/shared/.env`. The following commands are **read-only**; do not run `deploy.sh`, `up`, firewall mutation or restore commands as part of this inspection. Review outputs locally; never paste complete rendered environment variables or container environment/secrets. + +```bash +cd /opt/job-tracker/app +docker compose --env-file /opt/job-tracker/shared/.env -f docker-compose.yml --profile bundled-ollama config --format json | python3 scripts/validate-ingress.py +docker compose --env-file /opt/job-tracker/shared/.env -f docker-compose.yml ps +docker ps --format 'table {{.Names}}\t{{.Image}}\t{{.Ports}}' +sudo ss -lntp +if command -v nft >/dev/null; then sudo nft list ruleset; fi +if command -v iptables-save >/dev/null; then sudo iptables-save; fi +if command -v ip6tables-save >/dev/null; then sudo ip6tables-save; fi +if command -v ufw >/dev/null; then sudo ufw status verbose; fi +frontend_id=$(docker compose --env-file /opt/job-tracker/shared/.env -f docker-compose.yml ps -q frontend) +backend_id=$(docker compose --env-file /opt/job-tracker/shared/.env -f docker-compose.yml ps -q backend) +test -n "$frontend_id" && test -n "$backend_id" +docker inspect -f '{{json .NetworkSettings.Ports}} {{json .NetworkSettings.Networks}}' "$frontend_id" "$backend_id" +docker network inspect jobtracker_shared --format '{{json .IPAM.Config}} {{json .Containers}}' +docker network ls --filter label=com.docker.compose.network=web_proxy +docker exec "$frontend_id" nginx -T +docker exec "$backend_id" curl -fsS http://127.0.0.1:8080/health +docker exec "$frontend_id" wget -qO- http://127.0.0.1/health +``` + +Identify the actual Traefik service/process from `docker ps` / `ss`. If containerized, inspect its **mount paths and networks**, then inspect the identified configuration files locally; do not assume a filename or container name. If host-managed, inspect the identified systemd/service configuration. Confirm exact Host rule, TLS certificate/entrypoint, upstream host:3000 target, redirect behavior and whether `forwardedHeaders.insecure`/trusted upstream settings permit client forgery. Determine any real CDN/LB hop separately; it is not present in this repository. Inspect actual provider firewall/security-group settings as well as Docker's NAT/FORWARD rules; a UFW INPUT rule alone is not proof for Docker-published ports. + +Obtain the **actual socket source seen at nginx**, not a guessed Docker subnet. Where host tools permit, a bounded SYN-only capture in nginx's network namespace avoids capturing HTTP bodies/credentials: + +```bash +frontend_pid=$(docker inspect -f '{{.State.Pid}}' "$frontend_id") +test "$frontend_pid" -gt 0 +sudo nsenter -t "$frontend_pid" -n tcpdump -nn -i any -c 10 'tcp dst port 80 and (tcp[tcpflags] & tcp-syn != 0)' +``` + +Use that only while issuing explicitly controlled health/config requests through Traefik and from an untrusted test client. If `nsenter`/`tcpdump` is unavailable, use the operator's equivalent packet/flow evidence; do not install tools or change production networking implicitly. If both routes share the same observed NAT source, **stop**. Choose a topology-specific binding/firewall or dedicated proxy route with an explicit rollback before allowing that peer. Configure `INGRESS_PROXY_CIDRS` only after this evidence; a wrong list will deny legitimate ingress. Normal deployment preflight fails before service replacement if the setting is absent. + +After the change is separately approved and deployed, from an independently controlled **external test machine**, set values from verified DNS/host inventory, not guesses: + +```bash +public_host='the-verified-canonical-host' +public_ip='the-verified-host-address' +curl --max-time 10 -sS -D - -o /dev/null "https://$public_host/api/auth/config" +curl --max-time 10 -sS -D - -o /dev/null "https://$public_host/api/auth/config" \ + -H 'X-Forwarded-For: 198.51.100.10' -H 'X-Forwarded-Proto: http' \ + -H 'X-Forwarded-Host: attacker.example.test' +curl --max-time 5 -sS -D - -o /dev/null "http://$public_ip:3000/api/auth/config" \ + -H "Host: $public_host" -H 'X-Forwarded-For: 198.51.100.10' -H 'X-Forwarded-Proto: https' +curl --max-time 5 -sS -o /dev/null "http://$public_ip:5202/health" +curl --max-time 5 -sS -o /dev/null "http://$public_ip:8080/health" +curl --max-time 10 -sS -D - -o /dev/null "http://$public_host/api/auth/config" +``` + +Expected: canonical TLS path succeeds with appropriate headers; spoofing at the external edge is overwritten or rejected, not trusted; direct port-3000 application request is firewall-denied or nginx **403**, never successful; development/API listeners are unavailable unless a separately documented protected service owns that port; public HTTP redirects according to the verified Traefik policy. Repeat for every published A/AAAA address, with IPv6 bracket syntax and `curl -6`; do not infer IPv6 rules from IPv4 results. Verify certificate hostname/chain without `-k`. Match DNS to the inspected ingress. Do not probe unrelated services/ports or production login quotas. A config endpoint's 200 alone cannot prove client-IP integrity; inspect trusted forwarding/flow evidence or repeat the synthetic limiter checks in staging. Capture intended login/session/CSV/PDF smoke through the canonical route with a dedicated test account after normal deployment authorization. + +## Files, residual risk and stop conditions + +Application/config: `docker-compose.yml`, `docker-compose.dev.yml`, nginx template/entrypoint, `ForwardedProxyConfiguration.cs`, `Program.cs` CORS block, `.env.example`; preflight in `deploy/deploy.sh`, new `scripts/validate-ingress.py`, CI safety-script invocation. Tests: `ForwardedIngressTests.cs`, `scripts/test-ingress.py`, `scripts/test-ingress-topology.py`, `playwright.ingress.config.ts`, `ingress-tests/production.spec.ts`. Documentation: active deployment docs, this record, validation README/queue/evidence, TODO and B06 evidence files. Earlier uncommitted B01–B05 changes and unrelated `docs/redesign` remain untouched; the entire working-tree diff is not B06. + +No schema or data migration. Backup-only automation does not require the new ingress gate. A bad peer list or unexpected forwarding chain can break the intended route; preflight/syntax/liveness checks alone cannot validate the peer value. Preserve existing images/env and verify both routes before rollout. Rolling back nginx to the old behavior reopens the header-laundering assumption unless network isolation is independently enforced; rollback therefore requires verified firewall/binding protection. Do not bypass the gate with trust-all networks. + +Limits remain process-local after B04/B03; additional API instances multiply budgets as documented there. B06 introduces no distributed system or new hosting platform. Host/container compromise and shared-network lateral access are not eliminated by a peer allowlist. Privileged Docker users can attach containers regardless of service names. No claim is made about external DB/Ollama listeners, external TLS, firewall correctness, raw backend network reachability from other trusted containers, or an uninspected additional ingress route. + +**Local acceptance:** criteria 2/4/6/7/8/9 have repository/API/test evidence; backend publication checks for criterion 1 pass, but the nginx closure supporting criteria 1/3/5 still requires real runtime/topology verification. **Do not mark B06 locally complete or F04 resolved yet. Production verification is also incomplete.** The next action is to complete these B06 gates and supply the observed production proxy peers. **Next queued implementation ticket: B07, only after the required B06 decision/verification and a further instruction; not started.** + +## Runtime verification follow-up 2026-09-09 + +**Outcome: B06 implementation complete, but F04 remains open.** Implementation complete describes the existing application/configuration change; it does **not** mean full local-runtime acceptance or production verification passed. This follow-up changed evidence/documentation only. No application, Compose, nginx, B01–B05 behavior, credentials, DNS, certificate or production service was changed. B07 was not started. + +The user explicitly authorized targeted public DNS/TLS/direct-access checks in this follow-up. These were bounded unauthenticated GETs to the documented hostname and its observed A address, not production load tests. No login budgets, user data or credentials were used. Response bodies and cookies were discarded; retained headers are allowlisted. [Runtime evidence](evidence/b06-runtime-observations.json), [test/evidence manifest](evidence/b06-runtime-results.json) and [reproducible public probe](evidence/b06-runtime-public-probe.py.txt) preserve commands and observations. Raw local files are under `.artifacts/b06-runtime/`. + +### Actual execution and listener evidence + +The real production Compose file was rendered again, including the optional Ollama profile, with the existing synthetic `.artifacts/b06/compose.env`. The preflight passed. [Sanitized effective configuration](evidence/b06-runtime-compose.json) confirms frontend TCP **3000 → 80**, host address unspecified, and **zero published backend/AI/Ollama ports**. The internal `web_proxy` network contains frontend/backend in configuration; backend also joins `shared_services`, `default` and `ai_internal`. The synthetic `192.0.2.10/32` ingress value is a test input, **not an observed Traefik peer or a recommended production value**. + +The actual topology could not be started: + +- `docker desktop status` returned exit 1, unable to retrieve status. +- `docker desktop start --detach` returned exit 0 (startup dispatched), then Docker Desktop **4.86.0** reported the same fatal inference-manager socket failure supplied by Connor. This exit code is not engine readiness. +- A server-version request timed out at 20 seconds; a final bounded request timed out at 10 seconds. Earlier missing-pipe evidence remains preserved. +- The failing `C:/Users/Connor/AppData/Local/Docker/run/dockerInference` entry is zero bytes with `Archive, ReparsePoint` attributes, last modified 2026-08-24. Read-only `fsutil reparsepoint query` returned **Error 1920: The file cannot be accessed by the system**. This does not establish the underlying Docker/Windows defect or prove the entry is unused. +- A graceful `docker desktop stop` attempt did not complete; the waiting command was interrupted. Failed Desktop/backend processes remained observable. No socket, Docker data, WSL distribution or configuration was deleted/renamed/reset, and no process was force-killed to improvise a repair. +- The inspected **Windows workstation**, not the Ubuntu deployment host, had no listeners on 80, 443, 3000 or 5202. Its `127.0.0.1:8080` listener belonged to **steamwebhelper**, not Jobjakt. Windows firewall profiles were enabled; their default actions were `NotConfigured`. Neither observation establishes production firewall/NAT behavior. + +Consequently there is **no new running-container evidence** for `nginx -t`, actual host bindings, network membership, trusted-peer matching, direct nginx rejection, internal API reachability, container restarts or missing/invalid-variable startup. No actual peer CIDR was guessed. The existing echo-backend/simulated-peer nginx probe would be supplementary only; it cannot satisfy this request for **real Traefik + real application + actual Compose** and was not represented as such. + +### Observed public DNS, HTTPS and direct access + +Measurements were taken around **2026-09-09 10:43–10:47 UTC** from Connor's Windows workstation (`192.168.50.2`). Requests used `curl --noproxy '*'`, TLS verification enabled, connect timeout 4 seconds, total timeout 8 seconds and at most three simultaneous harmless GETs. Independence from the production LAN/NAT is **not established**; these observations must be repeated from a known external network. + +| Check | Observed result | What this proves / does not prove | +|---|---|---| +| Resolver plus both authoritative servers, `oaklyn.ns.cloudflare.com` and `thaddeus.ns.cloudflare.com` | `jobs.cesnimda.uk` CNAME → `cesnimda.uk`; A answer **109.189.21.248**; no AAAA answer (SOA/NODATA) | Consistent current answers for this hostname. No additional A/AAAA address was observed. Intended address ownership, other hostnames and complete provider-zone inventory remain operator checks. | +| `https://jobs.cesnimda.uk/api/auth/config` | **200**, valid hostname/chain; TLS 1.3 independently verified with Python's default trust context | Public HTTPS application path works. It does not identify Traefik, nginx's peer, deployed image or backend client IP. | +| Certificate | SAN/CN `jobs.cesnimda.uk`, Let's Encrypt YR2; valid **2026-08-16 20:35:58 UTC to 2026-11-14 20:35:57 UTC** | Hostname/chain validation succeeded without bypass flags. No separate revocation audit is claimed. | +| HTTP port 80 | **301** to `https://jobs.cesnimda.uk:444/api/auth/config` | An actual redirect differs from the documented standard HTTPS origin. | +| Observed redirect target, HTTPS port 444 | **Connection failure**, curl exit **7** | HTTP-to-HTTPS navigation is broken from this vantage point. Operator must inspect the real redirect entrypoint/NAT configuration and establish the intended reachable target. No certificate or production proxy change was attempted. | +| HTTPS with forged XFF/XFP/XFH; HTTPS with malformed XFF chain | **200** in both cases | Availability only. A 200 cannot distinguish sanitized forwarding from a vulnerable acceptance path; backend effective identity is still unobserved. | +| HTTPS with `Host: attacker.example.test`, original canonical TLS SNI | **421 Misdirected Request** | This forged Host request was rejected at the observed public boundary. It does not prove every internal/generated-URL path. | +| Direct A-address TCP 3000, canonical Host, six cases: no forwarding, forged XFF, forged HTTPS, forged Host, combined headers, malformed chain | **6/6 connection failures**, curl exit **7**, approximately 2.0 seconds each | No HTTP request reached a listener from this vantage point. This is **not** evidence of nginx 403 or attribution to any specific firewall. | +| Direct A-address TCP 5202 and 8080 `/health` | **2/2 connection failures**, curl exit **7** | Neither tested port was reachable here. No claim about other interfaces, IPv6 firewall policy, host-local/container access or unknown alternate ports. | + +Public responses include HSTS, nosniff, `X-Frame-Options: SAMEORIGIN`, `Referrer-Policy: same-origin` and a Permissions Policy. These differ in part from the checked-in nginx template (`DENY`, `strict-origin-when-cross-origin`). An upstream override or a different deployed revision could explain this; the source cannot be attributed without operator configuration/image evidence. Public success therefore cannot certify that the current B06 working tree is deployed. + +### Fresh automated checks and preserved broader results + +| Check in this follow-up | Result | Evidence classification | +|---|---|---| +| Effective production/development Compose tests, including 17 hostile mutations | **3 passed**; preflight of newly rendered production JSON also exit 0 | Proven by automated test | +| Forwarded ingress, canonical origin, B01 HTTP transport, B02 session middleware, B03 admission, B04 rate keys and B05 CSV | **166 passed**, 0 failed/skipped; Release compilation succeeded | Proven by automated test; API/native boundary, not live proxy chain | +| Native Production ingress HTTP suite | **6 passed**, 0 unexpected/flaky/skipped | Proven by automated test, real native API | +| Native B04 HTTP suite | **9 passed**, 0 unexpected/flaky/skipped | Proven by automated test, real native API | +| B04 synthetic shared-IP evidence | In each exhaustion order, A **2 admitted / 1 rejected**, B **2 admitted / 1 rejected**; concurrent two-session case admits **4 total**, then each account rejects **2** attempts using other IPs; cookie/bearer share owner budgets | Does not prove real Traefik/nginx IP propagation. No production quotas were consumed. | +| Existing broader verification, unchanged application source | Prior **915 backend / 298 frontend / 18 browser** passes, Release builds and targeted lint retained above; broader suites were not relabeled as rerun | Preserved prior automated evidence | +| Standalone TypeScript | Prior unrelated `src/interview-prep.test.tsx(120,10): TS2571` remains the documented result; not rerun in this evidence-only follow-up | Existing limitation; no claimed fix | +| Final evidence checks | `git diff --check` **exit 0** (Git line-ending notices only); probe Python syntax and all new evidence JSON parse checks pass | Evidence/documentation verification | + +Exact fresh commands (repository root unless stated otherwise): + +```powershell +python scripts/test-ingress.py +docker compose --env-file .artifacts/b06/compose.env -f docker-compose.yml --profile bundled-ollama config --format json +# Output saved to .artifacts/b06-runtime/compose-effective.json; synthetic inputs only. +Get-Content .artifacts/b06-runtime/compose-effective.json -Raw | python scripts/validate-ingress.py +& C:/Users/Connor/.dotnet/dotnet.exe test JobTrackerApi.Tests/JobTrackerApi.Tests.csproj --configuration Release --no-restore --filter 'FullyQualifiedName~ForwardedIngressTests|FullyQualifiedName~ExternalOriginTests|FullyQualifiedName~JobImportHttpTransportTests|FullyQualifiedName~SessionContextMiddlewareTests|FullyQualifiedName~PdfRenderAdmissionTests|FullyQualifiedName~RateLimitPartitionKeysTests|FullyQualifiedName~CsvExportTests' --logger 'trx;LogFileName=regressions.trx' --results-directory .artifacts/b06-runtime -- xUnit.parallelizeTestCollections=false xUnit.maxParallelThreads=1 +# In job-tracker-ui, serially: +$env:DOTNET_HOST_PATH='C:/Users/Connor/.dotnet/dotnet.exe' +$env:NEXT_TELEMETRY_DISABLED='1' +node node_modules/@playwright/test/cli.js test --config=playwright.ingress.config.ts --reporter=json +node node_modules/@playwright/test/cli.js test --config=playwright.rate-limits.config.ts --reporter=json +# Repository root: +python .artifacts/b06-runtime/probe.py +Resolve-DnsName jobs.cesnimda.uk -Type A -DnsOnly +Resolve-DnsName jobs.cesnimda.uk -Type AAAA -DnsOnly +Resolve-DnsName jobs.cesnimda.uk -Type CNAME -DnsOnly +Resolve-DnsName cesnimda.uk -Type NS -DnsOnly -NoHostsFile +# Repeat A/AAAA/CNAME using -Server for each observed authoritative nameserver. +Get-NetTCPConnection -State Listen +Get-NetFirewallProfile +git diff --check +``` + +### Operator handoff and evidence required for closure + +Permitted deployment docs/scripts/CI configuration refer only to `PROD_HOST`, `PROD_USER` and `PROD_SSH_KEY` secret names. `C:/Users/Connor/.ssh/config` does **not exist**. No unambiguous SSH alias/username was found; no SSH connection was guessed, and no CI secrets, keys/passwords or live `.env` were retrieved. Production read-only inspection therefore remains an operator handoff under Connor's explicit instruction. + +Run the earlier [exact host procedure](#required-deployment-verification--exact-safe-procedure) on the actual Ubuntu host. It uses the documented `/opt/job-tracker/app` checkout and `/opt/job-tracker/shared/.env`, prints validation results rather than secret environment data, inventories listeners/published ports/networks, checks real nginx configuration/internal health and obtains SYN-only peer evidence. Add these read-only checks: + +```bash +cd /opt/job-tracker/app +git rev-parse HEAD +git status --short +docker inspect --format '{{.Name}} image={{.Image}} ports={{json .NetworkSettings.Ports}}' "$frontend_id" "$backend_id" +# Use frontend_id/backend_id established by the earlier procedure. +# Observe the marked request in the existing nginx access log without sharing unrelated requests: +docker logs --since 2m "$frontend_id" 2>&1 | grep -F '/api/auth/config?b06_probe=runtime-check' +``` + +While capturing the SYN peer, issue `curl --noproxy '*' --max-time 10 -sS -o /dev/null 'https://jobs.cesnimda.uk/api/auth/config?b06_probe=runtime-check'` from the controlled client. Compare the original socket source with nginx's generated `geo`/`set_real_ip_from` entries, and the access log's effective client address with the known test source. The access-log format must be inspected before interpreting fields. These alone do not prove the IP finally used by backend rate limiting; that requires the real-chain synthetic A/B scenarios on an isolated staging/test topology. + +| Evidence to return, with secrets/cookies/user content removed | Safe result | Unsafe or unresolved result | +|---|---|---| +| Host/provider identity, deployed revision/image IDs, Compose preflight result and frontend/backend `Ports`/network subsets | Current intended build/configuration, only intended frontend host publication; API unpublished | Old/unknown build, dev overlay, host networking or extra API/public internal-service binding | +| Relevant `ss`, Docker NAT/FORWARD, IPv4/IPv6 firewall and provider rule excerpts | Only documented public routes; direct app path blocked or explicitly peer-restricted | Rule comments alone, unknown IPv6 handling or externally reachable API | +| Traefik entrypoint/router/upstream and forwarded-header directives only; generated nginx trust entries; marked SYN peer observation | Actual Traefik source matches narrow allowed peer; untrusted source differs; client forwarding sanitized | Trust-all, guessed subnet or shared NAT identity for trusted and untrusted paths — stop before permitting that peer | +| Internal nginx/API health and controlled canonical-route result | Healthy internal API with working canonical ingress | Public API port required for health, internal 502 or legitimate caller rejected | +| Direct nginx attempts from an actually untrusted reachable test network, plus spoof/malformed variants | 403/444 or independently explained network denial; no forged identity reaches backend | Successful application response from forbidden peer or caller-selected IP/scheme/host | +| Fresh A/AAAA/provider inventory, certificate validation and HTTP redirect result from a known external network | Intended ingress address(es), valid TLS, HTTP target reachable at canonical HTTPS origin | Stale/alternate bypass record, invalid chain or the observed unreachable `:444` redirect | +| Isolated real-chain A/B budgets, actual backend client-IP observations and B02 context checks | Each owner isolated; sessions share owner; spoofing cannot select IP budget; stale context rejected | Backend sees only proxy IP or client-forged IP; header/session selects another owner | + +Return only selected sanitized excerpts and the timestamp/test vantage point. **Do not send complete `.env`, full `docker inspect`, authentication headers, tokens, passwords, private keys/certificates or unrelated access logs.** Do not run restart or missing/invalid-configuration tests against production as part of this read-only handoff. Those remain mandatory in the isolated real-container test environment after Docker is operational; record startup failure, restart health, peer/network stability and repeat direct-access rejection. + +### Closure checklist by evidence level + +1. **Proven by automated test:** effective production port/trust constraints, fail-closed preflight, native trusted/untrusted forwarding behavior, B04 principal/IP separation, B02 session guard, B01/B03/B05 focused regressions. +2. **Proven in running local/container topology:** **none in this follow-up**. Docker failure prevents actual Compose startup, real nginx syntax/enforcement, actual Traefik peer capture, internal routing, restart/config-failure tests and real-chain A/B verification. +3. **Proven against the deployed public endpoint from this workstation:** DNS observations, valid HTTPS/200, forged Host 421, direct-port connection failures from this vantage, HTTP redirect to unreachable 444. This is partial public-endpoint evidence, not certification of the internal deployed chain. +4. **Still requires operator verification:** deployed build/topology/trust CIDRs, actual socket peers, backend client IP/scheme, real-chain session/account/IP behavior, host/container listeners and other network memberships, host/provider IPv4/IPv6 firewall/NAT, independent external reachability, intended DNS-address inventory and correct HTTP redirect target. + +The blocking items are therefore the unavailable local Docker engine, absent verified production host access/evidence, missing real-chain peer/identity tests, and the observed HTTP redirect mismatch. **B06 implementation complete, but F04 remains open.** No change to F04's conditional severity or closure status is justified by connection failures alone. + +## Authorized release attempt and CI prerequisite correction + +Connor subsequently authorized commit/push to live. B01–B06 were committed as `0954876`, and the redesign brief as `1bbbd05`; both were pushed to `main`. [Gitea run 736](https://git.cesnimda.uk/cesnimda/jobtrackingapp/actions/runs/736/jobs/1607) failed before backend/frontend testing: `scripts/test-ingress.py` raised `FileNotFoundError: 'docker'` in the runner. Deployment was skipped; the push was not a successful live rollout. + +The first correction (`f485f72`) attempted to install pinned Compose v5.3.1 through the [official Docker setup action](https://github.com/docker/setup-compose-action), pinned to commit `2fe291b7677a45ee1269ec56a42604c143505e7e`. Gitea tokens were deliberately not passed to GitHub. The config test accepts standalone Compose, retaining `docker compose` on existing Docker installations. Nothing is skipped and the production ingress gate remains unchanged. + +Validation before the corrective push: **3/3 tests through `docker compose` and 3/3 through the real standalone binary** with Docker discovery disabled for the latter fixture; tracked-secret scan and `git diff --check` pass. This corrects a CI dependency assumption; it neither repairs local Docker Desktop nor supplies any missing F04 deployment evidence. + +[Run 737](https://git.cesnimda.uk/cesnimda/jobtrackingapp/actions/runs/737/jobs/1609) then failed in the action itself: `Parameter token or opts.auth is required`; deployment again skipped. The final setup uses the [official standalone release](https://github.com/docker/compose/releases/tag/v5.3.1) directly, in a runner-local temporary directory. It pins Linux x86_64 and SHA-256 `f9ebc6ebdb19d769b793c245a736caaeb198c62587f13b25c660c13b4987f959`, verifies before execution, and needs neither a daemon nor an authentication token. A fresh local download matches that official release digest; 3/3 ingress config tests and diff validation pass. Runner execution and deployment are tracked separately from these local checks. + +[Run 738](https://git.cesnimda.uk/cesnimda/jobtrackingapp/actions/runs/738/jobs/1611), for `97fce89`, passed Compose installation, all repository safety scripts, browser-extension tests, secret/SBOM checks, parser tests and the Release API build. Test-project build then failed with MSB3021/MSB3024: the Linux `PdfBrowserStub` apphost was copied to an output path already used as the fixture directory. Windows' `.exe` suffix had hidden the collision. The project reference now sets `Private="false"`; it still builds the fixture, while the existing explicit subdirectory copy is its sole output owner. SDK target inspection confirms this disables transitive content copying. No renderer or admission behavior changes. + +Local verification: `C:/Users/Connor/.dotnet/dotnet.exe test JobTrackerApi.Tests/JobTrackerApi.Tests.csproj --configuration Release --no-restore --filter FullyQualifiedName~PdfRenderAdmissionTests --logger "console;verbosity=minimal"` built successfully and passed **22/22** tests, including real exporter/fixture failure recovery. A preliminary unqualified `dotnet` invocation could not find the pinned SDK because PATH selected the system runtime; the explicit installed SDK resolved it. Diff checks pass. Linux runner confirmation remains required after push; a build/package fix does not close F04. + +[Run 739](https://git.cesnimda.uk/cesnimda/jobtrackingapp/actions/runs/739/jobs/1613), for `23cc250`, failed earlier in the unchanged API compiler: `csc.dll` exited 139 on both the ordinary and existing single-process retry. This is the previously documented runner crash signature, not a compile diagnostic or test assertion. It prevented Linux confirmation of the packaging correction. No retries/gates were weakened. Fresh local broader verification with the explicit SDK passed **915/915** selected backend tests, no selected skips, with only the same documented external-DNS-dependent IMAP exclusion; `.artifacts/redesign-audit/backend.trx` records the run. + +Pushing this evidence provides one further complete CI attempt with unchanged application/test logic. If the existing bounded retry is exhausted again, stop release attempts and obtain runner-host diagnostics under the [runner investigation](../../../infrastructure/runner-investigation.md); do not increase retry loops indefinitely or deploy past a red test gate. Phase 1 redesign audit is separately committed on `codex/ui-redesign-audit`, with no redesign implementation. F04 remains open regardless of CI outcome until its runtime/production checklist is proven. + +[Run 740](https://git.cesnimda.uk/cesnimda/jobtrackingapp/actions/runs/740/jobs/1615) recovered from the compiler issue, confirmed the Linux fixture **build** fix, and executed all 916 backend cases: **903 passed, 13 failed**. These were actionable portability failures rather than another compiler crash: two TLS-rejection cases recorded an empty HTTP read instead of null, and eleven PDF process cases could not start the .NET fixture (exit 131 or ensuing start-marker timeout). The workflow installed .NET in `/root/.dotnet`, but exported `DOTNET_ROOT` only inside the setup step; PATH alone does not supply the runtime location to a native apphost ([Microsoft runtime-location documentation](https://learn.microsoft.com/en-us/dotnet/core/tools/dotnet-environment-variables#dotnet_root-dotnet_rootx86-dotnet_root_x86-dotnet_root_x64)). + +The correction persists the already-validated runtime path through `GITHUB_ENV`. The TLS assertion accepts null **or an empty string**, while still requiring `HttpRequestException` with `AuthenticationException`, original-host SNI, no custom validation callback and zero HTTP request content. It does not accept whitespace or any HTTP bytes, trust a bad certificate or alter the transport. Fresh explicit-SDK Release verification with `DOTNET_ROOT` set: **98/98** `PdfRenderAdmissionTests|JobImportHttpTransportTests` pass; `.artifacts/redesign-audit/portability.trx`. No application code or test filters changed. The earlier stop rule applies to repeated compiler crashes, not evidence-backed correction of these now-observed test/setup defects. + +### Release dependency gate — 2026-09-10 local / 2026-09-09 UTC + +[Run 741](https://git.cesnimda.uk/cesnimda/jobtrackingapp/actions/runs/741/jobs/1617), `2024c37`, confirmed **916/916 Linux backend tests passed, zero skipped**. Compose/safety/parser/build checks also passed. Deployment was then skipped because `npm audit --audit-level=high` found three vulnerable package entries: Next critical, sharp high and js-yaml high. A fresh local audit reproduced the same report. These advisories were updated in the public database on September 8; earlier audit results are historical, not proof of the current dependency gate. + +The targeted patch raises the Next and matching lint-config floor to 16.3.3 (lock resolves 16.3.4), updates the existing sharp override from 0.35.3 to 0.35.4, and updates the transitive js-yaml 3.x lock to 3.15.2. Its 4.x copy already uses patched 4.3.2. Lockfile review shows only these dependency families and their required platform/compiler artifacts changed. React, MUI, routing, API and schema contracts were not changed. + +Primary advisory evidence: [Next Windows-hosted server issue](https://github.com/advisories/GHSA-p293-qw3h-jr36), [Next AVIF optimization issue](https://github.com/advisories/GHSA-2xp9-vwfh-vxw4), [sharp/libheif](https://github.com/advisories/GHSA-rgj7-g3m4-5g8c), [js-yaml merge budget](https://github.com/advisories/GHSA-2883-xcg3-v3hh). The intended production frontend is a static nginx export, not a Windows Next server or a running Next image optimizer; the advisories alone do not establish production exploitation. Build/development dependencies still fall within the existing release policy, so the gate is retained and patched instead of suppressed. + +Initial commands: `npm install --package-lock-only --ignore-scripts --no-audit --no-fund`, then `npm update js-yaml --package-lock-only --ignore-scripts --no-audit --no-fund`; `npm audit --package-lock-only --audit-level=high` reports **zero vulnerabilities**. Actual installation, frontend regression/build and new CI results follow separately; a lockfile-only audit is not runtime verification. + +Fresh installation with `npm ci --no-audit --no-fund` succeeded (820 packages); installed-tree `npm audit --audit-level=high` also reports **zero vulnerabilities**. Next's updated lint preset introduced `no-location-assign-relative-destination`, which assumes Next Router. This static SPA uses React Router and deliberately performs full navigation after account deletion to clear private state. That one inapplicable navigation rule is disabled with a configuration comment; both existing full-navigation calls and B02 cleanup remain unchanged. + +The patched Next build compiled application code but then exposed the historical `interview-prep.test.tsx:120` TS2571 as a build blocker. The smallest test-only correction uses `objectContaining({ content: expect.not.stringMatching(...) })` on the unknown Axios request argument. The existing positive assertion still requires the exact endpoint/category/title and a string containing the debrief prompt; the negative assertion still rejects invented company/achievement/metric content. No cast, type exclusion, strictness relaxation or production behavior change was introduced. R01a's explicit standalone CI command remains outstanding; its original reproduction is retained. + +Fresh local release verification after these changes: + +| Command (UI directory unless noted) | Result | +|---|---| +| `npm test -- --watchAll=false --runInBand` | **67 suites / 298 tests passed**, 102.468s, after the dependency update | +| `npm test -- --watchAll=false --runInBand src/interview-prep.test.tsx` | **13/13 passed** after the assertion correction | +| `node node_modules/typescript/bin/tsc --noEmit --incremental false` | **Exit 0** after correction; original error reproduced before it | +| `npm run build` with `NEXT_PUBLIC_API_BASE_URL=http://localhost:5302/api` and telemetry disabled | **Exit 0**, Next 16.3.4 static export, including TypeScript | +| `npm run test:e2e` with `PLAYWRIGHT_STATIC_EXPORT=true`, `PLAYWRIGHT_PREBUILT_EXPORT=true`, explicit installed `DOTNET_HOST_PATH`/`DOTNET_ROOT` | **18/18 passed**, 1.4m; Release API build zero warnings/errors. Includes B02 account isolation, B05 exact CSV bytes, anonymous public CV and long searchable PDF | +| `npm run lint` | **Exit 0**, zero warnings after the rule compatibility adjustment; one redundant concurrent run raced Playwright's temporary `test-results` cleanup (ENOENT), so final lint is run serially | +| Root: `python scripts/supply-chain.py scan-secrets`; `git diff --check` | **Pass**; line-ending notices only | + +Logs are under `.artifacts/redesign-audit/`. The latest full Linux backend run remains **916/916 at `2024c37`**; no backend source changed in this dependency correction. Deployment still requires the next full CI run and production preflight. This is authorized release repair, not Phase 2 redesign or B07; **F04 remains open**. + +### Runner dependency corruption after the patch + +[Run 742](https://git.cesnimda.uk/cesnimda/jobtrackingapp/actions/runs/742/jobs/1619), `a861ca4`, again passed **916/916 backend tests**, no skips (16s), repository/parser/build checks, locked frontend installation and the dependency audit. Lint stopped before analyzing application code with exit 2: installed `es-abstract/2025/RegExpCreate.js:19` contained `var"flags`, causing `SyntaxError: Invalid or unexpected token`. Subsequent gates and deployment were skipped. + +Independent verification downloaded the exact `es-abstract` **1.24.2** tarball URL from the lockfile, verified its SHA-512 integrity against that lock and read the member without extracting it. The official member and locally installed file match byte-for-byte; line 19 is `var flags = typeof F === 'undefined' ? '' : ToString(F);`. This is evidence of different dependency bytes in the runner's failure, not a source lint violation or a need to edit the dependency. The corruption mechanism is unproven; the existing [runner investigation](../../../infrastructure/runner-investigation.md) documents analogous failures. + +This evidence-only push makes **one further unchanged-code CI attempt** with the workflow's existing isolated install cache. No new retry loop, lint suppression or gate bypass is added. If corrupted dependency bytes or assertion-free process crashes recur, stop release retries and request runner-host diagnostics; do not generate repeated empty commits. Public `/health` still reports version **299**, `/ready` reports ready before this attempt; that is the existing deployment, not proof that `a861ca4` is live. The completed Phase 1 audit is pushed separately at `957fb57` on `codex/ui-redesign-audit`. + +### Final release retry — 2026-09-10 + +[Run 743, test job 1621](https://git.cesnimda.uk/cesnimda/jobtrackingapp/actions/runs/743/jobs/1621), for `27657f31131cd95d42f0e1290853ec9c9b3959a4`, completed in **7m25s**; Gitea recorded the failed status at **2026-09-09 22:32:25 UTC** (September 10 locally). Repository safety/Compose, extension, parser, secret/SBOM, backend build and dependency checks passed. The full backend suite passed **916/916**, zero skipped (15s test duration); frontend passed **67/67 suites, 298/298 tests** (82.867s). Locked installation, npm audit, lint and the production frontend build all passed. The previous malformed `es-abstract` source did not recur at lint. + +The browser gate failed: **6 passed, 12 failed** (1.4m). The first failure was `worker process exited unexpectedly (code=null, signal=SIGSEGV)` in the authenticated accessibility case; its existing retry timed out in `beforeEach` after 30 seconds. The remaining eleven failures report `page.goto: Target page, context or browser has been closed`, including after their existing retries. There is no reported accessibility violation or application assertion mismatch in these failures. This establishes a process/browser execution failure, but does **not** establish its underlying host, runtime or hardware cause. The local **18/18** browser result above remains separate evidence and cannot replace the failed CI gate. + +[Deploy job 1622](https://git.cesnimda.uk/cesnimda/jobtrackingapp/actions/runs/743/jobs/1622) was **skipped**. At **2026-09-10 05:07:21 UTC**, read-only HTTPS requests still returned `/health` → `{"status":"ok","version":"299"}` and `/ready` → `{"status":"ready"}`. The requested commits are pushed to main, but this release has **not deployed**. No production service was replaced by this run. + +The documented stop condition is now reached: **no further unchanged-code release retries or gate bypasses**. Capture runner diagnostics before another attempt. This result is recorded on `codex/ui-redesign-audit` so that documenting the block does not itself start another main deployment. Phase 1 is complete; Phase 2 and B07 have not started. **B06 implementation complete, but F04 remains open**, for the runtime/production reasons in the closure checklist above as well as the unshipped release. + +#### Runner evidence to collect before another attempt + +Run these read-only commands on the actual Ubuntu **runner host**, which must be identified independently of the application host. No SSH alias or credentials are assumed. Use the run's timestamps for a bounded kernel-log window; the example covers this observed attempt. Do not reset Docker, delete caches, alter limits or restart production services just to obtain evidence. + +```bash +date -u +uname -srmo +free -h +df -h / /tmp /dev/shm +df -i / /tmp +docker ps -a --no-trunc --format '{{.ID}} {{.Names}} {{.Image}} {{.Status}}' +docker stats --no-stream --format '{{.Name}} memory={{.MemUsage}} cpu={{.CPUPerc}} pids={{.PIDs}}' +sudo journalctl -k --utc --since '2026-09-09 22:20:00 UTC' --until '2026-09-09 22:40:00 UTC' --no-pager \ + | grep -Ei 'segfault|oom|out of memory|killed process|I/O error|machine check|hardware error|EDAC' +# If systemd-coredump is installed: list metadata only, not memory dumps. +command -v coredumpctl >/dev/null && \ + coredumpctl --since '2026-09-09 22:20:00 UTC' --until '2026-09-09 22:40:00 UTC' --no-pager list +# Select a verified runner/job container ID from the inventory, if it still exists. +read -r -p 'Observed runner/job container ID: ' runner_container_id +test -n "$runner_container_id" && docker inspect --format \ + '{{.Name}} image={{.Image}} state={{json .State}} memory={{.HostConfig.Memory}} memorySwap={{.HostConfig.MemorySwap}} pidsLimit={{.HostConfig.PidsLimit}} shmSize={{.HostConfig.ShmSize}} ulimits={{json .HostConfig.Ulimits}}' \ + "$runner_container_id" +``` + +Return selected, sanitized output plus the job-container identity/image and exact failure window. **Do not provide environment dumps, runner registration/config files, tokens, core files, full Docker inspection or unrelated logs.** A removed job container or unavailable kernel/core records is an evidence gap, not a clean bill of health. Host free memory after completion does not prove the earlier job's cgroup or shared-memory capacity. OOM/cgroup failures, segfault records, or storage/hardware errors would direct the next investigation; absent records do not justify guessing a cause. Retain the existing failed-job browser trace/error-context artifacts if available, keeping synthetic test data separate from production information. A successful full CI run, actual deploy completion and live version/health verification are still required before calling the push live; F04 then additionally requires its independent ingress evidence. diff --git a/docs/audits/2026-09-07/validation/evidence.md b/docs/_archive/pre-docs-cleanup-2026-09-11/docs/audits/2026-09-07/validation/evidence.md similarity index 100% rename from docs/audits/2026-09-07/validation/evidence.md rename to docs/_archive/pre-docs-cleanup-2026-09-11/docs/audits/2026-09-07/validation/evidence.md diff --git a/docs/audits/2026-09-07/validation/evidence/b02-browser-config.ts.txt b/docs/_archive/pre-docs-cleanup-2026-09-11/docs/audits/2026-09-07/validation/evidence/b02-browser-config.ts.txt similarity index 100% rename from docs/audits/2026-09-07/validation/evidence/b02-browser-config.ts.txt rename to docs/_archive/pre-docs-cleanup-2026-09-11/docs/audits/2026-09-07/validation/evidence/b02-browser-config.ts.txt diff --git a/docs/audits/2026-09-07/validation/evidence/b06-generated-nginx.conf.txt b/docs/_archive/pre-docs-cleanup-2026-09-11/docs/audits/2026-09-07/validation/evidence/b06-generated-nginx.conf.txt similarity index 100% rename from docs/audits/2026-09-07/validation/evidence/b06-generated-nginx.conf.txt rename to docs/_archive/pre-docs-cleanup-2026-09-11/docs/audits/2026-09-07/validation/evidence/b06-generated-nginx.conf.txt diff --git a/docs/audits/2026-09-07/validation/evidence/b06-runtime-public-probe.py.txt b/docs/_archive/pre-docs-cleanup-2026-09-11/docs/audits/2026-09-07/validation/evidence/b06-runtime-public-probe.py.txt similarity index 100% rename from docs/audits/2026-09-07/validation/evidence/b06-runtime-public-probe.py.txt rename to docs/_archive/pre-docs-cleanup-2026-09-11/docs/audits/2026-09-07/validation/evidence/b06-runtime-public-probe.py.txt diff --git a/docs/audits/2026-09-07/validation/evidence/b06-topology-blocked.txt b/docs/_archive/pre-docs-cleanup-2026-09-11/docs/audits/2026-09-07/validation/evidence/b06-topology-blocked.txt similarity index 100% rename from docs/audits/2026-09-07/validation/evidence/b06-topology-blocked.txt rename to docs/_archive/pre-docs-cleanup-2026-09-11/docs/audits/2026-09-07/validation/evidence/b06-topology-blocked.txt diff --git a/docs/_archive/pre-docs-cleanup-2026-09-11/docs/audits/2026-09-07/validation/implementation-queue.md b/docs/_archive/pre-docs-cleanup-2026-09-11/docs/audits/2026-09-07/validation/implementation-queue.md new file mode 100644 index 0000000..0ba8421 --- /dev/null +++ b/docs/_archive/pre-docs-cleanup-2026-09-11/docs/audits/2026-09-07/validation/implementation-queue.md @@ -0,0 +1,132 @@ +# Validated implementation queue + +Companion to the [independent verdict](README.md). This is a proposed queue, not authorization to start implementation. Original finding IDs remain stable; V01 strengthens F08 and V02 strengthens F11. No application changes were made by this pass. + +**Status update, 2026-09-09:** the user separately authorized B01–B05, now complete locally: [B01](b01-completion.md), [B02](b02-completion.md), [B03](b03-completion.md), [B04](b04-completion.md), [B05](b05-completion.md). B05 resolves F05 under its documented spreadsheet-text policy; actual Excel before/after proves the arithmetic formula is now text and Norwegian encoding is preserved. Final verification: 902 selected backend, 298 frontend, 18 browser tests, nine B04 rate-policy HTTP scenarios and the unchanged B03 admission probe pass. Release builds and targeted lint pass; R01a's pre-existing standalone TypeScript error remains. HTTP F11/V02, F08/V01 and F06 are resolved locally; overall F11 remains open for B07/deployment. F03 admission is resolved locally; R04 containment/capacity evidence remains open. **B06 was subsequently authorized; implementation and pending verification are recorded below.** + +**B06 runtime follow-up, 2026-09-09:** [Report and operator handoff](b06-completion.md#runtime-verification-follow-up-2026-09-09). **Implementation complete; full runtime/production verification incomplete; F04 open.** Fresh 166 focused backend, 3 Compose, 6 Production HTTP and 9 B04 HTTP checks pass. Public DNS/valid HTTPS are now observed; direct ports 3000/5202/8080 are unreachable from this workstation, without proof of the rejecting layer. HTTP redirects to unreachable HTTPS port 444. Docker's inference socket failure still blocks actual Compose/Traefik/nginx tests; production peers, backend client IP and host/provider firewall remain unverified. No SSH alias/user was found in permitted non-secret sources. B01–B05 preserved; B07 not started. + +**Sizing:** S = one narrow boundary/configuration change and its meaningful tests; M = one cohesive service/API/client contract that can be understood, implemented, tested and reviewed in one focused session. These are scope estimates, not elapsed-time promises. If a ticket needs unrelated modules, split it at the acceptance boundary below. Do not create a generic framework merely to make tickets share code. + +The numbered order is the default integration order. Independent work may start sooner only along the explicitly permitted parallel branches. A conditional feature can stay disabled while unrelated safe work proceeds; a failed recovery gate blocks migrations, not read-only planning or non-schema tests. + +## Batch 0 — Security, identity and immediate admission + +**Findings:** F11/V02, F08/V01, F03, F06, F05 and conditional F04. These require no data migration and address the most direct ways to cross a boundary or consume shared resources. CSV remains P2 but is inexpensive and isolated. + +| Order / ticket | Size and ownership | Concrete scope and acceptance | Dependencies / rollback risk | +|---|---|---|---| +| **B01 HTTP import destination enforcement** | M. `Services/JobImport/JobImportService.cs`, resolver/address helper only if needed, named import HTTP registration in `Program.cs`, import/network tests. | Normalize mapped addresses and enforce the same approved-address policy at the actual socket connection. Keep HTTP/HTTPS, redirect refusal, bounded content, cancellation and hostname/TLS verification. Before/after tests must show mapped private/link-local targets never reach a connect/send boundary, validation-to-connect DNS changes cannot redirect a socket privately, and an allowed destination still works. | First ticket. No schema. Network-handler rollback reopens the defect; retain the previous handler only as an emergency operational option with equivalent egress restriction. Do not alter all HttpClients. | +| **B02 Cross-account form/session boundary** | M. `src/auth.ts`, `api.ts`, `App.tsx`, profile/editor state binding, a narrow server precondition if required, auth/browser tests. | Bind retained state and outgoing writes to the identity/session generation that loaded it. Synchronize tabs, invalidate owner caches, ignore late responses, refuse mismatched dispatch. Close the shared-cookie race with a server-checked mismatch precondition where client timing alone is insufficient. Test actual A edit → B login in second tab → A Save, asserting B and A database values unchanged; include slow old GET, logout, Back and same-account reauth. | Independent of record revisions, and must precede persistent drafts or editor refactoring. Coordinate `Program.cs` integration with B01/B04 if adding middleware. No schema. Older clients lacking required protection must be forced to refresh/re-auth safely; do not silently fall back to unsafe writes. | +| **B03 Shared PDF admission — complete locally** | M. Singleton exporter, PDF callers/error mapping, exporter tests. | [Completion and evidence](b03-completion.md). Default one active render per process, one per document owner, zero waiting; validated options, safe 503/504, cancellation/timeout/shutdown cleanup and failure recovery. Permanent public/variant/job/profile boundary tests plus real Chromium smoke. Implementation inspection corrected the original package reference: packages do not render PDFs. | No schema. The simultaneous owner allowance is enforced inside the exporter independently of middleware; time-window owner/IP rate limits remain B04. Tune process capacity and verify containment under R04. No durable queue or renderer service added. | +| **B04 Resolve principal before owner limiting — complete locally** | S. `Program.cs`, existing rate-key helper, permanent real HTTP tests. | [Completion, inventory and evidence](b04-completion.md). Authentication **and endpoint authorization** precede account limiting, including explicit-scheme principal selection; validated owner/IP namespaces, shared-IP account isolation, shared same-account sessions/credentials, preserved IP controls, safe 429/Retry-After. Concurrent, crafted identity, invalid credential, B02 context and trusted/untrusted proxy tests pass. | No schema or policy-window changes. B01/B02/B03 preserved. Process-local counters and B06 deployed ingress verification remain explicit. | +| **B05 Spreadsheet-safe CSV fields — complete locally** | S. `ExportController`, small text helper, byte-preserving UI download and export tests. | [Completion and evidence](b05-completion.md). All 13 text columns neutralize leading `= + - @`, including after whitespace/controls/Unicode format characters, before existing CSV escaping. Typed numbers/dates, stored data and JSON remain unchanged. Parser, HTTP/browser-byte and local Excel checks pass; UTF-8 BOM corrects reproduced Norwegian decoding. | No schema/framework/dependency change. Spreadsheet-safe CSV may visibly retain the apostrophe; raw JSON remains available. LibreOffice runtime verification unavailable; defined policy and compatibility limits documented. | +| **B06 Verify and close ingress bypass path — implementation complete; runtime verification pending** | S source/config change plus operator verification. Compose/nginx/forwarded-proxy tests/docs. | [Implementation, runtime observations and gates](b06-completion.md). Production render has frontend 3000 and no API publication. Fresh API/config tests pass. DNS/valid public HTTPS and failed direct-port connections are observed; HTTP redirects to unreachable port 444. Actual Compose/Traefik/nginx execution is blocked by Docker failure; deployed peer/IP/firewall evidence is absent. F04 remains open. | Read-only production inspection authorized, but no unambiguous SSH alias/user exists in permitted sources; use operator handoff. Deployment changes remain separate. Complete real-container and production evidence; do not start B07. A bad binding can cause downtime; retain a tested topology-specific rollback. | +| **B07 IMAP connected-address enforcement** | M. `ImapService`, focused network policy/connector and IMAP SSRF tests. | Reuse the B01 address-policy conclusion, but respect MailKit's transport. Enforce an approved connected peer on initial connection and reconnect; retain certificate hostname validation and timeouts. Existing mapped normalization is already correct: do not fix it again. | After B01 establishes the small shared policy, with sequential integration if it shares helper code. No schema. Test TLS identity with an injected/local fixture; do not disable validation to connect by IP. | + +**Batch stop condition:** V01's actual persisted cross-account save is prevented; HTTP/IMAP outbound address paths and shared PDF admission have failing-before/passing-after evidence; correct owner/IP partitioning remains enforced. Actual ingress reachability proof is required before a public deployment. B01 alone does not close all of F11. + +## Batch 1 — Release pipeline and recovery before migrations + +**Findings:** F12, F17, F19, conditional F20; the immediate containment portion of F03/F18. This batch intentionally precedes schema-changing concurrency and billing work. A migration is not safe merely because its unit tests pass when the recovery/rollback path is unproven. + +| Order / ticket | Size and ownership | Concrete scope and acceptance | Dependencies / rollback risk | +|---|---|---|---| +| **R01a Standalone type gate — partially resolved during authorized release repair, 2026-09-10** | S. `interview-prep.test.tsx`, UI package script, CI workflow. | The patched Next build exposed the historical TS2571. A test-only object matcher now preserves the positive/negative content assertions without dereferencing an unknown Axios payload; 13 interview tests, standalone tsc and Next build pass. An explicit standalone CI command remains outstanding; keep strictness and test coverage. See [release evidence](b06-completion.md#release-dependency-gate--2026-09-10-local--2026-09-09-utc). | Can run in parallel with Batch 0. No production-code behavior or schema. Do not weaken the gate to preserve a green build. | +| **R01b Production DB configuration validation** | S. Provider-selection code in `Program.cs` and startup tests. | Fail startup for an unknown production provider or missing required configured connection. Preserve deliberate dev SQLite defaults and valid explicit SQLite/MariaDB. Assert no fallback database is created on rejected startup. | Integrate after Batch 0 `Program.cs` mutations. No schema; rejecting an existing invalid environment can prevent boot, so validate deployed inputs first. | +| **R02 Consistent recovery-set creation** | M. `deploy.sh`, existing backup runner/scripts, isolated fixture and operator documentation. | Replace live SQLite tar as the DB consistency mechanism; coordinate file inventory/checkpoint or a short write pause, manifest/checksums and incomplete-set marking. Preserve keys/tombstone recovery. For MariaDB, coordinate the file set with the consistent SQL dump. | No new app schema. Can develop synthetic fixture in parallel with Batch 0. A backup that is only present is not yet verified. Keep previous verified sets. | +| **R03 Restore drill and schema-compatible rollback** | M. Deploy rollback logic, isolated restore harness and runbook; no production restore in the implementation session. | Restore latest/previous synthetic sets under active-source writes; integrity, FK owner boundaries, attachment hashes, provider-token decryptability and deletion replay must hold. Run previous/new binaries against the intended schema; allow automatic image rollback only when compatible. Record measured RPO/RTO, required key custody and operator steps. | Depends on R02 and Connor's recovery objectives. **Blocks every schema-changing ticket below.** Failed restore is a stop, not permission to reset the source DB. Do not run downgrade migrations automatically. | +| **R04 PDF/container containment evidence** | S/M. API Dockerfile/Compose and bounded local container test. | Verify non-root filesystem permissions, resource/PID limits and renderer operation. Evaluate sandbox/local-file flags against real templates. Measure a small controlled burst after B03 and pick capacity from evidence. | Depends on B03; coordinate Compose changes with B06. Container changes can break rendering/storage; verify permission/health/PDF before rollout. A separate renderer service is a decision only if adequate containment cannot otherwise be demonstrated. | +| **R05 AI worker survival and recovery** | M. `AiOperationQueue.cs`, worker tests/health signal. | Catch at each worker loop, classify cancellation/transient/fatal outcomes, bound retries and expose lost/degraded workers. Inject claim/owner/result persistence failures at concurrency 1 and 2; core host survives recoverable faults, accepted operations resume after lease recovery, no lost result or endless tight retry. | Independent of schema work; required before enabling this worker. Keep its existing kill switch. Do not change the global background exception policy. | + +**Batch stop condition:** strict frontend typing and configured-provider startup are deterministic; a recovery set actually restores, and compatibility governs rollback. AI/PDF enabled features need their own containment proof. R03 must be green before Batch 2 or paid-ledger schema changes are applied. + +## Batch 2 — Record integrity, intent retries and storage accounting + +**Findings:** F01 and the concrete F10 integrity/admission subset. Agree the convention in D01, then apply it to bounded aggregates. Do not introduce a generic concurrency service, universal event sourcing or globally unique job content. + +| Order / ticket | Size and ownership | Concrete scope and acceptance | Dependencies / rollback risk | +|---|---|---|---| +| **D01 CV variant revision protocol** | M. CV variant model/service/controller/editor, context migration and CV tests. | Expected revision/conditional update, transactional history, distinct conflict response and retained draft. Reconcile duplicate revision labels preserving every snapshot, then enforce uniqueness. Include restore-versus-save, stale name/settings and old-client handling. Prove with two contexts and SQLite/MariaDB. | B02 + R03. Establish request/response convention before D02/D03. One cohesive vertical change; keep templates/layout/AI tooling outside the diff. Additive rollout first; old unconditional writers must be rejected or refreshed when enforcement starts. Do not revert to an old writer without a compatibility plan. | +| **D02 Career Profile atomic revision/projection** | M. Career profile service/controller/model, legacy user projection, profile page/tests and migration. | Separate aggregate revision from structured-document schema `version`. Atomically replace children, append history and update the derived legacy projection, or document/test a recoverable projection only if a shared transaction is demonstrably impossible. Cover restore, projection-write failure, delete-versus-save and losing drafts. | D01 contract + R03. Preserve old versions/child IDs as required; no identity/profile-field redesign. Same-account transaction correctness does not replace B02. | +| **D03a Job mutation preconditions** | M. Job aggregate/controller/status operations, admin undo and generated-draft writers that touch the same aggregate; model/migration/tests. | Inventory exact conflicting write sets; apply the agreed revision rule to user read-modify-write paths and prevent background writes from silently replacing newer user drafts. One conflict must not partially update events/opportunity projection. Explicitly justify field-disjoint writes allowed to commute. | D01 convention + R03. Own the schema snapshot serially. If inventory is too broad, split status/full-edit and draft-generation writers into consecutive tickets with each unprotected path left visibly open. Do not claim F01 closed between them. | +| **D03b Job client conflict handling** | M. Existing job detail/edit/status/draft client paths and browser tests. | Send loaded revisions, retain losing edits, explain reload/compare/copy without blind retries; delayed responses cannot replace current owner state. | D03a + B02. Integrate before enabling mandatory job preconditions. No independent schema. No dashboard redesign. | +| **D04 Creation intent idempotency** | M. Manual/imported job creation boundary, relevant client submission state, narrow intent persistence and relational tests. | One key per user creation intent, unique owner/key constraint, payload fingerprint and returned resource identity committed with the creation. Same key/same payload returns the original; same key/different payload conflicts; two owners are independent; a deliberate new application uses a new key. Test lost response/retry and concurrent identical requests. | R03 and settled job creation transaction. Can prepare after D01; integrate after D03 schema changes. Do not deduplicate by company/title/URL globally or retrofit every endpoint. Retain intent records for an explicit retry window; expired-key behavior must be documented. | +| **D05 Atomic attachment byte reservation** | M. Attachment admission/storage service, owner quota state and focused migration/tests. | Reserve bytes atomically before staging, release/commit correctly on cancellation/failure; two near-limit uploads cannot both pass. DB transaction/reservation works across processes. Preserve existing staged-file reconciliation and safe path handling. | R03; integrate after other schema tickets. No silently deleted files to meet a quota. Reservation cleanup/reconciliation must survive crashes. | +| **D06 Generated-file accounting** | M. Existing `AccountOwnedFileInventory`, PDF/artifact/package producers, quota reconciliation/tests. | Account for generated artifacts and temporary/retained export copies with explicit limits and cleanup. Reuse D05 accounting contract; avoid counting only attachments or charging the same retained file twice. Test copy/delete/crash and old existing inventory. | D05 + B03. Rollback retains existing files; a reconciliation disagreement blocks new growth safely and alerts, rather than purging unknown user data. | +| **D07 Bounded exports/history growth** | M per export family. Account export first; jobs export separately if needed. | Measure synthetic 1k/10k owner histories, enforce peak-memory/request bounds, stream JSON/rows into the current ZIP/output as needed. Add conservative growth admission where required. Test account export remains available for existing over-quota data. History pruning is excluded until Connor approves restore promises. | D05/D06 for resource contract; R03 for any persistent limits. Do not jump directly to a queued export platform or optimize 100k rows without evidence. No destructive retention migration. | + +**Batch stop condition:** stale client writes cannot win silently; histories remain complete/unambiguous; projection/children updates are atomic; creation retries have a stated contract; bytes cannot exceed admission through concurrency or omitted producers. Both providers need meaningful relational evidence. Preserve all pre-migration data and safe old-client behavior. + +## Batch 3 — Paid account lifecycle + +**Finding:** F02, with retention/deletion policy and conditional premium worker dependencies. Correct entitlement and deletion behavior precedes billing UX. This batch can be prepared earlier, but paid behavior stays gated until it is complete. + +| Order / ticket | Size and ownership | Concrete scope and acceptance | Dependencies / rollback risk | +|---|---|---|---| +| **P01 Purchase intent** | M. Billing checkout/gateway, narrow persisted attempt and billing tests. | Stable purchase-attempt identity reused after double-click/timeout, provider idempotency and explicit handling of an existing/pending subscription. Two simultaneous requests and an unknown first result cannot create a second intended purchase. | R03; reuse D04's intent principles, not necessarily its table. Billing schema integration has one owner. Retain attempt/customer IDs during rollback; disable new checkout on unresolved state. | +| **P02 Entitlement reconciliation** | M. Billing handler, entitlement derivation, local transaction/reconciliation and tests. | Reconcile relevant current customer subscriptions, not whichever event's subscription was processed last. Atomically commit local provider state and role consequences; failures cause safe retry. Test active+cancelled subscriptions, delayed/out-of-order/concurrent events, duplicate delivery and failure between local writes. | P01/local identity contract. A synchronous design with provider retries is acceptable; durable inbox only if acknowledging deferred work. Reconciliation must recover missed delivery. Old images cannot safely reinterpret a new billing ledger without a reviewed compatibility plan. | +| **P03 Deletion versus paid lifecycle** | M. Existing deletion stages, billing cancellation seam, UI confirmation only as required and lifecycle tests. | First guard active/uncertain billing and pending checkout. Then implement the chosen cancellation/access policy; retain provider identifiers until confirmed resolution. Retry unavailable provider safely, handle deletion racing checkout/webhooks, and preserve minimal approved billing/deletion records. | P01/P02 + Connor's deletion/retention policy. Sandbox must prove the charged-account scenario. Do not erase the identity then attempt cancellation. Preserve pending stages across rollback; suspend new deletion admission if reconciliation is unhealthy. | + +**Batch stop condition:** authorized Stripe sandbox confirms one intended purchase, correct entitlement, provider retry/reconciliation and no continuing unresolved charge after completed deletion. Disabled deletion is a temporary containment state, not completion. Enabled premium features also require R05 where applicable. No paid-beta admission until these conditions and recovery/accessibility gates hold. + +## Batch 4 — Accessibility and shared design foundations + +**Finding:** F14 plus the test coverage gap. These tickets may begin in parallel early on their own files; their integration must preserve B02/D01 form behavior. Structural fixes come before cosmetic redesign. + +| Order / ticket | Size and ownership | Concrete scope and acceptance | Dependencies / rollback risk | +|---|---|---|---| +| **A01 Semantic theme/component colors** | S/M. `src/theme.ts`, necessary shared MUI overrides and loaded-state accessibility fixtures. | Correct small-text success/primary contrast in light/dark default/hover/focus/selected states. Check actual foreground/background pairs across chips, tabs and outlined buttons; preserve filled-button contrast. | Can proceed independently of backend work. No schema. Theme changes have broad visual impact: inspect both themes on existing pages before integration. Do not change every page or replace MUI. | +| **A02 Builder preview keyboard structure** | S. `CvBuilderEditor` preview region and focused browser tests. | Make the overflowing preview region named/focusable as appropriate; prove keyboard scrolling, focus visibility and no trap for iframe/page content. Include mobile→desktop transition, one/multiple pages and editing/preview controls. | A01 can proceed independently, but do not edit this file simultaneously with D01. No schema; avoid blanket tabindex additions. | +| **A03 Accessibility coverage and manual gate** | M test/documentation session; defects found become bounded follow-ups. | Loaded builder/profile/job detail + key dialogs/errors, EN/NB, both themes, mobile and desktop. Wait for meaningful data, not merely main. Complete keyboard/NVDA-or-Narrator, zoom/reflow/text spacing, forced colors and focus/error/reauthentication checks. | A01/A02 and stable B02/D01 conflicts. Automated success cannot substitute for manual evidence. No new accessibility compliance claim from axe alone. | + +**Batch stop condition:** primary workflows, save/conflict recovery and preview are operable by keyboard with usable focus and announcements; shared contrast fixes and representative loaded-state axe coverage pass. New primary-flow structural defects block public exposure; purely cosmetic work does not. + +## Batch 5 — Product and workflow corrections + +**Findings:** F21, F07, F15, F13, F16 and F09. These are scoped product/operational fixes, not grounds for a simultaneous whole-app rewrite. + +| Order / ticket | Size and ownership | Concrete scope and acceptance | Dependencies / rollback risk | +|---|---|---|---| +| **W01 Date-only calendar contract** | S/M. Calendar shared event conversion/provider-link/ICS tests and minimal labels if necessary. | Follow-ups/deadlines remain dates and export all-day with correct exclusive end date. Test UTC control, offset-bearing inputs separately, DST transitions and both database/server timezone settings. | Can prepare early; avoid database date migration by default. Resolve any real timed-field evidence separately. Existing external events will not be magically corrected; describe behavior for newly exported events. | +| **W02 Reminder outcome semantics** | M. Reminder worker/sender result contract and existing durable email-attempt seam where reusable. | Distinguish suppressed, accepted, failed and unknown; persist per-reminder outcome/dedup intent. Test later-send failure, commit failure, disabled sender and uncertain SMTP result. Auth resend remains recoverable; do not promise exactly-once SMTP. | Use R05 failure-boundary convention and R03 before any attempt schema. No second email platform. Provider sandbox/local mailbox sink before enabling revised sending. Unknown sends are not blindly replayed. | +| **W03 Sensitive response cache policy** | S. Private export/document/auth response policy and header tests. | Apply appropriate no-store to sensitive personalized responses, retain public/shared route semantics and verify proxy behavior. Handle CSP separately if it affects preview frames. | B02 is independently required; headers do not fix stale React state. No schema. Test public CV/static assets and PDF preview before changing global headers. | +| **W04 Admin event/data minimization** | M. Admin diagnostics and security mutation metadata; targeted logging cleanup may be a separate S ticket. | Minimize unnecessary private fields, record high-impact actor/action/target/outcome, avoid full URLs/documents/provider errors in logs, test role revocation/denied verbs. Preserve intentional admin capabilities. | Connor's actual support/retention policy; R03 before new audit storage. No speculative support role, impersonation or tamper-proof claims. Audit record migration must retain existing evidence. | +| **W05 Draft recovery decision and narrow pilot** | M, one editor only. Existing draft/dirty-state helpers and the chosen editor. | Keep warnings and in-memory edits; if approved, add opt-in owner/schema/expiry-scoped local recovery with explicit restore. Test offline, expired session, denied/quota storage, account change and conflicting revision. | **Must follow B02 and D01/D02**, plus Connor's local-content decision. No service worker/PWA or indiscriminate private caches. Rollback must not mix or silently apply old drafts. | + +**Batch stop condition:** each workflow's narrower acceptance passes; no silent time shift, suppressed-as-delivered message, ownerless draft or accidental private logging. Decisions about retention/local drafts remain explicit rather than hidden in implementation defaults. + +## Batch 6 — Broader UX and measured operational hardening + +**Findings:** remaining F16/F18 and measured F10 scale concerns; original product proposals with no reproduced blocker. CPU admission, quota safety, restore and worker containment are already in earlier batches. + +| Order / ticket | Size and ownership | Concrete scope and acceptance | Dependencies / rollback risk | +|---|---|---|---| +| **U01 Route-by-route UX correction** | S/M per chosen workflow, one page/interaction family. | Prioritize observed task completion problems, terminology and empty/error/loading states. Preserve owner/revision/focus primitives. Test the full user task in EN/NB and both themes. | B02/D01–D03/A01–A03. Do not redesign every route in one session; no schema unless separately scoped. Revert one route safely. | +| **O01 Reproducible release inputs** | S/M per build ecosystem. API Docker restore/SDK inputs first, then other lock/pin/scan evidence. | Actual image build honors intended SDK/build props and lock files; retain release provenance and an explicit advisory exception policy. Verify supported versions/current advisories only when external access is authorized. | Coordinate with R04 Dockerfile and R01 CI ownership; do not combine broad runtime upgrades with audit repairs. Rebuild/test the changed artifact and preserve known-good images. | +| **O02 Capacity, monitoring and operator drills** | M per measured bottleneck or drill. | Measure representative rows/export sizes/query plans/RSS; fix demonstrated hotspots, alert on storage, restored-backup age, worker health and billing divergence. Validate support/deletion/key rotation/incident procedures. | Early bounds and recovery already complete. Tune limits with evidence; no speculative Redis, broker, read replica or service split. No destructive load against production. | + +**Batch stop condition:** release evidence matches the intended audience and enabled features. Public paid readiness depends on verified real operational/support behavior, not completion of every cosmetic item in the original audit inventory. + +## Safe parallel work and prohibited overlap + +- B01 HTTP boundary, B02 identity analysis/tests, B03 exporter, B05 CSV, R01a typing, R02 synthetic backup fixture and A01 theme can proceed independently with assigned file ownership. Shared `Program.cs`, CI and Compose edits still integrate sequentially. +- B07 follows the address-policy decision in B01. Do not develop competing outbound-policy helpers or two versions of the same HTTP handler. +- Only **one migration/context snapshot owner** integrates at a time. D01 → D02 → D03a → D04 → D05/P01 schema commits are serialized even if isolated test/client work is prepared in parallel. +- D03b may be prepared against D03a's agreed API but cannot independently change the protocol. P01/P02/P03 share billing state and must integrate in that order; do not build billing UX against unsettled entitlement rules. +- B02/auth/editor lifecycle must settle before W05 persistent drafts. D01 and A02 both touch `CvBuilderEditor`: serialize edits or explicitly divide non-overlapping responsibility and review their combined behavior. +- A01 shared tokens precedes U01 visual redesign. A03 final accessibility evidence follows the combined form/identity/conflict changes, even if fixtures are written earlier. +- R02/R03 may run alongside non-schema security fixes. Their failure blocks migration/deployment advancement; it does not block safe source inspection or independent unit-test preparation. + +## B01 first-session handoff — completed 2026-09-08 (scope retained) + +**Take B01 only. Objective:** prevent HTTP job import from connecting to private/local/link-local destinations through mapped IPv4 or a validation-versus-connection DNS change, while preserving legitimate imports. + +1. Read the validated F11/V02 evidence and current `JobImportService`, `IHostAddressResolver`, named import HttpClient registration and existing import tests. Reproduce the mapped-address guard failure with a non-network fake. +2. Add the narrow mapped-address normalization and move/enforce policy at the actual connection boundary. Prefer the existing .NET HTTP/socket facilities; preserve original hostname/SNI/certificate checks, redirects disabled, bounded response size and cancellation. Do not modify unrelated HttpClients or add a network framework. +3. Add regression tests for IPv4/IPv6/mapped/loopback/link-local/mixed answers, differing resolution answers, allowed address, timeout/cancel and TLS hostname behavior. Use controlled local/fake transport; no real private host/metadata or third-party DNS calls. A validator-only unit test does not prove the DNS race is closed. +4. Run the focused backend import/network tests and the affected API build. Review the diff for accidental request, TLS, proxy or parser changes; document any deployment egress assumptions and the remaining IMAP ticket. + +**Owned files:** import service and a narrowly justified adjacent address/connection helper, the named import HTTP registration in `Program.cs`, and related tests. **Excluded:** IMAP implementation (B07), CV/billing/schema/UI changes, deployment, global policy rewrites and dependency upgrades. No migration. If safe connected-peer enforcement cannot be verified locally, retain the passing normalization fix as a bounded partial change but leave B01/F11 open; do not claim validator-only protection closes SSRF. + +**Handoff acceptance:** the retained exploit fixtures fail against the original and pass against the change; connection-time tests prove no denied peer is attempted; allowed requests still behave correctly; no TLS relaxation, source data change or unrelated edits. These criteria now pass for B01; see the [completion record](b01-completion.md). Subsequently authorized B02–B05 also pass their local acceptance criteria with documented limits; see their completion records above. B06 was subsequently authorized and implemented, with local topology and production verification still pending. Complete those gates before closure; B07 and the broad overhaul remain unstarted. diff --git a/docs/audits/2026-09-07/verification.md b/docs/_archive/pre-docs-cleanup-2026-09-11/docs/audits/2026-09-07/verification.md similarity index 100% rename from docs/audits/2026-09-07/verification.md rename to docs/_archive/pre-docs-cleanup-2026-09-11/docs/audits/2026-09-07/verification.md diff --git a/docs/audits/audit-progress.md b/docs/_archive/pre-docs-cleanup-2026-09-11/docs/audits/audit-progress.md similarity index 100% rename from docs/audits/audit-progress.md rename to docs/_archive/pre-docs-cleanup-2026-09-11/docs/audits/audit-progress.md diff --git a/docs/audits/audit-remediation-backlog.md b/docs/_archive/pre-docs-cleanup-2026-09-11/docs/audits/audit-remediation-backlog.md similarity index 100% rename from docs/audits/audit-remediation-backlog.md rename to docs/_archive/pre-docs-cleanup-2026-09-11/docs/audits/audit-remediation-backlog.md diff --git a/docs/audits/evidence/accessibility-evidence.md b/docs/_archive/pre-docs-cleanup-2026-09-11/docs/audits/evidence/accessibility-evidence.md similarity index 100% rename from docs/audits/evidence/accessibility-evidence.md rename to docs/_archive/pre-docs-cleanup-2026-09-11/docs/audits/evidence/accessibility-evidence.md diff --git a/docs/audits/evidence/ai-001/README.md b/docs/_archive/pre-docs-cleanup-2026-09-11/docs/audits/evidence/ai-001/README.md similarity index 100% rename from docs/audits/evidence/ai-001/README.md rename to docs/_archive/pre-docs-cleanup-2026-09-11/docs/audits/evidence/ai-001/README.md diff --git a/docs/audits/evidence/ai-002/README.md b/docs/_archive/pre-docs-cleanup-2026-09-11/docs/audits/evidence/ai-002/README.md similarity index 100% rename from docs/audits/evidence/ai-002/README.md rename to docs/_archive/pre-docs-cleanup-2026-09-11/docs/audits/evidence/ai-002/README.md diff --git a/docs/audits/evidence/ai-003/README.md b/docs/_archive/pre-docs-cleanup-2026-09-11/docs/audits/evidence/ai-003/README.md similarity index 100% rename from docs/audits/evidence/ai-003/README.md rename to docs/_archive/pre-docs-cleanup-2026-09-11/docs/audits/evidence/ai-003/README.md diff --git a/docs/audits/evidence/ai-004/README.md b/docs/_archive/pre-docs-cleanup-2026-09-11/docs/audits/evidence/ai-004/README.md similarity index 100% rename from docs/audits/evidence/ai-004/README.md rename to docs/_archive/pre-docs-cleanup-2026-09-11/docs/audits/evidence/ai-004/README.md diff --git a/docs/audits/evidence/browser-evidence.md b/docs/_archive/pre-docs-cleanup-2026-09-11/docs/audits/evidence/browser-evidence.md similarity index 100% rename from docs/audits/evidence/browser-evidence.md rename to docs/_archive/pre-docs-cleanup-2026-09-11/docs/audits/evidence/browser-evidence.md diff --git a/docs/audits/evidence/career-001/README.md b/docs/_archive/pre-docs-cleanup-2026-09-11/docs/audits/evidence/career-001/README.md similarity index 100% rename from docs/audits/evidence/career-001/README.md rename to docs/_archive/pre-docs-cleanup-2026-09-11/docs/audits/evidence/career-001/README.md diff --git a/docs/audits/evidence/career-002/README.md b/docs/_archive/pre-docs-cleanup-2026-09-11/docs/audits/evidence/career-002/README.md similarity index 100% rename from docs/audits/evidence/career-002/README.md rename to docs/_archive/pre-docs-cleanup-2026-09-11/docs/audits/evidence/career-002/README.md diff --git a/docs/audits/evidence/core-001-runtime/synthetic-attachment.txt b/docs/_archive/pre-docs-cleanup-2026-09-11/docs/audits/evidence/core-001-runtime/synthetic-attachment.txt similarity index 100% rename from docs/audits/evidence/core-001-runtime/synthetic-attachment.txt rename to docs/_archive/pre-docs-cleanup-2026-09-11/docs/audits/evidence/core-001-runtime/synthetic-attachment.txt diff --git a/docs/audits/evidence/dependency-evidence.md b/docs/_archive/pre-docs-cleanup-2026-09-11/docs/audits/evidence/dependency-evidence.md similarity index 100% rename from docs/audits/evidence/dependency-evidence.md rename to docs/_archive/pre-docs-cleanup-2026-09-11/docs/audits/evidence/dependency-evidence.md diff --git a/docs/audits/evidence/mail-001/README.md b/docs/_archive/pre-docs-cleanup-2026-09-11/docs/audits/evidence/mail-001/README.md similarity index 100% rename from docs/audits/evidence/mail-001/README.md rename to docs/_archive/pre-docs-cleanup-2026-09-11/docs/audits/evidence/mail-001/README.md diff --git a/docs/audits/evidence/pol-001/README.md b/docs/_archive/pre-docs-cleanup-2026-09-11/docs/audits/evidence/pol-001/README.md similarity index 100% rename from docs/audits/evidence/pol-001/README.md rename to docs/_archive/pre-docs-cleanup-2026-09-11/docs/audits/evidence/pol-001/README.md diff --git a/docs/audits/evidence/pol-002/README.md b/docs/_archive/pre-docs-cleanup-2026-09-11/docs/audits/evidence/pol-002/README.md similarity index 100% rename from docs/audits/evidence/pol-002/README.md rename to docs/_archive/pre-docs-cleanup-2026-09-11/docs/audits/evidence/pol-002/README.md diff --git a/docs/audits/evidence/prod-002/README.md b/docs/_archive/pre-docs-cleanup-2026-09-11/docs/audits/evidence/prod-002/README.md similarity index 100% rename from docs/audits/evidence/prod-002/README.md rename to docs/_archive/pre-docs-cleanup-2026-09-11/docs/audits/evidence/prod-002/README.md diff --git a/docs/audits/evidence/qa-001/README.md b/docs/_archive/pre-docs-cleanup-2026-09-11/docs/audits/evidence/qa-001/README.md similarity index 100% rename from docs/audits/evidence/qa-001/README.md rename to docs/_archive/pre-docs-cleanup-2026-09-11/docs/audits/evidence/qa-001/README.md diff --git a/docs/audits/evidence/repository-inventory.md b/docs/_archive/pre-docs-cleanup-2026-09-11/docs/audits/evidence/repository-inventory.md similarity index 100% rename from docs/audits/evidence/repository-inventory.md rename to docs/_archive/pre-docs-cleanup-2026-09-11/docs/audits/evidence/repository-inventory.md diff --git a/docs/audits/evidence/runtime-evidence.md b/docs/_archive/pre-docs-cleanup-2026-09-11/docs/audits/evidence/runtime-evidence.md similarity index 100% rename from docs/audits/evidence/runtime-evidence.md rename to docs/_archive/pre-docs-cleanup-2026-09-11/docs/audits/evidence/runtime-evidence.md diff --git a/docs/audits/evidence/two-user-isolation.md b/docs/_archive/pre-docs-cleanup-2026-09-11/docs/audits/evidence/two-user-isolation.md similarity index 100% rename from docs/audits/evidence/two-user-isolation.md rename to docs/_archive/pre-docs-cleanup-2026-09-11/docs/audits/evidence/two-user-isolation.md diff --git a/docs/audits/evidence/ux-001/README.md b/docs/_archive/pre-docs-cleanup-2026-09-11/docs/audits/evidence/ux-001/README.md similarity index 100% rename from docs/audits/evidence/ux-001/README.md rename to docs/_archive/pre-docs-cleanup-2026-09-11/docs/audits/evidence/ux-001/README.md diff --git a/docs/audits/evidence/ux-002/README.md b/docs/_archive/pre-docs-cleanup-2026-09-11/docs/audits/evidence/ux-002/README.md similarity index 100% rename from docs/audits/evidence/ux-002/README.md rename to docs/_archive/pre-docs-cleanup-2026-09-11/docs/audits/evidence/ux-002/README.md diff --git a/docs/audits/full-application-audit.md b/docs/_archive/pre-docs-cleanup-2026-09-11/docs/audits/full-application-audit.md similarity index 100% rename from docs/audits/full-application-audit.md rename to docs/_archive/pre-docs-cleanup-2026-09-11/docs/audits/full-application-audit.md diff --git a/docs/audits/security-threat-model.md b/docs/_archive/pre-docs-cleanup-2026-09-11/docs/audits/security-threat-model.md similarity index 100% rename from docs/audits/security-threat-model.md rename to docs/_archive/pre-docs-cleanup-2026-09-11/docs/audits/security-threat-model.md diff --git a/docs/audits/user-journey-audit.md b/docs/_archive/pre-docs-cleanup-2026-09-11/docs/audits/user-journey-audit.md similarity index 100% rename from docs/audits/user-journey-audit.md rename to docs/_archive/pre-docs-cleanup-2026-09-11/docs/audits/user-journey-audit.md diff --git a/docs/audits/verification-log.md b/docs/_archive/pre-docs-cleanup-2026-09-11/docs/audits/verification-log.md similarity index 100% rename from docs/audits/verification-log.md rename to docs/_archive/pre-docs-cleanup-2026-09-11/docs/audits/verification-log.md diff --git a/docs/auth/2fa.md b/docs/_archive/pre-docs-cleanup-2026-09-11/docs/auth/2fa.md similarity index 100% rename from docs/auth/2fa.md rename to docs/_archive/pre-docs-cleanup-2026-09-11/docs/auth/2fa.md diff --git a/docs/auth/login.md b/docs/_archive/pre-docs-cleanup-2026-09-11/docs/auth/login.md similarity index 100% rename from docs/auth/login.md rename to docs/_archive/pre-docs-cleanup-2026-09-11/docs/auth/login.md diff --git a/docs/auth/oauth.md b/docs/_archive/pre-docs-cleanup-2026-09-11/docs/auth/oauth.md similarity index 100% rename from docs/auth/oauth.md rename to docs/_archive/pre-docs-cleanup-2026-09-11/docs/auth/oauth.md diff --git a/docs/auth/overview.md b/docs/_archive/pre-docs-cleanup-2026-09-11/docs/auth/overview.md similarity index 100% rename from docs/auth/overview.md rename to docs/_archive/pre-docs-cleanup-2026-09-11/docs/auth/overview.md diff --git a/docs/auth/signup.md b/docs/_archive/pre-docs-cleanup-2026-09-11/docs/auth/signup.md similarity index 100% rename from docs/auth/signup.md rename to docs/_archive/pre-docs-cleanup-2026-09-11/docs/auth/signup.md diff --git a/docs/career-workspace-branch-assessment.md b/docs/_archive/pre-docs-cleanup-2026-09-11/docs/career-workspace-branch-assessment.md similarity index 100% rename from docs/career-workspace-branch-assessment.md rename to docs/_archive/pre-docs-cleanup-2026-09-11/docs/career-workspace-branch-assessment.md diff --git a/docs/career-workspace-implementation-roadmap.md b/docs/_archive/pre-docs-cleanup-2026-09-11/docs/career-workspace-implementation-roadmap.md similarity index 100% rename from docs/career-workspace-implementation-roadmap.md rename to docs/_archive/pre-docs-cleanup-2026-09-11/docs/career-workspace-implementation-roadmap.md diff --git a/docs/career-workspace-product-strategy.md b/docs/_archive/pre-docs-cleanup-2026-09-11/docs/career-workspace-product-strategy.md similarity index 100% rename from docs/career-workspace-product-strategy.md rename to docs/_archive/pre-docs-cleanup-2026-09-11/docs/career-workspace-product-strategy.md diff --git a/docs/career-workspace-ux-refactor.md b/docs/_archive/pre-docs-cleanup-2026-09-11/docs/career-workspace-ux-refactor.md similarity index 100% rename from docs/career-workspace-ux-refactor.md rename to docs/_archive/pre-docs-cleanup-2026-09-11/docs/career-workspace-ux-refactor.md diff --git a/docs/career/career-profile.md b/docs/_archive/pre-docs-cleanup-2026-09-11/docs/career/career-profile.md similarity index 100% rename from docs/career/career-profile.md rename to docs/_archive/pre-docs-cleanup-2026-09-11/docs/career/career-profile.md diff --git a/docs/career/cover-letters.md b/docs/_archive/pre-docs-cleanup-2026-09-11/docs/career/cover-letters.md similarity index 100% rename from docs/career/cover-letters.md rename to docs/_archive/pre-docs-cleanup-2026-09-11/docs/career/cover-letters.md diff --git a/docs/career/cv-ai.md b/docs/_archive/pre-docs-cleanup-2026-09-11/docs/career/cv-ai.md similarity index 100% rename from docs/career/cv-ai.md rename to docs/_archive/pre-docs-cleanup-2026-09-11/docs/career/cv-ai.md diff --git a/docs/career/cv-rendering.md b/docs/_archive/pre-docs-cleanup-2026-09-11/docs/career/cv-rendering.md similarity index 100% rename from docs/career/cv-rendering.md rename to docs/_archive/pre-docs-cleanup-2026-09-11/docs/career/cv-rendering.md diff --git a/docs/career/cv-tailoring.md b/docs/_archive/pre-docs-cleanup-2026-09-11/docs/career/cv-tailoring.md similarity index 100% rename from docs/career/cv-tailoring.md rename to docs/_archive/pre-docs-cleanup-2026-09-11/docs/career/cv-tailoring.md diff --git a/docs/career/cv-themes.md b/docs/_archive/pre-docs-cleanup-2026-09-11/docs/career/cv-themes.md similarity index 100% rename from docs/career/cv-themes.md rename to docs/_archive/pre-docs-cleanup-2026-09-11/docs/career/cv-themes.md diff --git a/docs/career/documents.md b/docs/_archive/pre-docs-cleanup-2026-09-11/docs/career/documents.md similarity index 100% rename from docs/career/documents.md rename to docs/_archive/pre-docs-cleanup-2026-09-11/docs/career/documents.md diff --git a/docs/career/education.md b/docs/_archive/pre-docs-cleanup-2026-09-11/docs/career/education.md similarity index 100% rename from docs/career/education.md rename to docs/_archive/pre-docs-cleanup-2026-09-11/docs/career/education.md diff --git a/docs/career/experience.md b/docs/_archive/pre-docs-cleanup-2026-09-11/docs/career/experience.md similarity index 100% rename from docs/career/experience.md rename to docs/_archive/pre-docs-cleanup-2026-09-11/docs/career/experience.md diff --git a/docs/career/future-features.md b/docs/_archive/pre-docs-cleanup-2026-09-11/docs/career/future-features.md similarity index 100% rename from docs/career/future-features.md rename to docs/_archive/pre-docs-cleanup-2026-09-11/docs/career/future-features.md diff --git a/docs/career/master-profile.md b/docs/_archive/pre-docs-cleanup-2026-09-11/docs/career/master-profile.md similarity index 100% rename from docs/career/master-profile.md rename to docs/_archive/pre-docs-cleanup-2026-09-11/docs/career/master-profile.md diff --git a/docs/career/overview.md b/docs/_archive/pre-docs-cleanup-2026-09-11/docs/career/overview.md similarity index 100% rename from docs/career/overview.md rename to docs/_archive/pre-docs-cleanup-2026-09-11/docs/career/overview.md diff --git a/docs/career/portfolio.md b/docs/_archive/pre-docs-cleanup-2026-09-11/docs/career/portfolio.md similarity index 100% rename from docs/career/portfolio.md rename to docs/_archive/pre-docs-cleanup-2026-09-11/docs/career/portfolio.md diff --git a/docs/career/projects.md b/docs/_archive/pre-docs-cleanup-2026-09-11/docs/career/projects.md similarity index 100% rename from docs/career/projects.md rename to docs/_archive/pre-docs-cleanup-2026-09-11/docs/career/projects.md diff --git a/docs/career/public-profile.md b/docs/_archive/pre-docs-cleanup-2026-09-11/docs/career/public-profile.md similarity index 100% rename from docs/career/public-profile.md rename to docs/_archive/pre-docs-cleanup-2026-09-11/docs/career/public-profile.md diff --git a/docs/career/skills.md b/docs/_archive/pre-docs-cleanup-2026-09-11/docs/career/skills.md similarity index 100% rename from docs/career/skills.md rename to docs/_archive/pre-docs-cleanup-2026-09-11/docs/career/skills.md diff --git a/docs/career/versioning.md b/docs/_archive/pre-docs-cleanup-2026-09-11/docs/career/versioning.md similarity index 100% rename from docs/career/versioning.md rename to docs/_archive/pre-docs-cleanup-2026-09-11/docs/career/versioning.md diff --git a/docs/cover-letters/ai.md b/docs/_archive/pre-docs-cleanup-2026-09-11/docs/cover-letters/ai.md similarity index 100% rename from docs/cover-letters/ai.md rename to docs/_archive/pre-docs-cleanup-2026-09-11/docs/cover-letters/ai.md diff --git a/docs/cover-letters/builder.md b/docs/_archive/pre-docs-cleanup-2026-09-11/docs/cover-letters/builder.md similarity index 100% rename from docs/cover-letters/builder.md rename to docs/_archive/pre-docs-cleanup-2026-09-11/docs/cover-letters/builder.md diff --git a/docs/cover-letters/overview.md b/docs/_archive/pre-docs-cleanup-2026-09-11/docs/cover-letters/overview.md similarity index 100% rename from docs/cover-letters/overview.md rename to docs/_archive/pre-docs-cleanup-2026-09-11/docs/cover-letters/overview.md diff --git a/docs/cv-builder-competitor-deep-research.md b/docs/_archive/pre-docs-cleanup-2026-09-11/docs/cv-builder-competitor-deep-research.md similarity index 100% rename from docs/cv-builder-competitor-deep-research.md rename to docs/_archive/pre-docs-cleanup-2026-09-11/docs/cv-builder-competitor-deep-research.md diff --git a/docs/cv-builder-product-teardown.md b/docs/_archive/pre-docs-cleanup-2026-09-11/docs/cv-builder-product-teardown.md similarity index 100% rename from docs/cv-builder-product-teardown.md rename to docs/_archive/pre-docs-cleanup-2026-09-11/docs/cv-builder-product-teardown.md diff --git a/docs/cv-builder/ai.md b/docs/_archive/pre-docs-cleanup-2026-09-11/docs/cv-builder/ai.md similarity index 100% rename from docs/cv-builder/ai.md rename to docs/_archive/pre-docs-cleanup-2026-09-11/docs/cv-builder/ai.md diff --git a/docs/cv-builder/content.md b/docs/_archive/pre-docs-cleanup-2026-09-11/docs/cv-builder/content.md similarity index 100% rename from docs/cv-builder/content.md rename to docs/_archive/pre-docs-cleanup-2026-09-11/docs/cv-builder/content.md diff --git a/docs/cv-builder/customisation.md b/docs/_archive/pre-docs-cleanup-2026-09-11/docs/cv-builder/customisation.md similarity index 100% rename from docs/cv-builder/customisation.md rename to docs/_archive/pre-docs-cleanup-2026-09-11/docs/cv-builder/customisation.md diff --git a/docs/cv-builder/exports.md b/docs/_archive/pre-docs-cleanup-2026-09-11/docs/cv-builder/exports.md similarity index 100% rename from docs/cv-builder/exports.md rename to docs/_archive/pre-docs-cleanup-2026-09-11/docs/cv-builder/exports.md diff --git a/docs/cv-builder/overview.md b/docs/_archive/pre-docs-cleanup-2026-09-11/docs/cv-builder/overview.md similarity index 100% rename from docs/cv-builder/overview.md rename to docs/_archive/pre-docs-cleanup-2026-09-11/docs/cv-builder/overview.md diff --git a/docs/cv-builder/rendering.md b/docs/_archive/pre-docs-cleanup-2026-09-11/docs/cv-builder/rendering.md similarity index 100% rename from docs/cv-builder/rendering.md rename to docs/_archive/pre-docs-cleanup-2026-09-11/docs/cv-builder/rendering.md diff --git a/docs/cv-builder/themes.md b/docs/_archive/pre-docs-cleanup-2026-09-11/docs/cv-builder/themes.md similarity index 100% rename from docs/cv-builder/themes.md rename to docs/_archive/pre-docs-cleanup-2026-09-11/docs/cv-builder/themes.md diff --git a/docs/cv-builder/workflow.md b/docs/_archive/pre-docs-cleanup-2026-09-11/docs/cv-builder/workflow.md similarity index 100% rename from docs/cv-builder/workflow.md rename to docs/_archive/pre-docs-cleanup-2026-09-11/docs/cv-builder/workflow.md diff --git a/docs/_archive/pre-docs-cleanup-2026-09-11/docs/decisions/ADR-002-job-application-model.md b/docs/_archive/pre-docs-cleanup-2026-09-11/docs/decisions/ADR-002-job-application-model.md new file mode 100644 index 0000000..49f41f0 --- /dev/null +++ b/docs/_archive/pre-docs-cleanup-2026-09-11/docs/decisions/ADR-002-job-application-model.md @@ -0,0 +1,113 @@ +# ADR-002 — Separating Job (the opportunity) from JobApplication (the pursuit) + +- **Status:** Accepted (partially implemented) +- **Date:** 2026-07-17 +- **Phase:** 0 (foundation corrections) +- **Supersedes:** the 0-byte placeholder previously at `docs/_archive/decisions/ADR-002-job-application-model.md` + +--- + +## Context + +`docs/MASTER_IMPLEMENTATION_GUIDE.md` names this as the product's #2 priority workflow: + +``` +Find Job → Import Job → Review Details → Prepare CV → Prepare Cover Letter + → Add Supporting Files → Submit Application → Track Progress +``` + +`docs/01-glossary.md` is explicit that "A Job may exist before an application is submitted" and models the hierarchy as `Company → Job Opportunity → Application`. + +**The code could not represent any of that.** The 2026-07-17 discovery audit found: + +1. There was no `Job` entity. `JobApplication` was the only entity, carrying 43 members that mixed opportunity data (`JobTitle`, `Description`, `JobUrl`, `Salary*`, `Deadline`, `Tags`, `Location`) with application data (`Status`, `DateApplied`, `ResponseReceived`, `FollowUpAt`). +2. `JobPipeline.Stages` began at `Applied`. There was no `Saved`/`Interested`/`Preparing`. +3. `DateApplied` was non-nullable with a `DateTime.UtcNow` default. + +Consequences: + +- A job you had not applied to could not be tracked. The 6-step add-job wizard (`components/AddJobModal.tsx`) walks a user through *preparing* an application and then had nowhere to save it except `Applied` with a fabricated date. +- Applying twice to the same reposted role duplicated the entire job description. +- Teal — the market leader — offers `Saved` in its **free** tier. This was a table-stakes gap. + +## Decision + +Split `Job` from `JobApplication`, **additively and in stages**. Phase 0 lays the foundation only. + +### Delivered in Phase 0 + +1. **`Job` entity** (`JobTrackerApi/Models/Job.cs`) — the opportunity: company, title, description, URL, location, salary, deadline, tags, plus `SavedAt`. Owner-scoped by the same deny-on-null global query filter as every other tenant entity. +2. **`JobApplication.JobId`** — a nullable FK to `Job`, `OnDelete: SetNull`. +3. **Prospect stages** — `JobPipeline` gained `PipelineCategory.Prospect` and the stages `Saved`(1), `Interested`(2), `Preparing`(3), ahead of `Applied`(4). No schema change: `Status` is a free-text column by deliberate prior design. +4. **`DateApplied` is now nullable**, and `SavedAt` was added to `JobApplication`. +5. **`JobPipeline.SyncAppliedDate`** — the single enforcement point for the invariant below. + +### Explicitly NOT done in Phase 0 + +- **No dual-write.** Nothing writes or reads `Job` yet. `JobApplication` remains the sole source of truth for every read and write. +- **No backfill of `Job` rows.** The table ships empty. +- **No legacy columns dropped.** `JobApplication` keeps its full copy of the opportunity fields. + +This keeps Phase 0 a pure schema-and-vocabulary change with **zero behavioural change** to existing workflows, which is what "unblock future phases safely" requires. + +### Phase 1 status (2026-07-31) + +Create and update paths now synchronize `Job`, and startup idempotently backfills a linked `Job` for every legacy application missing one. Reads and legacy columns remain unchanged until the production validation and observation release in `docs/operations/job-opportunity-cutover.md` complete. + +### The invariant + +> `DateApplied` is set **if and only if** the job has left the pre-application stages. + +Enforced in exactly one place — `JobPipeline.SyncAppliedDate(job, now)` — called from all three status-write paths (`POST /jobapplications`, `PUT /jobapplications/{id}`, `PATCH /jobapplications/{id}/status`) so they cannot drift. + +- Entering a real stage stamps `DateApplied` if unset. +- Moving **back** into a Prospect stage **clears** it. + +The backward clear is deliberate. The alternative — a `Saved` job still carrying an applied date — would silently count it as applied in analytics and expose it to the follow-up/ghosting rules. The original date stays recoverable from the `StatusChanged` `JobEvent` history, so this is denormalised-field loss, not data loss. + +## Consequences + +### Safe by construction + +- **`RulesEngine`** already whitelisted `Applied`/`Offer`/`Rejected`/`Waiting` and explicitly refused to ghost anything else, so prospects were never at risk. A `JobPipeline.IsProspect` guard was added anyway, ahead of any date arithmetic, plus a null-`DateApplied` guard on the `Applied` branch — a null must fail safe rather than read as "infinitely old" and auto-ghost the job. Covered by `RulesEngineProspectTests`. +- **`StageAnalytics`** already filtered to `PipelineCategory.Active`, so prospects drop out of time-in-stage automatically. + +### Deliberate behaviour changes + +- `DaysSince` is now `int?`, null for prospects. Returning `0` would render as "Applied 0 days ago" — a lie. The API DTO and the frontend `JobApplication` type follow; the UI renders `—`. +- Applied-volume analytics and average-days-since-applied now filter `DateApplied != null`, so prospects cannot drag the average toward zero or inflate applied counts. +- `JobFlowBar` omits the "Applied" milestone entirely when there is no applied date. +- Custom (non-canonical) statuses are **not** treated as prospects. They predate this split and have always counted as applied; assuming otherwise would silently drop them out of existing users' analytics. + +### Costs accepted + +- `JobApplication` temporarily carries both `JobId` and its own opportunity columns — real duplication, time-boxed to the Phase 1 cutover. +- An empty `Jobs` table ships. Preferred over a backfill that no code consumes and that a later, better-informed cutover might shape differently. + +## Migration notes (read before the next migration) + +`20260717071417_AddJobEntityAndProspectStages` was **hand-edited after scaffolding**. `dotnet ef migrations add` additionally emitted `CreateTable` for `TrustedDevices`, `TwoFactorRecoveryCodes`, `UserSessions` and `AddColumn` for six `AspNetUsers` columns (`Microsoft*`, `Totp*`). + +Those tables **already exist in every real database** — they were provisioned by the idempotent reconciler in `StartupInitializationExtensions`, not by a migration, so the prior `ModelSnapshot` did not know them and the scaffolder diffed them as missing. Verified directly against the live dev database: all three tables are present. Leaving the scaffolded statements in would have failed the deploy with "table already exists". They were removed; the reconciler still creates them on a fresh boot via `CREATE TABLE IF NOT EXISTS`. + +`IX_JobApplications_OwnerUserId_IsDeleted_Status` was removed for the same reason: the reconciler applies it, and MySQL needs a `Status(50)` prefix length that the scaffolded DDL does not carry (see the comment in `JobTrackerContext.OnModelCreating`). + +**The regenerated snapshot now includes those tables, so future migrations will not re-scaffold them.** This ADR's migration is the one that closes that drift. + +EF emits a warning that the `SavedAt` backfill `UPDATE` runs while a rebuild of `JobApplications` is pending. Verified empirically against a copy of the real dev database (13 rows): the migration applies cleanly, all 36 pre-existing columns survive the rebuild, `DateApplied` values are preserved, `SavedAt` backfills correctly with no `0001-01-01` sentinels remaining, and `DateApplied` ends up nullable. + +## Phase 1 cutover plan + +1. **Done:** dual-write `Job` on every persistence path (`JobApplicationsController` and Gmail suggestion creation; CSV import is preview-only). +2. **Done:** backfill one `Job` per existing `JobApplication`; link via `JobId`. +3. Flip reads to `Job`, one endpoint at a time. +4. Drop the duplicated opportunity columns from `JobApplication`. +5. Make `JobId` non-nullable. + +A `Job` with no `JobApplication` row is the eventual clean representation of a saved-but-not-applied job. Until step 4, the Prospect stages on `JobApplication.Status` carry that meaning instead. + +## Alternatives considered + +- **Full split in Phase 0.** Rejected: touches 38 endpoints, the Kanban, the table, the rules engine and the wizard at once, with no staging environment and a straight-to-prod deploy. The discovery report ranked this the highest-risk item in the plan. +- **Prospect stages only, no `Job` entity.** Would have unblocked the workflow, but leaves the duplication that makes applying twice to one role copy the whole description, and defers the schema foundation the roadmap's Phase 3/4 depend on. +- **Keep `DateApplied` non-nullable, add `SavedAt` alongside.** Rejected: a saved job would still carry a fabricated applied date — precisely the defect this ADR exists to remove. diff --git a/docs/_archive/pre-docs-cleanup-2026-09-11/docs/decisions/ADR-004-ai-provider-system.md b/docs/_archive/pre-docs-cleanup-2026-09-11/docs/decisions/ADR-004-ai-provider-system.md new file mode 100644 index 0000000..2d8a853 --- /dev/null +++ b/docs/_archive/pre-docs-cleanup-2026-09-11/docs/decisions/ADR-004-ai-provider-system.md @@ -0,0 +1,23 @@ +# ADR-004 — Deployment-selected AI provider + +- **Status:** Accepted +- **Date:** 2026-07-30 +- **Phase:** 5 (AI improvements) + +## Context + +The AI sidecar supports Ollama, Gemini, and Groq. Earlier documentation described a per-request provider abstraction with OpenAI, Claude, administrator controls, and user choice; none of those capabilities exist. Adding them now would increase credential handling, privacy exposure, testing, and billing complexity without demonstrated customer demand. + +## Decision + +Each deployment selects exactly one provider through `AI_PROVIDER`. The .NET application continues to call the private `ai-service` through `ISummarizerService`; application code does not branch on provider. Ollama remains the local privacy-first option. Gemini and Groq are operator-selected cloud alternatives. + +Provider choice is not exposed to users or administrators. Revisit this only when a customer requires provider choice and the deployment has explicit credential, privacy, quota, and audit rules for every enabled provider. + +## Consequences + +- One provider configuration and failure mode per deployment. +- No OpenAI or Claude support is implied. +- AI results remain suggestions that require user review. +- Usage metering belongs at the application interaction boundary, independent of provider. +- Cloud-provider privacy and cost are deployment responsibilities until SaaS quotas are introduced. diff --git a/docs/deferred-inputs.md b/docs/_archive/pre-docs-cleanup-2026-09-11/docs/deferred-inputs.md similarity index 100% rename from docs/deferred-inputs.md rename to docs/_archive/pre-docs-cleanup-2026-09-11/docs/deferred-inputs.md diff --git a/docs/deployment/backup-restore.md b/docs/_archive/pre-docs-cleanup-2026-09-11/docs/deployment/backup-restore.md similarity index 100% rename from docs/deployment/backup-restore.md rename to docs/_archive/pre-docs-cleanup-2026-09-11/docs/deployment/backup-restore.md diff --git a/docs/deployment/manual-smoke-test.md b/docs/_archive/pre-docs-cleanup-2026-09-11/docs/deployment/manual-smoke-test.md similarity index 100% rename from docs/deployment/manual-smoke-test.md rename to docs/_archive/pre-docs-cleanup-2026-09-11/docs/deployment/manual-smoke-test.md diff --git a/docs/_archive/pre-docs-cleanup-2026-09-11/docs/documentation-rules.md b/docs/_archive/pre-docs-cleanup-2026-09-11/docs/documentation-rules.md new file mode 100644 index 0000000..cebc058 --- /dev/null +++ b/docs/_archive/pre-docs-cleanup-2026-09-11/docs/documentation-rules.md @@ -0,0 +1,9 @@ +# documentation-rules + +Active documentation must describe verified current behavior or a clearly labelled future decision. +Prefer one authoritative document plus short topic guides that link to it; do not copy long architecture +sections across files. Update behavior docs in the same change as code. + +Use `docs/architecture/current.md` for the verified system, `docs/implementation-roadmap.md` for delivery +status, `docs/MASTER_IMPLEMENTATION_GUIDE.md` for product constraints, and `BLOCKERS.md` for external +decisions or credentials. `docs/_archive` is historical evidence and must not be treated as current work. diff --git a/docs/implementation-roadmap.md b/docs/_archive/pre-docs-cleanup-2026-09-11/docs/implementation-roadmap.md similarity index 100% rename from docs/implementation-roadmap.md rename to docs/_archive/pre-docs-cleanup-2026-09-11/docs/implementation-roadmap.md diff --git a/docs/infrastructure/database-ownership.md b/docs/_archive/pre-docs-cleanup-2026-09-11/docs/infrastructure/database-ownership.md similarity index 100% rename from docs/infrastructure/database-ownership.md rename to docs/_archive/pre-docs-cleanup-2026-09-11/docs/infrastructure/database-ownership.md diff --git a/docs/infrastructure/runner-investigation.md b/docs/_archive/pre-docs-cleanup-2026-09-11/docs/infrastructure/runner-investigation.md similarity index 100% rename from docs/infrastructure/runner-investigation.md rename to docs/_archive/pre-docs-cleanup-2026-09-11/docs/infrastructure/runner-investigation.md diff --git a/docs/jobs/adding-jobs.md b/docs/_archive/pre-docs-cleanup-2026-09-11/docs/jobs/adding-jobs.md similarity index 100% rename from docs/jobs/adding-jobs.md rename to docs/_archive/pre-docs-cleanup-2026-09-11/docs/jobs/adding-jobs.md diff --git a/docs/jobs/analytics.md b/docs/_archive/pre-docs-cleanup-2026-09-11/docs/jobs/analytics.md similarity index 100% rename from docs/jobs/analytics.md rename to docs/_archive/pre-docs-cleanup-2026-09-11/docs/jobs/analytics.md diff --git a/docs/jobs/applications.md b/docs/_archive/pre-docs-cleanup-2026-09-11/docs/jobs/applications.md similarity index 100% rename from docs/jobs/applications.md rename to docs/_archive/pre-docs-cleanup-2026-09-11/docs/jobs/applications.md diff --git a/docs/jobs/communication-tracking.md b/docs/_archive/pre-docs-cleanup-2026-09-11/docs/jobs/communication-tracking.md similarity index 100% rename from docs/jobs/communication-tracking.md rename to docs/_archive/pre-docs-cleanup-2026-09-11/docs/jobs/communication-tracking.md diff --git a/docs/jobs/company-management.md b/docs/_archive/pre-docs-cleanup-2026-09-11/docs/jobs/company-management.md similarity index 100% rename from docs/jobs/company-management.md rename to docs/_archive/pre-docs-cleanup-2026-09-11/docs/jobs/company-management.md diff --git a/docs/jobs/followups.md b/docs/_archive/pre-docs-cleanup-2026-09-11/docs/jobs/followups.md similarity index 100% rename from docs/jobs/followups.md rename to docs/_archive/pre-docs-cleanup-2026-09-11/docs/jobs/followups.md diff --git a/docs/jobs/future-features.md b/docs/_archive/pre-docs-cleanup-2026-09-11/docs/jobs/future-features.md similarity index 100% rename from docs/jobs/future-features.md rename to docs/_archive/pre-docs-cleanup-2026-09-11/docs/jobs/future-features.md diff --git a/docs/jobs/job-import.md b/docs/_archive/pre-docs-cleanup-2026-09-11/docs/jobs/job-import.md similarity index 100% rename from docs/jobs/job-import.md rename to docs/_archive/pre-docs-cleanup-2026-09-11/docs/jobs/job-import.md diff --git a/docs/jobs/job-lifecycle.md b/docs/_archive/pre-docs-cleanup-2026-09-11/docs/jobs/job-lifecycle.md similarity index 100% rename from docs/jobs/job-lifecycle.md rename to docs/_archive/pre-docs-cleanup-2026-09-11/docs/jobs/job-lifecycle.md diff --git a/docs/jobs/job-search.md b/docs/_archive/pre-docs-cleanup-2026-09-11/docs/jobs/job-search.md similarity index 100% rename from docs/jobs/job-search.md rename to docs/_archive/pre-docs-cleanup-2026-09-11/docs/jobs/job-search.md diff --git a/docs/jobs/overview.md b/docs/_archive/pre-docs-cleanup-2026-09-11/docs/jobs/overview.md similarity index 100% rename from docs/jobs/overview.md rename to docs/_archive/pre-docs-cleanup-2026-09-11/docs/jobs/overview.md diff --git a/docs/operations/calendar-integrations.md b/docs/_archive/pre-docs-cleanup-2026-09-11/docs/operations/calendar-integrations.md similarity index 100% rename from docs/operations/calendar-integrations.md rename to docs/_archive/pre-docs-cleanup-2026-09-11/docs/operations/calendar-integrations.md diff --git a/docs/operations/data-protection-key-rotation.md b/docs/_archive/pre-docs-cleanup-2026-09-11/docs/operations/data-protection-key-rotation.md similarity index 100% rename from docs/operations/data-protection-key-rotation.md rename to docs/_archive/pre-docs-cleanup-2026-09-11/docs/operations/data-protection-key-rotation.md diff --git a/docs/operations/google-drive-backups.md b/docs/_archive/pre-docs-cleanup-2026-09-11/docs/operations/google-drive-backups.md similarity index 100% rename from docs/operations/google-drive-backups.md rename to docs/_archive/pre-docs-cleanup-2026-09-11/docs/operations/google-drive-backups.md diff --git a/docs/operations/job-opportunity-cutover.md b/docs/_archive/pre-docs-cleanup-2026-09-11/docs/operations/job-opportunity-cutover.md similarity index 100% rename from docs/operations/job-opportunity-cutover.md rename to docs/_archive/pre-docs-cleanup-2026-09-11/docs/operations/job-opportunity-cutover.md diff --git a/docs/operations/production-backup-verification.md b/docs/_archive/pre-docs-cleanup-2026-09-11/docs/operations/production-backup-verification.md similarity index 100% rename from docs/operations/production-backup-verification.md rename to docs/_archive/pre-docs-cleanup-2026-09-11/docs/operations/production-backup-verification.md diff --git a/docs/operations/stripe-activation.md b/docs/_archive/pre-docs-cleanup-2026-09-11/docs/operations/stripe-activation.md similarity index 100% rename from docs/operations/stripe-activation.md rename to docs/_archive/pre-docs-cleanup-2026-09-11/docs/operations/stripe-activation.md diff --git a/docs/operations/stripe-billing-setup-walkthrough.md b/docs/_archive/pre-docs-cleanup-2026-09-11/docs/operations/stripe-billing-setup-walkthrough.md similarity index 100% rename from docs/operations/stripe-billing-setup-walkthrough.md rename to docs/_archive/pre-docs-cleanup-2026-09-11/docs/operations/stripe-billing-setup-walkthrough.md diff --git a/docs/phase-0-foundation-report.md b/docs/_archive/pre-docs-cleanup-2026-09-11/docs/phase-0-foundation-report.md similarity index 100% rename from docs/phase-0-foundation-report.md rename to docs/_archive/pre-docs-cleanup-2026-09-11/docs/phase-0-foundation-report.md diff --git a/docs/phase-2-1-cv-extraction-review.md b/docs/_archive/pre-docs-cleanup-2026-09-11/docs/phase-2-1-cv-extraction-review.md similarity index 100% rename from docs/phase-2-1-cv-extraction-review.md rename to docs/_archive/pre-docs-cleanup-2026-09-11/docs/phase-2-1-cv-extraction-review.md diff --git a/docs/phase-5-completion-report.md b/docs/_archive/pre-docs-cleanup-2026-09-11/docs/phase-5-completion-report.md similarity index 100% rename from docs/phase-5-completion-report.md rename to docs/_archive/pre-docs-cleanup-2026-09-11/docs/phase-5-completion-report.md diff --git a/docs/plans/post-audit-ux-reliability-program.md b/docs/_archive/pre-docs-cleanup-2026-09-11/docs/plans/post-audit-ux-reliability-program.md similarity index 100% rename from docs/plans/post-audit-ux-reliability-program.md rename to docs/_archive/pre-docs-cleanup-2026-09-11/docs/plans/post-audit-ux-reliability-program.md diff --git a/docs/product/business-model.md b/docs/_archive/pre-docs-cleanup-2026-09-11/docs/product/business-model.md similarity index 100% rename from docs/product/business-model.md rename to docs/_archive/pre-docs-cleanup-2026-09-11/docs/product/business-model.md diff --git a/docs/product/goals.md b/docs/_archive/pre-docs-cleanup-2026-09-11/docs/product/goals.md similarity index 100% rename from docs/product/goals.md rename to docs/_archive/pre-docs-cleanup-2026-09-11/docs/product/goals.md diff --git a/docs/product/mission.md b/docs/_archive/pre-docs-cleanup-2026-09-11/docs/product/mission.md similarity index 100% rename from docs/product/mission.md rename to docs/_archive/pre-docs-cleanup-2026-09-11/docs/product/mission.md diff --git a/docs/product/non-goals.md b/docs/_archive/pre-docs-cleanup-2026-09-11/docs/product/non-goals.md similarity index 100% rename from docs/product/non-goals.md rename to docs/_archive/pre-docs-cleanup-2026-09-11/docs/product/non-goals.md diff --git a/docs/product/personas.md b/docs/_archive/pre-docs-cleanup-2026-09-11/docs/product/personas.md similarity index 100% rename from docs/product/personas.md rename to docs/_archive/pre-docs-cleanup-2026-09-11/docs/product/personas.md diff --git a/docs/product/roadmap.md b/docs/_archive/pre-docs-cleanup-2026-09-11/docs/product/roadmap.md similarity index 100% rename from docs/product/roadmap.md rename to docs/_archive/pre-docs-cleanup-2026-09-11/docs/product/roadmap.md diff --git a/docs/product/vision.md b/docs/_archive/pre-docs-cleanup-2026-09-11/docs/product/vision.md similarity index 100% rename from docs/product/vision.md rename to docs/_archive/pre-docs-cleanup-2026-09-11/docs/product/vision.md diff --git a/docs/production-readiness-review.md b/docs/_archive/pre-docs-cleanup-2026-09-11/docs/production-readiness-review.md similarity index 100% rename from docs/production-readiness-review.md rename to docs/_archive/pre-docs-cleanup-2026-09-11/docs/production-readiness-review.md diff --git a/docs/production/ollama-model-benchmark.md b/docs/_archive/pre-docs-cleanup-2026-09-11/docs/production/ollama-model-benchmark.md similarity index 100% rename from docs/production/ollama-model-benchmark.md rename to docs/_archive/pre-docs-cleanup-2026-09-11/docs/production/ollama-model-benchmark.md diff --git a/docs/production/production-ai-hardware-assessment.md b/docs/_archive/pre-docs-cleanup-2026-09-11/docs/production/production-ai-hardware-assessment.md similarity index 100% rename from docs/production/production-ai-hardware-assessment.md rename to docs/_archive/pre-docs-cleanup-2026-09-11/docs/production/production-ai-hardware-assessment.md diff --git a/docs/production/production-ai-rollout-and-rollback.md b/docs/_archive/pre-docs-cleanup-2026-09-11/docs/production/production-ai-rollout-and-rollback.md similarity index 100% rename from docs/production/production-ai-rollout-and-rollback.md rename to docs/_archive/pre-docs-cleanup-2026-09-11/docs/production/production-ai-rollout-and-rollback.md diff --git a/docs/production/production-ai-validation.md b/docs/_archive/pre-docs-cleanup-2026-09-11/docs/production/production-ai-validation.md similarity index 100% rename from docs/production/production-ai-validation.md rename to docs/_archive/pre-docs-cleanup-2026-09-11/docs/production/production-ai-validation.md diff --git a/docs/release-candidate-review.md b/docs/_archive/pre-docs-cleanup-2026-09-11/docs/release-candidate-review.md similarity index 100% rename from docs/release-candidate-review.md rename to docs/_archive/pre-docs-cleanup-2026-09-11/docs/release-candidate-review.md diff --git a/docs/release-checklist.md b/docs/_archive/pre-docs-cleanup-2026-09-11/docs/release-checklist.md similarity index 100% rename from docs/release-checklist.md rename to docs/_archive/pre-docs-cleanup-2026-09-11/docs/release-checklist.md diff --git a/docs/release-final-report.md b/docs/_archive/pre-docs-cleanup-2026-09-11/docs/release-final-report.md similarity index 100% rename from docs/release-final-report.md rename to docs/_archive/pre-docs-cleanup-2026-09-11/docs/release-final-report.md diff --git a/docs/research/competitors.md b/docs/_archive/pre-docs-cleanup-2026-09-11/docs/research/competitors.md similarity index 100% rename from docs/research/competitors.md rename to docs/_archive/pre-docs-cleanup-2026-09-11/docs/research/competitors.md diff --git a/docs/research/cv-builder-patterns.md b/docs/_archive/pre-docs-cleanup-2026-09-11/docs/research/cv-builder-patterns.md similarity index 100% rename from docs/research/cv-builder-patterns.md rename to docs/_archive/pre-docs-cleanup-2026-09-11/docs/research/cv-builder-patterns.md diff --git a/docs/research/elegantcv-analysis.md b/docs/_archive/pre-docs-cleanup-2026-09-11/docs/research/elegantcv-analysis.md similarity index 100% rename from docs/research/elegantcv-analysis.md rename to docs/_archive/pre-docs-cleanup-2026-09-11/docs/research/elegantcv-analysis.md diff --git a/docs/research/feature-comparison.md b/docs/_archive/pre-docs-cleanup-2026-09-11/docs/research/feature-comparison.md similarity index 100% rename from docs/research/feature-comparison.md rename to docs/_archive/pre-docs-cleanup-2026-09-11/docs/research/feature-comparison.md diff --git a/docs/research/flowcv-analysis.md b/docs/_archive/pre-docs-cleanup-2026-09-11/docs/research/flowcv-analysis.md similarity index 100% rename from docs/research/flowcv-analysis.md rename to docs/_archive/pre-docs-cleanup-2026-09-11/docs/research/flowcv-analysis.md diff --git a/docs/research/job-platform-analysis.md b/docs/_archive/pre-docs-cleanup-2026-09-11/docs/research/job-platform-analysis.md similarity index 100% rename from docs/research/job-platform-analysis.md rename to docs/_archive/pre-docs-cleanup-2026-09-11/docs/research/job-platform-analysis.md diff --git a/docs/research/jobscan.md b/docs/_archive/pre-docs-cleanup-2026-09-11/docs/research/jobscan.md similarity index 100% rename from docs/research/jobscan.md rename to docs/_archive/pre-docs-cleanup-2026-09-11/docs/research/jobscan.md diff --git a/docs/research/market-positioning.md b/docs/_archive/pre-docs-cleanup-2026-09-11/docs/research/market-positioning.md similarity index 100% rename from docs/research/market-positioning.md rename to docs/_archive/pre-docs-cleanup-2026-09-11/docs/research/market-positioning.md diff --git a/docs/research/novoresume-analysis.md b/docs/_archive/pre-docs-cleanup-2026-09-11/docs/research/novoresume-analysis.md similarity index 100% rename from docs/research/novoresume-analysis.md rename to docs/_archive/pre-docs-cleanup-2026-09-11/docs/research/novoresume-analysis.md diff --git a/docs/research/overview.md b/docs/_archive/pre-docs-cleanup-2026-09-11/docs/research/overview.md similarity index 100% rename from docs/research/overview.md rename to docs/_archive/pre-docs-cleanup-2026-09-11/docs/research/overview.md diff --git a/docs/research/reactive-resume-analysis.md b/docs/_archive/pre-docs-cleanup-2026-09-11/docs/research/reactive-resume-analysis.md similarity index 100% rename from docs/research/reactive-resume-analysis.md rename to docs/_archive/pre-docs-cleanup-2026-09-11/docs/research/reactive-resume-analysis.md diff --git a/docs/research/recommendations.md b/docs/_archive/pre-docs-cleanup-2026-09-11/docs/research/recommendations.md similarity index 100% rename from docs/research/recommendations.md rename to docs/_archive/pre-docs-cleanup-2026-09-11/docs/research/recommendations.md diff --git a/docs/research/saas-analysis.md b/docs/_archive/pre-docs-cleanup-2026-09-11/docs/research/saas-analysis.md similarity index 100% rename from docs/research/saas-analysis.md rename to docs/_archive/pre-docs-cleanup-2026-09-11/docs/research/saas-analysis.md diff --git a/docs/security/ai-security.md b/docs/_archive/pre-docs-cleanup-2026-09-11/docs/security/ai-security.md similarity index 100% rename from docs/security/ai-security.md rename to docs/_archive/pre-docs-cleanup-2026-09-11/docs/security/ai-security.md diff --git a/docs/security/api-security.md b/docs/_archive/pre-docs-cleanup-2026-09-11/docs/security/api-security.md similarity index 100% rename from docs/security/api-security.md rename to docs/_archive/pre-docs-cleanup-2026-09-11/docs/security/api-security.md diff --git a/docs/_archive/pre-docs-cleanup-2026-09-11/docs/security/authentication.md b/docs/_archive/pre-docs-cleanup-2026-09-11/docs/security/authentication.md new file mode 100644 index 0000000..af46b3a --- /dev/null +++ b/docs/_archive/pre-docs-cleanup-2026-09-11/docs/security/authentication.md @@ -0,0 +1,84 @@ +# Authentication + +## Purpose + +Define user login and registration. + +--- + +# Supported Methods + +Users should be able to: + +## Email Registration + +Create account using: + +- Email. +- Password. +- Confirmation. + +--- + +## OAuth Registration/Login + +Support: + +- Google. +- Microsoft. + +--- + +# Expected Behaviour + +If OAuth user does not exist: + +Create account automatically. + +Example: + +User clicks: + +"Continue with Google" + +↓ + +Authenticate + +↓ + +No account exists + +↓ + +Create account + +↓ + +Login + +--- + +# Password Requirements + +Support: + +- Secure hashing. +- Password reset. +- Password change. + +--- + +# Authentication UX + +Users should have: + +- Clear errors. +- Loading states. +- Recovery options. + +Avoid: + +"Authentication failed" + +without explanation. \ No newline at end of file diff --git a/docs/_archive/pre-docs-cleanup-2026-09-11/docs/security/authorization.md b/docs/_archive/pre-docs-cleanup-2026-09-11/docs/security/authorization.md new file mode 100644 index 0000000..6165289 --- /dev/null +++ b/docs/_archive/pre-docs-cleanup-2026-09-11/docs/security/authorization.md @@ -0,0 +1,43 @@ +# Authorization + +## Purpose + +Control access to resources. + +--- + +# User Isolation + +A user must only access: + +- Their jobs. +- Their CV. +- Their files. +- Their emails. +- Their settings. + +--- + +# Admin Role + +Admins may access: + +- System settings. +- Provider configuration. +- Monitoring. + +--- + +# API Rules + +Every protected endpoint must verify: + +1. User identity. +2. User permission. +3. Resource ownership. + +--- + +# Never Trust Frontend + +The backend must enforce permissions. \ No newline at end of file diff --git a/docs/_archive/pre-docs-cleanup-2026-09-11/docs/security/file-security.md b/docs/_archive/pre-docs-cleanup-2026-09-11/docs/security/file-security.md new file mode 100644 index 0000000..743b543 --- /dev/null +++ b/docs/_archive/pre-docs-cleanup-2026-09-11/docs/security/file-security.md @@ -0,0 +1,56 @@ +# File Security + +## Purpose + +Protect uploaded documents. + +--- + +# File Types + +Potential uploads: + +- PDF. +- DOCX. +- Images. +- Portfolio files. + +--- + +# Validation + +Check: + +- File type. +- File size. +- File contents. + +Never trust extensions. + +--- + +# Processing + +Uploaded files may contain: + +- Malicious content. +- Embedded scripts. +- AI prompt injection. + +Treat as untrusted. + +--- + +# Storage + +Requirements: + +- Secure naming. +- Access control. +- User ownership checks. + +--- + +# PDF Processing + +Never directly trust extracted text. \ No newline at end of file diff --git a/docs/security/input-validation.md b/docs/_archive/pre-docs-cleanup-2026-09-11/docs/security/input-validation.md similarity index 100% rename from docs/security/input-validation.md rename to docs/_archive/pre-docs-cleanup-2026-09-11/docs/security/input-validation.md diff --git a/docs/security/overview.md b/docs/_archive/pre-docs-cleanup-2026-09-11/docs/security/overview.md similarity index 100% rename from docs/security/overview.md rename to docs/_archive/pre-docs-cleanup-2026-09-11/docs/security/overview.md diff --git a/docs/security/privacy.md b/docs/_archive/pre-docs-cleanup-2026-09-11/docs/security/privacy.md similarity index 100% rename from docs/security/privacy.md rename to docs/_archive/pre-docs-cleanup-2026-09-11/docs/security/privacy.md diff --git a/docs/security/saas-security.md b/docs/_archive/pre-docs-cleanup-2026-09-11/docs/security/saas-security.md similarity index 100% rename from docs/security/saas-security.md rename to docs/_archive/pre-docs-cleanup-2026-09-11/docs/security/saas-security.md diff --git a/docs/security/supply-chain-policy.md b/docs/_archive/pre-docs-cleanup-2026-09-11/docs/security/supply-chain-policy.md similarity index 100% rename from docs/security/supply-chain-policy.md rename to docs/_archive/pre-docs-cleanup-2026-09-11/docs/security/supply-chain-policy.md diff --git a/docs/security/two-factor-authentication.md b/docs/_archive/pre-docs-cleanup-2026-09-11/docs/security/two-factor-authentication.md similarity index 100% rename from docs/security/two-factor-authentication.md rename to docs/_archive/pre-docs-cleanup-2026-09-11/docs/security/two-factor-authentication.md diff --git a/docs/security/uploads.md b/docs/_archive/pre-docs-cleanup-2026-09-11/docs/security/uploads.md similarity index 100% rename from docs/security/uploads.md rename to docs/_archive/pre-docs-cleanup-2026-09-11/docs/security/uploads.md diff --git a/docs/security/user-data.md b/docs/_archive/pre-docs-cleanup-2026-09-11/docs/security/user-data.md similarity index 100% rename from docs/security/user-data.md rename to docs/_archive/pre-docs-cleanup-2026-09-11/docs/security/user-data.md diff --git a/docs/security/validation.md b/docs/_archive/pre-docs-cleanup-2026-09-11/docs/security/validation.md similarity index 100% rename from docs/security/validation.md rename to docs/_archive/pre-docs-cleanup-2026-09-11/docs/security/validation.md diff --git a/docs/technical/coding-standards.md b/docs/_archive/pre-docs-cleanup-2026-09-11/docs/technical/coding-standards.md similarity index 100% rename from docs/technical/coding-standards.md rename to docs/_archive/pre-docs-cleanup-2026-09-11/docs/technical/coding-standards.md diff --git a/docs/technical/deployment.md b/docs/_archive/pre-docs-cleanup-2026-09-11/docs/technical/deployment.md similarity index 100% rename from docs/technical/deployment.md rename to docs/_archive/pre-docs-cleanup-2026-09-11/docs/technical/deployment.md diff --git a/docs/technical/docker.md b/docs/_archive/pre-docs-cleanup-2026-09-11/docs/technical/docker.md similarity index 100% rename from docs/technical/docker.md rename to docs/_archive/pre-docs-cleanup-2026-09-11/docs/technical/docker.md diff --git a/docs/technical/environment.md b/docs/_archive/pre-docs-cleanup-2026-09-11/docs/technical/environment.md similarity index 100% rename from docs/technical/environment.md rename to docs/_archive/pre-docs-cleanup-2026-09-11/docs/technical/environment.md diff --git a/docs/technical/git-workflow.md b/docs/_archive/pre-docs-cleanup-2026-09-11/docs/technical/git-workflow.md similarity index 100% rename from docs/technical/git-workflow.md rename to docs/_archive/pre-docs-cleanup-2026-09-11/docs/technical/git-workflow.md diff --git a/docs/technical/logging.md b/docs/_archive/pre-docs-cleanup-2026-09-11/docs/technical/logging.md similarity index 100% rename from docs/technical/logging.md rename to docs/_archive/pre-docs-cleanup-2026-09-11/docs/technical/logging.md diff --git a/docs/technical/monitoring.md b/docs/_archive/pre-docs-cleanup-2026-09-11/docs/technical/monitoring.md similarity index 100% rename from docs/technical/monitoring.md rename to docs/_archive/pre-docs-cleanup-2026-09-11/docs/technical/monitoring.md diff --git a/docs/technical/overview.md b/docs/_archive/pre-docs-cleanup-2026-09-11/docs/technical/overview.md similarity index 100% rename from docs/technical/overview.md rename to docs/_archive/pre-docs-cleanup-2026-09-11/docs/technical/overview.md diff --git a/docs/technical/performance.md b/docs/_archive/pre-docs-cleanup-2026-09-11/docs/technical/performance.md similarity index 100% rename from docs/technical/performance.md rename to docs/_archive/pre-docs-cleanup-2026-09-11/docs/technical/performance.md diff --git a/docs/technical/testing.md b/docs/_archive/pre-docs-cleanup-2026-09-11/docs/technical/testing.md similarity index 100% rename from docs/technical/testing.md rename to docs/_archive/pre-docs-cleanup-2026-09-11/docs/technical/testing.md diff --git a/docs/technical/troubleshooting.md b/docs/_archive/pre-docs-cleanup-2026-09-11/docs/technical/troubleshooting.md similarity index 100% rename from docs/technical/troubleshooting.md rename to docs/_archive/pre-docs-cleanup-2026-09-11/docs/technical/troubleshooting.md diff --git a/docs/todo/ollama.md b/docs/_archive/pre-docs-cleanup-2026-09-11/docs/todo/ollama.md similarity index 100% rename from docs/todo/ollama.md rename to docs/_archive/pre-docs-cleanup-2026-09-11/docs/todo/ollama.md diff --git a/docs/todo/work.md b/docs/_archive/pre-docs-cleanup-2026-09-11/docs/todo/work.md similarity index 99% rename from docs/todo/work.md rename to docs/_archive/pre-docs-cleanup-2026-09-11/docs/todo/work.md index 2b517d5..b96773c 100644 --- a/docs/todo/work.md +++ b/docs/_archive/pre-docs-cleanup-2026-09-11/docs/todo/work.md @@ -1,5 +1,7 @@ # Redesign — 2026-09-09 +- [ ] P4-02 only from `115441a`: inventory actual unnamed controls; link Select/prompt/command names and dialog descriptions, verify safe modal focus and load completion, preserve RichTextField/drafts; final full regression + visual evidence, then one commit/push checkpoint. See [P4-02 record](../redesign/phase-4/p4-02/README.md). No P4-03/B07; F04 open. + - [x] Phase 2 from `2d0f131`, on `codex/ui-redesign-phase2`: [preservation checkpoint](../redesign/phase-2/README.md), 57 stable PRES groups mapped, 15 added component cases, six added browser cases, explicit standalone CI type gate and traceability. Fresh 916 backend / 313 frontend pass; browser 23 actual passes + one documented SQLite expected failure, B04/B06/PDF/config regressions pass. No application/visual redesign, B07 or F04 closure. - [ ] P2-D01 separate baseline repair before package/tailoring restructuring: SQLite DateTimeOffset ordering fails in native tailoring/capture. Retain the expected-failure browser oracle; prove native capture and unchanged historical binaries after source edits/deletion before removing annotation. P2-O01 initial rules 500 remains unconfirmed; direct and browser save controls pass. See [baseline evidence](../redesign/phase-2/BASELINE_DEFECTS.md). - [x] Read the Astra handoff and required product documents; complete Phase 1 source audit with 31 concrete route patterns, functionality preservation ledger, 266 API call sites, component/test inventory, identity/storage map, responsive/localisation assessment and migration gates. See `docs/redesign/audit/README.md`. No redesign implementation or B07 started. diff --git a/docs/ux/accessibility.md b/docs/_archive/pre-docs-cleanup-2026-09-11/docs/ux/accessibility.md similarity index 100% rename from docs/ux/accessibility.md rename to docs/_archive/pre-docs-cleanup-2026-09-11/docs/ux/accessibility.md diff --git a/docs/ux/dashboard.md b/docs/_archive/pre-docs-cleanup-2026-09-11/docs/ux/dashboard.md similarity index 100% rename from docs/ux/dashboard.md rename to docs/_archive/pre-docs-cleanup-2026-09-11/docs/ux/dashboard.md diff --git a/docs/ux/design-system.md b/docs/_archive/pre-docs-cleanup-2026-09-11/docs/ux/design-system.md similarity index 100% rename from docs/ux/design-system.md rename to docs/_archive/pre-docs-cleanup-2026-09-11/docs/ux/design-system.md diff --git a/docs/ux/empty-states.md b/docs/_archive/pre-docs-cleanup-2026-09-11/docs/ux/empty-states.md similarity index 100% rename from docs/ux/empty-states.md rename to docs/_archive/pre-docs-cleanup-2026-09-11/docs/ux/empty-states.md diff --git a/docs/ux/errors.md b/docs/_archive/pre-docs-cleanup-2026-09-11/docs/ux/errors.md similarity index 100% rename from docs/ux/errors.md rename to docs/_archive/pre-docs-cleanup-2026-09-11/docs/ux/errors.md diff --git a/docs/ux/loading-states.md b/docs/_archive/pre-docs-cleanup-2026-09-11/docs/ux/loading-states.md similarity index 100% rename from docs/ux/loading-states.md rename to docs/_archive/pre-docs-cleanup-2026-09-11/docs/ux/loading-states.md diff --git a/docs/ux/navigation.md b/docs/_archive/pre-docs-cleanup-2026-09-11/docs/ux/navigation.md similarity index 100% rename from docs/ux/navigation.md rename to docs/_archive/pre-docs-cleanup-2026-09-11/docs/ux/navigation.md diff --git a/docs/ux/notifications.md b/docs/_archive/pre-docs-cleanup-2026-09-11/docs/ux/notifications.md similarity index 100% rename from docs/ux/notifications.md rename to docs/_archive/pre-docs-cleanup-2026-09-11/docs/ux/notifications.md diff --git a/docs/ux/onboarding.md b/docs/_archive/pre-docs-cleanup-2026-09-11/docs/ux/onboarding.md similarity index 100% rename from docs/ux/onboarding.md rename to docs/_archive/pre-docs-cleanup-2026-09-11/docs/ux/onboarding.md diff --git a/docs/ux/profile.md b/docs/_archive/pre-docs-cleanup-2026-09-11/docs/ux/profile.md similarity index 100% rename from docs/ux/profile.md rename to docs/_archive/pre-docs-cleanup-2026-09-11/docs/ux/profile.md diff --git a/docs/ux/responsive.md b/docs/_archive/pre-docs-cleanup-2026-09-11/docs/ux/responsive.md similarity index 100% rename from docs/ux/responsive.md rename to docs/_archive/pre-docs-cleanup-2026-09-11/docs/ux/responsive.md diff --git a/docs/ux/settings.md b/docs/_archive/pre-docs-cleanup-2026-09-11/docs/ux/settings.md similarity index 100% rename from docs/ux/settings.md rename to docs/_archive/pre-docs-cleanup-2026-09-11/docs/ux/settings.md diff --git a/docs/ux/user-flows.md b/docs/_archive/pre-docs-cleanup-2026-09-11/docs/ux/user-flows.md similarity index 100% rename from docs/ux/user-flows.md rename to docs/_archive/pre-docs-cleanup-2026-09-11/docs/ux/user-flows.md diff --git a/docs/verification/ai-001-durable-ai-queue.md b/docs/_archive/pre-docs-cleanup-2026-09-11/docs/verification/ai-001-durable-ai-queue.md similarity index 100% rename from docs/verification/ai-001-durable-ai-queue.md rename to docs/_archive/pre-docs-cleanup-2026-09-11/docs/verification/ai-001-durable-ai-queue.md diff --git a/docs/verification/ai-002-provider-routing.md b/docs/_archive/pre-docs-cleanup-2026-09-11/docs/verification/ai-002-provider-routing.md similarity index 100% rename from docs/verification/ai-002-provider-routing.md rename to docs/_archive/pre-docs-cleanup-2026-09-11/docs/verification/ai-002-provider-routing.md diff --git a/docs/verification/ai-003-strategy-snapshot-queue.md b/docs/_archive/pre-docs-cleanup-2026-09-11/docs/verification/ai-003-strategy-snapshot-queue.md similarity index 100% rename from docs/verification/ai-003-strategy-snapshot-queue.md rename to docs/_archive/pre-docs-cleanup-2026-09-11/docs/verification/ai-003-strategy-snapshot-queue.md diff --git a/docs/verification/ai-004-cv-processing-queue.md b/docs/_archive/pre-docs-cleanup-2026-09-11/docs/verification/ai-004-cv-processing-queue.md similarity index 100% rename from docs/verification/ai-004-cv-processing-queue.md rename to docs/_archive/pre-docs-cleanup-2026-09-11/docs/verification/ai-004-cv-processing-queue.md diff --git a/docs/verification/application-action-matrix.md b/docs/_archive/pre-docs-cleanup-2026-09-11/docs/verification/application-action-matrix.md similarity index 100% rename from docs/verification/application-action-matrix.md rename to docs/_archive/pre-docs-cleanup-2026-09-11/docs/verification/application-action-matrix.md diff --git a/docs/verification/bg-001-tenant-workers.md b/docs/_archive/pre-docs-cleanup-2026-09-11/docs/verification/bg-001-tenant-workers.md similarity index 100% rename from docs/verification/bg-001-tenant-workers.md rename to docs/_archive/pre-docs-cleanup-2026-09-11/docs/verification/bg-001-tenant-workers.md diff --git a/docs/verification/career-001-career-workspace.md b/docs/_archive/pre-docs-cleanup-2026-09-11/docs/verification/career-001-career-workspace.md similarity index 100% rename from docs/verification/career-001-career-workspace.md rename to docs/_archive/pre-docs-cleanup-2026-09-11/docs/verification/career-001-career-workspace.md diff --git a/docs/verification/career-002-cv-builder.md b/docs/_archive/pre-docs-cleanup-2026-09-11/docs/verification/career-002-cv-builder.md similarity index 100% rename from docs/verification/career-002-cv-builder.md rename to docs/_archive/pre-docs-cleanup-2026-09-11/docs/verification/career-002-cv-builder.md diff --git a/docs/verification/core-001-sqlite-provider-parity.md b/docs/_archive/pre-docs-cleanup-2026-09-11/docs/verification/core-001-sqlite-provider-parity.md similarity index 100% rename from docs/verification/core-001-sqlite-provider-parity.md rename to docs/_archive/pre-docs-cleanup-2026-09-11/docs/verification/core-001-sqlite-provider-parity.md diff --git a/docs/verification/core-002-route-uniqueness.md b/docs/_archive/pre-docs-cleanup-2026-09-11/docs/verification/core-002-route-uniqueness.md similarity index 100% rename from docs/verification/core-002-route-uniqueness.md rename to docs/_archive/pre-docs-cleanup-2026-09-11/docs/verification/core-002-route-uniqueness.md diff --git a/docs/verification/dep-001-frontend-advisories.md b/docs/_archive/pre-docs-cleanup-2026-09-11/docs/verification/dep-001-frontend-advisories.md similarity index 100% rename from docs/verification/dep-001-frontend-advisories.md rename to docs/_archive/pre-docs-cleanup-2026-09-11/docs/verification/dep-001-frontend-advisories.md diff --git a/docs/verification/jobs-001-job-discovery.md b/docs/_archive/pre-docs-cleanup-2026-09-11/docs/verification/jobs-001-job-discovery.md similarity index 100% rename from docs/verification/jobs-001-job-discovery.md rename to docs/_archive/pre-docs-cleanup-2026-09-11/docs/verification/jobs-001-job-discovery.md diff --git a/docs/verification/jobs-002-application-workspace.md b/docs/_archive/pre-docs-cleanup-2026-09-11/docs/verification/jobs-002-application-workspace.md similarity index 100% rename from docs/verification/jobs-002-application-workspace.md rename to docs/_archive/pre-docs-cleanup-2026-09-11/docs/verification/jobs-002-application-workspace.md diff --git a/docs/verification/jt-017-dotnet-provenance.md b/docs/_archive/pre-docs-cleanup-2026-09-11/docs/verification/jt-017-dotnet-provenance.md similarity index 100% rename from docs/verification/jt-017-dotnet-provenance.md rename to docs/_archive/pre-docs-cleanup-2026-09-11/docs/verification/jt-017-dotnet-provenance.md diff --git a/docs/verification/jt-018-developer-documentation.md b/docs/_archive/pre-docs-cleanup-2026-09-11/docs/verification/jt-018-developer-documentation.md similarity index 100% rename from docs/verification/jt-018-developer-documentation.md rename to docs/_archive/pre-docs-cleanup-2026-09-11/docs/verification/jt-018-developer-documentation.md diff --git a/docs/verification/jt-019-schema-ownership.md b/docs/_archive/pre-docs-cleanup-2026-09-11/docs/verification/jt-019-schema-ownership.md similarity index 100% rename from docs/verification/jt-019-schema-ownership.md rename to docs/_archive/pre-docs-cleanup-2026-09-11/docs/verification/jt-019-schema-ownership.md diff --git a/docs/verification/mail-001-job-email-hub.md b/docs/_archive/pre-docs-cleanup-2026-09-11/docs/verification/mail-001-job-email-hub.md similarity index 100% rename from docs/verification/mail-001-job-email-hub.md rename to docs/_archive/pre-docs-cleanup-2026-09-11/docs/verification/mail-001-job-email-hub.md diff --git a/docs/verification/ops-001a-durable-operations.md b/docs/_archive/pre-docs-cleanup-2026-09-11/docs/verification/ops-001a-durable-operations.md similarity index 100% rename from docs/verification/ops-001a-durable-operations.md rename to docs/_archive/pre-docs-cleanup-2026-09-11/docs/verification/ops-001a-durable-operations.md diff --git a/docs/verification/ops-001b-notifications.md b/docs/_archive/pre-docs-cleanup-2026-09-11/docs/verification/ops-001b-notifications.md similarity index 100% rename from docs/verification/ops-001b-notifications.md rename to docs/_archive/pre-docs-cleanup-2026-09-11/docs/verification/ops-001b-notifications.md diff --git a/docs/verification/ops-001c-operation-ui.md b/docs/_archive/pre-docs-cleanup-2026-09-11/docs/verification/ops-001c-operation-ui.md similarity index 100% rename from docs/verification/ops-001c-operation-ui.md rename to docs/_archive/pre-docs-cleanup-2026-09-11/docs/verification/ops-001c-operation-ui.md diff --git a/docs/verification/pol-001-free-pro-entitlements.md b/docs/_archive/pre-docs-cleanup-2026-09-11/docs/verification/pol-001-free-pro-entitlements.md similarity index 100% rename from docs/verification/pol-001-free-pro-entitlements.md rename to docs/_archive/pre-docs-cleanup-2026-09-11/docs/verification/pol-001-free-pro-entitlements.md diff --git a/docs/verification/pol-002-ai-privacy.md b/docs/_archive/pre-docs-cleanup-2026-09-11/docs/verification/pol-002-ai-privacy.md similarity index 100% rename from docs/verification/pol-002-ai-privacy.md rename to docs/_archive/pre-docs-cleanup-2026-09-11/docs/verification/pol-002-ai-privacy.md diff --git a/docs/verification/prod-002-ai-evaluation.md b/docs/_archive/pre-docs-cleanup-2026-09-11/docs/verification/prod-002-ai-evaluation.md similarity index 100% rename from docs/verification/prod-002-ai-evaluation.md rename to docs/_archive/pre-docs-cleanup-2026-09-11/docs/verification/prod-002-ai-evaluation.md diff --git a/docs/verification/product-001-honest-plans.md b/docs/_archive/pre-docs-cleanup-2026-09-11/docs/verification/product-001-honest-plans.md similarity index 100% rename from docs/verification/product-001-honest-plans.md rename to docs/_archive/pre-docs-cleanup-2026-09-11/docs/verification/product-001-honest-plans.md diff --git a/docs/verification/qa-001-job-term-quality.md b/docs/_archive/pre-docs-cleanup-2026-09-11/docs/verification/qa-001-job-term-quality.md similarity index 100% rename from docs/verification/qa-001-job-term-quality.md rename to docs/_archive/pre-docs-cleanup-2026-09-11/docs/verification/qa-001-job-term-quality.md diff --git a/docs/verification/sec-001-canonical-origin.md b/docs/_archive/pre-docs-cleanup-2026-09-11/docs/verification/sec-001-canonical-origin.md similarity index 100% rename from docs/verification/sec-001-canonical-origin.md rename to docs/_archive/pre-docs-cleanup-2026-09-11/docs/verification/sec-001-canonical-origin.md diff --git a/docs/verification/sec-002-ingress-compose.md b/docs/_archive/pre-docs-cleanup-2026-09-11/docs/verification/sec-002-ingress-compose.md similarity index 100% rename from docs/verification/sec-002-ingress-compose.md rename to docs/_archive/pre-docs-cleanup-2026-09-11/docs/verification/sec-002-ingress-compose.md diff --git a/docs/verification/sec-003-microsoft-tenant.md b/docs/_archive/pre-docs-cleanup-2026-09-11/docs/verification/sec-003-microsoft-tenant.md similarity index 100% rename from docs/verification/sec-003-microsoft-tenant.md rename to docs/_archive/pre-docs-cleanup-2026-09-11/docs/verification/sec-003-microsoft-tenant.md diff --git a/docs/verification/sec-004-microsoft-identity.md b/docs/_archive/pre-docs-cleanup-2026-09-11/docs/verification/sec-004-microsoft-identity.md similarity index 100% rename from docs/verification/sec-004-microsoft-identity.md rename to docs/_archive/pre-docs-cleanup-2026-09-11/docs/verification/sec-004-microsoft-identity.md diff --git a/docs/verification/sec-005a-session-revocation.md b/docs/_archive/pre-docs-cleanup-2026-09-11/docs/verification/sec-005a-session-revocation.md similarity index 100% rename from docs/verification/sec-005a-session-revocation.md rename to docs/_archive/pre-docs-cleanup-2026-09-11/docs/verification/sec-005a-session-revocation.md diff --git a/docs/verification/sec-005b-email-ownership.md b/docs/_archive/pre-docs-cleanup-2026-09-11/docs/verification/sec-005b-email-ownership.md similarity index 100% rename from docs/verification/sec-005b-email-ownership.md rename to docs/_archive/pre-docs-cleanup-2026-09-11/docs/verification/sec-005b-email-ownership.md diff --git a/docs/verification/sec-006-parser-dependencies.md b/docs/_archive/pre-docs-cleanup-2026-09-11/docs/verification/sec-006-parser-dependencies.md similarity index 100% rename from docs/verification/sec-006-parser-dependencies.md rename to docs/_archive/pre-docs-cleanup-2026-09-11/docs/verification/sec-006-parser-dependencies.md diff --git a/docs/verification/sec-007-parser-isolation.md b/docs/_archive/pre-docs-cleanup-2026-09-11/docs/verification/sec-007-parser-isolation.md similarity index 100% rename from docs/verification/sec-007-parser-isolation.md rename to docs/_archive/pre-docs-cleanup-2026-09-11/docs/verification/sec-007-parser-isolation.md diff --git a/docs/verification/sec-008-attachment-consistency.md b/docs/_archive/pre-docs-cleanup-2026-09-11/docs/verification/sec-008-attachment-consistency.md similarity index 100% rename from docs/verification/sec-008-attachment-consistency.md rename to docs/_archive/pre-docs-cleanup-2026-09-11/docs/verification/sec-008-attachment-consistency.md diff --git a/docs/verification/sec-009-account-lifecycle.md b/docs/_archive/pre-docs-cleanup-2026-09-11/docs/verification/sec-009-account-lifecycle.md similarity index 100% rename from docs/verification/sec-009-account-lifecycle.md rename to docs/_archive/pre-docs-cleanup-2026-09-11/docs/verification/sec-009-account-lifecycle.md diff --git a/docs/verification/ux-001-unified-authentication.md b/docs/_archive/pre-docs-cleanup-2026-09-11/docs/verification/ux-001-unified-authentication.md similarity index 100% rename from docs/verification/ux-001-unified-authentication.md rename to docs/_archive/pre-docs-cleanup-2026-09-11/docs/verification/ux-001-unified-authentication.md diff --git a/docs/verification/ux-002-deterministic-theme-state.md b/docs/_archive/pre-docs-cleanup-2026-09-11/docs/verification/ux-002-deterministic-theme-state.md similarity index 100% rename from docs/verification/ux-002-deterministic-theme-state.md rename to docs/_archive/pre-docs-cleanup-2026-09-11/docs/verification/ux-002-deterministic-theme-state.md diff --git a/docs/verification/ux-003-kanban-theme.md b/docs/_archive/pre-docs-cleanup-2026-09-11/docs/verification/ux-003-kanban-theme.md similarity index 100% rename from docs/verification/ux-003-kanban-theme.md rename to docs/_archive/pre-docs-cleanup-2026-09-11/docs/verification/ux-003-kanban-theme.md diff --git a/docs/verification/ver-001-complete-regression.md b/docs/_archive/pre-docs-cleanup-2026-09-11/docs/verification/ver-001-complete-regression.md similarity index 100% rename from docs/verification/ver-001-complete-regression.md rename to docs/_archive/pre-docs-cleanup-2026-09-11/docs/verification/ver-001-complete-regression.md diff --git a/docs/work-programmes/decisions.md b/docs/_archive/pre-docs-cleanup-2026-09-11/docs/work-programmes/decisions.md similarity index 100% rename from docs/work-programmes/decisions.md rename to docs/_archive/pre-docs-cleanup-2026-09-11/docs/work-programmes/decisions.md diff --git a/docs/work-programmes/master-progress.md b/docs/_archive/pre-docs-cleanup-2026-09-11/docs/work-programmes/master-progress.md similarity index 100% rename from docs/work-programmes/master-progress.md rename to docs/_archive/pre-docs-cleanup-2026-09-11/docs/work-programmes/master-progress.md diff --git a/docs/work-programmes/master-work-plan.md b/docs/_archive/pre-docs-cleanup-2026-09-11/docs/work-programmes/master-work-plan.md similarity index 100% rename from docs/work-programmes/master-work-plan.md rename to docs/_archive/pre-docs-cleanup-2026-09-11/docs/work-programmes/master-work-plan.md diff --git a/docs/work-programmes/session-handoff.md b/docs/_archive/pre-docs-cleanup-2026-09-11/docs/work-programmes/session-handoff.md similarity index 100% rename from docs/work-programmes/session-handoff.md rename to docs/_archive/pre-docs-cleanup-2026-09-11/docs/work-programmes/session-handoff.md diff --git a/docs/_archive/pre-docs-cleanup-2026-09-11/job-tracker-ui/README.md b/docs/_archive/pre-docs-cleanup-2026-09-11/job-tracker-ui/README.md new file mode 100644 index 0000000..20cec0e --- /dev/null +++ b/docs/_archive/pre-docs-cleanup-2026-09-11/job-tracker-ui/README.md @@ -0,0 +1,64 @@ +# Job Tracker frontend + +The frontend is a statically exported Next.js 16 application using React 19, TypeScript, Material UI, and React Router. Next.js supplies the build shell; authenticated and public application routes are defined in `src/App.tsx`. + +## Requirements + +- Node.js 24 (the version used by CI) +- npm 11 or a compatible npm release +- The Job Tracker API at `http://localhost:5202` for normal local use + +Install the lockfile exactly: + +```powershell +npm ci +``` + +## Development + +Start the application on Next.js's default development port: + +```powershell +npm run dev +``` + +Open `http://localhost:3000`. The client calls `http://localhost:5202/api` on localhost unless `NEXT_PUBLIC_API_BASE_URL` overrides it. + +The Playwright harness uses port `3300` and starts its own isolated API and SQLite database. Do not point it at a development or production database. + +## Quality gates + +```powershell +npm run lint +npm test -- --runInBand +npm run build +``` + +The production build is written to `out/` and is served by nginx in the frontend container. + +Run the browser suite after installing Chromium once: + +```powershell +npx playwright install chromium +npm run test:e2e +``` + +The browser suite covers login, job creation, Career Workspace routing, and public CV/PDF access against isolated services. + +## Useful paths + +- `app/` — thin Next.js application shell +- `src/App.tsx` — application routes and shared authenticated shell +- `src/api.ts` — API client and local/default base URL +- `src/components/` — feature and shared UI components +- `src/i18n/` — English and Norwegian Bokmål UI messages +- `src/theme.ts` — application theme +- `e2e/` — Playwright browser tests +- `next.config.js` — static-export configuration +- `playwright.config.ts` — isolated browser-test services + +## Runtime configuration + +`NEXT_PUBLIC_API_BASE_URL` is the only browser-visible API setting. Leave it unset in the production container so nginx proxies same-origin `/api` requests. Values prefixed with `NEXT_PUBLIC_` are embedded at build time and must never contain secrets. + +See the repository `README.md` for full-stack development and `deploy/README.md` for production requirements. diff --git a/docs/_archive/pre-docs-cleanup-2026-09-11/tools/summarizer/README.md b/docs/_archive/pre-docs-cleanup-2026-09-11/tools/summarizer/README.md new file mode 100644 index 0000000..31300fc --- /dev/null +++ b/docs/_archive/pre-docs-cleanup-2026-09-11/tools/summarizer/README.md @@ -0,0 +1,94 @@ +# Local AI Service + +This service runs a local Hugging Face summarization model and also exposes document text extraction with OCR for supported PDFs and images. + +## Capabilities +- job/role summarization +- PDF text extraction +- OCR fallback for scanned PDFs +- OCR for image uploads (`png`, `jpg`, `jpeg`, `webp`) +- DOCX / TXT / MD extraction +- optional Ollama-backed CV block classification for harder sectioning + +## Install + +Windows: + +```powershell +python -m venv .venv +.\.venv\Scripts\Activate.ps1 +pip install -r requirements-dev.txt +python -m uvicorn app:app --host 127.0.0.1 --port 8001 --workers 1 +``` + +Linux / macOS: + +```bash +python3 -m venv .venv +source .venv/bin/activate +pip install -r requirements-dev.txt +python -m uvicorn app:app --host 127.0.0.1 --port 8001 --workers 1 +``` + +If the host is missing `python3-venv` or `pip`, use the bootstrap script instead: + +```bash +./scripts/bootstrap-and-test.sh bootstrap +``` + +## Docker +The Dockerfile installs Tesseract OCR so scanned PDFs and supported images can be processed inside the container. + +## Document parser isolation + +`/extract-text` performs only bounded upload and signature checks in the web process. Actual TXT, Markdown, PDF, DOCX and image decoding runs in a single-capacity child process with a minimal environment. Linux children receive CPU, address-space, output-size and file-descriptor limits; every platform enforces a parent deadline and terminates the parser process tree on timeout. + +Production Compose runs the service as a non-root user with a read-only root filesystem, no Linux capabilities, `no-new-privileges`, PID/CPU/memory limits and a bounded `/tmp` tmpfs. The Hugging Face cache is the only named writable volume. Parser work directories are removed after each request and stale `work-*` directories are reconciled on the next extraction. + +Parser limits can be made more conservative with `PARSER_TIMEOUT_SECONDS`, `PARSER_CPU_SECONDS`, `PARSER_MEMORY_MIB` and `PARSER_STALE_WORK_SECONDS`. Increasing them should follow measured benign-CV evidence; disabling import is the safe fallback when the budget is insufficient. + +## Tests + +Run the summarizer unit tests with: + +```bash +./scripts/bootstrap-and-test.sh test +``` + +The script: +- creates `.venv` with stdlib `venv` when available +- falls back to user-space `virtualenv` when host `venv` support is missing +- installs `requirements-dev.txt` +- writes pytest cache under `tmp/pytest-cache` to avoid stale root-owned `.pytest_cache` directories + +## API +- `GET /health` — health check and runtime capabilities, including lazy model state (`model_loaded`, `model_disabled`, `summarize_available`, `model_load_error`) plus Ollama version/model metadata when configured +- `POST /summarize` — JSON body `{ "text": "...", "max_length": 150, "min_length": 30 }` +- `POST /extract-text` — multipart file upload, returns extracted text and OCR metadata +- `POST /cv/classify-block` — JSON body `{ "block": "..." }`, uses Ollama when `OLLAMA_MODEL` is configured + +## Ollama +Set these before starting the service if you want the hybrid CV classifier enabled: + +```bash +export OLLAMA_BASE_URL=http://ollama:11434 +export OLLAMA_MODEL=qwen2.5:7b +``` + +Choose the model by setting `OLLAMA_MODEL` and then warming it with the helper script: + +```bash +OLLAMA_MODEL=qwen2.5:7b ./scripts/start-ollama-cv.sh +``` + +Equivalent manual flow: + +```bash +docker compose up -d ollama +docker compose exec ollama ollama pull qwen2.5:7b +docker compose up -d ai-service +``` + +- Model weights are downloaded on first pull. +- OCR quality depends on scan quality and language support. +- Default OCR language is English (`eng`). diff --git a/docs/ai/README.md b/docs/ai/README.md new file mode 100644 index 0000000..85dea24 --- /dev/null +++ b/docs/ai/README.md @@ -0,0 +1,13 @@ +# AI + +AI is an optional assistive subsystem. Manual job tracking and document editing remain usable without it. + +- [Architecture and feature boundaries](architecture.md) +- [Providers and routing](providers.md) +- [Prompt and output rules](prompts.md) +- [Safety and privacy](safety.md) +- [Document processing](document-processing.md) + +Implemented behavior must be verified in the API and `tools/summarizer/`; future ideas are tracked only in [roadmap](../roadmap/README.md). + +For local service commands and parser limits, see the [summarizer service README](../../tools/summarizer/README.md). diff --git a/docs/ai/architecture.md b/docs/ai/architecture.md new file mode 100644 index 0000000..b89a234 --- /dev/null +++ b/docs/ai/architecture.md @@ -0,0 +1,7 @@ +# AI architecture + +The API calls the internal FastAPI service at `AI_SERVICE_BASE_URL`. The service provides local job summarisation, text extraction/OCR, and CV normalization/classification/rewrite routes. The local summarizer uses `sshleifer/distilbart-cnn-12-6`; structured CV tasks use Ollama by default and may use Gemini or Groq only when external processing is enabled and policy permits it. + +Current feature surfaces include job summaries, deterministic match scoring, candidate-fit/focus-plan narratives, CV extraction and normalization, section rewriting, tailored CVs, cover-letter/application drafts, recruiter/follow-up drafts, and interview preparation. AI output is reviewable/editable; sending mail is a separate user action. + +The API owns user authorization, quotas, and feature policy. The sidecar owns provider routing, parser limits, and the shared service-token check. `/health` is the only intentionally open sidecar path. diff --git a/docs/ai/document-processing.md b/docs/ai/document-processing.md new file mode 100644 index 0000000..01e682e --- /dev/null +++ b/docs/ai/document-processing.md @@ -0,0 +1,5 @@ +# Document processing + +Supported extraction includes PDF, DOCX, TXT, Markdown, and PNG/JPEG/WEBP image inputs. Scanned documents can use Tesseract OCR. `/extract-text` performs size/signature checks in the web process and delegates decoding to a bounded child process; temporary work directories are removed and stale work is reconciled. + +Relevant limits are configured with `PARSER_TIMEOUT_SECONDS`, `PARSER_CPU_SECONDS`, `PARSER_MEMORY_MIB`, and `PARSER_STALE_WORK_SECONDS`. `PARSER_TEMP_ROOT` is an optional local override. When a document exceeds safe limits, reject it or ask the user for a smaller/cleaner file; do not disable validation to make a parser pass. diff --git a/docs/ai/prompts.md b/docs/ai/prompts.md index 8b4bfeb..a6dc15f 100644 --- a/docs/ai/prompts.md +++ b/docs/ai/prompts.md @@ -1,51 +1,5 @@ -# AI Prompt Management +# Prompt and output rules -## Purpose +Prompts are assembled by feature services/controllers from the job, career profile, reviewed correspondence, and explicitly selected attachments. Inputs are bounded before leaving the API/sidecar. Structured tasks request typed JSON or constrained sections; callers validate and sanitize the result before storing or rendering it. -Centralise prompts. - -Avoid random prompts scattered throughout code. - ---- - -# Requirements - -Prompts should: - -- Be version controlled. -- Be documented. -- Have tests where possible. - ---- - -# Prompt Structure - -Every prompt should define: - -Purpose. - -Inputs. - -Expected output. - -Restrictions. - ---- - -# Example - -Task: - -Improve CV bullet point. - -Input: - -Existing bullet. - -Output: - -Improved bullet. - -Restrictions: - -Do not add unsupported achievements. \ No newline at end of file +Prompts must preserve factual career data, avoid inventing credentials or experience, respect the selected locale, and keep user edits recoverable. Do not add automatic employer-facing sends to an AI feature without a separate review and delivery design. diff --git a/docs/ai/providers.md b/docs/ai/providers.md index f1ffb64..b04de15 100644 --- a/docs/ai/providers.md +++ b/docs/ai/providers.md @@ -1,66 +1,5 @@ -# AI Providers +# AI providers and routing -## Purpose +`AI_PROVIDER` selects the structured CV provider: `ollama` (default/local), `gemini`, or `groq`. `AI_ROUTING_MODE` is `local_only`, `local_first`, or `external_only`; external processing also requires the administrator gate, an allowed task, and the user's applicable opt-in/entitlement. Job summarisation remains local in the sidecar. -Support multiple AI providers. - ---- - -# Current/Future Providers - -Possible: - -- OpenAI. -- Anthropic. -- Google Gemini. -- Groq. -- Ollama. -- Other compatible APIs. - ---- - -# Provider Architecture - -Use abstraction: - -AI Provider Interface - -↓ - -Provider Implementation - -↓ - -Model - ---- - -# Requirements - -Users should eventually be able to: - -- Select provider. -- Select model. -- Configure API keys. - ---- - -# Admin Controls - -Admins should control: - -- Available providers. -- Allowed models. -- Usage limits. - ---- - -# SaaS Consideration - -Different plans may have: - -Free models. - -Premium models. - -BYO-key support. \ No newline at end of file +Ollama uses `OLLAMA_BASE_URL` and `OLLAMA_MODEL`. Gemini and Groq use their model, base URL, and API-key variables. Keep provider keys in the environment only. If a provider/model is unavailable, show a retriable/unavailable state rather than silently selecting an unevaluated model. diff --git a/docs/ai/safety.md b/docs/ai/safety.md index 48b2a20..5636330 100644 --- a/docs/ai/safety.md +++ b/docs/ai/safety.md @@ -1,8 +1,5 @@ -# safety +# AI safety and privacy -AI output is suggestion-only and cannot overwrite the Career Profile automatically. Prompts delimit -untrusted source text, require preservation of factual claims, and instruct providers not to invent -employers, titles, dates, or metrics. Attachments are included only when explicitly selected. +Local processing is the default. External CV processing is opt-in and gated; provider selection and task type are recorded in the AI interaction/usage paths. The sidecar is private to the backend network in Compose, requires `AI_SERVICE_TOKEN` on protected endpoints, runs with reduced container privileges, and bounds parser work. -Provider outages degrade AI features without blocking core tracking. Calls and tokens are metered and -limited by plan. See `docs/ai/security.md` and `docs/ai/privacy.md`. +Known limits: local tests do not prove provider equivalence, production privacy configuration, or real outage behavior. Do not paste personal CVs, tokens, or provider responses into audit notes. See [security notes](../security/security-notes.md) for open verification gates. diff --git a/docs/architecture/backend.md b/docs/architecture/backend.md index a9152c5..7ec488f 100644 --- a/docs/architecture/backend.md +++ b/docs/architecture/backend.md @@ -1,75 +1,7 @@ -# Backend Architecture +# Backend architecture -## Purpose +`JobTrackerApi/` is the ASP.NET Core 9 host. Controllers expose `/api` endpoints, services contain domain orchestration, `JobTrackerContext` owns EF Core persistence and tenant filters, and hosted services handle scheduled or recoverable work. -Define backend organisation. +The API supports local email/password accounts, Google and Microsoft identity sign-in where configured, cookie sessions with CSRF protection, Identity roles, TOTP two-factor authentication, session revocation, email verification/reset, rate limits, and admin-only system/audit surfaces. ---- - -# Preferred Structure - -Example: - -Application - -├── Features - -├── Domain - -├── Infrastructure - -├── Services - -├── API - ---- - -# Responsibilities - -## API Layer - -Handles: - -- HTTP requests. -- Authentication. -- Validation. -- Responses. - -Should not contain business logic. - ---- - -## Application Layer - -Handles: - -- Workflows. -- Business operations. -- Coordination. - ---- - -## Domain Layer - -Contains: - -- Entities. -- Rules. -- Business concepts. - ---- - -## Infrastructure - -Handles: - -- Database. -- External APIs. -- File storage. -- AI providers. - ---- - -# Rule - -Controllers should remain thin. \ No newline at end of file +The largest orchestration boundaries are `JobApplicationsController`, `ProfileCvController`, and `StartupInitializationExtensions`. They are recorded as conditional refactoring debt; file size alone is not a reason for a rewrite. See [API reference](../backend/api.md), [background jobs](../backend/background-jobs.md), and [technical debt](../roadmap/technical-debt.md). diff --git a/docs/architecture/current.md b/docs/architecture/current.md index 7710de1..7e451e4 100644 --- a/docs/architecture/current.md +++ b/docs/architecture/current.md @@ -1,431 +1,5 @@ -# Jobjakt — Current Architecture +# Current architecture -> **This document describes the system as it actually is.** Every claim was verified against code. -> 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. -> Topic documentation is maintained alongside this file; dated completion and review reports remain -> historical snapshots. When documentation conflicts, prefer this file and the code. +This compatibility entry points to the single authoritative architecture description: [Architecture overview](overview.md). ---- - -## 1. What the product is - -Jobjakt is a self-hosted, multi-user job application tracking platform with local-AI career assistance: - -- Track jobs and applications end-to-end (pipeline stages, follow-ups, deadlines, salary, tags, notes). -- Company CRM (pipeline stage, contact dates, recruiter details) — company-level only, no people entities. -- Correspondence log per application: **Gmail OAuth import with a human review queue**, IMAP, Microsoft Graph. -- Attachments per application with purpose metadata and AI-inclusion toggles. -- CV platform: upload → OCR/extraction → structured parsing → per-job tailored CV drafts → templated PDF via Playwright. -- AI drafts: cover letters, recruiter messages, follow-up drafts, job summaries, match scoring, interview prep. -- Rules engine (auto-ghosting), reminder emails, daily JSON export, event trail, automated DB backup. -- Admin: user management, audit log, system readiness. -- Production: `https://jobs.cesnimda.uk` via Gitea Actions → SSH → Docker Compose. - -**Product hierarchy** (from `docs/MASTER_IMPLEMENTATION_GUIDE.md` — job tracking is the core; career tools support it): -Job Tracking → Applications → Workflow → Follow-ups → Communication, then Career Profile → Master CV → CV Builder → Cover Letters → Portfolio → Interview Prep, then Job Discovery. - ---- - -## 2. Architecture overview - -```mermaid -flowchart LR - subgraph Client - UI[React 19 SPA
MUI 7, react-router 6
Next.js 16 CSR shell] - end - - subgraph Frontend container - NGINX[nginx 1.29-alpine
serves static export + proxies /api] - end - - subgraph Backend container - API[ASP.NET Core net9.0
JobTrackerApi host] - BG[7 hosted services:
Rules, FollowUpReminder, DailyExport,
JobEnrichment, SummarizerProbe,
CvProcessing, DatabaseBackup] - DB[(SQLite default
or MariaDB/MySQL)] - FS[/Data root:
Attachments, CvArtifacts,
exports, DP keys/] - end - - subgraph AI stack - AISVC[FastAPI ai-service :8001
distilbart summarizer,
OCR, docx/pdf extraction] - PROV[Provider via AI_PROVIDER env:
ollama qwen2.5:7b / gemini / groq] - end - - EXT1[Google OAuth / Gmail API] - EXT2[Microsoft Graph / IMAP] - EXT3[Job sites: Finn, NAV,
LinkedIn, Jobbnorge] - EXT4[SMTP] - EXT5[LibreTranslate optional] - - UI --> NGINX --> API - API --> DB - API --> FS - API --> AISVC --> PROV - API --> EXT1 - API --> EXT2 - API --> EXT3 - API --> EXT4 - API --> EXT5 - BG --> DB -``` - -### Solution layout - -| Project | Role | -|---|---| -| `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. | -| `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. | - -> 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. - ---- - -## 3. Technology stack - -**Backend:** ASP.NET Core net9.0, EF Core 9 (SQLite default; Pomelo MySQL/MariaDB via `Database:Provider`), ASP.NET Identity Core, JWT bearer (smart policy scheme: local + Google), built-in RateLimiter, DataProtection (file-system keys), Playwright (PDF export). - -**Frontend:** **Next.js 16** + React 19 + **TypeScript 5.9** + MUI 7 (+ x-data-grid, x-date-pickers, lab), axios, react-router-dom 7, @tanstack/react-table, i18n EN + NB (custom provider), Jest/RTL. - -> 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. - -**Infra:** Docker Compose (backend, frontend/nginx 1.29-alpine, ai-service, ollama opt-in via `bundled-ollama` profile w/ GPU), Gitea Actions CI → SSH deploy → `deploy/deploy.sh`, external `jobtracker_shared` network. - ---- - -## 4. Frontend architecture - -**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 `` inside a catch-all `Shell` for authenticated routes. - -Development leaves static-export mode disabled and rewrites deep links to the root shell; production exports that 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}`. - -> `/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. - -**Styling:** MUI `sx` + custom `src/theme.ts` (439 lines), light/dark. Design tokens live inline in component `sx` props rather than in the theme (e.g. the same `boxShadow: "0px 8px 24px -12px rgba(15,23,42,0.12)"` is repeated across pages). There is no component primitives layer and no Storybook. - -**Oversized components** (refactor targets, in order): `JobDetailsDialog.tsx` (1400), `CareerProfilePage.tsx` (1293), `JobTable.tsx` (786), `Correspondence.tsx` (732), `DashboardView.tsx` (666), `AdminSystemPage.tsx` (623), `AddJobModal.tsx` (618). (`ProfilePage.tsx` was 1368; Phase 2.2 split it — see §4a.) - ---- - -## 4a. Profile / Career separation (Phase 2, 2026-07-17) - -`/profile` and `/career` were one 1368-line component (`ProfilePage`) forked by a `careerOnly` -boolean. Phase 2 scoped their saves; Phase 2.2 split them into two dedicated components. This is -the reference model for how account identity and the master career profile relate. - -### Components & routes - -| Route | Component | Owns | -|---|---|---| -| `/profile` | `views/ProfilePage.tsx` (~490 lines) | Account identity + security + preferences | -| `/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 a separate routed workspace and consumes the Career Profile as its source of truth. - -### Request flow - -``` -/profile → ProfilePage - load: GET /auth/me (account row only) - save: PUT /auth/profile { email, userName, firstName, lastName, displayName } - -/career → CareerWorkspacePage → CareerProfilePage - load: GET /auth/me - GET /profile-cv/runs (extraction history) - GET /jobapplications?… (for per-job CV tailoring context) - save: PUT /auth/profile { profileCvText, profileCvStructureJson } - (ProfileCvController paths additionally dual-write CareerProfileService — - CareerProfiles / CareerProfileVersions — see §9 / §16) -``` - -### Data ownership (the invariant) - -Both surfaces persist through the **same** endpoint, `PUT /auth/profile`, which does **partial -updates** (`AuthController.UpdateProfile`): a field is touched only if the request carries it — -`null`/omitted leaves it unchanged, `""` clears it, a value sets it. Email and UserName are never -cleared (login identifiers). - -| Field(s) on `ApplicationUser` | Owner (only surface that writes them) | -|---|---| -| `Email`, `UserName`, `FirstName`, `LastName`, `DisplayName`, `AvatarImageDataUrl`, password, TOTP/2FA, linked OAuth accounts | **/profile** | -| `ProfileCvText`, `ProfileCvStructureJson` (the master career profile) | **/career** | - -Because the endpoint is partial, `/profile` saving identity does **not** null the master profile, -and `/career` saving the profile does **not** null identity. This is enforced by tests -(`AuthAndSystemControllerTests`: identity-save-keeps-CV, career-save-keeps-identity, empty-clears, -null-leaves). - -### API responsibilities - -- `AuthController.UpdateProfile` (`PUT /auth/profile`) — partial update of the account row; the - single write path for both surfaces. Local accounts only. -- `AuthController` (`GET /auth/me`) — returns the whole account row; each surface reads the fields - it owns. -- `ProfileCvController` — CV ingest/parse/rewrite/export and the `CareerProfileService` dual-write. - Called from `/career`. -- `JobApplicationsController` — `/career` reads job list for tailoring context only. - -### Future extension points - -- **`CareerProfilePage` is the foundation for all future career outputs** (Phase 3/4): CV Builder, - tailored CVs, cover letters, portfolio, interview prep. They attach here, referencing the master - profile — never duplicating it (per `docs/MASTER_IMPLEMENTATION_GUIDE.md`). -- **Source-of-truth flip (F5):** today `ProfileCvStructureJson` is authoritative and - `CareerProfileService` mirrors it. A later phase makes `CareerProfiles`/`CareerProfileVersions` - authoritative; the `/career` save would then route through `CareerProfileService` rather than the - blob column. The partial-update endpoint and the ownership split above do not change. -- **Full decomposition (roadmap 2.2 residue):** `CareerProfilePage` is still large because it owns - the whole master-CV surface; the CV Builder work will extract sub-components from it. - ---- - -## 5. Authentication & authorization - -- **Smart policy scheme:** inspects the bearer token issuer — Google ID tokens (`accounts.google.com`) → `google` handler (validated against `Auth:GoogleClientId`); everything else → `local` JWT (symmetric `Auth:JwtKey`, issuer/audience validated, 2-min clock skew). -- **Cookie sessions:** local handler also reads `jobtracker_auth` (HttpOnly, SameSite=Lax, Secure-configurable, 30d when persistent). **CSRF double-submit** middleware enforces cookie+header match on all mutating requests when a session cookie is present (login/register/reset/csrf exempt). -- `Auth:Require=true` sets a fallback authorize-all policy (prod compose sets it). Dev without a JWT key generates an ephemeral key + warning; **fails closed** if auth is required but unconfigured. -- Local tokens **must** carry a subject claim (`LocalAuthIdentity`), enforced in `OnTokenValidated` — hardened after finding M013-2. -- **Multi-tenancy:** every tenant entity carries `OwnerUserId`; `JobTrackerContext` applies global query filters `CurrentUserId != null && OwnerUserId == CurrentUserId` (**deny-on-null**). Correspondence/JobEvent/CV entities filter through their parent's owner. Covered by `JobApplicationsAuthorizationTests`, `OwnershipGuardTests`. -- Roles via Identity: admin-only `UsersController`, `AdminAuditController`, `AdminSystemController`. -- **2FA:** TOTP (`Otp.NET`), encrypted secrets, QR enrolment (`QRCoder`), recovery codes, trusted devices (`jobtracker_td` cookie), pending-token flow. -- **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:** 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. - ---- - -## 6. Database - -EF Core, **11 migrations**. App DbSets + Identity tables. - -```mermaid -erDiagram - ApplicationUser ||--o{ Company : owns - ApplicationUser ||--o{ Job : owns - ApplicationUser ||--o{ JobApplication : owns - ApplicationUser ||--o| UserRuleSettings : has - ApplicationUser ||--o{ GmailConnection : has - ApplicationUser ||--o{ CvUploadArtifact : owns - ApplicationUser ||--o{ CvExtractionRun : owns - ApplicationUser ||--o{ UserSession : has - ApplicationUser ||--o{ TrustedDevice : has - Company ||--o{ Job : "posts" - Company ||--o{ JobApplication : "has jobs" - Job ||--o{ JobApplication : "applied to via" - JobApplication ||--o{ Correspondence : messages - JobApplication ||--o{ Attachment : attachments - JobApplication ||--o{ JobEvent : events - JobApplication ||--o| TailoredCvDraft : "1:1 draft" - CvUploadArtifact ||--o{ CvExtractionRun : "source of" -``` - -**Entities:** `Company`, **`Job`**, `JobApplication`, `Correspondence`, `GmailConnection`, `GmailReviewDecision`, `MicrosoftGraphConnection`, `ImapConnection`, `Attachment`, `RuleSettings`, `UserRuleSettings`, `SystemEmailSettings`, `JobEvent`, `CvUploadArtifact`, `CvExtractionRun`, `TailoredCvDraft`, `TwoFactorRecoveryCode`, `TrustedDevice`, `UserSession`. - -**Key notes:** -- `ApplicationUser` (IdentityUser) also stores `ProfileCvText`, **`ProfileCvStructureJson`** (the master career profile — a JSON blob, not relational), `AvatarImageDataUrl` (base64 in a column, on the `/auth/me` hot path), Google/Microsoft link info, TOTP secrets, current CV artifact/run pointers. -- **`Job` vs `JobApplication`** — `Job` is the opportunity (title, company, description, URL, salary, location, deadline, tags); `JobApplication` is the user's pursuit of it (status, dates, follow-ups, correspondence, attachments). `JobApplication.JobId` remains nullable for compatibility, but startup backfills missing links and persistence paths synchronize both copies. Legacy reads and columns remain until the production cutover report passes. See §16 and `docs/decisions/ADR-002-job-application-model.md`. -- 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 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 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-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`. - ---- - -## 7. API surface (19 controllers, all under `/api`) - -| Controller | Lines | Highlights | -|---|---|---| -| `JobApplicationsController` | **2394** | **37 endpoints.** CRUD, paging/filter/sort, board, reminders, stats, analytics, history, status/follow-up PATCH, soft delete/restore, duplicate-check, **plus** the AI surface: match-score, candidate-fit, focus-plan, generated interview-prep brief, readiness, tailored-CV draft/preview/export/generate, application-drafts, application-package, follow-up drafts + send, ai-metrics. The canonical timeline and editable interview board live in their focused controllers. | -| `ProfileCvController` | **2249** | CV upload artifacts, extraction runs, structure parsing, reprocess/rebuild/improve, rewrite-section, rewrite-preview, templates, Playwright PDF export, benchmark harness. | -| `GmailController` | **1023** | OAuth connect/callback, sync, review queue, import decisions, job matching. | -| `AuthController` | **879** | login/register/me/config, Google + Microsoft exchange and link/unlink, avatar, password change/reset, email verification, session cookie + CSRF. | -| `AdminSystemController` | 342 | System readiness (DB/Gmail/AI). | -| `TwoFactorController` | 341 | TOTP enrol/verify/disable, recovery codes. | -| `AttachmentsController` | 342 | Validated staged upload, managed-root download, metadata-only rename, quarantined delete, purpose/AI-inclusion metadata and restart reconciliation. See `attachment-storage.md`. | -| `UsersController` | 229 | Admin user/role management. | -| `AdminAuditController` | 219 | Audit trail. | -| `CorrespondenceController` | 185 | Per-job messages CRUD. | -| `CompaniesController` | 150 | CRUD, idempotent create-by-name, recruiter/pipeline fields. | -| `MicrosoftGraphController` | 150 | Outlook/M365 mail linking. | -| `SessionsController` | 104 | List/revoke sessions. | -| `ExportController` | 102 | JSON/CSV export. | -| `RulesController` | 101 | Global + per-user rule settings, clamped. | -| `ClientErrorsController` | 100 | Frontend error intake → logs. | -| `ImapController` | 96 | IMAP mail linking (SSRF-guarded). | -| `BackupController` | 89 | Manual backup trigger. | -| `JobImportController` | **27** | **One endpoint:** `POST /preview`. URL parse only — no persistence, no import history. | - -**God controllers are a top debt.** `JobApplicationsController` and `ProfileCvController` mix HTTP, business logic, AI prompt construction, and persistence. `docs/MASTER_IMPLEMENTATION_GUIDE.md` forbids exactly this ("Avoid: Massive controllers"). Refactor needs test cover first — the tests exist. - -**OpenAPI** is wired (`AddOpenApi` / `MapOpenApi`) but **dev-only** — guarded by `app.Environment.IsDevelopment()`, not exposed in production. - ---- - -## 8. Background services (7 hosted services) - -| Service | Function | -|---|---| -| `RulesHostedService` → `RulesEngine` | Periodic auto-transitions (e.g. → Ghosted) from rule settings | -| `FollowUpReminderHostedService` | Reminder emails for due/upcoming follow-ups (dedup via `LastReminderEmailSentAt`) | -| `DailyExportHostedService` | Daily JSON export at a configured local hour | -| `JobEnrichmentHostedService` | Backfills summaries/enrichment | -| `SummarizerProbeHostedService` | Probes AI service readiness | -| `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) | - -Rules, reminders, daily export and enrichment now enumerate owners explicitly, then re-enter normal tenant-filtered scopes. All four have deny-by-default worker switches and remain inactive until their notification/privacy/entitlement/operations prerequisites are ready; see `docs/architecture/background-workers.md`. 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. - ---- - -## 9. AI pipeline - -**Architecture:** the backend does **not** call any LLM in-process. It HTTP-calls a FastAPI sidecar (`tools/summarizer/app.py`) exposing `/health`, `/cv/normalize`, `/cv/classify-block`, `/cv/rewrite`, `/summarize`, `/extract-text`. The sidecar picks **one** provider from the process-wide `AI_PROVIDER` env var ∈ {`ollama`, `gemini`, `groq`}. - -> **Important — `docs/00-ai-context.md` is wrong about this.** It describes a provider interface fanning out to OpenAI/Gemini/Claude/Ollama, admin-controlled, with users never locked to one model. **None of that exists.** There is one env var, one provider per deployment, no OpenAI, no Claude, no admin control, no per-user selection. Product decision 2026-07-17: **the docs get fixed, the abstraction does not get built** — revisit only if a customer asks. - -**Data flow:** -1. **Job import:** URL → plugin parse (Finn/NAV/LinkedIn/Jobbnorge) or universal JSON-LD parser → optional LibreTranslate → language detect + skill tagging → preview → user accepts → persisted. -2. **Summaries:** `SummarizerService` → `/summarize` (distilbart, TTL-cached, GPU if available) → persisted `ShortSummary`. -3. **CV ingest:** upload (PDF/DOCX/image) → `/extract-text` (OCR) → block classification (`CvAiClassifier`/`CvAiNormalizer` via `/cv/classify-block`) → `StructuredCvProfile` → `ProfileCvStructureJson` on the user. -4. **Tailoring:** job description + structured CV → `/cv/rewrite` → `TailoredCvDraft` (separate entity, per application) → `CvTemplateRenderer` → Playwright → PDF. -5. **Drafts:** cover letter / recruiter message / follow-up per job, attachment-aware context selection. - -**Invariant that holds:** the master profile is **never** auto-overwritten. Tailored output lands in `TailoredCvDraft`, a separate entity. This is the most important documented rule and it is correctly implemented — do not break it. - -**Degradation:** if the AI service or provider is down, core tracking still works (probe service; AI is not a deploy gate). - -**CV templates are hardcoded** — `CvTemplateRenderer.Render` is a C# `switch` over 6 template IDs (`ats-minimal`, `harvard`, `auckland`, `edinburgh`, `monarch`, `fjord`), each a function interpolating HTML strings, with booleans like `roundedPhoto`/`curvedHeader`. **There is no theme model and nothing is user-customisable.** This is a structural dead end for the CV Builder — see `docs/application-discovery-report.md` §10. - ---- - -## 10. Email - -`SmtpEmailSender` + `EmailSettingsResolver`: config from env/appsettings **or** DB-stored `SystemEmailSettings` (admin-editable). Gmail SMTP + app password in prod. Flows: password reset, email verification, follow-up reminders. `App:PublicBaseUrl` is the canonical external origin for generated links, OAuth callbacks, billing redirects, secure cookies and production Host validation. - -Inbound: `GmailOAuthService` (655), `MicrosoftGraphOAuthService` (507), `ImapService` (345, SSRF-guarded). - ---- - -## 11. Configuration & secrets - -- `.env` (git-ignored) → docker-compose env → ASP.NET config. `.env.example` documents the shape. -- `appsettings.Development.json` holds only `CHANGE_ME_*` placeholders. -- Key knobs: `Database:Provider`, `ConnectionStrings:JobTracker`, `Data:Root`, `Cors:Origins`, `Ai:BaseUrl`, `Summarizer:BaseUrl`, **`Ai:ServiceToken`**, `Auth:*` (incl. `Auth:AllowRegistration`), `Email:*`, `Exports:*`, `App:*`, `HttpsRedirection:*` (TLS terminated at the reverse proxy). -- AI service knobs (compose): `AI_PROVIDER`, **`AI_SERVICE_TOKEN`**, `OLLAMA_BASE_URL`, `OLLAMA_MODEL`, `GEMINI_API_KEY`, `GROQ_API_KEY`. -- **`AI_SERVICE_TOKEN` is mandatory.** Both `Ai__ServiceToken` (backend) and `AI_SERVICE_TOKEN` (ai-service) use `${AI_SERVICE_TOKEN:?...}`, so `docker compose up` fails loudly rather than booting an unauthenticated AI service. Generate with `python -c "import secrets; print(secrets.token_hex(32))"`. Rotating it requires recreating **both** containers together — they must agree. -- Note both those compose entries are **quoted**: the `:?` error message contains a colon-space, which YAML would otherwise parse as a map (`services.backend.environment.[20]: unexpected type map[string]interface{}`). -- `ProductionConfigTests.cs` guards prod config shape. -- Ollama is **intentionally not bundled by default** (`bundled-ollama` compose profile) so deploys reuse a shared instance. `AI_PROVIDER=gemini` exists specifically to offload a weak local GPU in prod. - ---- - -## 12. Build, CI/CD, deployment - -- **CI is Gitea, not GitHub** — `.gitea/workflows/ci-deploy.yml`. **There is no `.github/` directory.** -- On PR + push-to-main: build backend (Release) → run **all** backend tests → `npm ci` → run the **whole** frontend suite → build frontend. - - > Corrected 2026-07-17: the archived overview said CI runs "an explicit whitelist of 10 frontend test files". **The whitelist is gone.** The workflow now runs `npm test -- --watchAll=false --runInBand` and carries a comment forbidding its return: the previous whitelist "silently skipped new suites and let two regressions reach main." - -- The workflow is heavily defended against a flaky self-hosted runner: dotnet install retry, `npm ci` SIGSEGV retry, frontend build OOM retry. -- **Deploy** (push to main only): SSH to prod → `git reset --hard ` in `/opt/job-tracker/app` → `deploy/deploy.sh` (compose build/up with retry + cache-prune fallbacks) → verify containers. AI health is non-blocking. -- **No staging environment.** Deploys go straight to prod after CI. - ---- - -## 13. Testing - -- **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/`). -- **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 - -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. - ---- - -## 15. Security posture - -**Verified strong:** -- Multi-tenancy via deny-on-null global query filters, with a dedicated authorization test suite. -- CSRF double-submit on mutating requests; HttpOnly SameSite=Lax session cookie. -- Auth fails closed when required but unconfigured; subjectless-JWT rejected (M013-2). -- SSRF on job import and IMAP fixed and retested (DNS resolution check, private/loopback/link-local rejection, redirects disabled). -- Rate-limited login/email/2FA endpoints; Identity PBKDF2 hashing. -- OpenAPI dev-only. `.env` git-ignored; DP keys and runtime exports untracked (`519c32e`). -- 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. - -**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`) | **Closed 2026-08-02 — production keys rotated by the operator** | -| 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 | **Closed — NuGet and production npm audit reporting are in CI** | - ---- - -## 16. Phase 0 changes (2026-07-17) - -Full record: `docs/phase-0-foundation-report.md`. What changed architecturally: - -- **AI sidecar secured — three layers, verified against the running stack (2026-07-17):** - 1. **No host port.** `ports: "8001:8001"` removed; `expose:` only. - 2. **Private network.** `ai-service` sits on a new `ai_internal` bridge and **nothing else**. It was removed from `default` (which the frontend shares) and from `shared_services` — the latter is `external: true` (`jobtracker_shared`), so any other compose stack on the host could join it and reach port 8001. `ai_internal` has exactly two members: `ai-service` and `backend`. It is **not** `internal: true`, because ai-service needs egress to Gemini/Groq. - 3. **Shared secret.** `X-Ai-Service-Token` required on every endpoint except `/health`, compared with `hmac.compare_digest`. Backend sends it via `Ai:ServiceToken`; sidecar reads `AI_SERVICE_TOKEN`. Unset = open (local dev/tests), but compose declares both with `:?` so the stack **refuses to start** without it. - - **Only the backend can reach the AI service.** Verified live: host → connection refused; frontend container → cannot even resolve `ai-service`; unauthenticated calls to `/summarize`, `/cv/rewrite`, `/extract-text` → 401; wrong token → 401; backend (172.23.0.3) with token → **200 OK**. - - > If you point `OLLAMA_BASE_URL` at an Ollama in **another** compose stack, address it by host IP (e.g. `http://:11435`) — `ai-service` can no longer resolve container names on `shared_services`, by design. The bundled `ollama` profile is on `ai_internal` and still works by name. -- **Pipeline expanded beyond `Applied`** — `JobPipeline` now models pre-application stages (`Saved`, `Interested`, `Preparing`) in a new `PipelineCategory.Prospect`, so a job can be tracked before it is applied to. `Saved` is the new default for wizard-created jobs; `Applied` remains the default for the legacy create path. -- **`DateApplied` is nullable** + `SavedAt` added — a saved job no longer carries a fabricated application date. -- **`Job` entity introduced** alongside `JobApplication`; linked rows are now created and backfilled, while reads remain on the legacy columns pending production cutover validation. - ---- - -## 17. Known debt - -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. - ---- - -## 18. Historical decisions worth knowing - -Recorded nowhere else in active docs: - -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. +Use the overview and its linked topic documents for current behavior. Older expanded architecture snapshots are preserved under [`../_archive/`](../_archive/). diff --git a/docs/architecture/data-flow.md b/docs/architecture/data-flow.md new file mode 100644 index 0000000..f223897 --- /dev/null +++ b/docs/architecture/data-flow.md @@ -0,0 +1,17 @@ +# Key data flows + +## Add and track a job + +The UI submits a reviewed manual or URL-import form to the API. The API validates and normalizes the payload, writes the opportunity/application records, and records a job event. Follow-up, correspondence, attachments, reminders, analytics, and exports build on that application record. + +## Career and CV + +`/career` owns the master career profile. CV upload/extraction produces bounded artifacts and extraction runs; tailored CVs and cover-letter revisions retain their own editable/history records while referring back to the profile and job context. Playwright renders PDFs in the API process with admission and timeout limits. + +## Mail and notifications + +Gmail, Microsoft Graph, and IMAP connections are user-owned and encrypted where appropriate. Gmail imports use a human review queue before uncertain matches are linked. SMTP sends password/reset, verification, reminder, and reviewed employer messages; delivery state is retained where the feature supports it. + +## AI + +The API sends authenticated, policy-qualified requests to the internal FastAPI service. The service performs local summarisation/extraction and can route selected CV tasks to an explicitly enabled provider. Inputs are bounded, parser work is isolated, and AI output remains editable/reviewable. diff --git a/docs/architecture/frontend.md b/docs/architecture/frontend.md index e623a95..7963407 100644 --- a/docs/architecture/frontend.md +++ b/docs/architecture/frontend.md @@ -1,64 +1,7 @@ -# Frontend Architecture +# Frontend architecture -## Goals +The frontend is a statically exported Next.js 16 application. Next provides the build shell (`app/layout.tsx` and `app/page.tsx`); it does not provide server-side data fetching or application routing. React Router owns public and authenticated navigation inside the client shell. -The frontend should be: +The application uses local React state and feature hooks rather than Redux, Zustand, or React Query. API access is centralized in `job-tracker-ui/src/api.ts`. MUI 7 supplies the component library and `src/theme.ts` supplies light/dark theme configuration. English and Norwegian Bokmål translations live in `src/i18n/`. -- Maintainable. -- Consistent. -- Easy to extend. - ---- - -# Structure - -Organise by feature. - -Example: - -features/ - -jobs/ - -career/ - -cv-builder/ - -settings/ - -auth/ - - ---- - -# Components - -Prefer: - -Reusable components. - -Small focused components. - ---- - -# Pages - -Pages should: - -- Compose components. -- Handle routing. -- Manage page-level state. - ---- - -# State - -Avoid unnecessary global state. - -Use: - -- Local state. -- Feature state. -- Server state. - -Where appropriate. \ No newline at end of file +Production builds are served by nginx. Same-origin `/api` proxying is the default; `NEXT_PUBLIC_API_BASE_URL` is an explicit build-time override and must not contain secrets. See [Frontend guide](../frontend/README.md) and [UI system](../frontend/ui-system.md). diff --git a/docs/architecture/integrations.md b/docs/architecture/integrations.md new file mode 100644 index 0000000..027b9cb --- /dev/null +++ b/docs/architecture/integrations.md @@ -0,0 +1,16 @@ +# Integrations + +| Integration | Purpose | Configuration | +|---|---|---| +| Google Identity | Optional sign-in/linking | `AUTH_GOOGLE_CLIENT_ID` | +| Gmail API | OAuth mail connection, sync, review queue | Gmail client secret and redirect settings | +| Microsoft Identity/Graph | Optional sign-in and Outlook mail linking | `AUTH_MICROSOFT_*`, `MICROSOFT_*` | +| IMAP | User-provided mailbox connection | Configured through the API/UI; validate host/port safely | +| SMTP | Verification, reset, reminder, and admin test mail | `EMAIL_*` | +| NAV/job sites | Official job discovery/import providers | Provider-specific API settings in the API configuration | +| LibreTranslate | Optional job-description translation | `Translation:Provider` and provider settings | +| Ollama | Local CV classification/generation | `OLLAMA_BASE_URL`, `OLLAMA_MODEL` | +| Gemini/Groq | Explicit external CV fallback | AI provider keys and routing/consent settings | +| Stripe | Hosted Premium checkout, portal, and webhooks | `STRIPE_*`; activation remains operator-blocked | + +External-provider checks are not implied by local tests. See [security notes](../security/security-notes.md) and [roadmap blockers](../roadmap/known-issues.md). diff --git a/docs/architecture/overview.md b/docs/architecture/overview.md index 9b24b57..cbad53a 100644 --- a/docs/architecture/overview.md +++ b/docs/architecture/overview.md @@ -1,93 +1,42 @@ -# Application Architecture Overview +# Architecture overview -## Purpose +Last verified: 2026-09-11 against the current repository. -Define the target architecture direction for Jobjakt. +## Components -This document does not require an immediate rewrite. +```mermaid +flowchart LR + Browser --> Nginx[nginx static frontend] + Nginx --> API[ASP.NET Core API] + API --> DB[(SQLite or MariaDB/MySQL)] + API --> Data[Data root: uploads, CV artifacts, exports, keys] + API --> AI[FastAPI AI service] + AI --> Local[local transformers/Ollama] + AI --> Cloud[optional Gemini or Groq] + API --> Mail[Google/Gmail, Microsoft Graph, IMAP, SMTP] + API --> Jobs[NAV and job-site import clients] +``` -The goal is incremental improvement while keeping the application functional. +The frontend is a client-rendered React application inside a Next.js static-export shell. nginx serves the export and proxies same-origin `/api` requests to the API. The API owns HTTP endpoints, business services, EF Core models/migrations, authentication, background services, files, and integrations. The AI service is an internal network dependency; it is not a user-authenticated application. ---- +## Technology and ownership -# Product Architecture +| Component | Current implementation | +|---|---| +| UI | Next.js 16, React 19, TypeScript 5.9, MUI 7, React Router 7 | +| API | ASP.NET Core 9, EF Core 9, ASP.NET Identity, JWT bearer, cookie sessions | +| Data | SQLite default; Pomelo MySQL/MariaDB provider for server deployments | +| AI | FastAPI, transformers/distilbart summarizer, OCR and document extractors, Ollama/Gemini/Groq CV routes | +| Runtime | Docker Compose, nginx 1.29-alpine, optional Ollama profile | +| Delivery | Gitea Actions → SSH deployment → Docker Compose | -The application consists of: +Further detail: [frontend architecture](frontend.md), [backend architecture](backend.md), [data flow](data-flow.md), [integrations](integrations.md), [database schema](../database/schema.md), and [deployment](../infrastructure/deployment.md). -## Core Platform +The older [architecture/current.md](current.md) path is retained as a compatibility pointer to this overview. -Job Tracking +## Important boundaries -↓ - -Applications - -↓ - -Communication - -↓ - -Follow-ups - - ---- - -## Supporting Platform - -Career Workspace - -↓ - -Master Career Profile - -↓ - -CV Builder - -↓ - -Documents - - ---- - -## Intelligence Layer - -AI Services - -↓ - -Recommendations - -↓ - -Generation - ---- - -# Architecture Goals - -The system should be: - -- Maintainable. -- Testable. -- Extensible. -- Secure. -- SaaS-ready. - ---- - -# Evolution Strategy - -Prefer: - -- Refactoring. -- Extraction. -- Clear boundaries. -- Incremental migrations. - -Avoid: - -- Large rewrites. -- Replacing working systems unnecessarily. \ No newline at end of file +- `Job` is the opportunity; `JobApplication` is the user's pursuit of it. Compatibility columns and dual writes remain until the production cutover gate is completed. +- Every tenant-owned record is filtered by the authenticated owner. A missing owner is denied, not treated as an anonymous tenant. +- AI is best-effort and policy-gated. Manual tracking remains usable when the AI service or a provider is unavailable. +- The current deployment is single-backend-instance. Worker leasing is intentionally not implemented for multi-replica operation. diff --git a/docs/audits/2026-09-07/validation/README.md b/docs/audits/2026-09-07/validation/README.md index 2d25ef3..7b8d9d1 100644 --- a/docs/audits/2026-09-07/validation/README.md +++ b/docs/audits/2026-09-07/validation/README.md @@ -1,228 +1,3 @@ -# Independent validation of the Jobjakt audit +# Archived audit validation -Validated on 2026-09-07 against application commit `7d641edc7bca866ccf06012897ba1d107c07e59f`. This is a verification and planning pass. No application behavior, schema, deployment configuration or production data was changed. The original audit is preserved. - -**Subsequent implementation status, 2026-09-09:** [B01 HTTP import enforcement](b01-completion.md), [B02 account/session isolation](b02-completion.md), [B03 shared PDF admission](b03-completion.md), [B04 authenticated rate partitioning](b04-completion.md) and [B05 spreadsheet-safe CSV](b05-completion.md) are complete locally. B05 verification passed 902 selected backend tests, 298 frontend tests, 18 browser tests, nine B04 HTTP scenarios and the unchanged B03 admission probe. Release builds and targeted lint pass; the existing R01a TypeScript failure remains. The historical analysis below describes the pre-fix baseline. F05 is resolved locally under its defined export policy; F08/V01 and F06 are resolved locally. F11 remains open for B07/IMAP and deployment evidence. F03 admission is resolved locally, with R04 containment/capacity evidence still open. B06 was subsequently authorized; see the separate progress and remaining gates below. - -**B06 runtime follow-up, 2026-09-09:** [Report and operator handoff](b06-completion.md#runtime-verification-follow-up-2026-09-09). **Implementation complete; full runtime/production verification incomplete; F04 open.** Fresh 166 focused backend, 3 Compose, 6 Production HTTP and 9 B04 HTTP checks pass. Public DNS/valid HTTPS are now observed; direct ports 3000/5202/8080 are unreachable from this workstation, without proof of the rejecting layer. HTTP redirects to unreachable HTTPS port 444. Docker's inference socket failure still blocks actual Compose/Traefik/nginx tests; production peers, backend client IP and host/provider firewall remain unverified. No SSH alias/user was found in permitted non-secret sources. B01–B05 preserved; B07 not started. - -**Verdict: Suitable for private beta only.** Keep participation controlled, with informed testers, an operator recovery path and paid checkout disabled until its gates pass. This is not approval to expose the current import/PDF surfaces to unrestricted registration. The evidence does not establish a universal tenant authorization failure that would make all external use inappropriate, but it does establish account-switch corruption, silent lost updates and insufficient abuse containment. Those prevent a public free beta. Paid use adds unresolved billing/deletion and operational proof requirements. - -**Confidence in the original audit: 7/10.** It finds substantial real problems, provides useful source locations, and often labels unverified conditions honestly. Its 416-row coverage inventory is a map of evidence and open questions, not 416 completed validations. Severity is too broad for CSV, calendar and privileged-support concerns; the account-switch consequence is understated; the import address filter has a simpler bypass than the reported DNS race. Its implementation order puts recovery too late and puts identity isolation too close to a general UI cleanup. - -- [Validated implementation queue, ticket sizes, dependencies and handoff](implementation-queue.md) -- [Evidence, commands, limitations and reproduction instructions](evidence.md) -- [Independent browser/API observations](evidence/observations.json) -- [Independent .NET boundary observations](evidence/boundary-results.json) - -## Audit confidence and method - -Read the original README, findings, coverage, verification, resilience, product-operations, threat-data, source-index, `break-jobjakt.mjs`, retained break results and relevant `docs/todo/work.md` items. Inspected the implementation and tests behind the high-priority claims, rather than accepting their descriptions. The pasted expanded audit request provides background; the current validation-only instruction controls this pass. - -Fresh verification: **149 backend tests passed** in two disjoint targeted runs (70 + 79); **13 interview frontend tests passed**; standalone TypeScript failed with the exact original error. Independent probes used synthetic SQLite databases, real local HTTP endpoints, two browser tabs, the real PDF exporter with three inert child executables, and a real .NET host with a one-shot database fault. Existing original full-suite/build results are historical evidence, not new executions. No live Stripe, SMTP, DNS rebinding, production host, production database, external website or cloud metadata endpoint was contacted. - -The original harness is useful observational evidence, not a regression gate: recording a failed expectation does not itself make its exit code fail. Its simultaneous HTTP saves do not force both EF contexts to load the same revision. Its repeated POST does not establish an advertised idempotency contract. Its combined overflow/axe result conflates layout and accessibility. The independent probes add a deterministic two-context schedule, a UTC calendar control, actual cross-account saving, and process/fault boundaries. These probes also collect observations; implementation tickets must turn the relevant failures into assertions. - -## Every original P0/P1 finding - -There are **no original P0s and 11 original P1s**, including conditional P1s. None is omitted below. Classifications distinguish a proven implementation defect from its unverified production impact. No new P0 is supported. - -### F01 — stale saves and revision history - -**Confirmed. Retain P1.** The HTTP stale save returned 200 and replaced the newer state. Two independently preloaded real SQLite contexts both saved revision 1; the final row was Writer B/revision 2 and history was **[1, 2, 2]**. This is recoverable only in part: settings snapshots exist, but variant names are not included in those snapshots, and duplicate revision numbers make restore identification ambiguous. - -The root cause is correct: [SaveAsync](../../../../JobTrackerApi/Services/CvVariantService.cs#L108) increments an ordinary integer after reading, [SaveRequest](../../../../JobTrackerApi/Controllers/CvVariantController.cs#L34) supplies no expected revision, and [the model/index](../../../../JobTrackerApi/Data/JobTrackerContext.cs#L498) does not make the update conditional. A database transaction alone will not reject a stale client. Career Profile child/history writes already have a transaction; their missing precondition and the later legacy user projection are separate remaining problems. Job full-document updates likewise have no client revision precondition. - -**Corrected fix:** define a small revision contract and implement it aggregate by aggregate. Atomically compare expected revision and increment on update; make history insertion part of that transaction. Reconcile existing duplicate labels without discarding snapshots before adding uniqueness. Retain the draft on 409/412; do not automatically retry it over newer content. `StructuredCvProfile.version` is a document schema version, not a concurrency revision: introduce a distinct field. Include restore and every mutation capable of changing that aggregate; do not assume one PUT covers status patches, admin undo or generated drafts. - -**Acceptance:** the proposed tests are appropriate but require deterministic barriers/two contexts and both supported relational providers, not an in-memory provider or two loosely timed requests. Assert one winner, no partial children/projection/history, a preserved losing draft, unambiguous restore, delete-versus-save behavior and an older open frontend receiving a safe refusal. Do not promise transparent compatibility for old unconditional writers while claiming lost updates are prevented. Queue: D01–D04. - -### F02 — billing and account deletion - -**Mostly confirmed. Retain P1 for paid operation; provider effects need runtime verification.** [Checkout](../../../../JobTrackerApi/Controllers/BillingController.cs#L61) and [the Stripe gateway](../../../../JobTrackerApi/Services/StripeBillingGateway.cs#L14) create sessions without a durable purchase-attempt identity. Two requests can create two sessions; that is not proof of two charges, since completion still matters. The webhook verifies the signature, fetches current subscription state and checks ownership/price: ordinary sequential replay is already handled better than an event-order-only design. Identity's concurrency stamp also means some races fail and retry rather than blindly overwrite. - -The remaining cause is split subscription/role updates plus an account model that does not reconcile all relevant subscriptions. An active subscription and a different cancelled subscription for the same account cannot be resolved reliably by treating each event's subscription as the entire account truth. [Deletion](../../../../JobTrackerApi/Services/AccountDeletionService.cs) removes identity/billing IDs without a billing cancellation stage. Deletion admission is **disabled by default**; do not describe an active paid deletion incident as observed. - -**Corrected fix:** purchase-attempt idempotency, one explicit entitlement derivation, atomic local subscription/role state and retryable reconciliation. Start with a guarded synchronous handler and provider retries if that meets the contract. A new event inbox, broker or general outbox is not intrinsically required; durable receipt is required before acknowledging work that will happen asynchronously. Reuse existing deletion stages, preserve billing identifiers until resolution, and initially reject/defer deletion when active or uncertain billing cannot be resolved. This containment can precede full cancellation automation. - -**Acceptance:** existing billing tests cover configured price, rejected product ID, signed sequential lifecycle/replay and bad signature; they do not prove concurrent purchase/deletion correctness. Add crash-between-local-writes, two subscriptions, delayed conflicting events, retry after an unknown checkout result, pending checkout versus deletion, unavailable provider and verified cancellation tests. Follow with an authorized Stripe sandbox run. Queue: P01–P03. - -### F03 — PDF host exhaustion - -**Remediation update, 2026-09-08:** [B03](b03-completion.md) closes shared admission and normal failure/cancellation lifecycle defects. Default one active render per API process, one per document owner, zero queue, validated configuration and safe 503/504 behavior now have permanent before/after and multi-route regression evidence. The original finding below remains historical; F03 is only partially resolved until R04 proves deployment containment and capacity. - -**Confirmed missing admission control. Retain P1 for an untrusted public audience.** The singleton [exporter](../../../../JobTrackerApi/Services/PlaywrightCvPdfExporter.cs#L37) starts one child process per call. Three calls through that actual implementation started three inert child processes while all were held open. A per-process 60-second timeout bounds duration, not total concurrency. The authenticated variant export has no PDF rate policy. Other PDF endpoints share this exporter, so endpoint-only patches leave bypasses. - -Root cause is shared resource admission and lifecycle, not a proven Chromium exploit. Root container execution and sandbox-disabled flags increase containment concerns but do not establish RCE. **Corrected fix:** a small configurable shared concurrency bound, bounded or zero waiting, cancellation/cleanup, owner burst limits and host resource limits. Preserve legitimate downloads and distinguish admission denial from renderer failure. Measure before splitting the renderer into another service; no durable queue is required for request-scoped exports. Per-process admission is sufficient only for the current process topology; account for additional API replicas explicitly. - -**Acceptance:** retain the proposed fake-process test, cover all exporter entry points, cancellation, timeout and partial output cleanup; then a small controlled host test measures RSS/process count with container limits. The three-process probe proves missing serialization, not an exhaustion threshold or safe production capacity. Queue: B03, R04. - -### F04 — forwarded headers and direct ingress - -**Needs runtime verification. Retain conditional P1.** [Compose](../../../../docker-compose.yml#L140) publishes nginx on host port 3000; [nginx](../../../../job-tracker-ui/nginx.conf.template#L64) forwards incoming forwarding headers while the backend trusts the nginx network. This is a real unsafe assumption if that path is externally reachable without the expected sanitizing proxy. A canonical Host header is not caller authentication. - -The original root cause and qualified recommendation are sound. **Do not assert that the deployed firewall is bypassed:** its rules/topology were not inspected. Verify both the direct and normal routes, IPv4/IPv6 and effective backend client IP. Then close the direct route or sanitize at the correct trusted hop. Loopback binding is appropriate only if the upstream proxy is local. Avoid a speculative proxy redesign. The proposed isolated topology test is necessary, followed by operator evidence from the actual host. Queue: B06; it gates exposed release, not local source work. - -**Subsequent B06 status:** [Peer enforcement/configuration changes](b06-completion.md) are implemented and native API regressions pass. F04 is **not resolved**: real nginx/container execution is blocked and actual host/proxy evidence is still required. Historical findings above remain preserved. - -### F05 — spreadsheet formula injection - -**Confirmed. Downgrade P1 → P2.** A harmless title `=1+1` remains a formula in CSV. [Esc](../../../../JobTrackerApi/Controllers/ExportController.cs#L78) performs CSV syntax quoting, which does not change spreadsheet cell interpretation. Opening attacker-influenced imported content in a spreadsheet is a plausible additional user step, but no command execution, credential disclosure or server compromise was demonstrated. The current evidence does not justify a blanket P1. - -The root cause and small fix are correct. Use one text-field policy, preserve JSON/raw portability, distinguish real numeric fields and verify leading whitespace/control characters and delimiters through a CSV parser. A huge generic export framework is unnecessary. The proposed acceptance needs a harmless manual spreadsheet check because parser round-trip alone does not establish spreadsheet interpretation. Fix early because it is cheap and protects a shipped export; it is not an independent reason to prohibit all external use. Queue: B05. - -**Subsequent resolution, 2026-09-09:** [B05 completion](b05-completion.md) preserves this historical finding and closes the reproduced export failure locally. Actual Excel before/after changes `=1+1` from an evaluated formula to apostrophe-prefixed literal text; all 13 text columns share the tested policy. Typed cells and stored/JSON data remain unchanged. UTF-8 BOM plus byte-preserving browser download fixes observed Norwegian decoding. LibreOffice runtime and arbitrary spreadsheet reprocessing are not claimed verified. - -### F10 — aggregate storage and resource growth - -**Mostly confirmed. Retain P1 for storage/admission exposure; split off P2 duplicate-intent and unmeasured scale work.** [Attachment admission](../../../../JobTrackerApi/Controllers/AttachmentsController.cs#L269) sums current rows before staging/writing; concurrent uploads can both pass. It omits generated exports/artifacts/package copies. [Account export](../../../../JobTrackerApi/Services/AccountDataExportService.cs#L42) streams the ZIP file but materializes collections and serializes full JSON byte arrays. It is incorrect to call the entire ZIP an in-memory buffer; peak per-table memory remains a real concern. - -No production disk exhaustion or 100k-row memory threshold was measured. Two job POSTs with the same unrecognized `Idempotency-Key` produced different IDs. That proves no implemented key contract, not an HTTP violation or a need for unique company/title jobs. Repeated applications can be legitimate. Idempotency belongs to a specific creation intent and does not replace quotas. - -**Corrected fix:** hard admission bounds first; reuse the existing [owned-file inventory](../../../../JobTrackerApi/Services/AccountOwnedFileInventory.cs) for accounting/reconciliation. Use a database-safe owner reservation, not a process-local quota lock. Include crash/release handling and generated categories. Stream/batch large exports when measurements require it. Do not silently prune user history to meet a new limit. Separate job creation retry semantics from this resource ticket and from F01. - -**Acceptance:** simultaneous near-limit reservations, file/DB failure cleanup, generated artifacts and multi-instance accounting; synthetic bounded-memory exports; existing over-quota accounts remain readable/exportable. Same-intent/different-payload/retry tests belong to D04. Do not require every query to be redesigned before safe initial bounds ship. Queue: D04–D07; PDF overlap belongs only to B03. - -### F11 — outbound URL boundary - -**Remediation status:** HTTP B01/V02 resolved in the working tree on 2026-09-08; [connection-time regression evidence and limitations](b01-completion.md). Overall F11 remains partially remediated because IMAP B07 is unstarted. The following finding/evidence is preserved from the original validation. - -**Confirmed boundary defect; retain P1. Production reachability is unverified.** In addition to the source-confirmed validation/connection DNS gap, the real [import validator](../../../../JobTrackerApi/Services/JobImport/JobImportService.cs#L199) accepts IPv4-mapped IPv6 representations of denied IPv4 addresses. A fake HTTP handler was invoked for mapped private, link-local and metadata addresses, while ordinary IPv4 representations were blocked. No network socket was opened by that probe. This bypass does not need changing DNS answers. - -The original explanation is incomplete: normalize mapped addresses before classification, and bind address validation to the address actually connected. The registered HTTP handler resolves separately after validation. IMAP also validates then calls `ConnectAsync(host, ...)`; it **already normalizes mapped IPv4** in its guard, so do not attribute the import-specific normalization bug to IMAP. It still needs connection-time enforcement. - -**Corrected fix:** protect the actual socket boundary while retaining hostname/TLS validation, redirect rejection, size limits and cancellation. A proven restrictive egress layer can satisfy the boundary, but none was verified here. Reuse a narrow address policy only where the policies are genuinely the same; do not build a universal network platform or disable TLS checks. The original proposed mixed/mapped/DNS/TLS acceptance matrix is good, but it was a proposed test, not proof that the existing mapped check worked. Queue: B01 HTTP, B07 IMAP. - -### F12 — backup/restore and rollback - -**Mostly confirmed. Retain P1 as a release/recovery gate.** The [daily runner](../../../../JobTrackerApi/Services/DatabaseBackupRunner.cs#L68) uses SQLite `VACUUM INTO`, a consistent database snapshot. The pre-deploy [live-volume tar](../../../../deploy/deploy.sh#L314) is not equivalent; concurrently changing DB/WAL/files have no demonstrated common recovery point. MariaDB's transactional SQL dump does not make separate file archives transactionally consistent. No corrupt backup was reproduced or alleged. - -The failure trap [restores previous images](../../../../deploy/deploy.sh#L638), not the old database schema. This is safe for some additive changes and unsafe for incompatible migrations. Do not disable every image rollback indiscriminately. **Corrected fix:** verified recovery sets, a deliberate file checkpoint/write pause, checksums and an isolated restore drill; attach schema compatibility to rollback decisions. Keep keys and deletion tombstones recoverable with appropriate independent custody. Reuse existing backup machinery. - -**Acceptance:** restore while the source has synthetic writes, validate integrity/ownership/files/decryption/tombstone replay, kill incomplete backups and prove compatibility with the previous binary. The original tests are sufficient in intent but unexecuted for deployment recovery. Run this **before the first new schema migration**, not after concurrency, billing and quota migrations. Queue: R02–R03. - -### F13 — privileged support, auditing and retention - -**Partially confirmed. Downgrade engineering finding P1 → P2.** [AdminAuditController](../../../../JobTrackerApi/Controllers/AdminAuditController.cs#L12) is explicitly Admin-authorized. Cross-owner activity and undo are deliberate admin capabilities; they are not a normal-user tenant escape. Its returned private event fields and absence of a dedicated security event ledger support minimization/audit work. Infrastructure root access and absence of a support/impersonation role are not vulnerabilities by themselves. - -The finding combines three different things: admin product permissions, accountable security mutations, and owner/legal retention decisions. **Corrected fix:** document the existing admin boundary, minimize fields where they are unnecessary, add safe actor/action/target/outcome events for high-impact changes, and use recent authentication where warranted. Do not invent impersonation, a role hierarchy or tamper-proof logging claims. A restricted support role is justified only by an actual support use case. - -**Acceptance:** unauthorized verbs remain denied, intentionally authorized admin verbs remain functional, privilege revocation takes effect, and audit metadata excludes arbitrary content/secrets. Off-host audit custody and policy approval require operator evidence; unit tests cannot certify them. Retention/deletion policy remains a public/paid operational prerequisite without converting every suggested admin improvement into P1. Queue: W04 and P03 policy dependency. - -### F20 — AI worker failure containment - -**Confirmed when enabled. Retain conditional P1.** A real host running the actual [worker](../../../../JobTrackerApi/Services/AiOperationQueue.cs#L254) with concurrency 1 stopped after a single injected database timeout. With concurrency 2, one loop failed but the host and combined task did not stop during the observation window: `Task.WhenAll` waits on the surviving loop. The audit misses this second mode, where capacity silently shrinks rather than immediately stopping the host. - -The cause is failure escaping the loop boundary, not simply the host exception policy. Compose defaults this worker **off**, so a default deployment outage is not established. **Corrected fix:** catch/classify at the loop, bounded retry/backoff, explicit degraded health and lease recovery. Do not globally ignore background-service exceptions or hide a dead worker behind a healthy API. - -**Acceptance:** one- and multi-worker tests, claim/owner/result persistence failures, cancellation, recovery and lease expiry, followed by bounded outage drills. The local probe confirms host/worker behavior, not endpoint availability during a deployed DB outage or eventual recovery. Keep the feature disabled until those acceptance tests pass. Queue: R05. - -### F21 — calendar/timezone - -**Mostly confirmed. Downgrade P1 → P2 for the currently implemented workflow.** Offset-bearing input lost offset information after SQLite reload; the reproduced spring value exported 09:00Z instead of 07:00Z, and the two autumn offsets collapsed. However, the **UTC input control preserved 07:00Z**. This is not evidence that every production UTC event shifts. Server timezone affects DateTime parsing; production timezone/provider behavior was not exercised. - -More fundamentally, the normal job editor and Interview Prep send **date-only follow-up values**. [Calendar conversion](../../../../JobTrackerApi/Controllers/ApplicationCalendarController.cs#L129) turns an unspecified date into a UTC timed event. The independent date-only control became midnightZ, which can display on a different local date. The root cause is a missing date-versus-instant contract, not a universal need for named-zone recurrence. - -**Corrected fix:** preserve the current date-only product semantics and emit all-day follow-up/deadline events in ICS and provider links. Specify UTC normalization for any genuinely timed API field. Do not infer historical timezones or shift stored dates globally. A timed appointment/recurrence feature needs its own product scope and evidence. Current impact is workflow correctness; P1 would need evidence that a promised timed scheduling flow is routinely losing important appointments. - -**Acceptance:** date-only all-day values and exclusive end dates, UTC control, offset cases, both providers and server timezone permutations. The original offset tests are valuable but insufficient without the date-only control. Queue: W01, with no broad timestamp migration assumed. - -## New findings and severity increases - -### V01 — account A's open form can overwrite account B (F08 raised to P1) - -**Remediation status:** B02/F08/V01 resolved in the working tree on 2026-09-08. [Server session precondition, frontend isolation, before/after regression evidence and rollout limits](b02-completion.md). The original finding below is preserved as historical evidence. - -**Confirmed with real UI and HTTP persistence.** Tab A loaded account A's Career Profile and edited its full name. Tab B used the actual sign-out menu and signed into B. Tab A remained mounted; clicking its Save changes button returned **200**. An independent API session read the A draft in **B's stored profile**, while A's stored profile remained unchanged. - -[auth.ts](../../../../job-tracker-ui/src/auth.ts) broadcasts same-window events; [App](../../../../job-tracker-ui/src/App.tsx#L248) does not synchronize the authentication boundary across tabs. [api.ts](../../../../job-tracker-ui/src/api.ts) uses shared current cookies/CSRF state. [CareerProfilePage](../../../../job-tracker-ui/src/views/CareerProfilePage.tsx#L855) sends its retained local form to an endpoint whose owner is implicit in those cookies. Backend ownership checks correctly resolve B; the client submits A's content as B. This is not a direct-object-reference authorization bypass, and it is more serious than private text staying visible after logout. - -**Required scope:** one account/session generation boundary; promptly clear/unmount private owner state on logout/account change, cancel or ignore late responses, and refuse dispatch from a form loaded under a different identity. Include a server-checked expected-owner/session precondition where needed to close the cookie-switch race; it can only reject a mismatch, never grant authority. Reading the latest localStorage identity at send time defeats the purpose: bind to the identity that loaded the form. Revision numbers alone do not solve this because A and B can have equal revisions. Tests must repeat this exact two-tab save and assert both accounts' stored profiles, not just navigation. Queue: B02. - -### V02 — mapped-address import bypass (F11 strengthened) - -The new address-normalization proof above removes DNS timing as a prerequisite for passing the application guard. Track it under F11/B01, not as a second duplicate SSRF implementation project. F20's multi-worker failure mode likewise extends its existing ticket. No additional independent P0/P1 is claimed merely because an area lacks complete test coverage. - -## Other highlighted findings and negative evidence - -| Area | Independent conclusion | -|---|---| -| F06 account/IP rate limiting | **Confirmed P2; resolved locally by [B04](b04-completion.md), 2026-09-08.** Historical behavior: A's two exports made B's first return 429 because rate limiting preceded authentication. Authentication and endpoint authorization now resolve the final trusted resource principal before account partitioning; permanent real HTTP tests prove A/B independence, shared same-account sessions, invalid/forged credential handling and preserved IP policies. Deployment proxy reachability remains B06. | -| F09 draft recovery | **Partially confirmed P2.** The original user accepted an unload warning and refreshed while offline. Lack of durable recovery is real, but this is not a violated guaranteed-save contract. Preserve current dirty-state warnings; do not introduce persistent private browser drafts without an ownership/retention decision. W05 follows identity and revision work. | -| F14 accessibility | **Confirmed P2, with a structural release acceptance gap.** Shared contrast defects and a responsive preview scrolling defect reproduce; see the focused review below. Axe's `serious` label is not automatically product severity P1. | -| F15 export/cache/CSP | Owner checks and recent-auth account export are present. Missing no-store headers are hardening evidence, not a demonstrated cross-user CDN cache disclosure. Review sensitive responses and preview-compatible CSP without conflating it with V01. W03. | -| F17 TypeScript | **Confirmed P2 CI gap**, test-only diagnostic; exact explanation below. R01. | -| F19 provider fallback | Source confirms silent SQLite fallback for a missing connection or unknown provider. Keep P2 with a release-startup gate: production misconfiguration should fail closed. No proof the deployed instance points at the wrong database. R01. | -| Tenant/document authorization | Fresh owner-boundary checks denied B's access to A's variant/preview/PDF. Version listing returns **200 with []**, which reveals no other-owner history and is not an IDOR. Acceptance should require no data/no effects rather than insist every safe empty listing be 404. Query filters and explicit owner checks remain important positive evidence. | -| Auth, admin, entitlement | Fresh authorization/session/role/identity tests passed. The Pro handler re-reads user roles/AI preference, rather than relying only on stale JWT entitlement claims. No newly demonstrated ordinary-user admin access or session-revocation bypass. Admin content access remains intentional and needs policy/minimization, not an automatic security rewrite. | -| AI and uploads | Privacy/ownership tests passed; AI output remains untrusted and must stay inside existing structured validation and review boundaries. Existing parser/file staging/type/size/owner checks constrain the obvious paths. No new cross-owner document download, executable upload or model-driven privileged action was demonstrated. Resource admission and URL boundaries remain the evidenced problems. | -| Migrations, secrets and deployment | Fresh migration-chain tests passed; no new destructive migration/data-drop defect was reproduced. They do not prove rolling back old binaries or restoring files/keys. The tracked-secret scanner and lock files are useful but do not certify Git history, deployed secrets or current dependency advisories. External/host verification remains open; no secrets or live environments were inspected. | - -## TypeScript and production release gates - -Exact fresh error: `src/interview-prep.test.tsx(120,10): error TS2571: Object is of type 'unknown'.` - -The [test](../../../../job-tracker-ui/src/interview-prep.test.tsx#L120) reads `.content` from `mockedApi.post.mock.calls[0][1]`; the generic Axios request body is `unknown` at that boundary. The test's earlier runtime matcher does not narrow that later expression for TypeScript. All 13 tests in the suite passed under Babel/Jest, which transpiles TypeScript without performing this static check. The installed Next `runTypeCheck.js` explicitly filters `.test`, `.spec`, `__tests__` and `__mocks__` diagnostics; Next build success is therefore consistent with standalone tsc failure. - -The [CI workflow](../../../../.gitea/workflows/ci-deploy.yml) runs frontend tests/lint/build/browser checks without an explicit standalone `tsc --noEmit` gate. This is a real configuration gap. The diagnostic is in a test and does not demonstrate a broken emitted production component. It would not alone justify an emergency production rollback or P1 incident, but the next planned release should have a clean explicit type gate. Correct the test's typed payload boundary narrowly; do not disable strictness, exclude all tests or use an unrelated framework migration. No fix was made in this pass. - -## Accessibility conclusions - -The [normal browser accessibility suite](../../../../job-tracker-ui/e2e/accessibility.spec.ts) checks `/`, `/login`, `/dashboard`, `/jobs`, `/kanban`, `/career`, `/settings` and `/admin/system`. It forces English and waits primarily for `body`/`main` visibility. It does not ensure loaded content or open interactive states, and does not systematically cover dark mode or Bokmål. - -Missing important coverage includes `/career/builder`, `/career/builder/:id`, actual profile/notes states, job detail/edit and interview interactions, public CV, registration/reset/second-factor flows, admin users/audit, menus/dialogs, errors/conflicts and mobile preview/edit transitions. A green suite is valid evidence for its sampled states, not accessibility certification for the product. - -The fresh loaded-builder probe found light-mode success chips at **3.29:1** (`#16a34a` on white), including Saved and ATS chips. Dark primary text appears at **3.50–3.66:1** on the two dark surfaces. [theme.ts](../../../../job-tracker-ui/src/theme.ts) reuses the accent as primary text across themes; outlined buttons/tabs/chips inherit these values. This proves a shared token/component relationship. Fix semantic foreground/background pairs and relevant MUI state overrides centrally, then verify existing surfaces; do not patch each page's color or add a new design-system framework. - -The preview's scroll region also fails `scrollable-region-focusable` in the mobile-to-desktop transition. It is not present in every initial viewport/theme sample. The offending overflow region contains the rendered page iframe and has no explicit focusable region contract; [CvBuilderEditor:480](../../../../job-tracker-ui/src/views/CvBuilderEditor.tsx#L480) is the corresponding preview container. Repair named region/focus/keyboard scrolling and verify actual behavior, rather than adding tabindex to every Box. The editor controls already include keyboard-capable template cards; do not label every custom card inaccessible without testing it. - -Before public release, manually complete the primary tracking/profile/builder flow using keyboard only and NVDA or Narrator, including preview scroll, tab order, focus return, error/conflict announcements and reauthentication. Test 200%/400% zoom and 320 CSS-pixel reflow, text spacing, Windows forced colors, reduced motion, touch/keyboard alternatives to drag-and-drop, and real mobile keyboard overlap. No screen-reader/manual accessibility certification was performed here. Structural keyboard and focus failures take precedence over polish. Queue: A01–A03. - -## Shared root causes, without a generic overhaul - -1. **Record preconditions:** CV, job and Career Profile updates lack a client/server revision contract. Share the convention and error semantics; keep aggregate-specific transactions and conflict UI. Do not confuse this with schema version numbers. -2. **Identity lifecycle:** cross-tab cookies, retained forms and late responses have no common account-generation boundary. Fix this before adding persistent drafts or rebuilding editors. It is not fixed by per-record revisions. -3. **Admission before side effects:** PDF launch, file quotas and owner rate policies do not consistently enforce limits at the boundary that consumes the resource. Reuse existing services, with a shared exporter bound and database-safe storage reservations. An idempotency key limits repeated intent; it is not a resource quota. -4. **Validated target versus connected target:** import and IMAP share a time-of-check/time-of-use boundary, while only import misses mapped-address normalization. Share only the proven common policy/connection principle. -5. **Split local/provider state:** billing/deletion and profile legacy projection need explicit commit/recovery boundaries. They do not all require a new event bus. The profile projection can usually join its transaction; external billing cannot. -6. **Shared visual semantics:** theme tokens create cross-page contrast failures; preview focus is a local structural component issue. Fix these before changing page layouts. -7. **Proof at the wrong layer:** helper tests do not prove middleware order, a green shell does not prove loaded accessibility, a tar file does not prove restore, and a green build does not prove the standalone TypeScript program. Add the missing boundary tests rather than expanding every suite indiscriminately. - -## Confirmed launch blockers and release levels - -For a **public free beta**, close V01/F08 identity corruption, F01 lost writes, F11 outbound target enforcement, F03 PDF admission and the concrete F10 storage admission gaps. Establish a verified recovery set/compatible rollback and confirm actual ingress/configuration safety. Resolve primary-flow structural accessibility failures and retain manual evidence. These are correctness/security/recovery gates; a broad UX overhaul is not required. - -For a **limited paid beta**, additionally prove F02 purchase/entitlement/deletion handling in sandbox, a workable retention/deletion/support policy, verified key/data recovery and functioning enabled premium features. F20 blocks enabling its worker until containment/recovery tests pass. Keeping paid deletion disabled is not a complete long-term paid-account policy. - -For a **public paid launch**, complete representative end-to-end paid journeys, accessibility/manual verification, support and recovery drills, measured capacity at the intended exposure, and production ingress/secrets/release evidence. Exact public host, Stripe, mail, off-site backup, advisory and operator controls remain unverified. Their absence from this checkout is a missing release proof, not proof that deployed controls do not exist. - -The rejected stronger verdict is public paid launch readiness today. Conversely, the evidence does not support demanding a new broker, renderer microservice, role platform, PWA, total page redesign or 100k-record optimization as prerequisites to a controlled beta. - -## Decisions Required From Connor - -These decisions can be supplied when the dependent ticket starts; none is needed to read or approve this validation. - -| Decision | Why the repository cannot decide it | Proposed default / dependency | -|---|---|---| -| Paid deletion timing | Whether deletion cancels paid service immediately or keeps access until period end changes provider calls, messaging and retention timing. | Immediate cancellation only on explicit delete confirmation; never erase billing identity before verified resolution. Decide before P03. | -| Recovery objectives and custody | Acceptable data-loss window, recovery time and who can recover encryption keys require an operator/business commitment. | Use 24-hour RPO and 4-hour RTO as proposed drill targets, not measured promises; approve or replace before R03. | -| Private device-local drafts | Persisting CV/profile content on shared devices is a privacy/product choice. | Keep memory-only drafts plus dirty-state warnings until an explicit opt-in, owner scope and expiry policy are approved. W05. | -| Version retention/over-quota promises | Pruning historical CV/profile versions changes users' restore expectations and paid-plan promises. | Preserve existing history; reject further growth with a recoverable explanation before silently pruning. Decide before D07 expands retention. | -| Operator content access and retention | Which support actions actually need private document/event content and how long account/export/deletion records remain are policy decisions. | Retain current admin-only boundary, minimize unnecessary content, add no impersonation role. W04/P03. | - -Do not ask whether follow-ups should retain their existing date-only behavior, whether a concurrency token is needed, or whether to add a type gate: the repository/evidence answer those engineering questions. New timed scheduling is a separate future product request. - -## Recommended implementation order and first handoff - -Use the exact ticket order and permitted parallel branches in [implementation-queue.md](implementation-queue.md). The essential order is: **contain security/identity/resource defects → establish release and recovery proof → introduce data-integrity migrations → correct paid lifecycle → accessibility foundations → workflow corrections → broader UX and measured operations**. Independent theme/type tests may run earlier; they do not authorize skipping a dependency. - -The original first handoff was **B01 only: HTTP import target enforcement and its regression tests**. B01–B05 are now complete locally; see the linked completion records above. **B06 verify and close ingress bypass path** was subsequently authorized; its implementation is present, with runtime/host gates still pending. Do not start B07. Completing B01 does not close F11's separately scoped IMAP/deployment work; B03 does not close F03's R04 containment/capacity work; B04 does not close B06's deployment ingress proof. - -## Stop conditions - -- Do not close a finding from source inspection alone when its acceptance requires a socket, two contexts, browser tabs, provider sandbox or restored database. -- Do not start schema-changing tickets until R02/R03 produce a restorable set and an explicit old/new binary compatibility decision. Never delete duplicate CV snapshots merely to make an index migration pass. -- Do not enable open registration/exposed costly features while their boundary/admission findings remain open. Keep AI workers and paid lifecycle operations gated until their own tests pass. -- Do not begin page redesign or persistent draft work while shared identity, revision or accessibility primitives it depends on are unresolved. -- Stop a rollout on owner-state contamination, silent conflict overwrite, unexpected entitlement divergence, unresolved billing before identity deletion, failed restore/integrity, unbounded resource admission, or primary-flow keyboard regression. Preserve evidence and existing data; use the rehearsed compatible rollback or forward repair. -- The validation itself authorized no implementation. Subsequent explicit instructions authorized B01–B05, now complete locally. B06 was subsequently authorized and implemented with verification gates still pending. Stop before B07 or any other ticket without a further instruction. +The dated audit packet is historical evidence and is preserved in the pre-cleanup archive. Current implementation status belongs in [`docs/roadmap/`](../../../roadmap/README.md) and current security notes in [`docs/security/`](../../../security/README.md). diff --git a/docs/audits/2026-09-07/validation/b06-completion.md b/docs/audits/2026-09-07/validation/b06-completion.md index f58f2ee..01a4c99 100644 --- a/docs/audits/2026-09-07/validation/b06-completion.md +++ b/docs/audits/2026-09-07/validation/b06-completion.md @@ -1,404 +1,3 @@ -# B06 — Ingress boundary implementation and verification record +# B06 historical validation pointer -**Latest runtime follow-up:** [2026-09-09 observations and remaining gates](#runtime-verification-follow-up-2026-09-09). Public DNS/TLS and bounded direct-access observations are now available. Docker still cannot start; real container/Traefik verification remains blocked. **B06 implementation complete, but F04 remains open.** The initial implementation-pass record below is preserved chronologically; statements about no public requests/DNS/TLS apply to that earlier pass. - -Date: **2026-09-09**. **Repository/application changes implemented; full local acceptance and production verification are NOT complete.** B01–B05 regression coverage passes. B07 was not started. No deployment, host firewall change, schema change, external scan, image pull or Docker reset was performed. - -Two gates remain: (1) execute the real nginx/container topology tests with a working Docker engine and the intended image; (2) establish the deployed Traefik socket peers, forwarding configuration and host/provider network controls. **F04 remains open, conditional P1.** This file is a completion record for work performed, not a claim that the remaining gates passed. - -## Finding and independently established boundary - -The original [F04 validation](README.md#f04--forwarded-headers-and-direct-ingress) and [B04 deferral](b04-completion.md) remain historical evidence. The vulnerability is **not a production API host-port mapping**: rendered base Compose has no backend published port. It publishes frontend/nginx **host 3000 → container 80**, without a host-IP restriction. Nginx accepts the canonical Host and previously passed arbitrary incoming `X-Forwarded-For` / `X-Forwarded-Proto` to the API. The API legitimately trusts nginx's dedicated network, so bypassing the external sanitizing proxy could launder attacker-supplied headers through a trusted peer. A canonical Host is not proof of a trusted caller. - -Before changing application/configuration code, `docker compose --env-file .artifacts/b06/compose.env -f docker-compose.yml --profile bundled-ollama config --format json` confirmed this exposure using synthetic values. The new permanent effective-config test failed because production had no upstream-peer enforcement setting. Source tracing confirmed verbatim forwarding in all three nginx proxy locations. **No pre-fix live nginx exploit was reproduced in this environment**, because the Docker engine was unavailable. The optional `--baseline` topology probe can test HEAD's original nginx sources before these edits are committed. It is not a substitute for an immutable baseline after committing; original source excerpts/observations remain in the audit. - -Deployment evidence conflicts: `docs/architecture/deployment.md` asserted a firewall blocked port 3000; `deploy/README.md` claimed no frontend host mapping and container-direct Traefik routing; `sec-002-ingress-compose.md` retained a host-port compatibility dependency while also listing a contradictory “production ports absent” check. Current rendered configuration and deployment scripts take precedence. These claims are now corrected in active documentation. The old verification record is retained as historical evidence, not current proof. - -## Production-intended topology and actual configuration evidence - -```text -Internet → DNS/provider network/host firewall [unverified] - → operator-owned Traefik [TLS termination/routing/config unverified] - → Docker host TCP 3000 [current compatibility mapping] - → frontend nginx TCP 80 [new explicit socket-peer allowlist] - → dedicated internal web_proxy network - → ASP.NET API TCP 8080 [unpublished] -``` - -The repository establishes the expected contract, **not the observed current production topology**. No Traefik configuration, live `.env`, DNS, firewall rules or infrastructure credentials were available/used. A request for the real Traefik placement/observed peer addresses remains unanswered; “continue” was not treated as a network value or production approval. - -| Surface | Rendered production configuration | After B06 | Evidence level | -|---|---|---|---| -| Frontend nginx | TCP 3000 → 80, host IP unspecified; Docker normally publishes broadly, platform IPv6 behavior must be checked | Mapping preserved; application requests require configured Traefik socket peers, single client IP and HTTPS metadata | Mapping proven by Compose; nginx enforcement source/generation checked, runtime pending | -| API | `expose: 8080`, no `ports`; listens on container interfaces | Same exposure; explicit `ASPNETCORE_ENVIRONMENT=Production` | Proven in rendered configuration | -| `web_proxy` | Dedicated bridge, `internal: true`, configured `WEB_PROXY_SUBNET` (synthetic/default /29) | Only frontend/backend members permitted by preflight; API known network must match exactly | Proven in configuration; live membership pending | -| `jobtracker_shared` | External network; frontend and backend both join | Preserved for existing operator-managed service dependencies | Membership configured; actual other members/access need host inspection | -| Backend `default` / `ai_internal` | Backend also joins both; ai-service only `ai_internal` | Unchanged | Normal bridge networks are not air gaps or protection from a Docker administrator | -| AI 8001 / optional Ollama 11434 | No host mappings, including `bundled-ollama` profile | Unchanged; new preflight rejects publications | Proven in rendered configuration | -| Database | External MariaDB/MySQL or volume-backed SQLite; no DB service/port in this Compose | Unchanged | External DB listener/network requires operator verification | -| PDF renderer | API child process, no separate listening service | B03 admission unchanged | Source and regression tests | -| Development | Explicit `-f docker-compose.dev.yml`, frontend 3000, API 5202, optional Ollama 11434 | Explicit direct nginx mode, HTTP localhost, API forwarding trust disabled | Rendered and tested separately; cannot pass production preflight | - -Frontend and API are intended to share the canonical HTTPS origin; UI API base is `/api`. Preflight rejects a different production build-time API URL. Public 80/443 handling belongs to operator-owned Traefik and cannot be proved here. No cache/debug/control service is published by base Compose. `expose` is metadata, not a firewall. Host processes, attached containers, Docker administrators and routed/direct-container-IP configurations remain separate trust considerations. - -## Implementation and trust rules - -1. **Nginx original socket peer:** `INGRESS_MODE=proxy` is explicit in production. `INGRESS_PROXY_CIDRS` must list observed Traefik peers; there is no permissive default. The generated `geo $realip_remote_addr` gate checks the original socket peer, before allowing application locations. Untrusted peers receive **403**, including callers that know the canonical Host and forge every forwarding header. -2. **Normalized forwarding:** nginx `set_real_ip_from` uses that same explicit list; `real_ip_header X-Forwarded-For`, `real_ip_recursive off`. It sends `$remote_addr` as one normalized XFF value, canonical Host/port and canonical HTTPS scheme. It strips `X-Forwarded-Host` and RFC `Forwarded`; raw headers do not select an identity. All `/api/`, canonical `/health` and `/ready` proxy locations use this rule. Authentication/B02 headers pass through normally and retain their existing server checks. -3. **Malformed/extra metadata:** missing XFF, missing/nonexact `https`, or comma-separated XFF chains return **400** at nginx in proxy mode. An invalid single IP makes nginx realip retain the actual peer; only that peer's address is emitted, not the invalid supplied text. This safely charges the proxy peer rather than manufacturing a client partition; it also signals a broken upstream contract. This nginx fallback behavior still needs the runtime probe. ASP.NET's independent malformed/rightmost-hop behavior is tested and proven. Traefik must overwrite attacker headers, not append arbitrary chains. -4. **Development:** `INGRESS_MODE=direct` is permitted only with an HTTP canonical origin. It does not enable realip or trust incoming forwarding metadata; nginx forwards the actual socket IP and HTTP scheme. Production preflight rejects direct mode/Development environment. Local API-port convenience remains unchanged. -5. **Fail-safe configuration:** nginx rejects empty, broad/default-route or directive-shaped peer inputs and calls `nginx -t` before serving. The standard-library Python preflight validates the actual Compose JSON, requires narrow peer networks (private /24 or narrower IPv4; /64 or narrower IPv6; individual public proxy addresses), exact dedicated backend trust, same-origin HTTPS, Production environment, no host networking and no additional service publications. It rejects `DOTNET_ENVIRONMENT=Development` and automatic framework-forwarding enablement. Shell startup enforces CIDR characters/prefix bounds; nginx itself validates address syntax. Prefer individual /32 or /128 peers rather than trusting other containers in a subnet. -6. **API defence:** `ForwardedProxyConfiguration` still clears implicit known proxies/networks, trusts one configured hop, processes XFF/XFP only, and now rejects `/0`. Untrusted sockets—including mapped IPv4—cannot change effective IP/scheme. Existing `ExternalOrigin` still rejects noncanonical application Hosts in Production, allows only internal liveness exceptions, generates reset/verification/OAuth links from configuration and controls secure cookies. Forwarded Host is never enabled. `AllowedHosts=*` remains as a framework setting; the existing explicit canonical-host middleware supplies the actual Production host restriction. -7. **CORS correction:** Production previously defaulted to `http://localhost:3000` when no allowlist existed. It now defaults to the configured canonical origin; conflicting overrides/wildcards fail before listening. Credentialed requests remain allowed only for that origin. Development's configured allowlists remain usable. This is not a network-access control. - -B04 middleware ordering is unchanged: canonical host/proxy handling → existing CSRF → authentication → endpoint authorization → rate limiter → B02 session context → controller. Owner quotas remain principal-based; anonymous/security quotas use the effective socket/proxy client IP. B03 admission is still inside the shared exporter, independent of ingress or endpoint. B01 outbound transport and B05 CSV behavior were not changed. - -The host-port mapping is deliberately retained because the repository records a live dependency and does not reveal whether Traefik is host-local, containerized or remote. Blind loopback binding/removal could cause downtime. The new nginx peer gate is the repository's application-level closure for that compatibility listener. **If Docker/NAT makes trusted and untrusted traffic appear as the same peer, this gate is insufficient: do not allowlist that shared gateway.** Establish a verified binding/firewall or dedicated proxy route before rollout. This is a production stop condition, not a claim that a guessed CIDR fixes the host. - -## Headers, limits and health - -Nginx explicitly supplies `X-Content-Type-Options: nosniff`, `X-Frame-Options: DENY`, `Referrer-Policy: strict-origin-when-cross-origin` and restrictive camera/microphone/geolocation permissions on canonical responses. No new duplicate headers were added. Base API HTTPS redirection/HSTS are disabled because the external proxy owns TLS/redirects; actual Traefik HSTS/redirect/header policy is unverified. No proxy request-body or timeout tuning is configured in this nginx template: nginx defaults apply. B06 does not claim a special rate/WAF/body policy at Traefik. Existing application size/time limits and B03 admission remain; tuning broader limits is separate work. - -Container health requires no host API publication: backend curls `127.0.0.1:8080/health`; frontend checks `127.0.0.1:80/health`. The nginx default-host `/health` returns only static liveness and remains accessible even to untrusted peers. Canonical `/health` still proxies backend liveness; `/ready` remains canonical/proxy-gated. Default-host application paths are rejected. Health checks do not prove that the new trusted-proxy route is configured correctly, so the route smoke below is mandatory. - -## Permanent tests and local results - -| Check | Result / exact scope | -|---|---| -| `scripts/test-ingress.py` | **3 tests pass**, including real production/dev Compose rendering and 17 unsafe-config mutations; no daemon needed. Before config edits, safe-production test failed as expected. | -| `ForwardedIngressTests` plus existing origin/rate-key tests | **30 focused pass**; 13 new backend cases cover trusted/untrusted/mapped/IPv6 peers, scheme, host, malformed/rightmost chains and trust-all rejection. | -| `ingress-tests/production.spec.ts` | **6 real HTTP scenarios pass**: Production CORS/default/unsafe startup, Host/XFH, secure-cookie scheme independence, untrusted IP-limit forgery, trusted clients and excess-chain defence. | -| Broader backend | **915 pass**, no selected skips; includes B01–B05. One pre-existing external-DNS IMAP test excluded because internet access was not authorized. | -| Frontend | **298 tests / 67 suites pass**, including nginx-health, B02/B03 and B05 download regressions. | -| Browser | **18 pass**, no failures/flaky outcomes/skips; includes B02 switching and B05 actual download. | -| B04 HTTP / B03 admission probes | **9 / 1 pass** unchanged. | -| Build/static checks | API/test-project Release warnings-as-errors: **0 warnings/errors**. Next production build and targeted lint pass. Shell syntax checks, Python compilation and `git diff --check` pass. | -| Standalone TypeScript | Existing unrelated `src/interview-prep.test.tsx(120,10): error TS2571: Object is of type 'unknown'.` remains; exit **1**. No suppression/fix added. | -| Entrypoint generation | **9 local guard/render cases pass**, seven rejected configurations plus production and development generation. Temporary probe substitutes only fixture paths and disables `nginx -t`; explicitly **not nginx runtime proof**. | -| Real nginx/container topology | **BLOCKED before resource creation**. Docker Desktop engine pipe absent; attempted startup crashed on inaccessible `dockerInference` socket. No factory reset/repair, image pull or production request attempted. | - -The first native Production fixture run passed 2/4 but inherited the project's Development launch profile despite its environment setting. The fixture now uses `dotnet run --no-launch-profile`; all six final tests prove Production behavior. This was a fixture correction, not a relaxation of host/CORS assertions. Final results/hashes: [manifest](evidence/b06-test-results.json). Safe effective topology/guard observations: [observations](evidence/b06-observations.json). Raw local files remain in `.artifacts/b06`. - -The permanent `scripts/test-ingress-topology.py` uses only an already-installed nginx image, synthetic data, unique containers/networks and loopback-only ephemeral host publication. It exercises this repository's real nginx template/entrypoint and a header-echo backend, plus a simulated trusted proxy peer. It does **not** claim to run production Traefik or a containerized full API. API behavior is covered separately by the real native HTTP tests. The topology test asserts trusted IPv4/IPv6 forwarding, two clients, host/header normalization, unchanged B02 header transport, untrusted direct nginx denial, missing/bad scheme and chain rejection, safe malformed-IP fallback, no backend publication and denied edge-to-backend routing. It cleans up only resources it created. Its runtime result is still pending. - -### Commands run - -Repository root (PowerShell): - -```powershell -python scripts/test-ingress.py -docker compose --env-file .artifacts/b06/compose.env -f docker-compose.yml --profile bundled-ollama config --format json > .artifacts/b06/compose-after.json -Get-Content .artifacts/b06/compose-after.json -Raw | python scripts/validate-ingress.py -& C:/Users/Connor/.dotnet/dotnet.exe test JobTrackerApi.Tests/JobTrackerApi.Tests.csproj --configuration Release --no-restore --filter 'FullyQualifiedName~ForwardedIngressTests|FullyQualifiedName~ExternalOriginTests|FullyQualifiedName~RateLimitPartitionKeysTests' --logger 'trx;LogFileName=focused.trx' --results-directory .artifacts/b06 -- xUnit.parallelizeTestCollections=false xUnit.maxParallelThreads=1 -& C:/Users/Connor/.dotnet/dotnet.exe test JobTrackerApi.Tests/JobTrackerApi.Tests.csproj --configuration Release --no-restore --no-build --filter 'FullyQualifiedName!~ImapServiceSsrfGuardTests.ConnectAsync_rejects_unresolvable_host_without_leaking_dns_detail' --logger 'trx;LogFileName=backend.trx' --results-directory .artifacts/b06 -- xUnit.parallelizeTestCollections=false xUnit.maxParallelThreads=1 -& C:/Users/Connor/.dotnet/dotnet.exe build JobTrackerApi.Tests/JobTrackerApi.Tests.csproj --configuration Release --no-restore -warnaserror -& C:/Users/Connor/.cache/codex-runtimes/codex-primary-runtime/dependencies/native/git/usr/bin/sh.exe -n deploy/deploy.sh -& C:/Users/Connor/.cache/codex-runtimes/codex-primary-runtime/dependencies/native/git/usr/bin/sh.exe -n job-tracker-ui/configure-nginx-origin.sh -python -m py_compile scripts/validate-ingress.py scripts/test-ingress.py scripts/test-ingress-topology.py -python .artifacts/b06/render-probe.py -python scripts/test-ingress-topology.py # blocked: Docker engine unavailable -git diff --check -``` - -UI directory; API suites ran serially because all use port 5302: - -```powershell -$env:DOTNET_HOST_PATH='C:/Users/Connor/.dotnet/dotnet.exe' -$env:NEXT_TELEMETRY_DISABLED='1' -node node_modules/@playwright/test/cli.js test --config=playwright.ingress.config.ts --reporter=json > ../.artifacts/b06/ingress-api.json -node node_modules/@playwright/test/cli.js test --config=playwright.rate-limits.config.ts --reporter=json > ../.artifacts/b06/rate-limits.json -node node_modules/@playwright/test/cli.js test --config=playwright.pdf.config.ts --reporter=json > ../.artifacts/b06/pdf-admission.json -node node_modules/jest/bin/jest.js --runInBand --watchAll=false --json --outputFile=../.artifacts/b06/frontend.json -$env:PLAYWRIGHT_STATIC_EXPORT='true' -$env:PLAYWRIGHT_PREBUILT_EXPORT='false' -node node_modules/@playwright/test/cli.js test --config=../.artifacts/b06/playwright.offline.config.ts --reporter=json > ../.artifacts/b06/browser.json -$env:NEXT_PUBLIC_API_BASE_URL='/api' -node --max-old-space-size=4096 node_modules/next/dist/bin/next build -node node_modules/eslint/bin/eslint.js playwright.ingress.config.ts ingress-tests/production.spec.ts --max-warnings=0 -node node_modules/typescript/bin/tsc --noEmit --pretty false -``` - -The offline browser configuration was copied from the existing B02 fixture; external providers/workers were disabled. Its fresh test-targeted static build was subsequently replaced by the normal `/api` build. Neither was deployed. - -## Required deployment verification — exact safe procedure - -Run the container probe first on a **safe development/test Docker engine**, not the production host: - -```bash -python3 scripts/test-ingress.py -python3 scripts/test-ingress-topology.py --image nginx:1.29.8-alpine -# Image must already exist. The script never pulls it. Arrange any image acquisition separately. -``` - -On the documented Linux deployment host, use the existing checkout `/opt/job-tracker/app` and shared configuration `/opt/job-tracker/shared/.env`. The following commands are **read-only**; do not run `deploy.sh`, `up`, firewall mutation or restore commands as part of this inspection. Review outputs locally; never paste complete rendered environment variables or container environment/secrets. - -```bash -cd /opt/job-tracker/app -docker compose --env-file /opt/job-tracker/shared/.env -f docker-compose.yml --profile bundled-ollama config --format json | python3 scripts/validate-ingress.py -docker compose --env-file /opt/job-tracker/shared/.env -f docker-compose.yml ps -docker ps --format 'table {{.Names}}\t{{.Image}}\t{{.Ports}}' -sudo ss -lntp -if command -v nft >/dev/null; then sudo nft list ruleset; fi -if command -v iptables-save >/dev/null; then sudo iptables-save; fi -if command -v ip6tables-save >/dev/null; then sudo ip6tables-save; fi -if command -v ufw >/dev/null; then sudo ufw status verbose; fi -frontend_id=$(docker compose --env-file /opt/job-tracker/shared/.env -f docker-compose.yml ps -q frontend) -backend_id=$(docker compose --env-file /opt/job-tracker/shared/.env -f docker-compose.yml ps -q backend) -test -n "$frontend_id" && test -n "$backend_id" -docker inspect -f '{{json .NetworkSettings.Ports}} {{json .NetworkSettings.Networks}}' "$frontend_id" "$backend_id" -docker network inspect jobtracker_shared --format '{{json .IPAM.Config}} {{json .Containers}}' -docker network ls --filter label=com.docker.compose.network=web_proxy -docker exec "$frontend_id" nginx -T -docker exec "$backend_id" curl -fsS http://127.0.0.1:8080/health -docker exec "$frontend_id" wget -qO- http://127.0.0.1/health -``` - -Identify the actual Traefik service/process from `docker ps` / `ss`. If containerized, inspect its **mount paths and networks**, then inspect the identified configuration files locally; do not assume a filename or container name. If host-managed, inspect the identified systemd/service configuration. Confirm exact Host rule, TLS certificate/entrypoint, upstream host:3000 target, redirect behavior and whether `forwardedHeaders.insecure`/trusted upstream settings permit client forgery. Determine any real CDN/LB hop separately; it is not present in this repository. Inspect actual provider firewall/security-group settings as well as Docker's NAT/FORWARD rules; a UFW INPUT rule alone is not proof for Docker-published ports. - -Obtain the **actual socket source seen at nginx**, not a guessed Docker subnet. Where host tools permit, a bounded SYN-only capture in nginx's network namespace avoids capturing HTTP bodies/credentials: - -```bash -frontend_pid=$(docker inspect -f '{{.State.Pid}}' "$frontend_id") -test "$frontend_pid" -gt 0 -sudo nsenter -t "$frontend_pid" -n tcpdump -nn -i any -c 10 'tcp dst port 80 and (tcp[tcpflags] & tcp-syn != 0)' -``` - -Use that only while issuing explicitly controlled health/config requests through Traefik and from an untrusted test client. If `nsenter`/`tcpdump` is unavailable, use the operator's equivalent packet/flow evidence; do not install tools or change production networking implicitly. If both routes share the same observed NAT source, **stop**. Choose a topology-specific binding/firewall or dedicated proxy route with an explicit rollback before allowing that peer. Configure `INGRESS_PROXY_CIDRS` only after this evidence; a wrong list will deny legitimate ingress. Normal deployment preflight fails before service replacement if the setting is absent. - -After the change is separately approved and deployed, from an independently controlled **external test machine**, set values from verified DNS/host inventory, not guesses: - -```bash -public_host='the-verified-canonical-host' -public_ip='the-verified-host-address' -curl --max-time 10 -sS -D - -o /dev/null "https://$public_host/api/auth/config" -curl --max-time 10 -sS -D - -o /dev/null "https://$public_host/api/auth/config" \ - -H 'X-Forwarded-For: 198.51.100.10' -H 'X-Forwarded-Proto: http' \ - -H 'X-Forwarded-Host: attacker.example.test' -curl --max-time 5 -sS -D - -o /dev/null "http://$public_ip:3000/api/auth/config" \ - -H "Host: $public_host" -H 'X-Forwarded-For: 198.51.100.10' -H 'X-Forwarded-Proto: https' -curl --max-time 5 -sS -o /dev/null "http://$public_ip:5202/health" -curl --max-time 5 -sS -o /dev/null "http://$public_ip:8080/health" -curl --max-time 10 -sS -D - -o /dev/null "http://$public_host/api/auth/config" -``` - -Expected: canonical TLS path succeeds with appropriate headers; spoofing at the external edge is overwritten or rejected, not trusted; direct port-3000 application request is firewall-denied or nginx **403**, never successful; development/API listeners are unavailable unless a separately documented protected service owns that port; public HTTP redirects according to the verified Traefik policy. Repeat for every published A/AAAA address, with IPv6 bracket syntax and `curl -6`; do not infer IPv6 rules from IPv4 results. Verify certificate hostname/chain without `-k`. Match DNS to the inspected ingress. Do not probe unrelated services/ports or production login quotas. A config endpoint's 200 alone cannot prove client-IP integrity; inspect trusted forwarding/flow evidence or repeat the synthetic limiter checks in staging. Capture intended login/session/CSV/PDF smoke through the canonical route with a dedicated test account after normal deployment authorization. - -## Files, residual risk and stop conditions - -Application/config: `docker-compose.yml`, `docker-compose.dev.yml`, nginx template/entrypoint, `ForwardedProxyConfiguration.cs`, `Program.cs` CORS block, `.env.example`; preflight in `deploy/deploy.sh`, new `scripts/validate-ingress.py`, CI safety-script invocation. Tests: `ForwardedIngressTests.cs`, `scripts/test-ingress.py`, `scripts/test-ingress-topology.py`, `playwright.ingress.config.ts`, `ingress-tests/production.spec.ts`. Documentation: active deployment docs, this record, validation README/queue/evidence, TODO and B06 evidence files. Earlier uncommitted B01–B05 changes and unrelated `docs/redesign` remain untouched; the entire working-tree diff is not B06. - -No schema or data migration. Backup-only automation does not require the new ingress gate. A bad peer list or unexpected forwarding chain can break the intended route; preflight/syntax/liveness checks alone cannot validate the peer value. Preserve existing images/env and verify both routes before rollout. Rolling back nginx to the old behavior reopens the header-laundering assumption unless network isolation is independently enforced; rollback therefore requires verified firewall/binding protection. Do not bypass the gate with trust-all networks. - -Limits remain process-local after B04/B03; additional API instances multiply budgets as documented there. B06 introduces no distributed system or new hosting platform. Host/container compromise and shared-network lateral access are not eliminated by a peer allowlist. Privileged Docker users can attach containers regardless of service names. No claim is made about external DB/Ollama listeners, external TLS, firewall correctness, raw backend network reachability from other trusted containers, or an uninspected additional ingress route. - -**Local acceptance:** criteria 2/4/6/7/8/9 have repository/API/test evidence; backend publication checks for criterion 1 pass, but the nginx closure supporting criteria 1/3/5 still requires real runtime/topology verification. **Do not mark B06 locally complete or F04 resolved yet. Production verification is also incomplete.** The next action is to complete these B06 gates and supply the observed production proxy peers. **Next queued implementation ticket: B07, only after the required B06 decision/verification and a further instruction; not started.** - -## Runtime verification follow-up 2026-09-09 - -**Outcome: B06 implementation complete, but F04 remains open.** Implementation complete describes the existing application/configuration change; it does **not** mean full local-runtime acceptance or production verification passed. This follow-up changed evidence/documentation only. No application, Compose, nginx, B01–B05 behavior, credentials, DNS, certificate or production service was changed. B07 was not started. - -The user explicitly authorized targeted public DNS/TLS/direct-access checks in this follow-up. These were bounded unauthenticated GETs to the documented hostname and its observed A address, not production load tests. No login budgets, user data or credentials were used. Response bodies and cookies were discarded; retained headers are allowlisted. [Runtime evidence](evidence/b06-runtime-observations.json), [test/evidence manifest](evidence/b06-runtime-results.json) and [reproducible public probe](evidence/b06-runtime-public-probe.py.txt) preserve commands and observations. Raw local files are under `.artifacts/b06-runtime/`. - -### Actual execution and listener evidence - -The real production Compose file was rendered again, including the optional Ollama profile, with the existing synthetic `.artifacts/b06/compose.env`. The preflight passed. [Sanitized effective configuration](evidence/b06-runtime-compose.json) confirms frontend TCP **3000 → 80**, host address unspecified, and **zero published backend/AI/Ollama ports**. The internal `web_proxy` network contains frontend/backend in configuration; backend also joins `shared_services`, `default` and `ai_internal`. The synthetic `192.0.2.10/32` ingress value is a test input, **not an observed Traefik peer or a recommended production value**. - -The actual topology could not be started: - -- `docker desktop status` returned exit 1, unable to retrieve status. -- `docker desktop start --detach` returned exit 0 (startup dispatched), then Docker Desktop **4.86.0** reported the same fatal inference-manager socket failure supplied by Connor. This exit code is not engine readiness. -- A server-version request timed out at 20 seconds; a final bounded request timed out at 10 seconds. Earlier missing-pipe evidence remains preserved. -- The failing `C:/Users/Connor/AppData/Local/Docker/run/dockerInference` entry is zero bytes with `Archive, ReparsePoint` attributes, last modified 2026-08-24. Read-only `fsutil reparsepoint query` returned **Error 1920: The file cannot be accessed by the system**. This does not establish the underlying Docker/Windows defect or prove the entry is unused. -- A graceful `docker desktop stop` attempt did not complete; the waiting command was interrupted. Failed Desktop/backend processes remained observable. No socket, Docker data, WSL distribution or configuration was deleted/renamed/reset, and no process was force-killed to improvise a repair. -- The inspected **Windows workstation**, not the Ubuntu deployment host, had no listeners on 80, 443, 3000 or 5202. Its `127.0.0.1:8080` listener belonged to **steamwebhelper**, not Jobjakt. Windows firewall profiles were enabled; their default actions were `NotConfigured`. Neither observation establishes production firewall/NAT behavior. - -Consequently there is **no new running-container evidence** for `nginx -t`, actual host bindings, network membership, trusted-peer matching, direct nginx rejection, internal API reachability, container restarts or missing/invalid-variable startup. No actual peer CIDR was guessed. The existing echo-backend/simulated-peer nginx probe would be supplementary only; it cannot satisfy this request for **real Traefik + real application + actual Compose** and was not represented as such. - -### Observed public DNS, HTTPS and direct access - -Measurements were taken around **2026-09-09 10:43–10:47 UTC** from Connor's Windows workstation (`192.168.50.2`). Requests used `curl --noproxy '*'`, TLS verification enabled, connect timeout 4 seconds, total timeout 8 seconds and at most three simultaneous harmless GETs. Independence from the production LAN/NAT is **not established**; these observations must be repeated from a known external network. - -| Check | Observed result | What this proves / does not prove | -|---|---|---| -| Resolver plus both authoritative servers, `oaklyn.ns.cloudflare.com` and `thaddeus.ns.cloudflare.com` | `jobs.cesnimda.uk` CNAME → `cesnimda.uk`; A answer **109.189.21.248**; no AAAA answer (SOA/NODATA) | Consistent current answers for this hostname. No additional A/AAAA address was observed. Intended address ownership, other hostnames and complete provider-zone inventory remain operator checks. | -| `https://jobs.cesnimda.uk/api/auth/config` | **200**, valid hostname/chain; TLS 1.3 independently verified with Python's default trust context | Public HTTPS application path works. It does not identify Traefik, nginx's peer, deployed image or backend client IP. | -| Certificate | SAN/CN `jobs.cesnimda.uk`, Let's Encrypt YR2; valid **2026-08-16 20:35:58 UTC to 2026-11-14 20:35:57 UTC** | Hostname/chain validation succeeded without bypass flags. No separate revocation audit is claimed. | -| HTTP port 80 | **301** to `https://jobs.cesnimda.uk:444/api/auth/config` | An actual redirect differs from the documented standard HTTPS origin. | -| Observed redirect target, HTTPS port 444 | **Connection failure**, curl exit **7** | HTTP-to-HTTPS navigation is broken from this vantage point. Operator must inspect the real redirect entrypoint/NAT configuration and establish the intended reachable target. No certificate or production proxy change was attempted. | -| HTTPS with forged XFF/XFP/XFH; HTTPS with malformed XFF chain | **200** in both cases | Availability only. A 200 cannot distinguish sanitized forwarding from a vulnerable acceptance path; backend effective identity is still unobserved. | -| HTTPS with `Host: attacker.example.test`, original canonical TLS SNI | **421 Misdirected Request** | This forged Host request was rejected at the observed public boundary. It does not prove every internal/generated-URL path. | -| Direct A-address TCP 3000, canonical Host, six cases: no forwarding, forged XFF, forged HTTPS, forged Host, combined headers, malformed chain | **6/6 connection failures**, curl exit **7**, approximately 2.0 seconds each | No HTTP request reached a listener from this vantage point. This is **not** evidence of nginx 403 or attribution to any specific firewall. | -| Direct A-address TCP 5202 and 8080 `/health` | **2/2 connection failures**, curl exit **7** | Neither tested port was reachable here. No claim about other interfaces, IPv6 firewall policy, host-local/container access or unknown alternate ports. | - -Public responses include HSTS, nosniff, `X-Frame-Options: SAMEORIGIN`, `Referrer-Policy: same-origin` and a Permissions Policy. These differ in part from the checked-in nginx template (`DENY`, `strict-origin-when-cross-origin`). An upstream override or a different deployed revision could explain this; the source cannot be attributed without operator configuration/image evidence. Public success therefore cannot certify that the current B06 working tree is deployed. - -### Fresh automated checks and preserved broader results - -| Check in this follow-up | Result | Evidence classification | -|---|---|---| -| Effective production/development Compose tests, including 17 hostile mutations | **3 passed**; preflight of newly rendered production JSON also exit 0 | Proven by automated test | -| Forwarded ingress, canonical origin, B01 HTTP transport, B02 session middleware, B03 admission, B04 rate keys and B05 CSV | **166 passed**, 0 failed/skipped; Release compilation succeeded | Proven by automated test; API/native boundary, not live proxy chain | -| Native Production ingress HTTP suite | **6 passed**, 0 unexpected/flaky/skipped | Proven by automated test, real native API | -| Native B04 HTTP suite | **9 passed**, 0 unexpected/flaky/skipped | Proven by automated test, real native API | -| B04 synthetic shared-IP evidence | In each exhaustion order, A **2 admitted / 1 rejected**, B **2 admitted / 1 rejected**; concurrent two-session case admits **4 total**, then each account rejects **2** attempts using other IPs; cookie/bearer share owner budgets | Does not prove real Traefik/nginx IP propagation. No production quotas were consumed. | -| Existing broader verification, unchanged application source | Prior **915 backend / 298 frontend / 18 browser** passes, Release builds and targeted lint retained above; broader suites were not relabeled as rerun | Preserved prior automated evidence | -| Standalone TypeScript | Prior unrelated `src/interview-prep.test.tsx(120,10): TS2571` remains the documented result; not rerun in this evidence-only follow-up | Existing limitation; no claimed fix | -| Final evidence checks | `git diff --check` **exit 0** (Git line-ending notices only); probe Python syntax and all new evidence JSON parse checks pass | Evidence/documentation verification | - -Exact fresh commands (repository root unless stated otherwise): - -```powershell -python scripts/test-ingress.py -docker compose --env-file .artifacts/b06/compose.env -f docker-compose.yml --profile bundled-ollama config --format json -# Output saved to .artifacts/b06-runtime/compose-effective.json; synthetic inputs only. -Get-Content .artifacts/b06-runtime/compose-effective.json -Raw | python scripts/validate-ingress.py -& C:/Users/Connor/.dotnet/dotnet.exe test JobTrackerApi.Tests/JobTrackerApi.Tests.csproj --configuration Release --no-restore --filter 'FullyQualifiedName~ForwardedIngressTests|FullyQualifiedName~ExternalOriginTests|FullyQualifiedName~JobImportHttpTransportTests|FullyQualifiedName~SessionContextMiddlewareTests|FullyQualifiedName~PdfRenderAdmissionTests|FullyQualifiedName~RateLimitPartitionKeysTests|FullyQualifiedName~CsvExportTests' --logger 'trx;LogFileName=regressions.trx' --results-directory .artifacts/b06-runtime -- xUnit.parallelizeTestCollections=false xUnit.maxParallelThreads=1 -# In job-tracker-ui, serially: -$env:DOTNET_HOST_PATH='C:/Users/Connor/.dotnet/dotnet.exe' -$env:NEXT_TELEMETRY_DISABLED='1' -node node_modules/@playwright/test/cli.js test --config=playwright.ingress.config.ts --reporter=json -node node_modules/@playwright/test/cli.js test --config=playwright.rate-limits.config.ts --reporter=json -# Repository root: -python .artifacts/b06-runtime/probe.py -Resolve-DnsName jobs.cesnimda.uk -Type A -DnsOnly -Resolve-DnsName jobs.cesnimda.uk -Type AAAA -DnsOnly -Resolve-DnsName jobs.cesnimda.uk -Type CNAME -DnsOnly -Resolve-DnsName cesnimda.uk -Type NS -DnsOnly -NoHostsFile -# Repeat A/AAAA/CNAME using -Server for each observed authoritative nameserver. -Get-NetTCPConnection -State Listen -Get-NetFirewallProfile -git diff --check -``` - -### Operator handoff and evidence required for closure - -Permitted deployment docs/scripts/CI configuration refer only to `PROD_HOST`, `PROD_USER` and `PROD_SSH_KEY` secret names. `C:/Users/Connor/.ssh/config` does **not exist**. No unambiguous SSH alias/username was found; no SSH connection was guessed, and no CI secrets, keys/passwords or live `.env` were retrieved. Production read-only inspection therefore remains an operator handoff under Connor's explicit instruction. - -Run the earlier [exact host procedure](#required-deployment-verification--exact-safe-procedure) on the actual Ubuntu host. It uses the documented `/opt/job-tracker/app` checkout and `/opt/job-tracker/shared/.env`, prints validation results rather than secret environment data, inventories listeners/published ports/networks, checks real nginx configuration/internal health and obtains SYN-only peer evidence. Add these read-only checks: - -```bash -cd /opt/job-tracker/app -git rev-parse HEAD -git status --short -docker inspect --format '{{.Name}} image={{.Image}} ports={{json .NetworkSettings.Ports}}' "$frontend_id" "$backend_id" -# Use frontend_id/backend_id established by the earlier procedure. -# Observe the marked request in the existing nginx access log without sharing unrelated requests: -docker logs --since 2m "$frontend_id" 2>&1 | grep -F '/api/auth/config?b06_probe=runtime-check' -``` - -While capturing the SYN peer, issue `curl --noproxy '*' --max-time 10 -sS -o /dev/null 'https://jobs.cesnimda.uk/api/auth/config?b06_probe=runtime-check'` from the controlled client. Compare the original socket source with nginx's generated `geo`/`set_real_ip_from` entries, and the access log's effective client address with the known test source. The access-log format must be inspected before interpreting fields. These alone do not prove the IP finally used by backend rate limiting; that requires the real-chain synthetic A/B scenarios on an isolated staging/test topology. - -| Evidence to return, with secrets/cookies/user content removed | Safe result | Unsafe or unresolved result | -|---|---|---| -| Host/provider identity, deployed revision/image IDs, Compose preflight result and frontend/backend `Ports`/network subsets | Current intended build/configuration, only intended frontend host publication; API unpublished | Old/unknown build, dev overlay, host networking or extra API/public internal-service binding | -| Relevant `ss`, Docker NAT/FORWARD, IPv4/IPv6 firewall and provider rule excerpts | Only documented public routes; direct app path blocked or explicitly peer-restricted | Rule comments alone, unknown IPv6 handling or externally reachable API | -| Traefik entrypoint/router/upstream and forwarded-header directives only; generated nginx trust entries; marked SYN peer observation | Actual Traefik source matches narrow allowed peer; untrusted source differs; client forwarding sanitized | Trust-all, guessed subnet or shared NAT identity for trusted and untrusted paths — stop before permitting that peer | -| Internal nginx/API health and controlled canonical-route result | Healthy internal API with working canonical ingress | Public API port required for health, internal 502 or legitimate caller rejected | -| Direct nginx attempts from an actually untrusted reachable test network, plus spoof/malformed variants | 403/444 or independently explained network denial; no forged identity reaches backend | Successful application response from forbidden peer or caller-selected IP/scheme/host | -| Fresh A/AAAA/provider inventory, certificate validation and HTTP redirect result from a known external network | Intended ingress address(es), valid TLS, HTTP target reachable at canonical HTTPS origin | Stale/alternate bypass record, invalid chain or the observed unreachable `:444` redirect | -| Isolated real-chain A/B budgets, actual backend client-IP observations and B02 context checks | Each owner isolated; sessions share owner; spoofing cannot select IP budget; stale context rejected | Backend sees only proxy IP or client-forged IP; header/session selects another owner | - -Return only selected sanitized excerpts and the timestamp/test vantage point. **Do not send complete `.env`, full `docker inspect`, authentication headers, tokens, passwords, private keys/certificates or unrelated access logs.** Do not run restart or missing/invalid-configuration tests against production as part of this read-only handoff. Those remain mandatory in the isolated real-container test environment after Docker is operational; record startup failure, restart health, peer/network stability and repeat direct-access rejection. - -### Closure checklist by evidence level - -1. **Proven by automated test:** effective production port/trust constraints, fail-closed preflight, native trusted/untrusted forwarding behavior, B04 principal/IP separation, B02 session guard, B01/B03/B05 focused regressions. -2. **Proven in running local/container topology:** **none in this follow-up**. Docker failure prevents actual Compose startup, real nginx syntax/enforcement, actual Traefik peer capture, internal routing, restart/config-failure tests and real-chain A/B verification. -3. **Proven against the deployed public endpoint from this workstation:** DNS observations, valid HTTPS/200, forged Host 421, direct-port connection failures from this vantage, HTTP redirect to unreachable 444. This is partial public-endpoint evidence, not certification of the internal deployed chain. -4. **Still requires operator verification:** deployed build/topology/trust CIDRs, actual socket peers, backend client IP/scheme, real-chain session/account/IP behavior, host/container listeners and other network memberships, host/provider IPv4/IPv6 firewall/NAT, independent external reachability, intended DNS-address inventory and correct HTTP redirect target. - -The blocking items are therefore the unavailable local Docker engine, absent verified production host access/evidence, missing real-chain peer/identity tests, and the observed HTTP redirect mismatch. **B06 implementation complete, but F04 remains open.** No change to F04's conditional severity or closure status is justified by connection failures alone. - -## Authorized release attempt and CI prerequisite correction - -Connor subsequently authorized commit/push to live. B01–B06 were committed as `0954876`, and the redesign brief as `1bbbd05`; both were pushed to `main`. [Gitea run 736](https://git.cesnimda.uk/cesnimda/jobtrackingapp/actions/runs/736/jobs/1607) failed before backend/frontend testing: `scripts/test-ingress.py` raised `FileNotFoundError: 'docker'` in the runner. Deployment was skipped; the push was not a successful live rollout. - -The first correction (`f485f72`) attempted to install pinned Compose v5.3.1 through the [official Docker setup action](https://github.com/docker/setup-compose-action), pinned to commit `2fe291b7677a45ee1269ec56a42604c143505e7e`. Gitea tokens were deliberately not passed to GitHub. The config test accepts standalone Compose, retaining `docker compose` on existing Docker installations. Nothing is skipped and the production ingress gate remains unchanged. - -Validation before the corrective push: **3/3 tests through `docker compose` and 3/3 through the real standalone binary** with Docker discovery disabled for the latter fixture; tracked-secret scan and `git diff --check` pass. This corrects a CI dependency assumption; it neither repairs local Docker Desktop nor supplies any missing F04 deployment evidence. - -[Run 737](https://git.cesnimda.uk/cesnimda/jobtrackingapp/actions/runs/737/jobs/1609) then failed in the action itself: `Parameter token or opts.auth is required`; deployment again skipped. The final setup uses the [official standalone release](https://github.com/docker/compose/releases/tag/v5.3.1) directly, in a runner-local temporary directory. It pins Linux x86_64 and SHA-256 `f9ebc6ebdb19d769b793c245a736caaeb198c62587f13b25c660c13b4987f959`, verifies before execution, and needs neither a daemon nor an authentication token. A fresh local download matches that official release digest; 3/3 ingress config tests and diff validation pass. Runner execution and deployment are tracked separately from these local checks. - -[Run 738](https://git.cesnimda.uk/cesnimda/jobtrackingapp/actions/runs/738/jobs/1611), for `97fce89`, passed Compose installation, all repository safety scripts, browser-extension tests, secret/SBOM checks, parser tests and the Release API build. Test-project build then failed with MSB3021/MSB3024: the Linux `PdfBrowserStub` apphost was copied to an output path already used as the fixture directory. Windows' `.exe` suffix had hidden the collision. The project reference now sets `Private="false"`; it still builds the fixture, while the existing explicit subdirectory copy is its sole output owner. SDK target inspection confirms this disables transitive content copying. No renderer or admission behavior changes. - -Local verification: `C:/Users/Connor/.dotnet/dotnet.exe test JobTrackerApi.Tests/JobTrackerApi.Tests.csproj --configuration Release --no-restore --filter FullyQualifiedName~PdfRenderAdmissionTests --logger "console;verbosity=minimal"` built successfully and passed **22/22** tests, including real exporter/fixture failure recovery. A preliminary unqualified `dotnet` invocation could not find the pinned SDK because PATH selected the system runtime; the explicit installed SDK resolved it. Diff checks pass. Linux runner confirmation remains required after push; a build/package fix does not close F04. - -[Run 739](https://git.cesnimda.uk/cesnimda/jobtrackingapp/actions/runs/739/jobs/1613), for `23cc250`, failed earlier in the unchanged API compiler: `csc.dll` exited 139 on both the ordinary and existing single-process retry. This is the previously documented runner crash signature, not a compile diagnostic or test assertion. It prevented Linux confirmation of the packaging correction. No retries/gates were weakened. Fresh local broader verification with the explicit SDK passed **915/915** selected backend tests, no selected skips, with only the same documented external-DNS-dependent IMAP exclusion; `.artifacts/redesign-audit/backend.trx` records the run. - -Pushing this evidence provides one further complete CI attempt with unchanged application/test logic. If the existing bounded retry is exhausted again, stop release attempts and obtain runner-host diagnostics under the [runner investigation](../../../infrastructure/runner-investigation.md); do not increase retry loops indefinitely or deploy past a red test gate. Phase 1 redesign audit is separately committed on `codex/ui-redesign-audit`, with no redesign implementation. F04 remains open regardless of CI outcome until its runtime/production checklist is proven. - -[Run 740](https://git.cesnimda.uk/cesnimda/jobtrackingapp/actions/runs/740/jobs/1615) recovered from the compiler issue, confirmed the Linux fixture **build** fix, and executed all 916 backend cases: **903 passed, 13 failed**. These were actionable portability failures rather than another compiler crash: two TLS-rejection cases recorded an empty HTTP read instead of null, and eleven PDF process cases could not start the .NET fixture (exit 131 or ensuing start-marker timeout). The workflow installed .NET in `/root/.dotnet`, but exported `DOTNET_ROOT` only inside the setup step; PATH alone does not supply the runtime location to a native apphost ([Microsoft runtime-location documentation](https://learn.microsoft.com/en-us/dotnet/core/tools/dotnet-environment-variables#dotnet_root-dotnet_rootx86-dotnet_root_x86-dotnet_root_x64)). - -The correction persists the already-validated runtime path through `GITHUB_ENV`. The TLS assertion accepts null **or an empty string**, while still requiring `HttpRequestException` with `AuthenticationException`, original-host SNI, no custom validation callback and zero HTTP request content. It does not accept whitespace or any HTTP bytes, trust a bad certificate or alter the transport. Fresh explicit-SDK Release verification with `DOTNET_ROOT` set: **98/98** `PdfRenderAdmissionTests|JobImportHttpTransportTests` pass; `.artifacts/redesign-audit/portability.trx`. No application code or test filters changed. The earlier stop rule applies to repeated compiler crashes, not evidence-backed correction of these now-observed test/setup defects. - -### Release dependency gate — 2026-09-10 local / 2026-09-09 UTC - -[Run 741](https://git.cesnimda.uk/cesnimda/jobtrackingapp/actions/runs/741/jobs/1617), `2024c37`, confirmed **916/916 Linux backend tests passed, zero skipped**. Compose/safety/parser/build checks also passed. Deployment was then skipped because `npm audit --audit-level=high` found three vulnerable package entries: Next critical, sharp high and js-yaml high. A fresh local audit reproduced the same report. These advisories were updated in the public database on September 8; earlier audit results are historical, not proof of the current dependency gate. - -The targeted patch raises the Next and matching lint-config floor to 16.3.3 (lock resolves 16.3.4), updates the existing sharp override from 0.35.3 to 0.35.4, and updates the transitive js-yaml 3.x lock to 3.15.2. Its 4.x copy already uses patched 4.3.2. Lockfile review shows only these dependency families and their required platform/compiler artifacts changed. React, MUI, routing, API and schema contracts were not changed. - -Primary advisory evidence: [Next Windows-hosted server issue](https://github.com/advisories/GHSA-p293-qw3h-jr36), [Next AVIF optimization issue](https://github.com/advisories/GHSA-2xp9-vwfh-vxw4), [sharp/libheif](https://github.com/advisories/GHSA-rgj7-g3m4-5g8c), [js-yaml merge budget](https://github.com/advisories/GHSA-2883-xcg3-v3hh). The intended production frontend is a static nginx export, not a Windows Next server or a running Next image optimizer; the advisories alone do not establish production exploitation. Build/development dependencies still fall within the existing release policy, so the gate is retained and patched instead of suppressed. - -Initial commands: `npm install --package-lock-only --ignore-scripts --no-audit --no-fund`, then `npm update js-yaml --package-lock-only --ignore-scripts --no-audit --no-fund`; `npm audit --package-lock-only --audit-level=high` reports **zero vulnerabilities**. Actual installation, frontend regression/build and new CI results follow separately; a lockfile-only audit is not runtime verification. - -Fresh installation with `npm ci --no-audit --no-fund` succeeded (820 packages); installed-tree `npm audit --audit-level=high` also reports **zero vulnerabilities**. Next's updated lint preset introduced `no-location-assign-relative-destination`, which assumes Next Router. This static SPA uses React Router and deliberately performs full navigation after account deletion to clear private state. That one inapplicable navigation rule is disabled with a configuration comment; both existing full-navigation calls and B02 cleanup remain unchanged. - -The patched Next build compiled application code but then exposed the historical `interview-prep.test.tsx:120` TS2571 as a build blocker. The smallest test-only correction uses `objectContaining({ content: expect.not.stringMatching(...) })` on the unknown Axios request argument. The existing positive assertion still requires the exact endpoint/category/title and a string containing the debrief prompt; the negative assertion still rejects invented company/achievement/metric content. No cast, type exclusion, strictness relaxation or production behavior change was introduced. R01a's explicit standalone CI command remains outstanding; its original reproduction is retained. - -Fresh local release verification after these changes: - -| Command (UI directory unless noted) | Result | -|---|---| -| `npm test -- --watchAll=false --runInBand` | **67 suites / 298 tests passed**, 102.468s, after the dependency update | -| `npm test -- --watchAll=false --runInBand src/interview-prep.test.tsx` | **13/13 passed** after the assertion correction | -| `node node_modules/typescript/bin/tsc --noEmit --incremental false` | **Exit 0** after correction; original error reproduced before it | -| `npm run build` with `NEXT_PUBLIC_API_BASE_URL=http://localhost:5302/api` and telemetry disabled | **Exit 0**, Next 16.3.4 static export, including TypeScript | -| `npm run test:e2e` with `PLAYWRIGHT_STATIC_EXPORT=true`, `PLAYWRIGHT_PREBUILT_EXPORT=true`, explicit installed `DOTNET_HOST_PATH`/`DOTNET_ROOT` | **18/18 passed**, 1.4m; Release API build zero warnings/errors. Includes B02 account isolation, B05 exact CSV bytes, anonymous public CV and long searchable PDF | -| `npm run lint` | **Exit 0**, zero warnings after the rule compatibility adjustment; one redundant concurrent run raced Playwright's temporary `test-results` cleanup (ENOENT), so final lint is run serially | -| Root: `python scripts/supply-chain.py scan-secrets`; `git diff --check` | **Pass**; line-ending notices only | - -Logs are under `.artifacts/redesign-audit/`. The latest full Linux backend run remains **916/916 at `2024c37`**; no backend source changed in this dependency correction. Deployment still requires the next full CI run and production preflight. This is authorized release repair, not Phase 2 redesign or B07; **F04 remains open**. - -### Runner dependency corruption after the patch - -[Run 742](https://git.cesnimda.uk/cesnimda/jobtrackingapp/actions/runs/742/jobs/1619), `a861ca4`, again passed **916/916 backend tests**, no skips (16s), repository/parser/build checks, locked frontend installation and the dependency audit. Lint stopped before analyzing application code with exit 2: installed `es-abstract/2025/RegExpCreate.js:19` contained `var"flags`, causing `SyntaxError: Invalid or unexpected token`. Subsequent gates and deployment were skipped. - -Independent verification downloaded the exact `es-abstract` **1.24.2** tarball URL from the lockfile, verified its SHA-512 integrity against that lock and read the member without extracting it. The official member and locally installed file match byte-for-byte; line 19 is `var flags = typeof F === 'undefined' ? '' : ToString(F);`. This is evidence of different dependency bytes in the runner's failure, not a source lint violation or a need to edit the dependency. The corruption mechanism is unproven; the existing [runner investigation](../../../infrastructure/runner-investigation.md) documents analogous failures. - -This evidence-only push makes **one further unchanged-code CI attempt** with the workflow's existing isolated install cache. No new retry loop, lint suppression or gate bypass is added. If corrupted dependency bytes or assertion-free process crashes recur, stop release retries and request runner-host diagnostics; do not generate repeated empty commits. Public `/health` still reports version **299**, `/ready` reports ready before this attempt; that is the existing deployment, not proof that `a861ca4` is live. The completed Phase 1 audit is pushed separately at `957fb57` on `codex/ui-redesign-audit`. - -### Final release retry — 2026-09-10 - -[Run 743, test job 1621](https://git.cesnimda.uk/cesnimda/jobtrackingapp/actions/runs/743/jobs/1621), for `27657f31131cd95d42f0e1290853ec9c9b3959a4`, completed in **7m25s**; Gitea recorded the failed status at **2026-09-09 22:32:25 UTC** (September 10 locally). Repository safety/Compose, extension, parser, secret/SBOM, backend build and dependency checks passed. The full backend suite passed **916/916**, zero skipped (15s test duration); frontend passed **67/67 suites, 298/298 tests** (82.867s). Locked installation, npm audit, lint and the production frontend build all passed. The previous malformed `es-abstract` source did not recur at lint. - -The browser gate failed: **6 passed, 12 failed** (1.4m). The first failure was `worker process exited unexpectedly (code=null, signal=SIGSEGV)` in the authenticated accessibility case; its existing retry timed out in `beforeEach` after 30 seconds. The remaining eleven failures report `page.goto: Target page, context or browser has been closed`, including after their existing retries. There is no reported accessibility violation or application assertion mismatch in these failures. This establishes a process/browser execution failure, but does **not** establish its underlying host, runtime or hardware cause. The local **18/18** browser result above remains separate evidence and cannot replace the failed CI gate. - -[Deploy job 1622](https://git.cesnimda.uk/cesnimda/jobtrackingapp/actions/runs/743/jobs/1622) was **skipped**. At **2026-09-10 05:07:21 UTC**, read-only HTTPS requests still returned `/health` → `{"status":"ok","version":"299"}` and `/ready` → `{"status":"ready"}`. The requested commits are pushed to main, but this release has **not deployed**. No production service was replaced by this run. - -The documented stop condition is now reached: **no further unchanged-code release retries or gate bypasses**. Capture runner diagnostics before another attempt. This result is recorded on `codex/ui-redesign-audit` so that documenting the block does not itself start another main deployment. Phase 1 is complete; Phase 2 and B07 have not started. **B06 implementation complete, but F04 remains open**, for the runtime/production reasons in the closure checklist above as well as the unshipped release. - -#### Runner evidence to collect before another attempt - -Run these read-only commands on the actual Ubuntu **runner host**, which must be identified independently of the application host. No SSH alias or credentials are assumed. Use the run's timestamps for a bounded kernel-log window; the example covers this observed attempt. Do not reset Docker, delete caches, alter limits or restart production services just to obtain evidence. - -```bash -date -u -uname -srmo -free -h -df -h / /tmp /dev/shm -df -i / /tmp -docker ps -a --no-trunc --format '{{.ID}} {{.Names}} {{.Image}} {{.Status}}' -docker stats --no-stream --format '{{.Name}} memory={{.MemUsage}} cpu={{.CPUPerc}} pids={{.PIDs}}' -sudo journalctl -k --utc --since '2026-09-09 22:20:00 UTC' --until '2026-09-09 22:40:00 UTC' --no-pager \ - | grep -Ei 'segfault|oom|out of memory|killed process|I/O error|machine check|hardware error|EDAC' -# If systemd-coredump is installed: list metadata only, not memory dumps. -command -v coredumpctl >/dev/null && \ - coredumpctl --since '2026-09-09 22:20:00 UTC' --until '2026-09-09 22:40:00 UTC' --no-pager list -# Select a verified runner/job container ID from the inventory, if it still exists. -read -r -p 'Observed runner/job container ID: ' runner_container_id -test -n "$runner_container_id" && docker inspect --format \ - '{{.Name}} image={{.Image}} state={{json .State}} memory={{.HostConfig.Memory}} memorySwap={{.HostConfig.MemorySwap}} pidsLimit={{.HostConfig.PidsLimit}} shmSize={{.HostConfig.ShmSize}} ulimits={{json .HostConfig.Ulimits}}' \ - "$runner_container_id" -``` - -Return selected, sanitized output plus the job-container identity/image and exact failure window. **Do not provide environment dumps, runner registration/config files, tokens, core files, full Docker inspection or unrelated logs.** A removed job container or unavailable kernel/core records is an evidence gap, not a clean bill of health. Host free memory after completion does not prove the earlier job's cgroup or shared-memory capacity. OOM/cgroup failures, segfault records, or storage/hardware errors would direct the next investigation; absent records do not justify guessing a cause. Retain the existing failed-job browser trace/error-context artifacts if available, keeping synthetic test data separate from production information. A successful full CI run, actual deploy completion and live version/health verification are still required before calling the push live; F04 then additionally requires its independent ingress evidence. +The original B06 completion report was archived during the 2026-09-11 documentation cleanup. Use the current [known issues](../../../roadmap/known-issues.md) for active status and the [pre-cleanup archive](../../../_archive/pre-docs-cleanup-2026-09-11/docs/audits/2026-09-07/validation/b06-completion.md) for the historical evidence. diff --git a/docs/audits/2026-09-07/validation/implementation-queue.md b/docs/audits/2026-09-07/validation/implementation-queue.md index 0ba8421..2d38045 100644 --- a/docs/audits/2026-09-07/validation/implementation-queue.md +++ b/docs/audits/2026-09-07/validation/implementation-queue.md @@ -1,132 +1,3 @@ -# Validated implementation queue +# Historical implementation queue pointer -Companion to the [independent verdict](README.md). This is a proposed queue, not authorization to start implementation. Original finding IDs remain stable; V01 strengthens F08 and V02 strengthens F11. No application changes were made by this pass. - -**Status update, 2026-09-09:** the user separately authorized B01–B05, now complete locally: [B01](b01-completion.md), [B02](b02-completion.md), [B03](b03-completion.md), [B04](b04-completion.md), [B05](b05-completion.md). B05 resolves F05 under its documented spreadsheet-text policy; actual Excel before/after proves the arithmetic formula is now text and Norwegian encoding is preserved. Final verification: 902 selected backend, 298 frontend, 18 browser tests, nine B04 rate-policy HTTP scenarios and the unchanged B03 admission probe pass. Release builds and targeted lint pass; R01a's pre-existing standalone TypeScript error remains. HTTP F11/V02, F08/V01 and F06 are resolved locally; overall F11 remains open for B07/deployment. F03 admission is resolved locally; R04 containment/capacity evidence remains open. **B06 was subsequently authorized; implementation and pending verification are recorded below.** - -**B06 runtime follow-up, 2026-09-09:** [Report and operator handoff](b06-completion.md#runtime-verification-follow-up-2026-09-09). **Implementation complete; full runtime/production verification incomplete; F04 open.** Fresh 166 focused backend, 3 Compose, 6 Production HTTP and 9 B04 HTTP checks pass. Public DNS/valid HTTPS are now observed; direct ports 3000/5202/8080 are unreachable from this workstation, without proof of the rejecting layer. HTTP redirects to unreachable HTTPS port 444. Docker's inference socket failure still blocks actual Compose/Traefik/nginx tests; production peers, backend client IP and host/provider firewall remain unverified. No SSH alias/user was found in permitted non-secret sources. B01–B05 preserved; B07 not started. - -**Sizing:** S = one narrow boundary/configuration change and its meaningful tests; M = one cohesive service/API/client contract that can be understood, implemented, tested and reviewed in one focused session. These are scope estimates, not elapsed-time promises. If a ticket needs unrelated modules, split it at the acceptance boundary below. Do not create a generic framework merely to make tickets share code. - -The numbered order is the default integration order. Independent work may start sooner only along the explicitly permitted parallel branches. A conditional feature can stay disabled while unrelated safe work proceeds; a failed recovery gate blocks migrations, not read-only planning or non-schema tests. - -## Batch 0 — Security, identity and immediate admission - -**Findings:** F11/V02, F08/V01, F03, F06, F05 and conditional F04. These require no data migration and address the most direct ways to cross a boundary or consume shared resources. CSV remains P2 but is inexpensive and isolated. - -| Order / ticket | Size and ownership | Concrete scope and acceptance | Dependencies / rollback risk | -|---|---|---|---| -| **B01 HTTP import destination enforcement** | M. `Services/JobImport/JobImportService.cs`, resolver/address helper only if needed, named import HTTP registration in `Program.cs`, import/network tests. | Normalize mapped addresses and enforce the same approved-address policy at the actual socket connection. Keep HTTP/HTTPS, redirect refusal, bounded content, cancellation and hostname/TLS verification. Before/after tests must show mapped private/link-local targets never reach a connect/send boundary, validation-to-connect DNS changes cannot redirect a socket privately, and an allowed destination still works. | First ticket. No schema. Network-handler rollback reopens the defect; retain the previous handler only as an emergency operational option with equivalent egress restriction. Do not alter all HttpClients. | -| **B02 Cross-account form/session boundary** | M. `src/auth.ts`, `api.ts`, `App.tsx`, profile/editor state binding, a narrow server precondition if required, auth/browser tests. | Bind retained state and outgoing writes to the identity/session generation that loaded it. Synchronize tabs, invalidate owner caches, ignore late responses, refuse mismatched dispatch. Close the shared-cookie race with a server-checked mismatch precondition where client timing alone is insufficient. Test actual A edit → B login in second tab → A Save, asserting B and A database values unchanged; include slow old GET, logout, Back and same-account reauth. | Independent of record revisions, and must precede persistent drafts or editor refactoring. Coordinate `Program.cs` integration with B01/B04 if adding middleware. No schema. Older clients lacking required protection must be forced to refresh/re-auth safely; do not silently fall back to unsafe writes. | -| **B03 Shared PDF admission — complete locally** | M. Singleton exporter, PDF callers/error mapping, exporter tests. | [Completion and evidence](b03-completion.md). Default one active render per process, one per document owner, zero waiting; validated options, safe 503/504, cancellation/timeout/shutdown cleanup and failure recovery. Permanent public/variant/job/profile boundary tests plus real Chromium smoke. Implementation inspection corrected the original package reference: packages do not render PDFs. | No schema. The simultaneous owner allowance is enforced inside the exporter independently of middleware; time-window owner/IP rate limits remain B04. Tune process capacity and verify containment under R04. No durable queue or renderer service added. | -| **B04 Resolve principal before owner limiting — complete locally** | S. `Program.cs`, existing rate-key helper, permanent real HTTP tests. | [Completion, inventory and evidence](b04-completion.md). Authentication **and endpoint authorization** precede account limiting, including explicit-scheme principal selection; validated owner/IP namespaces, shared-IP account isolation, shared same-account sessions/credentials, preserved IP controls, safe 429/Retry-After. Concurrent, crafted identity, invalid credential, B02 context and trusted/untrusted proxy tests pass. | No schema or policy-window changes. B01/B02/B03 preserved. Process-local counters and B06 deployed ingress verification remain explicit. | -| **B05 Spreadsheet-safe CSV fields — complete locally** | S. `ExportController`, small text helper, byte-preserving UI download and export tests. | [Completion and evidence](b05-completion.md). All 13 text columns neutralize leading `= + - @`, including after whitespace/controls/Unicode format characters, before existing CSV escaping. Typed numbers/dates, stored data and JSON remain unchanged. Parser, HTTP/browser-byte and local Excel checks pass; UTF-8 BOM corrects reproduced Norwegian decoding. | No schema/framework/dependency change. Spreadsheet-safe CSV may visibly retain the apostrophe; raw JSON remains available. LibreOffice runtime verification unavailable; defined policy and compatibility limits documented. | -| **B06 Verify and close ingress bypass path — implementation complete; runtime verification pending** | S source/config change plus operator verification. Compose/nginx/forwarded-proxy tests/docs. | [Implementation, runtime observations and gates](b06-completion.md). Production render has frontend 3000 and no API publication. Fresh API/config tests pass. DNS/valid public HTTPS and failed direct-port connections are observed; HTTP redirects to unreachable port 444. Actual Compose/Traefik/nginx execution is blocked by Docker failure; deployed peer/IP/firewall evidence is absent. F04 remains open. | Read-only production inspection authorized, but no unambiguous SSH alias/user exists in permitted sources; use operator handoff. Deployment changes remain separate. Complete real-container and production evidence; do not start B07. A bad binding can cause downtime; retain a tested topology-specific rollback. | -| **B07 IMAP connected-address enforcement** | M. `ImapService`, focused network policy/connector and IMAP SSRF tests. | Reuse the B01 address-policy conclusion, but respect MailKit's transport. Enforce an approved connected peer on initial connection and reconnect; retain certificate hostname validation and timeouts. Existing mapped normalization is already correct: do not fix it again. | After B01 establishes the small shared policy, with sequential integration if it shares helper code. No schema. Test TLS identity with an injected/local fixture; do not disable validation to connect by IP. | - -**Batch stop condition:** V01's actual persisted cross-account save is prevented; HTTP/IMAP outbound address paths and shared PDF admission have failing-before/passing-after evidence; correct owner/IP partitioning remains enforced. Actual ingress reachability proof is required before a public deployment. B01 alone does not close all of F11. - -## Batch 1 — Release pipeline and recovery before migrations - -**Findings:** F12, F17, F19, conditional F20; the immediate containment portion of F03/F18. This batch intentionally precedes schema-changing concurrency and billing work. A migration is not safe merely because its unit tests pass when the recovery/rollback path is unproven. - -| Order / ticket | Size and ownership | Concrete scope and acceptance | Dependencies / rollback risk | -|---|---|---|---| -| **R01a Standalone type gate — partially resolved during authorized release repair, 2026-09-10** | S. `interview-prep.test.tsx`, UI package script, CI workflow. | The patched Next build exposed the historical TS2571. A test-only object matcher now preserves the positive/negative content assertions without dereferencing an unknown Axios payload; 13 interview tests, standalone tsc and Next build pass. An explicit standalone CI command remains outstanding; keep strictness and test coverage. See [release evidence](b06-completion.md#release-dependency-gate--2026-09-10-local--2026-09-09-utc). | Can run in parallel with Batch 0. No production-code behavior or schema. Do not weaken the gate to preserve a green build. | -| **R01b Production DB configuration validation** | S. Provider-selection code in `Program.cs` and startup tests. | Fail startup for an unknown production provider or missing required configured connection. Preserve deliberate dev SQLite defaults and valid explicit SQLite/MariaDB. Assert no fallback database is created on rejected startup. | Integrate after Batch 0 `Program.cs` mutations. No schema; rejecting an existing invalid environment can prevent boot, so validate deployed inputs first. | -| **R02 Consistent recovery-set creation** | M. `deploy.sh`, existing backup runner/scripts, isolated fixture and operator documentation. | Replace live SQLite tar as the DB consistency mechanism; coordinate file inventory/checkpoint or a short write pause, manifest/checksums and incomplete-set marking. Preserve keys/tombstone recovery. For MariaDB, coordinate the file set with the consistent SQL dump. | No new app schema. Can develop synthetic fixture in parallel with Batch 0. A backup that is only present is not yet verified. Keep previous verified sets. | -| **R03 Restore drill and schema-compatible rollback** | M. Deploy rollback logic, isolated restore harness and runbook; no production restore in the implementation session. | Restore latest/previous synthetic sets under active-source writes; integrity, FK owner boundaries, attachment hashes, provider-token decryptability and deletion replay must hold. Run previous/new binaries against the intended schema; allow automatic image rollback only when compatible. Record measured RPO/RTO, required key custody and operator steps. | Depends on R02 and Connor's recovery objectives. **Blocks every schema-changing ticket below.** Failed restore is a stop, not permission to reset the source DB. Do not run downgrade migrations automatically. | -| **R04 PDF/container containment evidence** | S/M. API Dockerfile/Compose and bounded local container test. | Verify non-root filesystem permissions, resource/PID limits and renderer operation. Evaluate sandbox/local-file flags against real templates. Measure a small controlled burst after B03 and pick capacity from evidence. | Depends on B03; coordinate Compose changes with B06. Container changes can break rendering/storage; verify permission/health/PDF before rollout. A separate renderer service is a decision only if adequate containment cannot otherwise be demonstrated. | -| **R05 AI worker survival and recovery** | M. `AiOperationQueue.cs`, worker tests/health signal. | Catch at each worker loop, classify cancellation/transient/fatal outcomes, bound retries and expose lost/degraded workers. Inject claim/owner/result persistence failures at concurrency 1 and 2; core host survives recoverable faults, accepted operations resume after lease recovery, no lost result or endless tight retry. | Independent of schema work; required before enabling this worker. Keep its existing kill switch. Do not change the global background exception policy. | - -**Batch stop condition:** strict frontend typing and configured-provider startup are deterministic; a recovery set actually restores, and compatibility governs rollback. AI/PDF enabled features need their own containment proof. R03 must be green before Batch 2 or paid-ledger schema changes are applied. - -## Batch 2 — Record integrity, intent retries and storage accounting - -**Findings:** F01 and the concrete F10 integrity/admission subset. Agree the convention in D01, then apply it to bounded aggregates. Do not introduce a generic concurrency service, universal event sourcing or globally unique job content. - -| Order / ticket | Size and ownership | Concrete scope and acceptance | Dependencies / rollback risk | -|---|---|---|---| -| **D01 CV variant revision protocol** | M. CV variant model/service/controller/editor, context migration and CV tests. | Expected revision/conditional update, transactional history, distinct conflict response and retained draft. Reconcile duplicate revision labels preserving every snapshot, then enforce uniqueness. Include restore-versus-save, stale name/settings and old-client handling. Prove with two contexts and SQLite/MariaDB. | B02 + R03. Establish request/response convention before D02/D03. One cohesive vertical change; keep templates/layout/AI tooling outside the diff. Additive rollout first; old unconditional writers must be rejected or refreshed when enforcement starts. Do not revert to an old writer without a compatibility plan. | -| **D02 Career Profile atomic revision/projection** | M. Career profile service/controller/model, legacy user projection, profile page/tests and migration. | Separate aggregate revision from structured-document schema `version`. Atomically replace children, append history and update the derived legacy projection, or document/test a recoverable projection only if a shared transaction is demonstrably impossible. Cover restore, projection-write failure, delete-versus-save and losing drafts. | D01 contract + R03. Preserve old versions/child IDs as required; no identity/profile-field redesign. Same-account transaction correctness does not replace B02. | -| **D03a Job mutation preconditions** | M. Job aggregate/controller/status operations, admin undo and generated-draft writers that touch the same aggregate; model/migration/tests. | Inventory exact conflicting write sets; apply the agreed revision rule to user read-modify-write paths and prevent background writes from silently replacing newer user drafts. One conflict must not partially update events/opportunity projection. Explicitly justify field-disjoint writes allowed to commute. | D01 convention + R03. Own the schema snapshot serially. If inventory is too broad, split status/full-edit and draft-generation writers into consecutive tickets with each unprotected path left visibly open. Do not claim F01 closed between them. | -| **D03b Job client conflict handling** | M. Existing job detail/edit/status/draft client paths and browser tests. | Send loaded revisions, retain losing edits, explain reload/compare/copy without blind retries; delayed responses cannot replace current owner state. | D03a + B02. Integrate before enabling mandatory job preconditions. No independent schema. No dashboard redesign. | -| **D04 Creation intent idempotency** | M. Manual/imported job creation boundary, relevant client submission state, narrow intent persistence and relational tests. | One key per user creation intent, unique owner/key constraint, payload fingerprint and returned resource identity committed with the creation. Same key/same payload returns the original; same key/different payload conflicts; two owners are independent; a deliberate new application uses a new key. Test lost response/retry and concurrent identical requests. | R03 and settled job creation transaction. Can prepare after D01; integrate after D03 schema changes. Do not deduplicate by company/title/URL globally or retrofit every endpoint. Retain intent records for an explicit retry window; expired-key behavior must be documented. | -| **D05 Atomic attachment byte reservation** | M. Attachment admission/storage service, owner quota state and focused migration/tests. | Reserve bytes atomically before staging, release/commit correctly on cancellation/failure; two near-limit uploads cannot both pass. DB transaction/reservation works across processes. Preserve existing staged-file reconciliation and safe path handling. | R03; integrate after other schema tickets. No silently deleted files to meet a quota. Reservation cleanup/reconciliation must survive crashes. | -| **D06 Generated-file accounting** | M. Existing `AccountOwnedFileInventory`, PDF/artifact/package producers, quota reconciliation/tests. | Account for generated artifacts and temporary/retained export copies with explicit limits and cleanup. Reuse D05 accounting contract; avoid counting only attachments or charging the same retained file twice. Test copy/delete/crash and old existing inventory. | D05 + B03. Rollback retains existing files; a reconciliation disagreement blocks new growth safely and alerts, rather than purging unknown user data. | -| **D07 Bounded exports/history growth** | M per export family. Account export first; jobs export separately if needed. | Measure synthetic 1k/10k owner histories, enforce peak-memory/request bounds, stream JSON/rows into the current ZIP/output as needed. Add conservative growth admission where required. Test account export remains available for existing over-quota data. History pruning is excluded until Connor approves restore promises. | D05/D06 for resource contract; R03 for any persistent limits. Do not jump directly to a queued export platform or optimize 100k rows without evidence. No destructive retention migration. | - -**Batch stop condition:** stale client writes cannot win silently; histories remain complete/unambiguous; projection/children updates are atomic; creation retries have a stated contract; bytes cannot exceed admission through concurrency or omitted producers. Both providers need meaningful relational evidence. Preserve all pre-migration data and safe old-client behavior. - -## Batch 3 — Paid account lifecycle - -**Finding:** F02, with retention/deletion policy and conditional premium worker dependencies. Correct entitlement and deletion behavior precedes billing UX. This batch can be prepared earlier, but paid behavior stays gated until it is complete. - -| Order / ticket | Size and ownership | Concrete scope and acceptance | Dependencies / rollback risk | -|---|---|---|---| -| **P01 Purchase intent** | M. Billing checkout/gateway, narrow persisted attempt and billing tests. | Stable purchase-attempt identity reused after double-click/timeout, provider idempotency and explicit handling of an existing/pending subscription. Two simultaneous requests and an unknown first result cannot create a second intended purchase. | R03; reuse D04's intent principles, not necessarily its table. Billing schema integration has one owner. Retain attempt/customer IDs during rollback; disable new checkout on unresolved state. | -| **P02 Entitlement reconciliation** | M. Billing handler, entitlement derivation, local transaction/reconciliation and tests. | Reconcile relevant current customer subscriptions, not whichever event's subscription was processed last. Atomically commit local provider state and role consequences; failures cause safe retry. Test active+cancelled subscriptions, delayed/out-of-order/concurrent events, duplicate delivery and failure between local writes. | P01/local identity contract. A synchronous design with provider retries is acceptable; durable inbox only if acknowledging deferred work. Reconciliation must recover missed delivery. Old images cannot safely reinterpret a new billing ledger without a reviewed compatibility plan. | -| **P03 Deletion versus paid lifecycle** | M. Existing deletion stages, billing cancellation seam, UI confirmation only as required and lifecycle tests. | First guard active/uncertain billing and pending checkout. Then implement the chosen cancellation/access policy; retain provider identifiers until confirmed resolution. Retry unavailable provider safely, handle deletion racing checkout/webhooks, and preserve minimal approved billing/deletion records. | P01/P02 + Connor's deletion/retention policy. Sandbox must prove the charged-account scenario. Do not erase the identity then attempt cancellation. Preserve pending stages across rollback; suspend new deletion admission if reconciliation is unhealthy. | - -**Batch stop condition:** authorized Stripe sandbox confirms one intended purchase, correct entitlement, provider retry/reconciliation and no continuing unresolved charge after completed deletion. Disabled deletion is a temporary containment state, not completion. Enabled premium features also require R05 where applicable. No paid-beta admission until these conditions and recovery/accessibility gates hold. - -## Batch 4 — Accessibility and shared design foundations - -**Finding:** F14 plus the test coverage gap. These tickets may begin in parallel early on their own files; their integration must preserve B02/D01 form behavior. Structural fixes come before cosmetic redesign. - -| Order / ticket | Size and ownership | Concrete scope and acceptance | Dependencies / rollback risk | -|---|---|---|---| -| **A01 Semantic theme/component colors** | S/M. `src/theme.ts`, necessary shared MUI overrides and loaded-state accessibility fixtures. | Correct small-text success/primary contrast in light/dark default/hover/focus/selected states. Check actual foreground/background pairs across chips, tabs and outlined buttons; preserve filled-button contrast. | Can proceed independently of backend work. No schema. Theme changes have broad visual impact: inspect both themes on existing pages before integration. Do not change every page or replace MUI. | -| **A02 Builder preview keyboard structure** | S. `CvBuilderEditor` preview region and focused browser tests. | Make the overflowing preview region named/focusable as appropriate; prove keyboard scrolling, focus visibility and no trap for iframe/page content. Include mobile→desktop transition, one/multiple pages and editing/preview controls. | A01 can proceed independently, but do not edit this file simultaneously with D01. No schema; avoid blanket tabindex additions. | -| **A03 Accessibility coverage and manual gate** | M test/documentation session; defects found become bounded follow-ups. | Loaded builder/profile/job detail + key dialogs/errors, EN/NB, both themes, mobile and desktop. Wait for meaningful data, not merely main. Complete keyboard/NVDA-or-Narrator, zoom/reflow/text spacing, forced colors and focus/error/reauthentication checks. | A01/A02 and stable B02/D01 conflicts. Automated success cannot substitute for manual evidence. No new accessibility compliance claim from axe alone. | - -**Batch stop condition:** primary workflows, save/conflict recovery and preview are operable by keyboard with usable focus and announcements; shared contrast fixes and representative loaded-state axe coverage pass. New primary-flow structural defects block public exposure; purely cosmetic work does not. - -## Batch 5 — Product and workflow corrections - -**Findings:** F21, F07, F15, F13, F16 and F09. These are scoped product/operational fixes, not grounds for a simultaneous whole-app rewrite. - -| Order / ticket | Size and ownership | Concrete scope and acceptance | Dependencies / rollback risk | -|---|---|---|---| -| **W01 Date-only calendar contract** | S/M. Calendar shared event conversion/provider-link/ICS tests and minimal labels if necessary. | Follow-ups/deadlines remain dates and export all-day with correct exclusive end date. Test UTC control, offset-bearing inputs separately, DST transitions and both database/server timezone settings. | Can prepare early; avoid database date migration by default. Resolve any real timed-field evidence separately. Existing external events will not be magically corrected; describe behavior for newly exported events. | -| **W02 Reminder outcome semantics** | M. Reminder worker/sender result contract and existing durable email-attempt seam where reusable. | Distinguish suppressed, accepted, failed and unknown; persist per-reminder outcome/dedup intent. Test later-send failure, commit failure, disabled sender and uncertain SMTP result. Auth resend remains recoverable; do not promise exactly-once SMTP. | Use R05 failure-boundary convention and R03 before any attempt schema. No second email platform. Provider sandbox/local mailbox sink before enabling revised sending. Unknown sends are not blindly replayed. | -| **W03 Sensitive response cache policy** | S. Private export/document/auth response policy and header tests. | Apply appropriate no-store to sensitive personalized responses, retain public/shared route semantics and verify proxy behavior. Handle CSP separately if it affects preview frames. | B02 is independently required; headers do not fix stale React state. No schema. Test public CV/static assets and PDF preview before changing global headers. | -| **W04 Admin event/data minimization** | M. Admin diagnostics and security mutation metadata; targeted logging cleanup may be a separate S ticket. | Minimize unnecessary private fields, record high-impact actor/action/target/outcome, avoid full URLs/documents/provider errors in logs, test role revocation/denied verbs. Preserve intentional admin capabilities. | Connor's actual support/retention policy; R03 before new audit storage. No speculative support role, impersonation or tamper-proof claims. Audit record migration must retain existing evidence. | -| **W05 Draft recovery decision and narrow pilot** | M, one editor only. Existing draft/dirty-state helpers and the chosen editor. | Keep warnings and in-memory edits; if approved, add opt-in owner/schema/expiry-scoped local recovery with explicit restore. Test offline, expired session, denied/quota storage, account change and conflicting revision. | **Must follow B02 and D01/D02**, plus Connor's local-content decision. No service worker/PWA or indiscriminate private caches. Rollback must not mix or silently apply old drafts. | - -**Batch stop condition:** each workflow's narrower acceptance passes; no silent time shift, suppressed-as-delivered message, ownerless draft or accidental private logging. Decisions about retention/local drafts remain explicit rather than hidden in implementation defaults. - -## Batch 6 — Broader UX and measured operational hardening - -**Findings:** remaining F16/F18 and measured F10 scale concerns; original product proposals with no reproduced blocker. CPU admission, quota safety, restore and worker containment are already in earlier batches. - -| Order / ticket | Size and ownership | Concrete scope and acceptance | Dependencies / rollback risk | -|---|---|---|---| -| **U01 Route-by-route UX correction** | S/M per chosen workflow, one page/interaction family. | Prioritize observed task completion problems, terminology and empty/error/loading states. Preserve owner/revision/focus primitives. Test the full user task in EN/NB and both themes. | B02/D01–D03/A01–A03. Do not redesign every route in one session; no schema unless separately scoped. Revert one route safely. | -| **O01 Reproducible release inputs** | S/M per build ecosystem. API Docker restore/SDK inputs first, then other lock/pin/scan evidence. | Actual image build honors intended SDK/build props and lock files; retain release provenance and an explicit advisory exception policy. Verify supported versions/current advisories only when external access is authorized. | Coordinate with R04 Dockerfile and R01 CI ownership; do not combine broad runtime upgrades with audit repairs. Rebuild/test the changed artifact and preserve known-good images. | -| **O02 Capacity, monitoring and operator drills** | M per measured bottleneck or drill. | Measure representative rows/export sizes/query plans/RSS; fix demonstrated hotspots, alert on storage, restored-backup age, worker health and billing divergence. Validate support/deletion/key rotation/incident procedures. | Early bounds and recovery already complete. Tune limits with evidence; no speculative Redis, broker, read replica or service split. No destructive load against production. | - -**Batch stop condition:** release evidence matches the intended audience and enabled features. Public paid readiness depends on verified real operational/support behavior, not completion of every cosmetic item in the original audit inventory. - -## Safe parallel work and prohibited overlap - -- B01 HTTP boundary, B02 identity analysis/tests, B03 exporter, B05 CSV, R01a typing, R02 synthetic backup fixture and A01 theme can proceed independently with assigned file ownership. Shared `Program.cs`, CI and Compose edits still integrate sequentially. -- B07 follows the address-policy decision in B01. Do not develop competing outbound-policy helpers or two versions of the same HTTP handler. -- Only **one migration/context snapshot owner** integrates at a time. D01 → D02 → D03a → D04 → D05/P01 schema commits are serialized even if isolated test/client work is prepared in parallel. -- D03b may be prepared against D03a's agreed API but cannot independently change the protocol. P01/P02/P03 share billing state and must integrate in that order; do not build billing UX against unsettled entitlement rules. -- B02/auth/editor lifecycle must settle before W05 persistent drafts. D01 and A02 both touch `CvBuilderEditor`: serialize edits or explicitly divide non-overlapping responsibility and review their combined behavior. -- A01 shared tokens precedes U01 visual redesign. A03 final accessibility evidence follows the combined form/identity/conflict changes, even if fixtures are written earlier. -- R02/R03 may run alongside non-schema security fixes. Their failure blocks migration/deployment advancement; it does not block safe source inspection or independent unit-test preparation. - -## B01 first-session handoff — completed 2026-09-08 (scope retained) - -**Take B01 only. Objective:** prevent HTTP job import from connecting to private/local/link-local destinations through mapped IPv4 or a validation-versus-connection DNS change, while preserving legitimate imports. - -1. Read the validated F11/V02 evidence and current `JobImportService`, `IHostAddressResolver`, named import HttpClient registration and existing import tests. Reproduce the mapped-address guard failure with a non-network fake. -2. Add the narrow mapped-address normalization and move/enforce policy at the actual connection boundary. Prefer the existing .NET HTTP/socket facilities; preserve original hostname/SNI/certificate checks, redirects disabled, bounded response size and cancellation. Do not modify unrelated HttpClients or add a network framework. -3. Add regression tests for IPv4/IPv6/mapped/loopback/link-local/mixed answers, differing resolution answers, allowed address, timeout/cancel and TLS hostname behavior. Use controlled local/fake transport; no real private host/metadata or third-party DNS calls. A validator-only unit test does not prove the DNS race is closed. -4. Run the focused backend import/network tests and the affected API build. Review the diff for accidental request, TLS, proxy or parser changes; document any deployment egress assumptions and the remaining IMAP ticket. - -**Owned files:** import service and a narrowly justified adjacent address/connection helper, the named import HTTP registration in `Program.cs`, and related tests. **Excluded:** IMAP implementation (B07), CV/billing/schema/UI changes, deployment, global policy rewrites and dependency upgrades. No migration. If safe connected-peer enforcement cannot be verified locally, retain the passing normalization fix as a bounded partial change but leave B01/F11 open; do not claim validator-only protection closes SSRF. - -**Handoff acceptance:** the retained exploit fixtures fail against the original and pass against the change; connection-time tests prove no denied peer is attempted; allowed requests still behave correctly; no TLS relaxation, source data change or unrelated edits. These criteria now pass for B01; see the [completion record](b01-completion.md). Subsequently authorized B02–B05 also pass their local acceptance criteria with documented limits; see their completion records above. B06 was subsequently authorized and implemented, with local topology and production verification still pending. Complete those gates before closure; B07 and the broad overhaul remain unstarted. +The original audit implementation queue was archived during the 2026-09-11 documentation cleanup. Use the [roadmap](../../../roadmap/README.md) for current work and the [pre-cleanup archive](../../../_archive/pre-docs-cleanup-2026-09-11/docs/audits/2026-09-07/validation/implementation-queue.md) for the historical queue. diff --git a/docs/backend/README.md b/docs/backend/README.md new file mode 100644 index 0000000..48123a0 --- /dev/null +++ b/docs/backend/README.md @@ -0,0 +1,10 @@ +# Backend + +The ASP.NET Core API is the system boundary for persistence, authentication, business rules, files, integrations, and background work. + +- [API reference](api.md) +- [Background jobs](background-jobs.md) +- [Error handling](error-handling.md) +- [Backend architecture](../architecture/backend.md) + +Run and test it through [local development](../development/getting-started.md). Do not point local acceptance tests at production data. diff --git a/docs/backend/api.md b/docs/backend/api.md new file mode 100644 index 0000000..5139150 --- /dev/null +++ b/docs/backend/api.md @@ -0,0 +1,18 @@ +# API reference + +All application routes are under `/api`. OpenAPI is mapped in Development only. The authoritative contracts are the controller request/response types and tests in `JobTrackerApi.Tests/`. + +| Area | Main routes | +|---|---| +| Authentication | `/auth/config`, `/auth/login`, `/auth/register`, `/auth/me`, password/email verification and reset | +| Jobs/applications | `/jobapplications` CRUD, board, pipeline, reminders, history, timeline, stats, follow-ups, soft-delete/restore, AI workspace | +| Companies | `/companies` CRUD and recruiter/pipeline context | +| Correspondence | `/correspondence` per-application messages and inbox views | +| Gmail/Graph/IMAP | `/gmail`, `/microsoft-graph`, `/imap` connect, sync, review, and relink flows | +| Attachments | `/attachments` validated upload, list, download, metadata rename, delete | +| Career/CV | `/profile-cv` extraction, structure, rewriting, tailoring, templates, export | +| Rules/exports/backups | `/rules`, `/export`, `/backup` | +| Admin | `/users`, `/admin/audit`, `/admin/system` | +| Billing | `/billing` hosted checkout, portal, and webhook when configured | + +Mutating requests using a session cookie require the CSRF double-submit header. Authorization and owner filtering are enforced in the API; a UI route is not a security boundary. See [authentication](../security/authentication.md) and [authorization](../security/authorization.md). diff --git a/docs/backend/background-jobs.md b/docs/backend/background-jobs.md new file mode 100644 index 0000000..b66d2f5 --- /dev/null +++ b/docs/backend/background-jobs.md @@ -0,0 +1,5 @@ +# Background jobs + +Hosted services currently cover rules/ghosting, follow-up reminders, saved-search alerts, daily exports, job enrichment, AI operations, CV processing, and database backups. The exact enablement switches and defaults are in `JobTrackerApi/appsettings.json` and [environment variables](../configuration/environment-variables.md). + +The deployment is intentionally single-backend-instance. CV and AI work persist enough state to recover queued/running work after restart, but there is no database lease for multiple API replicas. Add leasing before scaling horizontally. Worker failures must not make manual tracking unusable; readiness and logs are the operational signals. diff --git a/docs/backend/error-handling.md b/docs/backend/error-handling.md new file mode 100644 index 0000000..49e50af --- /dev/null +++ b/docs/backend/error-handling.md @@ -0,0 +1,5 @@ +# Error handling + +Unhandled API errors use RFC 7807-style Problem Details with a trace identifier. Client errors are logged best-effort through `/client-errors`; the client must not treat that endpoint as a required dependency for saving data. + +External failures are surfaced as bounded, actionable errors. AI, mail, translation, import, and PDF paths have timeouts or admission limits where implemented. Do not retry ambiguous writes automatically; preserve reviewed drafts and uncertain send state. Health (`/health`) is liveness-only, while `/ready` checks database readiness. diff --git a/docs/career/cv-builder b/docs/career/cv-builder deleted file mode 100644 index e69de29..0000000 diff --git a/docs/configuration/README.md b/docs/configuration/README.md new file mode 100644 index 0000000..f7bfee7 --- /dev/null +++ b/docs/configuration/README.md @@ -0,0 +1,5 @@ +# Configuration + +The repository uses layered configuration: `appsettings*.json`, Docker Compose environment mappings, `.env` for local/operator values, and frontend build arguments. [Environment variables](environment-variables.md) is the complete variable index; [environments](environments.md) explains local, test, and production differences. + +Never commit real secret values. `NEXT_PUBLIC_*` values are embedded into browser bundles and are public by design. diff --git a/docs/configuration/environment-variables.md b/docs/configuration/environment-variables.md new file mode 100644 index 0000000..35c1336 --- /dev/null +++ b/docs/configuration/environment-variables.md @@ -0,0 +1,114 @@ +# Environment variables + +This table covers the variables in `.env.example`, their Docker Compose mappings, and runtime variables read directly by the API, UI, AI service, or test harness. Blank means no repository default; “conditional” means required only when that feature/provider is enabled. + +## Database and application + +| Variable | Required | Service | Secret | Default | Description | +|---|---|---|---|---|---| +| `DATABASE_PROVIDER` | Production | Compose/API | No | `sqlite` | `sqlite`, `mariadb`, or `mysql`; also selects the backup path. | +| `JOBTRACKER_CONNECTION_STRING` | Conditional | API | Yes | — | Server database connection string; ignored for SQLite. | +| `APP_PUBLIC_BASE_URL` | Production | API/UI | No | — | Canonical external origin for links, callbacks, cookies, billing, and host validation. | +| `APP_VERSION` | No | API | No | — | Release/version metadata. | +| `APP_COMMIT_SHA` | No | API | No | — | Release commit metadata. | +| `APP_BUILD_STAMP` | No | API | No | — | Release build timestamp metadata. | +| `NEXT_PUBLIC_API_BASE_URL` | No | UI build | No | `/api` | Browser-visible API origin; leave blank for nginx same-origin proxy. | + +## Authentication, identity, and billing + +| Variable | Required | Service | Secret | Default | Description | +|---|---|---|---|---|---| +| `AUTH_JWT_KEY` | Production | API | Yes | — | Local JWT signing key; required when auth is required. | +| `AUTH_ADMIN_EMAIL` | Conditional | API | No | — | Seed/admin account email. | +| `AUTH_ADMIN_PASSWORD` | Conditional | API | Yes | — | Seed/admin account password. | +| `AUTH_ALLOW_REGISTRATION` | No | API | No | `false` | Enables local self-registration. | +| `AUTH_REQUIRE_EMAIL_VERIFICATION` | No | API | No | `true` | Requires verified local-account email before sign-in. | +| `ACCOUNT_DELETION_ENABLED` | No | API | No | `false` | Self-service deletion gate; keep disabled until retention/restore policy is approved. | +| `TURNSTILE_SITE_KEY` | Conditional | API/UI | No | — | Public Turnstile site key. | +| `TURNSTILE_SECRET_KEY` | Conditional | API | Yes | — | Server-side Turnstile verification secret. | +| `AUTH_GOOGLE_CLIENT_ID` | Conditional | API/UI | No | — | Google sign-in client ID. | +| `AUTH_MICROSOFT_CLIENT_ID` | Conditional | API/UI | No | — | Microsoft sign-in client ID. | +| `AUTH_MICROSOFT_TENANT` | No | API/UI | No | `common` | Microsoft sign-in tenant policy; distinct from Graph tenant. | +| `STRIPE_SECRET_KEY` | Conditional | API | Yes | — | Hosted Checkout/portal server key. | +| `STRIPE_PRICE_PREMIUM` | Conditional | API | No | — | Recurring Premium Stripe Price ID (`price_...`). | +| `STRIPE_WEBHOOK_SECRET` | Conditional | API | Yes | — | Signed billing webhook secret. | + +## Mail-provider connections + +| Variable | Required | Service | Secret | Default | Description | +|---|---|---|---|---|---| +| `GOOGLE_GMAIL_CLIENT_SECRET` | Conditional | API | Yes | — | Gmail OAuth client secret. | +| `MICROSOFT_CLIENT_ID` | Conditional | API | No | — | Microsoft Graph mail application ID; separate from sign-in ID. | +| `MICROSOFT_CLIENT_SECRET` | Conditional | API | Yes | — | Microsoft Graph OAuth secret. | +| `MICROSOFT_TENANT_ID` | Conditional | API | No | — | Graph mailbox tenant; separate from `AUTH_MICROSOFT_TENANT`. | + +## AI and document processing + +| Variable | Required | Service | Secret | Default | Description | +|---|---|---|---|---|---| +| `AI_SERVICE_BASE_URL` | Docker | API | No | `http://ai-service:8001` | Internal FastAPI service URL. | +| `AI_SERVICE_TOKEN` | Docker/prod | API/AI | Yes | — | Shared backend-to-sidecar token; required by Compose except `/health`. | +| `OLLAMA_BASE_URL` | No | AI | No | `http://ollama:11434` | Ollama endpoint. | +| `OLLAMA_MODEL` | No | AI | No | `qwen2.5:7b` | Local Ollama model. | +| `AI_PROVIDER` | No | API/AI | No | `ollama` | Structured CV provider: `ollama`, `gemini`, or `groq`. | +| `EXTERNAL_AI_ENABLED` | No | API/AI | No | `false` | Administrator gate for external processing. | +| `AI_ROUTING_MODE` | No | API/AI | No | `local_first` | `local_only`, `local_first`, or `external_only`. | +| `EXTERNAL_AI_ALLOWED_TASKS` | No | AI | No | CV task list | Allowlisted external task types. | +| `EXTERNAL_AI_MAX_PROMPT_CHARS` | No | AI | No | `24000` | External prompt-size ceiling. | +| `LOCAL_AI_CIRCUIT_FAILURE_THRESHOLD` | No | AI | No | `3` | Local-provider failures before circuit opens. | +| `LOCAL_AI_CIRCUIT_OPEN_SECONDS` | No | AI | No | `30` | Circuit-open duration. | +| `PARSER_TIMEOUT_SECONDS` | No | AI | No | `25` | Parser wall-clock budget. | +| `PARSER_CPU_SECONDS` | No | AI | No | `20` | Linux parser CPU budget. | +| `PARSER_MEMORY_MIB` | No | AI | No | `512` | Linux parser address-space budget. | +| `PARSER_STALE_WORK_SECONDS` | No | AI | No | `3600` | Age at which stale parser work is reconciled. | +| `GEMINI_API_KEY` | Conditional | AI | Yes | — | Gemini provider key. | +| `GEMINI_MODEL` | No | AI | No | `gemini-2.0-flash` | Gemini model. | +| `GROQ_API_KEY` | Conditional | AI | Yes | — | Groq provider key. | +| `GROQ_MODEL` | No | AI | No | `llama-3.3-70b-versatile` | Groq model. | +| `WORKER_AI_OPERATIONS_ENABLED` | No | API | No | `false` | Durable AI worker gate. | +| `AI_QUEUE_WORKER_CONCURRENCY` | No | API | No | `1` | AI worker concurrency. | +| `AI_QUEUE_GLOBAL_CAPACITY` | No | API | No | `100` | Global queued-operation capacity. | +| `AI_QUEUE_PER_USER_CAPACITY` | No | API | No | `10` | Per-user queued-operation capacity. | +| `AI_QUEUE_DEADLINE_MINUTES` | No | API | No | `15` | Queue deadline. | +| `AI_QUEUE_OPERATION_TIMEOUT_SECONDS` | No | API | No | `300` | Operation timeout. | + +## Ingress, deployment, maintenance, and backups + +| Variable | Required | Service | Secret | Default | Description | +|---|---|---|---|---|---| +| `WEB_PROXY_SUBNET` | Production | Compose | No | `172.31.250.0/29` | Dedicated nginx/backend network; verify no overlap. | +| `INGRESS_PROXY_CIDRS` | Production | Frontend | No | — | Trusted reverse-proxy socket peers. | +| `MAINTENANCE_GITEA_BASE_URL` | No | API | No | `https://git.cesnimda.uk` | Gitea API base for allowlisted maintenance jobs. | +| `MAINTENANCE_GITEA_OWNER` | No | API | No | `cesnimda` | Gitea owner. | +| `MAINTENANCE_GITEA_REPOSITORY` | No | API | No | `jobtrackingapp` | Gitea repository. | +| `MAINTENANCE_GITEA_TOKEN` | Conditional | API | Yes | — | Narrowly scoped workflow-dispatch token. | +| `BACKUP_TIMEZONE` | No | Ops | No | `Europe/Oslo` | Backup schedule timezone. | +| `BACKUP_DAILY_RETENTION_DAYS` | No | Ops | No | `30` | Daily recovery-set retention. | +| `BACKUP_MONTHLY_RETENTION_COUNT` | No | Ops | No | `12` | Monthly recovery-set count. | +| `BACKUP_RCLONE_REMOTE` | No | Ops | Yes/depends | — | Optional encrypted remote destination. | + +## Email and workers + +| Variable | Required | Service | Secret | Default | Description | +|---|---|---|---|---|---| +| `EMAIL_ENABLED` | No | API | No | `false` | Enables SMTP delivery. | +| `EMAIL_SMTP_HOST` | Conditional | API | No | `smtp.gmail.com` | SMTP host. | +| `EMAIL_SMTP_PORT` | Conditional | API | No | `587` | SMTP port. | +| `EMAIL_SMTP_USER` | Conditional | API | No | — | SMTP username. | +| `EMAIL_SMTP_PASSWORD` | Conditional | API | Yes | — | SMTP password/app password. | +| `EMAIL_FROM` | No | API | No | SMTP user | Sender address. | +| `EMAIL_FROM_NAME` | No | API | No | `Jobbjakt` | Sender display name. | +| `EMAIL_FOLLOWUPREMINDERS_ENABLED` | No | API | No | `false` | Reminder-email gate. | +| `EMAIL_FOLLOWUPREMINDERS_UPCOMINGDAYS` | No | API | No | `2` | Reminder look-ahead days. | +| `EMAIL_SMTP_ENABLE_SSL` | No | API | No | `true` | SMTP TLS setting. | +| `EMAIL_SMTP_TIMEOUT_MS` | No | API | No | `15000` | SMTP timeout. | +| `WORKER_RULES_ENABLED` | No | API | No | `false` | Rules/ghosting worker gate. | +| `WORKER_FOLLOWUP_REMINDERS_ENABLED` | No | API | No | `false` | Reminder worker gate. | +| `WORKER_SAVED_SEARCH_ALERTS_ENABLED` | No | API | No | `true` | Saved-search alert worker gate. | +| `SAVED_SEARCH_INTERVAL_HOURS` | No | API | No | `6` | Saved-search interval. | +| `WORKER_DAILY_EXPORT_ENABLED` | No | API | No | `false` | Daily export worker gate. | +| `WORKER_JOB_ENRICHMENT_ENABLED` | No | API | No | `false` | Job enrichment worker gate. | + +## Runtime-only variables + +The source also accepts `CV_FORCE_AI_NORMALIZER`, `CV_PDF_BROWSER_PATH`, `PARSER_TEMP_ROOT`, `GEMINI_BASE_URL`, `GROQ_BASE_URL`, `AI_SERVICE_SKIP_MODEL_LOAD`, and `AI_SERVICE_EAGER_MODEL_LOAD`. These are local/diagnostic/sidecar overrides and are intentionally not in the operator `.env.example`; see the relevant AI and testing guides. `GOOGLE_GMAIL_REDIRECT_URI` and `MICROSOFT_REDIRECT_URI` were present in the pre-cleanup local `.env` but have no current Compose/source consumer; they are retained locally for review and intentionally omitted from the template. `REACT_APP_API_BASE_URL` and `REACT_APP_GOOGLE_CLIENT_ID` are obsolete CRA names and must not be added back. diff --git a/docs/configuration/environments.md b/docs/configuration/environments.md new file mode 100644 index 0000000..870f051 --- /dev/null +++ b/docs/configuration/environments.md @@ -0,0 +1,13 @@ +# Environments + +## Local development + +SQLite and localhost defaults are supported. Run the API on 5202 and the UI on 3000, or use the development Compose override. Use seeded development credentials only from local settings and never reuse them in production. + +## Tests + +Jest, Playwright, xUnit, and the Python tests create isolated fixtures or temporary databases. They may set test-only variables such as `AI_SERVICE_SKIP_MODEL_LOAD`; they must not use a real database, mailbox, provider key, or production URL. + +## Production + +Production requires an explicit database provider, authentication key/configuration, AI service token, public base URL, proxy CIDRs, and external secret storage. Leave `NEXT_PUBLIC_API_BASE_URL` empty so nginx provides same-origin `/api` unless an intentional external API origin is required. diff --git a/docs/database/README.md b/docs/database/README.md new file mode 100644 index 0000000..6e6f6e0 --- /dev/null +++ b/docs/database/README.md @@ -0,0 +1,10 @@ +# Database + +EF Core owns the current schema and migrations. SQLite is the local default; MariaDB/MySQL is supported for server deployments. + +- [Schema overview](schema.md) +- [Migrations and compatibility](migrations.md) +- [Backups](backups.md) +- [Database ownership in the architecture](../architecture/overview.md) + +Never reset a development or production database as part of documentation work. Use disposable test databases for migration checks. diff --git a/docs/database/backups.md b/docs/database/backups.md new file mode 100644 index 0000000..b2a46df --- /dev/null +++ b/docs/database/backups.md @@ -0,0 +1,5 @@ +# Database backups + +SQLite backups use a consistent snapshot (`VACUUM INTO`) and are retained under the data root. The runtime also preserves data files, CV artifacts, exports, Data Protection keys, and account-deletion tombstones as separate recovery concerns. MariaDB/MySQL uses external database backup tooling rather than the SQLite job. + +Backup age, manifest completeness, restore correctness, key availability, and tombstone replay must be checked together. The current production restore rehearsal is still an operator gate; see [infrastructure backups](../infrastructure/backups.md) and [known issues](../roadmap/known-issues.md). diff --git a/docs/database/migrations.md b/docs/database/migrations.md new file mode 100644 index 0000000..9184437 --- /dev/null +++ b/docs/database/migrations.md @@ -0,0 +1,39 @@ +# Migrations and compatibility + +The API applies EF Core migrations on startup and performs guarded legacy-schema reconciliation for SQLite and MariaDB/MySQL compatibility. Fresh and repeated disposable SQLite migration chains are the normal local verification; generated provider SQL is not proof of a live production migration. + +The `Job`/`JobApplication` opportunity cutover is an expand/verify/contract change. Keep dual writes and compatibility reads until a production report and restored-backup rehearsal pass, then observe one release before removing legacy columns in a later migration. Never edit the history of an applied migration. + +### Production cutover validation + +After the additive backfill, run these read-only MariaDB/MySQL checks against production and a restored +backup. Every result must be `0`; do not turn them into an update script. + +```sql +SELECT COUNT(*) AS missing_job +FROM JobApplications +WHERE JobId IS NULL; + +SELECT COUNT(*) AS missing_target +FROM JobApplications a +LEFT JOIN Jobs j ON j.Id = a.JobId +WHERE a.JobId IS NOT NULL AND j.Id IS NULL; + +SELECT COUNT(*) AS owner_mismatch +FROM JobApplications a +JOIN Jobs j ON j.Id = a.JobId +WHERE NOT (a.OwnerUserId <=> j.OwnerUserId); + +SELECT COUNT(*) AS field_mismatch +FROM JobApplications a +JOIN Jobs j ON j.Id = a.JobId +WHERE NOT (a.CompanyId <=> j.CompanyId) + OR NOT (a.JobTitle <=> j.JobTitle) + OR NOT (a.Location <=> j.Location) + OR NOT (a.JobUrl <=> j.JobUrl) + OR NOT (a.Description <=> j.Description) + OR NOT (a.Salary <=> j.Salary); +``` + +Observe one release after the checks pass, then change reads, re-run the checks on a fresh restore, and +only then make `JobId` required and remove duplicated opportunity columns in a later migration. diff --git a/docs/database/schema.md b/docs/database/schema.md new file mode 100644 index 0000000..699639f --- /dev/null +++ b/docs/database/schema.md @@ -0,0 +1,7 @@ +# Schema overview + +Identity users own companies, jobs, applications, correspondence, attachments, CV artifacts/runs, sessions, trusted devices, and career data. `Job` represents the opportunity; `JobApplication` represents the user's pursuit. Applications also retain compatibility opportunity fields until the production cutover gate. + +Important relationships include company → jobs/applications, application → correspondence/attachments/events/checklist/AI and tailored-CV records, and CV upload → extraction runs. The career profile has relational child records plus a JSON long-tail field. Tags remain a JSON string for compatibility. Owner-prefixed indexes support tenant-scoped lists, boards, reminders, and CV queries. + +The model snapshot and migrations live in `JobTrackerApi/Migrations/`. Startup initialization performs compatibility reconciliation for historical schemas; this is a known ownership debt, not permission to rewrite applied migrations. diff --git a/docs/decisions/ADR-002-job-application-model.md b/docs/decisions/ADR-002-job-application-model.md index 49f41f0..1e438c0 100644 --- a/docs/decisions/ADR-002-job-application-model.md +++ b/docs/decisions/ADR-002-job-application-model.md @@ -3,20 +3,20 @@ - **Status:** Accepted (partially implemented) - **Date:** 2026-07-17 - **Phase:** 0 (foundation corrections) -- **Supersedes:** the 0-byte placeholder previously at `docs/_archive/decisions/ADR-002-job-application-model.md` +- **Supersedes:** the 0-byte placeholder previously preserved in the pre-cleanup archive. --- ## Context -`docs/MASTER_IMPLEMENTATION_GUIDE.md` names this as the product's #2 priority workflow: +The current architecture and product guides identify this as a core workflow: ``` Find Job → Import Job → Review Details → Prepare CV → Prepare Cover Letter → Add Supporting Files → Submit Application → Track Progress ``` -`docs/01-glossary.md` is explicit that "A Job may exist before an application is submitted" and models the hierarchy as `Company → Job Opportunity → Application`. +The current [product terminology](../product/terminology.md) is explicit that "A Job may exist before an application is submitted" and models the hierarchy as `Company → Job Opportunity → Application`. **The code could not represent any of that.** The 2026-07-17 discovery audit found: @@ -52,7 +52,7 @@ This keeps Phase 0 a pure schema-and-vocabulary change with **zero behavioural c ### Phase 1 status (2026-07-31) -Create and update paths now synchronize `Job`, and startup idempotently backfills a linked `Job` for every legacy application missing one. Reads and legacy columns remain unchanged until the production validation and observation release in `docs/operations/job-opportunity-cutover.md` complete. +Create and update paths now synchronize `Job`, and startup idempotently backfills a linked `Job` for every legacy application missing one. Reads and legacy columns remain unchanged until the [production cutover validation](../database/migrations.md#production-cutover-validation) and observation release complete. ### The invariant diff --git a/docs/decisions/README.md b/docs/decisions/README.md new file mode 100644 index 0000000..45d1d01 --- /dev/null +++ b/docs/decisions/README.md @@ -0,0 +1,3 @@ +# Architecture decisions + +Architecture decision records are kept separately from current behavior. The current records are [ADR-002 job/application model](ADR-002-job-application-model.md) and [ADR-004 AI provider system](ADR-004-ai-provider-system.md). Current implementation summaries live in [architecture](../architecture/overview.md). diff --git a/docs/development/README.md b/docs/development/README.md new file mode 100644 index 0000000..2a03042 --- /dev/null +++ b/docs/development/README.md @@ -0,0 +1,9 @@ +# Development + +- [Getting started](getting-started.md) +- [Conventions](conventions.md) +- [Troubleshooting](troubleshooting.md) +- [Configuration](../configuration/README.md) +- [Testing](../testing/README.md) + +Keep changes narrow, update current docs with behavior changes, and preserve unrelated work in the shared worktree. diff --git a/docs/development/conventions.md b/docs/development/conventions.md new file mode 100644 index 0000000..f6cbacb --- /dev/null +++ b/docs/development/conventions.md @@ -0,0 +1,5 @@ +# Development conventions + +Use nullable reference types and existing EF Core/service patterns. Put authorization and ownership checks at the API boundary, keep external work bounded and reviewable, and add focused regression tests for behavior changes. Prefer the existing libraries and component conventions over new abstractions. + +Do not edit applied migration history, reset shared databases, commit secrets, or treat an old report as current behavior. When an implementation decision matters, record it in `docs/decisions/` and link it from the relevant current guide. diff --git a/docs/development/getting-started.md b/docs/development/getting-started.md new file mode 100644 index 0000000..206b8f9 --- /dev/null +++ b/docs/development/getting-started.md @@ -0,0 +1,17 @@ +# Getting started + +Prerequisites: .NET SDK 9, Node.js 24/npm, Docker Compose for the container path, and Python 3.12 only when running the AI service outside Docker. + +## Local processes + +```powershell +cd JobTrackerApi +dotnet restore --locked-mode +dotnet run + +cd ..\job-tracker-ui +npm ci +npm run dev +``` + +Open `http://localhost:3000`; the API defaults to `http://localhost:5202`. For the full stack, create `.env` from `.env.example` and run `docker compose -f docker-compose.yml -f docker-compose.dev.yml up --build` from the repository root. See [environment variables](../configuration/environment-variables.md). diff --git a/docs/development/troubleshooting.md b/docs/development/troubleshooting.md new file mode 100644 index 0000000..4b85011 --- /dev/null +++ b/docs/development/troubleshooting.md @@ -0,0 +1,12 @@ +# Troubleshooting + +| Symptom | First checks | +|---|---| +| UI cannot reach API | Confirm API port 5202, `NEXT_PUBLIC_API_BASE_URL`, and nginx `/api` proxy configuration. | +| Stack is running but not ready | Check backend health/readiness and database provider/connection settings; inspect sanitized logs. | +| AI unavailable | Check `AI_SERVICE_BASE_URL`, matching `AI_SERVICE_TOKEN`, sidecar health, model/provider settings, and policy gates. | +| Mail/reset unavailable | Check SMTP enablement, host/port/TLS, credentials, sender, and provider limits. | +| Migration failure | Preserve the database and logs; do not delete/recreate it. Use a disposable reproduction and the migration guide. | +| Browser test failure | Confirm the harness owns its ports and isolated database; do not point it at dev/production data. | + +For deployment or restore incidents use [infrastructure](../infrastructure/README.md) and the root [BLOCKERS.md](../../BLOCKERS.md). diff --git a/docs/documentation-rules.md b/docs/documentation-rules.md index cebc058..2f56fe8 100644 --- a/docs/documentation-rules.md +++ b/docs/documentation-rules.md @@ -1,9 +1,12 @@ -# documentation-rules +# Documentation rules -Active documentation must describe verified current behavior or a clearly labelled future decision. -Prefer one authoritative document plus short topic guides that link to it; do not copy long architecture -sections across files. Update behavior docs in the same change as code. +Current documentation describes verified repository behavior or clearly labels future work. Use one authoritative guide per subject and link to it rather than copying long sections. -Use `docs/architecture/current.md` for the verified system, `docs/implementation-roadmap.md` for delivery -status, `docs/MASTER_IMPLEMENTATION_GUIDE.md` for product constraints, and `BLOCKERS.md` for external -decisions or credentials. `docs/_archive` is historical evidence and must not be treated as current work. +- `docs/README.md` is the entry point. +- `docs/architecture/overview.md` is the current system summary. +- `docs/configuration/environment-variables.md` is the environment-variable index. +- `docs/roadmap/` contains open work, debt, and known issues. +- `BLOCKERS.md` contains operator/external dependencies referenced by repository instructions. +- `docs/_archive/` is historical evidence and is not authoritative. + +Update behavior documentation in the same change as code. Do not put secrets, private user data, or temporary AI/planning transcripts in active documentation. diff --git a/docs/frontend/README.md b/docs/frontend/README.md new file mode 100644 index 0000000..71399ef --- /dev/null +++ b/docs/frontend/README.md @@ -0,0 +1,13 @@ +# Frontend and UI + +The UI is a client-rendered React application packaged by Next.js and served as static files through nginx. The primary application shell is in `job-tracker-ui/src/App.tsx`; feature views and components are under `src/views` and `src/components`. + +## Topics + +- [UI system](ui-system.md): theme, reusable MUI patterns, forms, dialogs, notifications, and responsive behavior. +- [Navigation](navigation.md): public/authenticated routes and shell ownership. +- [Localisation and accessibility](localisation-accessibility.md): English/Norwegian support, keyboard behavior, and verification limits. + +The current UI redesign work-in-progress remains under [`../redesign/`](../redesign/). Those phase records are implementation evidence, not a replacement for this current-system guide. + +The checked-in frontend entry point and capture extension have their own setup notes: [frontend README](../../job-tracker-ui/README.md) and [browser extension README](../../browser-extension/README.md). diff --git a/docs/frontend/localisation-accessibility.md b/docs/frontend/localisation-accessibility.md new file mode 100644 index 0000000..9a3d29c --- /dev/null +++ b/docs/frontend/localisation-accessibility.md @@ -0,0 +1,5 @@ +# Localisation and accessibility + +The UI ships English and Norwegian Bokmål translations through `job-tracker-ui/src/i18n/translations.ts`; language preference is persisted with the user settings model. New user-visible strings should be added to both locales and should not encode status meaning only in English text. + +Accessible names, descriptions, loaded-state announcements, keyboard focus, modal return focus, contrast, and narrow-screen behavior are covered by unit/browser checks where practical. The full keyboard/screen-reader/zoom review for the CV builder and production export remains a manual gate; see [known issues](../roadmap/known-issues.md). diff --git a/docs/frontend/navigation.md b/docs/frontend/navigation.md new file mode 100644 index 0000000..c2c8aea --- /dev/null +++ b/docs/frontend/navigation.md @@ -0,0 +1,5 @@ +# Navigation + +Public routes include `/`, `/login`, `/forgot-password`, `/reset-password`, and `/verify-email`. Authenticated routes include `/dashboard`, `/jobs`, `/reminders`, `/kanban`, `/companies`, `/correspondence`, `/correspondence/review`, `/profile`, `/career`, `/trash`, `/settings`, `/settings/connected-accounts`, and admin audit/users/system views. + +The `/profile` surface owns account identity and security preferences. `/career` owns the master career profile and is the source for CV and other career outputs. The application shell and route definitions are in `job-tracker-ui/src/App.tsx`. diff --git a/docs/frontend/ui-system.md b/docs/frontend/ui-system.md new file mode 100644 index 0000000..45245ac --- /dev/null +++ b/docs/frontend/ui-system.md @@ -0,0 +1,5 @@ +# UI system + +MUI 7 components and the shared theme in `job-tracker-ui/src/theme.ts` define the visual foundation. Light and dark modes are user preferences; status colors communicate pipeline meaning and must not be used as the only signal. Forms, dialogs, loading states, errors, attachments, CV editing, job workspaces, and admin settings are implemented as feature components rather than a separate design-system package. + +The UI supports responsive layouts for desktop and narrow screens, but route-specific overflow and keyboard/focus behavior still require the manual checks listed in [known issues](../roadmap/known-issues.md). Keep edits additive and preserve unsaved drafts, dialog return focus, and existing translated labels. diff --git a/docs/infrastructure/README.md b/docs/infrastructure/README.md new file mode 100644 index 0000000..fcbc223 --- /dev/null +++ b/docs/infrastructure/README.md @@ -0,0 +1,11 @@ +# Infrastructure and operations + +The supported runtime is Docker Compose with an nginx frontend, internal ASP.NET API, private FastAPI AI service, and optional bundled Ollama. Production deployment is performed by Gitea Actions over SSH and uses an external reverse proxy/network. + +- [Docker and Compose](docker.md) +- [Deployment](deployment.md) +- [Monitoring and logging](monitoring.md) +- [Backups](backups.md) +- [Disaster recovery and rollback](disaster-recovery.md) +- [Stripe activation](stripe-activation.md) +- [Environment configuration](../configuration/README.md) diff --git a/docs/infrastructure/backups.md b/docs/infrastructure/backups.md new file mode 100644 index 0000000..6d1ee30 --- /dev/null +++ b/docs/infrastructure/backups.md @@ -0,0 +1,13 @@ +# Operational backups + +A complete recovery set includes the database, uploaded/CV data, exports where needed, Data Protection keys, and account-deletion tombstones. SQLite has an application backup path; MariaDB/MySQL must use the external server backup path. Retention and remote encrypted-copy settings are configured through `BACKUP_*` variables. + +The operator must periodically prove backup freshness, manifest completeness, restore correctness, key availability, and post-restore deletion semantics. A backup that exists but cannot restore the application data and keys is not a verified backup. + +The seeded MariaDB backup → restore → application-start rehearsal and the later read-only production +checkpoint are historical evidence, not proof of a current production restore. They remain recoverable +in the [pre-cleanup backup verification record](../_archive/pre-docs-cleanup-2026-09-11/docs/operations/production-backup-verification.md). + +The pre-cleanup operator handoff recorded production Data Protection key rotation as complete on +2026-08-02. Current key custody, recovery-set inclusion, and post-restore decryption still require +verification. diff --git a/docs/infrastructure/deployment.md b/docs/infrastructure/deployment.md new file mode 100644 index 0000000..bf04f00 --- /dev/null +++ b/docs/infrastructure/deployment.md @@ -0,0 +1,19 @@ +# Deployment + +Production prerequisites are Docker Engine/Compose, a reverse proxy, the shared external network, and a server-side `.env` outside the repository. The deploy script links `/opt/job-tracker/shared/.env` into the checkout, builds/tests through Gitea Actions, takes a recovery set, deploys commit-tagged images, and validates `/health`, `/ready`, and `/api/auth/config`. + +Recommended sequence: validate configuration → run CI → take/inspect recovery set → deploy → verify liveness/readiness/version → run the authenticated smoke checklist. Do not bypass failed browser or production gates. Keep secrets out of Git and use [environment variables](../configuration/environment-variables.md). + +The repository runbooks are [the deployment entry point](../../deploy/README.md), [the first-production deployment procedure](../../deploy/first-production-deployment.md), and [MariaDB bootstrap/migration notes](../../deploy/MARIADB.md). The script is intended for the protected deployment workflow, not for an ad-hoc production shell run. + +## Production verification + +These checks require an authorised production session and are not replaced by local builds or public health probes: + +1. Before replacement, record the current commit/version and application/company row counts, and confirm the recovery set is fresh and restorable. See [operational backups](backups.md). +2. After replacement, confirm `/health` reports the deployed version, `/ready` reports `ready`, and `/api/auth/config` returns the expected public configuration. +3. Sign in with an existing account; confirm application/company counts, Career Profile data (including languages), the application workspace, CV variants/themes, public-CV refresh/PDF, and an existing attachment. +4. Exercise one AI operation, email verification/resend, and each configured OAuth provider only when the required external accounts are available. Record failures without placing provider payloads or credentials in logs or notes. +5. Rehearse failure injection on a disposable Linux Docker host. Roll back the code/image first; restore the database only when data or schema is the confirmed cause. See [disaster recovery and rollback](disaster-recovery.md). + +The historical release-review and production-inventory details remain in the pre-cleanup archive; this page is the current operator checklist. diff --git a/docs/infrastructure/disaster-recovery.md b/docs/infrastructure/disaster-recovery.md new file mode 100644 index 0000000..2c52c4c --- /dev/null +++ b/docs/infrastructure/disaster-recovery.md @@ -0,0 +1,5 @@ +# Disaster recovery and rollback + +For a bad application release, retain the database and roll back to the prior commit/image first. Restore the database only when data or schema is the problem; restoring loses newer rows. For an incident, preserve logs/manifests, stop repeated deploys, identify the exact failed component, and use an isolated restore rehearsal before a production restore. + +The repository-side rollback path is implemented, but a real failure-injection rehearsal on the production topology remains blocked by operator access and authorization. See [known issues](../roadmap/known-issues.md). diff --git a/docs/infrastructure/docker.md b/docs/infrastructure/docker.md new file mode 100644 index 0000000..fb7cb3a --- /dev/null +++ b/docs/infrastructure/docker.md @@ -0,0 +1,5 @@ +# Docker and Compose + +`docker-compose.yml` defines backend, frontend, ai-service, and an opt-in `bundled-ollama` profile. The backend stores persistent data in `/data`; the AI service is exposed only inside the private `ai_internal` network. The frontend publishes port 3000 and proxies `/api`; development additionally publishes the API on port 5202 and Ollama on 11434. + +Compose health checks are liveness-oriented: backend `/health` avoids database access, frontend checks nginx, and service dependencies wait for backend health. Use `docker compose config` only with sanitized output; never paste the resolved environment into tickets or documentation. diff --git a/docs/infrastructure/monitoring.md b/docs/infrastructure/monitoring.md new file mode 100644 index 0000000..79986ad --- /dev/null +++ b/docs/infrastructure/monitoring.md @@ -0,0 +1,5 @@ +# Monitoring and logging + +Use container health, `/health`, `/ready`, structured API logs, AI health metadata, backup age/manifests, worker failures, and provider delivery state as the baseline signals. Compose bounds local container logs to three 10 MB files; centralized aggregation is not yet configured. + +Do not log tokens, passwords, full CV text, private URLs, or provider payloads. Add an external sink only when multi-host operation or incident-response needs justify it; this is tracked as technical debt. diff --git a/docs/infrastructure/stripe-activation.md b/docs/infrastructure/stripe-activation.md new file mode 100644 index 0000000..a151ee5 --- /dev/null +++ b/docs/infrastructure/stripe-activation.md @@ -0,0 +1,35 @@ +# Stripe activation + +Billing code is present, but production activation is an operator gate. Configure and test one +recurring monthly Pro price in Stripe test mode before creating live-mode values. Test and live mode +use different keys, price IDs, webhook endpoints, signing secrets, and customer data. + +## Required configuration + +1. Create one recurring monthly Pro price and enable the customer portal. +2. Register `/api/billing/webhook` for + `customer.subscription.created`, `customer.subscription.updated`, and + `customer.subscription.deleted`. +3. Supply these deployment-only values through the server environment: + + ```text + STRIPE_SECRET_KEY= + STRIPE_PRICE_PREMIUM= + STRIPE_WEBHOOK_SECRET= + ``` + +Never put these values in Git, chat, screenshots, issue text, or CI logs. The webhook must receive the +raw request body for signature verification. + +## Acceptance check + +Use a disposable authenticated account: Upgrade opens hosted Checkout for the configured price; +completion returns to the billing-success route; the signed webhook stores the subscription and grants +the expected entitlement; Manage billing opens the customer portal; cancellation removes the +entitlement when the subscription is no longer active or trialing; and replaying an event remains +idempotent. + +For live mode, repeat the product, portal, endpoint, and secret setup as one coordinated change. If +activation must be stopped, removing the three values disables new Checkout/portal sessions without +deleting billing history; repair webhook delivery and replay the authoritative event instead of +manually changing roles. diff --git a/docs/product/README.md b/docs/product/README.md new file mode 100644 index 0000000..f985fed --- /dev/null +++ b/docs/product/README.md @@ -0,0 +1,11 @@ +# Product + +Job Tracker is a self-hosted workspace for managing the full job-search journey: opportunities, applications, companies, correspondence, follow-ups, attachments, career profile, CVs, and optional AI assistance. + +- [Current features](features.md) +- [Product constraints](constraints.md) +- [Terminology](terminology.md) +- [Architecture](../architecture/overview.md) +- [Remaining work](../roadmap/README.md) + +Product strategy, competitor research, and superseded plans are historical material under the archive unless explicitly linked from a current roadmap item. diff --git a/docs/product/constraints.md b/docs/product/constraints.md new file mode 100644 index 0000000..f218573 --- /dev/null +++ b/docs/product/constraints.md @@ -0,0 +1,9 @@ +# Product constraints + +These are current decisions that shape implementation: + +- Job tracking is the core workflow; career tools support applications rather than becoming a separate social/portfolio product. +- Job discovery uses reviewed manual capture and official provider integrations. Scraping and auto-apply bots are out of scope. +- Free/Premium differences are capability-gated, not arbitrary job/CV counts. Local AI and user data privacy are core product values. +- The Career Profile is the source of truth for career facts. CVs, tailored CVs, cover letters, and interview preparation are derived/editable outputs and must not silently overwrite the profile. +- A mockup or redesign record is not a license to invent routes or behavior; current source and tests decide what is implemented. diff --git a/docs/product/features.md b/docs/product/features.md new file mode 100644 index 0000000..6e0fd93 --- /dev/null +++ b/docs/product/features.md @@ -0,0 +1,8 @@ +# Current features + +- Job/application pipeline with statuses, search, filters, reminders, events, analytics, soft-delete/restore, and official/imported opportunities. +- Company and recruiter context plus correspondence logs and reviewed Gmail/Graph/IMAP integrations. +- Application attachments, CV upload/extraction, career profile, tailored CVs, cover letters, interview preparation, public CV links, and PDF export. +- Local and policy-gated external AI assistance, configurable follow-up/rules workers, exports, backups, authentication, 2FA, session management, admin readiness, and billing code awaiting operator activation. + +Feature details should be verified against the API/UI source. This list intentionally omits ideas that are only planned, such as separate portfolio hosting or auto-apply behavior. diff --git a/docs/product/terminology.md b/docs/product/terminology.md new file mode 100644 index 0000000..f7062c3 --- /dev/null +++ b/docs/product/terminology.md @@ -0,0 +1,11 @@ +# Terminology + +| Term | Meaning | +|---|---| +| Job | The opportunity, including title, company, description, URL, salary, location, and tags. | +| Application | A user's pursuit of a job, including status, dates, follow-up, correspondence, and attachments. | +| Career Profile | The user's editable source of truth for career facts. | +| CV | A presentation/output built from the career profile for a job or audience. | +| Tailored CV | An editable job-specific CV draft. | +| Review queue | Human decision step for uncertain imported mail/job matches. | +| Local AI | The repository's local summarizer/Ollama path; it does not mean production infrastructure is verified. | diff --git a/docs/redesign/README.md b/docs/redesign/README.md index d7dd860..48ff051 100644 --- a/docs/redesign/README.md +++ b/docs/redesign/README.md @@ -23,21 +23,21 @@ AI should support this flow naturally and contextually. It must never invent fac ## Documents -- `PRODUCT_VISION.md` — product positioning, principles, users and success criteria. -- `INFORMATION_ARCHITECTURE.md` — proposed navigation and page hierarchy. -- `UX_FLOWS.md` — core user journeys and interaction patterns. -- `DASHBOARDS.md` — personal and admin dashboard requirements. -- `DESIGN_SYSTEM.md` — visual direction, layout, components and motion. -- `AI_PRINCIPLES.md` — non-negotiable AI behaviour and UX rules. -- `RESPONSIVE_ACCESSIBILITY.md` — mobile, responsive and accessibility requirements. -- `FUNCTIONAL_GUARDRAILS.md` — rules for preserving existing behaviour during the redesign. -- `IMPLEMENTATION_PLAN.md` — phased redesign and migration strategy. -- `ASTRA_HANDOFF.md` — instructions for the coding agent that will eventually execute the redesign. -- `OPEN_DECISIONS.md` — deliberately unresolved product decisions. +- [PRODUCT_VISION.md](PRODUCT_VISION.md) — product positioning, principles, users and success criteria. +- [INFORMATION_ARCHITECTURE.md](INFORMATION_ARCHITECTURE.md) — proposed navigation and page hierarchy. +- [UX_FLOWS.md](UX_FLOWS.md) — core user journeys and interaction patterns. +- [DASHBOARDS.md](DASHBOARDS.md) — personal and admin dashboard requirements. +- [DESIGN_SYSTEM.md](DESIGN_SYSTEM.md) — visual direction, layout, components and motion. +- [AI_PRINCIPLES.md](AI_PRINCIPLES.md) — non-negotiable AI behaviour and UX rules. +- [RESPONSIVE_ACCESSIBILITY.md](RESPONSIVE_ACCESSIBILITY.md) — mobile, responsive and accessibility requirements. +- [FUNCTIONAL_GUARDRAILS.md](FUNCTIONAL_GUARDRAILS.md) — rules for preserving existing behaviour during the redesign. +- [IMPLEMENTATION_PLAN.md](IMPLEMENTATION_PLAN.md) — phased redesign and migration strategy. +- [ASTRA_HANDOFF.md](ASTRA_HANDOFF.md) — instructions for the coding agent that will eventually execute the redesign. +- [OPEN_DECISIONS.md](OPEN_DECISIONS.md) — deliberately unresolved product decisions. ## Status -2026-09-11: [P4-01 semantic color/focus checkpoint](phase-4/p4-01/README.md), based on `f696f23`, implements the bounded shared MUI foundation and loaded-state regression fixtures. Final 916 backend / 333 frontend pass; browser 35 actual passes + the unchanged expected failure. [Preservation and visual evidence](phase-4/p4-01/TRACEABILITY.md) records both themes and intentional gaps. P4-02, broader layout/route redesign and B07 remain unstarted; F04 remains open. Earlier status entries below are historical. +2026-09-11: [P4-01 semantic color/focus checkpoint](phase-4/p4-01/README.md), based on `f696f23`, implements the bounded shared MUI foundation and loaded-state regression fixtures. Final 916 backend / 333 frontend pass; browser 35 actual passes + the unchanged expected failure. [Preservation and visual evidence](phase-4/p4-01/TRACEABILITY.md) records both themes and intentional gaps. [Phase 4 checkpoints](phase-4/README.md) track the follow-up. P4-02, broader layout/route redesign and B07 remain unstarted; F04 remains open. Earlier status entries below are historical. 2026-09-10: [Phase 3 UX architecture checkpoint](phase-3/README.md), based on Phase 2 `d6d3dd9`, defines grouped navigation, all 31 route destinations, 57 PRES impacts, workflow/data boundaries and the bounded Phase 4 foundation plan. Documentation only; no visual implementation or B07. F04 remains open. [Phase 2](phase-2/README.md) retains the executed 916 backend / 313 frontend baseline and explicit browser/provider/production gaps. Earlier status entries below are historical. diff --git a/docs/roadmap/README.md b/docs/roadmap/README.md new file mode 100644 index 0000000..023792f --- /dev/null +++ b/docs/roadmap/README.md @@ -0,0 +1,9 @@ +# Roadmap and open work + +This directory contains only outstanding or decision-relevant work. Completed historical phase logs, audit packets, and implementation diaries are archived. + +- [TODO](todo.md) +- [Technical debt](technical-debt.md) +- [Known issues and blockers](known-issues.md) + +The root [BLOCKERS.md](../../BLOCKERS.md) remains the operator-facing source for external credentials, production access, and deployment decisions. diff --git a/docs/roadmap/known-issues.md b/docs/roadmap/known-issues.md new file mode 100644 index 0000000..591147d --- /dev/null +++ b/docs/roadmap/known-issues.md @@ -0,0 +1,15 @@ +# Known issues and blockers + +The following items are preserved from the current blocker/audit records and are not represented as resolved merely because local tests pass. + +- **Production release/verification:** authenticated production smoke, provider delivery, backup restore, rollback rehearsal, proxy topology, host firewall, and complete UI/browser verification require operator access. See root `BLOCKERS.md`. +- **Stripe activation:** code exists, but the recurring `price_...` value, portal, webhook, and production secrets are not fully configured/verified. +- **Account deletion:** self-service deletion stays disabled until retention, legal-hold, tombstone, restore, and provider-cleanup policy is approved and rehearsed. +- **Legacy schema cutover:** duplicated opportunity columns remain for compatibility until production data/backups and old-client requirements are validated. +- **Ingress/DNS:** outbound URL import destination validation is enforced at socket connection by completed B01, including mapped-address regression coverage. The separate inbound F04/B06 production proxy-peer, client-IP and firewall boundary still needs deployed evidence; local tests do not close it. +- **Email/AI operational durability:** local failure handling exists, but independent delivery alerts, provider outages, and worker containment require runtime verification. +- **Accessibility/UI:** full keyboard, screen-reader, zoom, contrast, focus, and CV-builder failure-state review remains partly manual. +- **Open audit carryovers verified against source:** stale CV/profile/job saves still expose version counters without a client revision precondition (F01); startup still falls back to SQLite when the provider or connection is missing (F19); and calendar export still treats unspecified `DateTime` values as UTC, so offset/DST preservation is not proven (F21). +- **Conditional worker risk:** AI worker processing is disabled by default, but the enabled hosted loop still has no outer catch/backoff boundary for unexpected database failures (F20). Treat worker survival as requiring verification before enabling it in production. + +For exact source IDs, dates, and evidence, use the archived audit packets rather than adding duplicate status reports here. diff --git a/docs/roadmap/technical-debt.md b/docs/roadmap/technical-debt.md new file mode 100644 index 0000000..848f4cd --- /dev/null +++ b/docs/roadmap/technical-debt.md @@ -0,0 +1,16 @@ +# Technical debt + +## P1 + +- EF migrations and startup reconciliation still share historical schema ownership. Keep compatibility bootstraps until a production restore rehearsal supports an expand/verify/contract migration. +- `JobApplication` duplicates opportunity data now owned by `Job`. Keep dual writes and compatibility reads until the production report/restore gate passes and one observation release completes. +- Background workers are single-instance; add database leasing before deploying more than one backend replica. + +## P2/P3 + +- Central log aggregation is deployment-owned; add it only when multi-host operation or incident response requires it. +- Tags remain a JSON string and attachment-purpose flags remain compatibility columns; normalize/remove only with measured need and an API/schema compatibility release. +- Checklist reads intentionally seed/synchronize system items and are non-cacheable until measured workspace volume justifies separation. +- Large controllers/components should be split only around a cohesive behavior change with regression coverage; do not perform a standalone rewrite. + +Intentional design choices are not debt: free-text statuses preserve custom user values, Next.js remains the static shell, and the current CV queue targets one backend instance. diff --git a/docs/roadmap/todo.md b/docs/roadmap/todo.md new file mode 100644 index 0000000..817c20c --- /dev/null +++ b/docs/roadmap/todo.md @@ -0,0 +1,25 @@ +# TODO + +Status reconciled: 2026-09-11. + +## Current repository work + +- **P4-02 — open:** finish the current UI semantic-control and modal-state review, preserve `RichTextField`/draft behavior, then run full regression and visual evidence. The active implementation record is [`docs/redesign/phase-4/p4-02/README.md`](../redesign/phase-4/p4-02/README.md). +- **P2-D01 — open:** repair the SQLite `DateTimeOffset` ordering baseline before package/tailoring restructuring; retain the expected-failure browser oracle until native capture and historical-binary preservation are proven. P2-O01's initial rules-500 behavior also remains unconfirmed. +- **P3-O01/P3-O02 — open:** address analytics population/period labels and navigation/query/error retention when those surfaces are next changed. Evidence is in the current [redesign decision records](../redesign/phase-3/OPEN_DECISIONS.md). +- **F04/B06 — open:** complete the authorized main release and production ingress/runtime verification; the browser gate and runner diagnostics remain unresolved. No gate should be bypassed. + +## Delivery and feature gates + +- Deploy the verified CV Builder/UI batch through protected `main` and confirm the live health/version transition. +- Run authenticated production CV editing/PDF, existing-data, attachment, email-verification, OAuth, and rollback smoke checks when an authorized session is available. +- Activate Stripe only after the recurring Pro price, portal, webhook, and external secrets are configured; see [known issues](known-issues.md). +- Rehearse backup/restore and the legacy `Job`/`JobApplication` cutover against a restored production backup before removing compatibility columns. + +## CV Builder preservation and next slice + +The current builder already includes expandable sections, preset/free-text custom sections, section AI review, Code and other capability-aware templates, accent/typography controls, shared preview/PDF rendering, and extraction confidence handling. Preserve these under PRES-C02/C07/C08/C09/C12; they are not new backlog requirements. See the [functionality inventory](../redesign/audit/FUNCTIONALITY_INVENTORY.md) and [57-group preservation matrix](../redesign/phase-2/PRESERVATION_MATRIX.md). + +P4-03's future scope is the loaded preview keyboard/viewport boundary, after review of P4-02. Broader editor changes require their own approved slice and preservation evidence; do not restart already-completed builder work from archived plans. + +Completed phase items are intentionally omitted here; their evidence remains recoverable in the pre-cleanup backup and existing archive. diff --git a/docs/security/README.md b/docs/security/README.md new file mode 100644 index 0000000..d593448 --- /dev/null +++ b/docs/security/README.md @@ -0,0 +1,11 @@ +# Security + +Security is enforced in the API and deployment topology, not by UI visibility. The current baseline includes tenant owner filters, local/Google/Microsoft identity options, cookie CSRF protection, rate limits, TOTP, session revocation, validated uploads/imports, bounded document parsing, private AI networking, and structured error handling. + +- [Authentication](authentication.md) +- [Authorization and tenancy](authorization.md) +- [Sessions and rate limits](sessions.md) +- [File and import security](file-security.md) +- [Current findings and verification limits](security-notes.md) + +Historical audits remain recoverable in [`../_archive/`](../_archive/); only verified current claims belong here. diff --git a/docs/security/authentication.md b/docs/security/authentication.md index af46b3a..897d7d2 100644 --- a/docs/security/authentication.md +++ b/docs/security/authentication.md @@ -1,84 +1,5 @@ # Authentication -## Purpose +Local email/password accounts issue signed local JWTs and can use an HttpOnly `jobtracker_auth` session cookie. Google and Microsoft identity sign-in are optional and configured separately from Gmail/Graph mail access. Registration is disabled by default; email verification, Turnstile, and production auth requirements are configuration gates. -Define user login and registration. - ---- - -# Supported Methods - -Users should be able to: - -## Email Registration - -Create account using: - -- Email. -- Password. -- Confirmation. - ---- - -## OAuth Registration/Login - -Support: - -- Google. -- Microsoft. - ---- - -# Expected Behaviour - -If OAuth user does not exist: - -Create account automatically. - -Example: - -User clicks: - -"Continue with Google" - -↓ - -Authenticate - -↓ - -No account exists - -↓ - -Create account - -↓ - -Login - ---- - -# Password Requirements - -Support: - -- Secure hashing. -- Password reset. -- Password change. - ---- - -# Authentication UX - -Users should have: - -- Clear errors. -- Loading states. -- Recovery options. - -Avoid: - -"Authentication failed" - -without explanation. \ No newline at end of file +Password reset and verification use SMTP. TOTP two-factor authentication includes encrypted secrets, recovery codes, trusted devices, and pending-token flow. JWT issuer, audience, key, lifetime, and external client IDs are documented in [environment variables](../configuration/environment-variables.md). diff --git a/docs/security/authorization.md b/docs/security/authorization.md index 6165289..678e214 100644 --- a/docs/security/authorization.md +++ b/docs/security/authorization.md @@ -1,43 +1,5 @@ -# Authorization +# Authorization and tenancy -## Purpose +Tenant-owned records carry `OwnerUserId` directly or through a parent. `JobTrackerContext` applies global owner filters with deny-on-null semantics; services and background work use explicit owner checks when they bypass request-scoped filters. Admin users access user management, audit, and system readiness surfaces through Identity roles. -Control access to resources. - ---- - -# User Isolation - -A user must only access: - -- Their jobs. -- Their CV. -- Their files. -- Their emails. -- Their settings. - ---- - -# Admin Role - -Admins may access: - -- System settings. -- Provider configuration. -- Monitoring. - ---- - -# API Rules - -Every protected endpoint must verify: - -1. User identity. -2. User permission. -3. Resource ownership. - ---- - -# Never Trust Frontend - -The backend must enforce permissions. \ No newline at end of file +A browser route or hidden control is not an authorization boundary. Add server-side authorization and an ownership regression test for every new resource or endpoint. diff --git a/docs/security/file-security.md b/docs/security/file-security.md index 743b543..6c02d78 100644 --- a/docs/security/file-security.md +++ b/docs/security/file-security.md @@ -1,56 +1,5 @@ -# File Security +# File and import security -## Purpose +Attachment uploads use a managed data root, validated metadata, size/type/signature checks, and safe download paths. Parser decoding is isolated and resource-bounded. URL import validates destinations and rejects unsafe redirects; DNS rebinding and actual socket binding remain a documented verification concern. -Protect uploaded documents. - ---- - -# File Types - -Potential uploads: - -- PDF. -- DOCX. -- Images. -- Portfolio files. - ---- - -# Validation - -Check: - -- File type. -- File size. -- File contents. - -Never trust extensions. - ---- - -# Processing - -Uploaded files may contain: - -- Malicious content. -- Embedded scripts. -- AI prompt injection. - -Treat as untrusted. - ---- - -# Storage - -Requirements: - -- Secure naming. -- Access control. -- User ownership checks. - ---- - -# PDF Processing - -Never directly trust extracted text. \ No newline at end of file +Generated CV previews are sandboxed and public CV/PDF links are random, revocable, noindex, and rate-limited. Never log or document private file contents, connection strings, or bearer tokens. diff --git a/docs/security/security-notes.md b/docs/security/security-notes.md new file mode 100644 index 0000000..4f37c5d --- /dev/null +++ b/docs/security/security-notes.md @@ -0,0 +1,16 @@ +# Current security notes + +## Verified locally + +- Tenant isolation, authorization replay, session revocation, upload/parser boundaries, AI sidecar token checks, PDF admission, and high-severity dependency gates have focused regression coverage. +- The AI service is not published to the host in the production Compose topology; only the backend joins its private network. +- Production Compose requires an AI service token and enables backend authentication by default. + +## Requires external or manual verification + +- Trusted proxy/ingress CIDRs, host firewall closure, TLS/redirect behavior, and production network topology. +- Authenticated production smoke tests, real OAuth/provider behavior, email delivery, backup/restore, rollback, and key custody. +- Full keyboard/screen-reader/zoom review of the CV builder and complete production UI console cleanliness. +- Account-deletion retention policy, tombstone protection, and restored-backup erasure behavior. + +These are not claims of resolved security findings. Dated evidence and the detailed P1/P2 audit packet are archived under `docs/_archive/pre-docs-cleanup-2026-09-11/docs/audits/` and the pre-existing archive. diff --git a/docs/security/sessions.md b/docs/security/sessions.md new file mode 100644 index 0000000..a1f78d3 --- /dev/null +++ b/docs/security/sessions.md @@ -0,0 +1,5 @@ +# Sessions and rate limits + +Session cookies are HttpOnly and SameSite=Lax, with Secure behavior controlled by deployment. Mutating cookie-authenticated requests use a CSRF double-submit cookie/header check. Users can list and revoke sessions; logout clears local client state. + +Dedicated fixed-window limits cover login, authentication email, two-factor challenges, and anonymous public-CV PDF export. AI usage is bounded by account/monthly generation and token policy. Rate limits are a security and capacity control; do not loosen them to mask provider or UI failures. diff --git a/docs/testing/README.md b/docs/testing/README.md new file mode 100644 index 0000000..bac5141 --- /dev/null +++ b/docs/testing/README.md @@ -0,0 +1,8 @@ +# Testing and quality gates + +The repository has backend xUnit tests, frontend Jest/React Testing Library tests, Python pytest tests, and Playwright browser suites. Use isolated data and never bypass authentication or external-provider gates in order to make a test green. + +- [Backend tests](backend.md) +- [Frontend tests](frontend.md) +- [Browser tests](browser.md) +- [Security tests](security.md) diff --git a/docs/testing/backend.md b/docs/testing/backend.md new file mode 100644 index 0000000..29a9b7c --- /dev/null +++ b/docs/testing/backend.md @@ -0,0 +1,9 @@ +# Backend tests + +From the repository root: + +```powershell +dotnet test JobTrackerApi.Tests/JobTrackerApi.Tests.csproj --no-restore +``` + +The suite covers controllers, ownership/tenancy, authentication, migrations/provider parity, file/parser boundaries, background work, AI policy, and integrations with synthetic data. Migration tests use disposable databases; they do not prove a production upgrade or restore. diff --git a/docs/testing/browser.md b/docs/testing/browser.md new file mode 100644 index 0000000..ac97e54 --- /dev/null +++ b/docs/testing/browser.md @@ -0,0 +1,5 @@ +# Browser tests + +Install Chromium once with `npx playwright install chromium`, then run `npm run test:e2e` from `job-tracker-ui/`. The harness starts isolated API/SQLite and frontend services, normally on ports 5302 and 3300. PDF and rate-limit suites use their dedicated configs. + +The browser suite covers authentication, job creation/workspace, Career Workspace, public CV/PDF access, and selected accessibility/security flows. A passing local suite is not proof of live OAuth, SMTP, payment, proxy, restore, or production network behavior. diff --git a/docs/testing/frontend.md b/docs/testing/frontend.md new file mode 100644 index 0000000..049e176 --- /dev/null +++ b/docs/testing/frontend.md @@ -0,0 +1,13 @@ +# Frontend tests + +From `job-tracker-ui/`: + +```powershell +npm ci +npm run lint +npm test -- --runInBand +npm run typecheck +npm run build +``` + +Keep the strict TypeScript/lint/build gates enabled. Unit tests should preserve translated labels, draft state, modal focus, and authenticated ownership behavior when UI components change. diff --git a/docs/testing/security.md b/docs/testing/security.md new file mode 100644 index 0000000..cd40733 --- /dev/null +++ b/docs/testing/security.md @@ -0,0 +1,5 @@ +# Security tests + +Security-focused tests cover owner isolation, authorization replay/session state, SSRF/import boundaries, file/parser limits, AI sidecar authentication, PDF admission, rate limits, dependency checks, and sensitive-output handling. Keep hostile fixtures synthetic and use the repository's existing scripts/configs. + +The dated audit evidence under the archive records what was and was not exercised. Manual production checks remain gates where local fixtures cannot establish the claim. diff --git a/job-tracker-ui/README.md b/job-tracker-ui/README.md index 20cec0e..af6d4ed 100644 --- a/job-tracker-ui/README.md +++ b/job-tracker-ui/README.md @@ -1,64 +1,20 @@ # Job Tracker frontend -The frontend is a statically exported Next.js 16 application using React 19, TypeScript, Material UI, and React Router. Next.js supplies the build shell; authenticated and public application routes are defined in `src/App.tsx`. +This directory contains the statically exported Next.js 16 / React 19 / TypeScript frontend. Next supplies the build shell; React Router and `src/App.tsx` define the application routes. -## Requirements - -- Node.js 24 (the version used by CI) -- npm 11 or a compatible npm release -- The Job Tracker API at `http://localhost:5202` for normal local use - -Install the lockfile exactly: +## Development and checks ```powershell npm ci -``` - -## Development - -Start the application on Next.js's default development port: - -```powershell npm run dev -``` - -Open `http://localhost:3000`. The client calls `http://localhost:5202/api` on localhost unless `NEXT_PUBLIC_API_BASE_URL` overrides it. - -The Playwright harness uses port `3300` and starts its own isolated API and SQLite database. Do not point it at a development or production database. - -## Quality gates - -```powershell npm run lint npm test -- --runInBand +npm run typecheck npm run build ``` -The production build is written to `out/` and is served by nginx in the frontend container. +Open `http://localhost:3000`; local API calls use `http://localhost:5202/api`. Install Chromium once and run `npm run test:e2e` for the isolated browser suite. -Run the browser suite after installing Chromium once: +`NEXT_PUBLIC_API_BASE_URL` is the only browser-visible API setting. Leave it empty in production so nginx proxies same-origin `/api` requests. It is embedded at build time and must never contain secrets. -```powershell -npx playwright install chromium -npm run test:e2e -``` - -The browser suite covers login, job creation, Career Workspace routing, and public CV/PDF access against isolated services. - -## Useful paths - -- `app/` — thin Next.js application shell -- `src/App.tsx` — application routes and shared authenticated shell -- `src/api.ts` — API client and local/default base URL -- `src/components/` — feature and shared UI components -- `src/i18n/` — English and Norwegian Bokmål UI messages -- `src/theme.ts` — application theme -- `e2e/` — Playwright browser tests -- `next.config.js` — static-export configuration -- `playwright.config.ts` — isolated browser-test services - -## Runtime configuration - -`NEXT_PUBLIC_API_BASE_URL` is the only browser-visible API setting. Leave it unset in the production container so nginx proxies same-origin `/api` requests. Values prefixed with `NEXT_PUBLIC_` are embedded at build time and must never contain secrets. - -See the repository `README.md` for full-stack development and `deploy/README.md` for production requirements. +See the repository [frontend guide](../docs/frontend/README.md) and [configuration reference](../docs/configuration/environment-variables.md). diff --git a/job-tracker-ui/src/components/ApplicationAssets.tsx b/job-tracker-ui/src/components/ApplicationAssets.tsx index c945967..2619320 100644 --- a/job-tracker-ui/src/components/ApplicationAssets.tsx +++ b/job-tracker-ui/src/components/ApplicationAssets.tsx @@ -25,7 +25,7 @@ import { useI18n } from "../i18n/I18nProvider"; // These compose systems that already exist. CV editing, preview, PDF export, themes and version // history all live in the CV builder at /cv-builder — this section only chooses WHICH variant the // application uses and links out. The cover letter is the one thing genuinely owned here, because it -// is application-specific by nature. docs/architecture/application-workspace.md. +// is application-specific by nature. docs/architecture/overview.md. function useAsset(load: () => Promise, deps: React.DependencyList) { const { t } = useI18n(); diff --git a/job-tracker-ui/src/components/ApplicationChecklist.tsx b/job-tracker-ui/src/components/ApplicationChecklist.tsx index ec1aa35..a108ae1 100644 --- a/job-tracker-ui/src/components/ApplicationChecklist.tsx +++ b/job-tracker-ui/src/components/ApplicationChecklist.tsx @@ -19,7 +19,7 @@ import { // One workflow surface, not a new tracker: the system items arrive already ticked wherever the // existing readiness signals say the work is done (CV attached, cover letter written, follow-up // scheduled...). Everything here is the user's to tick, add to, reorder or dismiss. -// docs/architecture/application-workspace.md. +// docs/architecture/overview.md. export default function ApplicationChecklist({ jobId, onChanged }: { jobId: number; onChanged?: () => void }) { const { t } = useI18n(); const [checklist, setChecklist] = useState(null); diff --git a/job-tracker-ui/src/components/ApplicationIntelligence.tsx b/job-tracker-ui/src/components/ApplicationIntelligence.tsx index 8ff296d..42aa6e5 100644 --- a/job-tracker-ui/src/components/ApplicationIntelligence.tsx +++ b/job-tracker-ui/src/components/ApplicationIntelligence.tsx @@ -16,7 +16,7 @@ import { useI18n } from "../i18n/I18nProvider"; // Everything here renders a deterministic, read-only backend answer. No component triggers an AI // generation stays in the dedicated document/preparation tools, so nothing on this page can // silently spend a token or change the user's data. -// docs/architecture/application-workspace.md. +// docs/architecture/overview.md. // One loader for all three sections: same fetch/loading/empty/error shape, so the sections stay // consistent and each one is just its own rendering. diff --git a/job-tracker-ui/src/components/InterviewPrep.tsx b/job-tracker-ui/src/components/InterviewPrep.tsx index 9547b2b..f99c60c 100644 --- a/job-tracker-ui/src/components/InterviewPrep.tsx +++ b/job-tracker-ui/src/components/InterviewPrep.tsx @@ -21,7 +21,7 @@ import { useConfirm } from "../confirm"; // // The prep content is the user's. AI suggestions are generated in this dedicated workspace and only // become prep items after an explicit accept action. -// docs/architecture/application-workspace.md. +// docs/architecture/overview.md. function Shell({ title, subtitle, loading, error, children }: { title: string; diff --git a/job-tracker-ui/src/views/ApplicationWorkspacePage.tsx b/job-tracker-ui/src/views/ApplicationWorkspacePage.tsx index c09ca96..deb76a4 100644 --- a/job-tracker-ui/src/views/ApplicationWorkspacePage.tsx +++ b/job-tracker-ui/src/views/ApplicationWorkspacePage.tsx @@ -44,7 +44,7 @@ import { // // This is a surface, not a new data store: the overview is one aggregate read, and each section // reuses the component that already owns that domain (for example Attachments and Correspondence). -// docs/architecture/application-workspace.md. +// docs/architecture/overview.md. export default function ApplicationWorkspacePage() { return ; } diff --git a/tools/summarizer/README.md b/tools/summarizer/README.md index 31300fc..b0d5647 100644 --- a/tools/summarizer/README.md +++ b/tools/summarizer/README.md @@ -1,94 +1,7 @@ -# Local AI Service +# Local AI service -This service runs a local Hugging Face summarization model and also exposes document text extraction with OCR for supported PDFs and images. +FastAPI service for local job summarisation, bounded PDF/DOCX/TXT/Markdown/image extraction, OCR, and optional Ollama/Gemini/Groq CV processing. -## Capabilities -- job/role summarization -- PDF text extraction -- OCR fallback for scanned PDFs -- OCR for image uploads (`png`, `jpg`, `jpeg`, `webp`) -- DOCX / TXT / MD extraction -- optional Ollama-backed CV block classification for harder sectioning +Run locally with Python 3.12 and `python -m uvicorn app:app --host 127.0.0.1 --port 8001`, or use Docker Compose. Protected endpoints require `AI_SERVICE_TOKEN` when configured; `/health` remains open for probes. Parser limits are controlled by `PARSER_*` variables. -## Install - -Windows: - -```powershell -python -m venv .venv -.\.venv\Scripts\Activate.ps1 -pip install -r requirements-dev.txt -python -m uvicorn app:app --host 127.0.0.1 --port 8001 --workers 1 -``` - -Linux / macOS: - -```bash -python3 -m venv .venv -source .venv/bin/activate -pip install -r requirements-dev.txt -python -m uvicorn app:app --host 127.0.0.1 --port 8001 --workers 1 -``` - -If the host is missing `python3-venv` or `pip`, use the bootstrap script instead: - -```bash -./scripts/bootstrap-and-test.sh bootstrap -``` - -## Docker -The Dockerfile installs Tesseract OCR so scanned PDFs and supported images can be processed inside the container. - -## Document parser isolation - -`/extract-text` performs only bounded upload and signature checks in the web process. Actual TXT, Markdown, PDF, DOCX and image decoding runs in a single-capacity child process with a minimal environment. Linux children receive CPU, address-space, output-size and file-descriptor limits; every platform enforces a parent deadline and terminates the parser process tree on timeout. - -Production Compose runs the service as a non-root user with a read-only root filesystem, no Linux capabilities, `no-new-privileges`, PID/CPU/memory limits and a bounded `/tmp` tmpfs. The Hugging Face cache is the only named writable volume. Parser work directories are removed after each request and stale `work-*` directories are reconciled on the next extraction. - -Parser limits can be made more conservative with `PARSER_TIMEOUT_SECONDS`, `PARSER_CPU_SECONDS`, `PARSER_MEMORY_MIB` and `PARSER_STALE_WORK_SECONDS`. Increasing them should follow measured benign-CV evidence; disabling import is the safe fallback when the budget is insufficient. - -## Tests - -Run the summarizer unit tests with: - -```bash -./scripts/bootstrap-and-test.sh test -``` - -The script: -- creates `.venv` with stdlib `venv` when available -- falls back to user-space `virtualenv` when host `venv` support is missing -- installs `requirements-dev.txt` -- writes pytest cache under `tmp/pytest-cache` to avoid stale root-owned `.pytest_cache` directories - -## API -- `GET /health` — health check and runtime capabilities, including lazy model state (`model_loaded`, `model_disabled`, `summarize_available`, `model_load_error`) plus Ollama version/model metadata when configured -- `POST /summarize` — JSON body `{ "text": "...", "max_length": 150, "min_length": 30 }` -- `POST /extract-text` — multipart file upload, returns extracted text and OCR metadata -- `POST /cv/classify-block` — JSON body `{ "block": "..." }`, uses Ollama when `OLLAMA_MODEL` is configured - -## Ollama -Set these before starting the service if you want the hybrid CV classifier enabled: - -```bash -export OLLAMA_BASE_URL=http://ollama:11434 -export OLLAMA_MODEL=qwen2.5:7b -``` - -Choose the model by setting `OLLAMA_MODEL` and then warming it with the helper script: - -```bash -OLLAMA_MODEL=qwen2.5:7b ./scripts/start-ollama-cv.sh -``` - -Equivalent manual flow: - -```bash -docker compose up -d ollama -docker compose exec ollama ollama pull qwen2.5:7b -docker compose up -d ai-service -``` - -- Model weights are downloaded on first pull. -- OCR quality depends on scan quality and language support. -- Default OCR language is English (`eng`). +See the repository [AI guide](../../docs/ai/README.md), [document-processing guide](../../docs/ai/document-processing.md), and [environment reference](../../docs/configuration/environment-variables.md).