No description
  • C# 58.1%
  • TypeScript 26.6%
  • HTML 11.8%
  • SCSS 3%
  • Dockerfile 0.5%
Find a file
Marvin Mees 523b218ca9
Merge branch 'bugfix/reveal-stacking-context-picker'
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>
2026-07-29 16:54:07 +02:00
backend feat(interviews): auto-create on status log + separate date/time picker 2026-07-24 11:54:34 +02:00
frontend/apploy-web fix(ui): stop .reveal sections trapping popups in a stacking context 2026-07-29 16:52:32 +02:00
.dockerignore feat(docker): containerize app with multi-stage build + compose 2026-07-29 10:32:18 +02:00
.gitignore test(frontend): unit specs + Playwright smoke 2026-07-24 11:25:31 +02:00
Apploy.slnx chore: scaffold backend solution and Angular app (Phase 0) 2026-07-22 11:08:14 +02:00
CLAUDE.md Added CLAUDE.md 2026-07-22 10:33:20 +02:00
docker-compose.yml feat(docker): containerize app with multi-stage build + compose 2026-07-29 10:32:18 +02:00
Dockerfile feat(docker): containerize app with multi-stage build + compose 2026-07-29 10:32:18 +02:00
PLAN.md fix(ui): stop .reveal sections trapping popups in a stacking context 2026-07-29 16:52:32 +02:00
README.md feat(docker): containerize app with multi-stage build + compose 2026-07-29 10:32:18 +02:00

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
PDF 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, and data/ 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 say bg-panel / text-ink-muted / text-accent and both themes come along for free. Charts read the same tokens at render time.