2026-08-09 16:07:33 +02:00

Job Tracker

Job Tracker is a simple, self-hosted app for tracking job applications with a React frontend and an ASP.NET Core API backed by SQLite.

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/: React app (runs on http://localhost:3000 in dev)
  • JobTrackerApi/: ASP.NET Core API (defaults to http://localhost:5202)
  • SQLite DB file: defaults to JobTrackerApi/jobtracker.db unless Data:Root / connection string overrides it
  • 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 (for the UI)
  • (Optional) Python 3.x if running the AI service without Docker

1) Run the API

cd JobTrackerApi
dotnet restore
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 install
npm start

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

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

Ideas to improve the app (next steps)

  • Add first-class “timeline” view combining JobEvent history + correspondence + attachments into a single chronological stream (this also naturally supports the “Applied → Interview → Reply → …” flow you mentioned).
  • Define a canonical pipeline/status model (enum + ordering) and drive UI badges/board columns from it; allow custom pipelines per user.
  • Add Swagger/OpenAPI for the controllers (so endpoint docs stay in sync) + include example requests/responses.
  • Add validation + problem-details responses consistently (and keep request/response DTOs stable and versioned).
  • Add search improvements (full-text search, filter by tags, filter by date ranges, saved views).
  • Add notifications (email/desktop) for follow-ups and upcoming deadlines.
S
Description
No description provided
Readme 97 MiB
Languages
C# 60.2%
TypeScript 35.9%
Shell 1.9%
Python 1.8%