cesnimda 86cdafb3ef
CI and Deploy / test (pull_request) Successful in 2m1s
CI and Deploy / deploy (pull_request) Has been skipped
fix(deploy): copy .npmrc before npm ci in frontend Dockerfile
Production deploy has been broken since the Next.js migration merged:
the Dockerfile ran `npm ci` right after COPY package*.json, before the
later `COPY . .` that would bring in .npmrc -- so the legacy-peer-deps
fix for react-scripts' stale TS ^4 peer constraint (added for CI in
dbb1580) never took effect in the actual deploy image, and every
deploy since has failed with the same ERESOLVE error CI hit before
that fix. Copy .npmrc alongside package*.json so npm ci sees it.
2026-07-12 02:15:49 +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
  • 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

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