cesnimda 32d16089f1 refactor(jobs): remove legacy details dialog
The routed job workspace owns analysis, correspondence, timeline, and interview flows. Remove the unreachable duplicate dialog and keep coverage on the live components and route contracts.
2026-08-30 23:22:40 +02:00
2026-08-30 11:12:25 +02:00
2026-08-30 11:12:25 +02:00

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.

Features (high level)

  • 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=<page url> 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

Architecture

  • 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/<jobId>/...
  • Optional local AI service: tools/summarizer/ (FastAPI) used by the API via Ai:BaseUrl

Quickstart (Docker)

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).
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)

Local development

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

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 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:

export AUTH_TOKEN="<admin bearer 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:

export AUTH_TOKEN="<bearer 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

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).

3) Run the browser smoke suite

cd job-tracker-ui
npx playwright install chromium
npm run test:e2e

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.

4) (Optional) Run the AI service

The API calls a local FastAPI service to generate summaries. If its not running, the app still works (summary generation may be empty / best-effort).

With Docker (recommended):

# 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 <Data:Root>/Attachments)
  • CvExports:RetainDays: generated PDF retention in days (default 30, clamped to 1365)
  • 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 (023) when the daily export runs
  • Backups:Enabled: enable/disable the automated SQLite backup job (default true)
  • Backups:HourLocal: local hour (023) when the daily database backup runs (default 3)
  • Backups:RetainCount: how many backup files to keep in <Data:Root>/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 <google_id_token>.
  • If its 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 InterviewingInterview); 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 (0100) 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.

S
Description
No description provided
Readme 101 MiB
Languages
C# 61.2%
TypeScript 34.3%
Python 2.8%
Shell 1.4%
JavaScript 0.1%