# /html-report - Generate Application Tracker Dashboard Generate a self-contained HTML dashboard from `job_search_tracker.csv` and the application archives under `documents/applications/`. The output is a single `.html` file — no server, no dependencies — that can be opened directly in a browser. ## Step 0: Parse Arguments - No argument → output to `reports/application-dashboard.html` - A path argument (e.g. `/html-report ~/Desktop/report.html`) → use that path - `--open` flag → after writing, tell the user to open the file (cannot open a browser directly) Create `reports/` if it does not exist. --- ## Step 1: Collect Data Read in parallel: 1. **`job_search_tracker.csv`** — the primary source. Parse every row into a record with fields: `date`, `company`, `sector`, `role`, `role_type`, `channel`, `status`, `contact_person`, `fit_rating`, `notes`, `cv_file`, `cover_letter_file`, `source`, `deadline` Rows written before `deadline` existed have thirteen fields and no fourteenth value. Treat the missing field as empty - never drop the row, and never infer a deadline from its `date`. 2. **`documents/applications/*/outcome.md`** — for each resolved application, read the outcome file to get the exact interview stages reached (the checkboxes) and any notes. Merge this into the matching tracker row by company+role fuzzy match (lowercase, ignore punctuation). If an archive exists for a row but there is no match, attach it as extra context anyway. Status normalisation — map tracker values to six canonical buckets before computing stats: - `drafted` → **Drafted** (documents written by `/apply`, not yet submitted) - `applied` → **Active** (resume submitted, no further signal) - `interview` → **Interview** - `offer` → **Offer** - `hired` → **Hired** - `rejected` / `no_response` / `no response` / `offer_declined` / `offer declined` / `withdrawn` → **Rejected/Closed** - anything else → **Rejected/Closed**, and name the unrecognised value once in the status breakdown — matching is case-insensitive The bucket map tolerates the legacy space spellings on read so nothing written before the canonical forms were locked drops out of the stats; the **Tracker status vocabulary** in `/outcome` is the authoritative set. --- ## Step 2: Compute Summary Stats From the normalised data compute: **Drafted rows are excluded from every statistic below** — they were never submitted. Report the Drafted count on its own, and include it only in the status breakdown. - **Total applications** - **By status bucket:** count per bucket - **By sector:** count per unique sector value - **By channel:** portal vs online vs referral vs other - **By year/season:** group by the `date` field (which may be a year like `2025` or a full date) - **Funnel rates:** what % progressed past resume screen (reached Interview or beyond). Compute stage-reached from history, not current status: an application counts as having reached a stage when its current status implies it **or** its merged `outcome.md` stage checkboxes (Step 1.2) show the stage was reached - a `rejected` row whose outcome file ticks an interview stage reached Interview, and a `hired` row reached every stage before Hired. Current status alone structurally undercounts every earlier stage: a finished search would read as though nobody ever interviewed. - **Rejection rate:** true rejections (`rejected`, `no_response`) ÷ applications with a final outcome. `offer_declined` (the candidate turned the offer down - a success) and `withdrawn` (candidate-initiated) are not rejections and stay out of the numerator; Interview and Offer rows are still unresolved, so they stay out of the denominator along with Active. The Rejected/Closed status *bucket* still groups all closed rows for the doughnut - the rate just must not reuse the bucket blindly. --- ## Step 3: Generate the HTML Write a single self-contained HTML file. All CSS is inline in a `