# 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//...` - 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`). ```bash 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 ```bash 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 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: ```bash export AUTH_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: ```bash export AUTH_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 ```bash 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): ```bash # 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 `/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 (0–23) 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 `. - 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). - `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.