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 onhttp://localhost:3000in dev)JobTrackerApi/: ASP.NET Core API (defaults tohttp://localhost:5202)- SQLite DB file: defaults to
JobTrackerApi/jobtracker.dbunlessData:Root/ connection string overrides it - Attachments: stored on disk under
DataRoot/Attachments/<jobId>/... - Optional local AI service:
tools/summarizer/(FastAPI) used by the API viaAi:BaseUrl
Quickstart (Docker)
This runs: frontend (nginx), backend API, and the AI service.
- Create a
.envfile next todocker-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_dataDocker volume (mounted at/datain the API container)
Local development
Prereqs
- .NET SDK
9.x(API targetsnet9.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 it’s not running, the app still works (summary generation may be empty / best-effort).
With Docker (recommended):
docker compose up --build ai-service
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 usesDataRoot/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 tohttp://localhost:3000; use"*"to allow all)Ai:BaseUrl: AI service base URL (defaulthttp://127.0.0.1:8001)Exports:DailyEnabled: enable/disable daily export background jobExports:DailyFolder: export destination (relative toData:Rootif not absolute)Exports:DailyHourLocal: local hour (0–23) when the daily export runsAuth:GoogleClientId: if set, enables JWT bearer validation for Google ID tokensAuth:JwtKey: secret used to sign local JWTs for username/password login (set via env varAuth__JwtKey)Auth:JwtIssuer: JWT issuer (defaultJobTrackerApi)Auth:JwtAudience: JWT audience (defaultjob-tracker-ui)Auth:JwtExpiresMinutes: access token lifetime in minutes (default720)Auth:AdminEmail/Auth:AdminPassword: optional seed admin user (created on startup if missing)Auth:AllowRegistration: allow self-service registration viaPOST /api/auth/register(defaultfalse)Auth:Require: iftrue, all endpoints require auth (except endpoints explicitly marked anonymous)Translation:Provider:none(default) orlibretranslateTranslation:LibreTranslate:BaseUrl: base URL for LibreTranslate (only if provider enabled)Translation:LibreTranslate:ApiKey: optional API key for LibreTranslateApp: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 emailsEmail:FollowUpReminders:UpcomingDays: how far ahead reminder emails look for upcoming follow-up dates (default2)
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:GoogleClientIdis configured, most endpoints requireAuthorization: Bearer <google_id_token>. - 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).
- Query:
GET /api/jobapplications/{id}- Returns a single
JobApplicationDto(includes computed follow-up flags and a “full” summary on demand).
- Returns a single
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
JobEventof typeCreated.
- Body:
PUT /api/jobapplications/{id}- Body:
UpdateJobApplicationRequest - Updates an application; records a
StatusChangedevent if the status changed.
- Body:
PATCH /api/jobapplications/{id}/status- Body:
{ "status": "..." } - Updates only status; records
StatusChangedif it changed.
- Body:
PATCH /api/jobapplications/{id}/followup- Body:
{ "followUpAt": "2026-03-13T12:00:00Z" }(ornull) - Sets/clears follow-up date; records a
FollowUpSetevent.
- Body:
GET /api/jobapplications/{id}/history- Returns
JobEventhistory for the job.
- Returns
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 aDeletedevent.
- Soft-deletes an application (
POST /api/jobapplications/{id}/restore- Restores a soft-deleted application; records a
Restoredevent.
- Restores a soft-deleted application; records a
Companies (/api/companies)
GET /api/companies: list companiesGET /api/companies/{id}: company by idPOST /api/companies: create (idempotent by name; returns existing if already present)- Body:
{ "name": "...", "location": "...?", "source": "...?" }
- Body:
PUT /api/companies/{id}: update- Body includes recruiter details and
pipelineStage,lastContactedAt,nextContactAt
- Body includes recruiter details and
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": "..."? }
- Body:
Attachments (/api/attachments)
GET /api/attachments/{jobId}: list attachments for a jobGET /api/attachments/download/{id}: download an attachment by attachment idPOST /api/attachments: upload files (multipart/form-data)- Form fields:
jobId(int),files(one or more)
- Form fields:
PATCH /api/attachments/{id}: rename attachment- Body:
{ "fileName": "NewName.pdf" }
- Body:
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.jsonor.csv).
- Downloads a file (
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).
- Returns an encrypted backup file (
Job import (/api/jobimport)
POST /api/jobimport/preview- Body:
{ "url": "https://..." } - Returns a parsed preview payload (
JobImportResult), if a matching plugin can parse it.
- Body:
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).
- Body:
Authentication (/api/auth)
GET /api/auth/config: returns auth feature flags for the UIPOST /api/auth/login: local email/password login (returns a signed JWT)POST /api/auth/register: local registration (only if enabled viaAuth:AllowRegistration=true)GET /api/auth/me: returns current user/claims summary for the UIPOST /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 + rolesPOST /api/users: create a user (and optionally roles)PUT /api/users/{id}/roles: replace roles for a userDELETE /api/users/{id}: delete userPOST /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
Ghostedbased on rule settings. - In Docker, the UI proxies
/api/*to the backend service (seejob-tracker-ui/nginx.conf).
Ideas to improve the app (next steps)
- Add first-class “timeline” view combining
JobEventhistory + 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.