2026-03-21 11:55:27 +01:00
2026-04-01 21:45:01 +02:00
2026-04-01 16:09:29 +02:00
2026-04-01 21:45:01 +02:00
2026-03-21 11:55:27 +01:00
2026-03-21 11:55:27 +01: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.)
  • 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
  • Optional local AI service for short/full descriptions
  • Optional Google sign-in (Google ID tokens) to protect the API

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 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) (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 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)
  • Cors:Origins: list of allowed origins (defaults to http://localhost:3000; use "*" to allow all)
  • Ai:BaseUrl: AI service base URL (default http://127.0.0.1:8001)
  • Exports:DailyEnabled: enable/disable daily export background job
  • Exports:DailyFolder: export destination (relative to Data:Root if not absolute)
  • Exports:DailyHourLocal: local hour (023) when the daily export runs
  • 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)
  • 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: public base URL used when generating links in emails (example: https://jobs.cesnimda.uk)
  • 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

  • REACT_APP_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.
  • 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.
  • 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 81 MiB
Languages
C# 53%
TypeScript 41.9%
Shell 2.5%
Python 2.4%