289 lines
14 KiB
Markdown
289 lines
14 KiB
Markdown
# 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`).
|
||
|
||
```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="<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:
|
||
|
||
```bash
|
||
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
|
||
|
||
```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 `<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 (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 <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).
|
||
- `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.
|