- C# 58.1%
- TypeScript 26.6%
- HTML 11.8%
- SCSS 3%
- Dockerfile 0.5%
Fix the CV month-range picker being painted behind later sections: the .reveal animation's `both` fill mode left a transform on every section, creating stacking contexts that trapped the popup. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|---|---|---|
| backend | ||
| frontend/apploy-web | ||
| .dockerignore | ||
| .gitignore | ||
| Apploy.slnx | ||
| CLAUDE.md | ||
| docker-compose.yml | ||
| Dockerfile | ||
| PLAN.md | ||
| README.md | ||
Apploy
A personal, single-user, local job-application manager — your application command deck. Track applications through their whole lifecycle, watch stale ones get auto-ghosted, read the statistics off a Sankey-equipped dashboard, keep reusable documents (CVs, certificates, cover-letter templates) in one library, compile a complete application PDF in one click, and generate a Harvard-style CV from structured data.
Everything runs on your machine: a .NET API over SQLite plus an Angular frontend. No accounts, no cloud, no telemetry.
Features
- Application tracking — company, role, source, job URL, salary note, free-form notes, and a full status timeline per application. Click any row to open it; filter by status, text, recruiter, or how many status steps an application has taken. Filters live in the URL, so browser-back from an application returns you to the exact filtered view.
- Application periods (campaigns) — every application belongs to a named period (e.g. "2026 search"). A header switcher scopes the list, dashboard and CSV export to the current period, so past search rounds can be revisited. Periods are managed in Settings.
- Recruiter directory — a managed list of recruiters/headhunters (name, agency, contact). Each application is flagged Myself or Recruiter and, when via a recruiter, linked to one — then filterable by recruiter.
- Companies overview — a cross-period page listing every company you have ever applied to, with how many times, the latest status, which periods, and which recruiters were involved; narrowable to the companies a given recruiter applied you to.
- Status state machine — applications move through a fixed, validated flow (see Status flow); invalid transitions are rejected by the domain, not the UI.
- Auto-ghosting — applications sitting in Applied or Interview with no interaction for 30 days (configurable) are swept to Ghosted by a daily background job. A ghosted application can be reactivated to exactly its pre-ghost status if the company resurfaces.
- Needs attention & follow-ups — the dashboard surfaces applications approaching auto-ghost
("ghosts in N days") and any with an explicit follow-up date due today or earlier. Each
application can carry a
NextActionDate+ note, and the list shows a ghost countdown badge. - Interview scheduling — schedule interviews per application (round, date/time, format — on-site / remote / phone / video —, location, interviewers, prep notes). A cross-application Upcoming interviews page lists everything still ahead, soonest first.
- Structured compensation — min/max, currency and period (yearly / monthly / hourly) per application, so offers are comparable and salary reads cleanly (localized currency formatting).
- Cover-letter templates — store a cover-letter template (text, PDF or Word
.docx) in the library and, on the compile card, start from template: its{{company}}/{{role}}/{{location}}/{{source}}/{{recruiter}}placeholders are filled in for the application, still editable. - Source analytics — a dashboard "By source" table shows which channels (LinkedIn, referral, …) actually convert to interviews and offers.
- Dashboard — stat tiles (response/interview/offer/ghost rate, average days to reply), a funnel, a current-status breakdown, and a Sankey diagram of every status transition.
- Document library — upload CVs, certificates, and cover-letter templates; mark one default per type; download or delete any time. Files live on disk, metadata in SQLite.
- One-click application PDF — write the cover letter, pick a CV and certificates from the library, and Apploy compiles a single merged PDF for the application.
- Harvard-style CV generator — structured education (with month-range periods), multi-stage experience per company (title, employment type, own bullet points), and skills; rendered to PDF, previewable, and savable straight into the document library.
- Demo mode — a Settings toggle seeds 150 generated applications (valid random walks through the state machine) plus a populated sample CV profile into a separate demo database. Real data is untouched and returns the moment the toggle goes off; the header shows a DEMO DATA chip while active.
- Bilingual (English / German) — a runtime language switch (header, and Settings) flips the whole interface and the documents Apploy generates: cover letters, the Harvard CV (localized section titles, employment types, periods) and the CSV export. The choice persists per browser.
- CSV export — download the currently filtered view (company, job title, status, status flow, last activity) as a localized CSV; German uses semicolon delimiters and a UTF-8 BOM for clean Excel import. Kept alongside the JSON backup for job centres / social services.
- Full JSON backup & restore — export everything (applications, timelines, interviews, periods, recruiters, the CV profile and every document including its bytes) as one JSON file, and restore from it. Managed under Settings → Backup & restore.
- Command-deck UI — dark tactical theme with a tuned light variant (follows the OS), self-hosted fonts, accessible contrast throughout, keyboard-navigable tables.
Tech stack
| Layer | Tech |
|---|---|
| Backend | .NET 10 · ASP.NET Core minimal APIs · EF Core + SQLite |
| QuestPDF (rendering) · PDFsharp (merging) · PdfPig / OpenXML (template text extraction) | |
| Backend tests | NUnit · Shouldly · Moq · Verify |
| Frontend | Angular 21 · TailwindCSS v4 · ngx-echarts (ECharts 6) |
| Fonts | Chakra Petch · Archivo · JetBrains Mono (self-hosted via Fontsource) |
Repository layout
Apploy.slnx
backend/
src/Apploy.Domain # entities, status state machine, pure domain services (no EF/IO)
src/Apploy.Infrastructure # EF Core + SQLite, document storage, PDF, ghosting job, demo seeder
src/Apploy.Api # minimal-API host, DTOs, DI, endpoint groups
tests/Apploy.Domain.Tests # NUnit + Shouldly + Verify
tests/Apploy.Api.Tests # WebApplicationFactory integration + Verify + Moq
frontend/apploy-web # Angular app (standalone components, signals, reactive forms)
Dockerfile # multi-stage build: SPA → .NET publish → aspnet runtime
docker-compose.yml # single-service stack with a persistent data volume
PLAN.md # execution state across sessions (phase tracker)
The domain layer is dependency-free: the state machine, ghosting policy, statistics, and CV
layout are pure and unit-tested in isolation. Endpoints use the DbContext directly — EF Core
is already a unit-of-work/repository.
Getting started
Prerequisites
- Docker + Docker Compose — the only requirement to run the app (see Run with Docker).
- For local development without Docker: .NET SDK 10, and Node.js 20+ with npm.
Run (development)
Two processes; the Angular dev server proxies /api to the backend (proxy.conf.json).
# 1) Backend API → http://localhost:5049
dotnet run --project backend/src/Apploy.Api
# 2) Frontend → http://localhost:4200
cd frontend/apploy-web && npm install && npm start
Open http://localhost:4200. Health check: curl http://localhost:5049/api/health →
{"status":"ok"}.
Run (production, single process)
Build the Angular app into the API's wwwroot and serve everything from one process — no dev
server, no proxy:
dotnet run --project backend/src/Apploy.Api -c Release -p:BuildFrontend=true
# then open the API's URL (e.g. http://localhost:5049) — it serves both the UI and /api
-p:BuildFrontend=true runs npm ci + npm run build and copies the output into wwwroot
(the flag is off by default, so ordinary builds and tests never need Node). Outside Development
the API hosts the SPA with an index.html fallback for client-side routes.
First run creates apploy.db (SQLite, migrated automatically at startup) and a data/
directory for stored files, both next to the API project. Want to explore with data before
entering your own? Flip Settings → Demo mode.
Run with Docker
The simplest way to run Apploy — no .NET or Node toolchain required, just Docker. A single multi-stage image builds the Angular SPA, publishes the .NET API, and serves both from one container.
docker compose up --build # build the image and start Apploy
# open http://localhost:8080 — UI + /api on the same origin
Stop it with docker compose down (add -v to also delete the data volume). To rebuild after
pulling changes, re-run docker compose up --build.
The SQLite databases and uploaded documents live in the named volume apploy-data (mounted at
/data in the container), so your data survives container restarts and rebuilds. Configuration is
overridden through environment variables in docker-compose.yml:
| Variable | Value | Overrides |
|---|---|---|
ConnectionStrings__Apploy |
Data Source=/data/apploy.db |
Real database location |
ConnectionStrings__ApployDemo |
Data Source=/data/apploy.demo.db |
Demo database location |
Storage__DataDirectory |
/data |
Root for stored documents |
Change the published port by editing the ports mapping (e.g. "3000:8080"). The container
exposes a health check at /api/health that Compose polls automatically.
Build & test
# Backend (from the repo root)
dotnet build Apploy.slnx
dotnet test Apploy.slnx
# Frontend (from frontend/apploy-web)
npm run build
npm test # Vitest unit tests
npm run e2e # Playwright end-to-end smoke (boots the single-deployable)
Configuration
All backend settings live in backend/src/Apploy.Api/appsettings.json:
| Key | Default | Meaning |
|---|---|---|
ConnectionStrings:Apploy |
Data Source=apploy.db |
The real database |
ConnectionStrings:ApployDemo |
Data Source=apploy.demo.db |
The demo-mode database |
Storage:DataDirectory |
data |
Root for stored files (demo mode uses data/demo/…) |
Ghosting:WindowDays |
30 |
Days without interaction before auto-ghosting |
Cors:AllowedOrigins |
http://localhost:4200 |
Frontend dev origin |
Status flow
The domain enforces a fixed transition map — the UI only ever offers legal next steps, and the API rejects anything else with a validation problem:
stateDiagram-v2
[*] --> Draft
[*] --> Applied
Draft --> Applied
Draft --> Withdrawn
Applied --> Interview
Applied --> Offer
Applied --> Rejected
Applied --> Ghosted
Applied --> Withdrawn
Interview --> Interview : next round
Interview --> Offer
Interview --> Rejected
Interview --> Ghosted
Interview --> Withdrawn
Offer --> Accepted
Offer --> Rejected
Offer --> Withdrawn
Ghosted --> Applied : reactivate
Ghosted --> Interview : reactivate
Accepted --> [*]
Rejected --> [*]
Withdrawn --> [*]
Notes:
- An application starts as Draft or Applied.
- Interview → Interview logs the next round; rounds auto-increment when not supplied.
- Only Applied and Interview are auto-ghosted — an open Offer never is.
- Ghosted can only be reactivated to the exact status held before ghosting.
- Accepted, Rejected, and Withdrawn are terminal.
- Timelines are kept in append order — back-dating an event never re-shuffles the flow.
API overview
All endpoints are JSON under /api; enums serialize as strings. Endpoints that produce
user-facing output — CSV export, the compiled application PDF and the CV PDF — accept an
optional ?lang=en|de (default English) that localizes text, dates and file names.
| Method & path | Purpose |
|---|---|
GET /api/health |
Liveness probe |
GET /api/applications |
List (filters: ?status=, ?search=, ?minSteps=, ?periodId=, ?recruiterId=); carries a ghost countdown |
GET /api/applications/attention |
Applications approaching auto-ghost or with a due follow-up (?periodId=, ?withinDays=) |
GET /api/applications/export.csv |
Export as a localized CSV (?lang= + the same filters as the list) |
POST /api/applications |
Create (initial status Draft or Applied; structured compensation + follow-up) |
GET /api/applications/{id} |
Detail incl. timeline + allowed next statuses |
PUT /api/applications/{id} |
Update details (never touches status) |
DELETE /api/applications/{id} |
Delete incl. timeline + generated files |
POST /api/applications/{id}/events |
Advance status (validated by the state machine) |
POST /api/applications/{id}/compile |
Compile cover letter + CV + certificates into one PDF (?lang=) |
GET /api/applications/{id}/pdf |
Download the latest compiled PDF |
GET /api/applications/{id}/cover-letter-draft |
Expand a text template (?documentId=) for this application |
GET · POST /api/applications/{id}/interviews · PUT · DELETE /api/interviews/{id} |
Manage scheduled interviews |
GET /api/interviews/upcoming |
Upcoming scheduled interviews across applications (?periodId=) |
GET /api/statistics |
Totals, funnel, rates, per-status breakdown (?periodId=) |
GET /api/statistics/sankey |
Status-transition flow for the Sankey diagram (?periodId=) |
GET /api/statistics/sources |
Per-source conversion (interview / offer / ghost rate) (?periodId=) |
GET /api/backup · POST /api/backup/restore |
Full JSON export / restore of all data (incl. document bytes) |
GET · POST /api/periods · PUT · DELETE /api/periods/{id} |
Manage application periods (campaigns) |
GET · POST /api/recruiters · PUT · DELETE /api/recruiters/{id} |
Manage the recruiter directory |
GET /api/recruiters/{id}/companies |
Companies a recruiter has applied you to |
GET /api/companies |
Cross-period company overview (?recruiterId=) |
GET /api/documents |
List library documents (filter: ?type=) |
POST /api/documents |
Upload (multipart: file, type, optional name) |
GET /api/documents/{id}/download |
Download the stored file |
PUT /api/documents/{id}/default |
Make this the default of its type (exclusive) |
DELETE /api/documents/{id} |
Delete row + stored file |
GET /api/cv · PUT /api/cv |
Read / save the CV profile |
GET /api/cv/pdf |
Render the Harvard-style CV PDF (?lang=) |
POST /api/cv/documents |
Save the rendered CV into the document library (?lang=) |
GET /api/demo · PUT /api/demo |
Read / toggle demo mode (enabling reseeds) |
POST /api/maintenance/ghosting-sweep |
Run the ghosting rule on demand |
DELETE /api/maintenance/all-data |
Danger zone: wipe applications, documents, files, CV |
Demo mode
PUT /api/demo {"enabled": true} (or the Settings toggle) switches every request to a separate
demo database and demo storage directory, then seeds 150 applications as weighted random
walks through the status state machine — built via the domain API, so every generated history
is valid by construction. Disabling flips straight back; the flag is in-memory, so a restart
always comes back in real mode. Real data cannot be touched by anything done while demo mode is
on — including Delete all data.
Data & privacy
apploy.db,apploy.demo.db, anddata/stay on your machine and are gitignored.- Settings → Danger zone → Delete all data wipes applications (with timelines + interviews), documents (rows and files), and the CV profile after a type-to-confirm.
Development notes
- Work is tracked phase-by-phase in
PLAN.md; one feature branch per phase, conventional commits, never merge a red build. - Backend suite: 168 tests (Domain 94 + API 74 as of phase 16); API tests boot the real
pipeline against in-memory SQLite via
WebApplicationFactory. Frontend: 12 Vitest unit tests plus a Playwright end-to-end smoke. - The frontend uses semantic design tokens (CSS vars registered through Tailwind v4
@theme inline) — components saybg-panel/text-ink-muted/text-accentand both themes come along for free. Charts read the same tokens at render time.