Skip to content
Career
Command

/html-report

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.

From plugin
ai-job-search
68k12 skills3 agents12 commands
$ npx -y skills add ai-job-search-dev/ai-job-search --agent claude-code

Command definition

html-report.md

/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 `<style>` block. All JS is inline in a `<script>` block. Draw the doughnut and bar charts as hand-generated inline SVG — no Chart.js, no CDN, no external dependencies of any kind. The report must render fully offline on every open.

**Escaping (required):** HTML-escape every CSV/outcome-file value (`&` `<` `>` `"` `'`) before interpolating it into the page — this includes table cells, `title` attributes on truncated notes, and any text placed inside SVG (`<text>` labels, chart tooltips). Notes and company names copied from job postings routinely contain these characters; unescaped, they break the layout or inject markup into a page the user opens routinely.

Layout

┌─────────────────────────────────────────────┐
│  🔍 Job Search Dashboard    Generated: DATE  │
├──────┬──────┬──────┬──────┬──────┬───────────┤
│Sent  │Draft │Active│Inter-│Offer │Rejected/  │  ← stat cards
│  N   │  N   │  N   │view N│  N   │Closed   N │
├──────┴──────┴──────┴──────┴──────┴───────────┤
│  Status breakdown (doughnut) │ By sector (bar)│  ← charts row
├─────────────────────────────────────────────  ┤
│  By channel (bar)  │  Funnel (horizontal bar) │  ← charts row
├──────────────────────────────────────────────  ┤
│  Applications  [Status ▾] [Sector ▾] [🔍 ...]│  ← table with filters
│  date │ company │ sector │ role │ status │ ... │
│  ...                                          │
└───────────────────────────────────────────────┘

Design spec

  • **Colour palette:** CSS custom properties. Status colours:
  • Drafted: `#64748b` (slate)
  • Active: `#3b82f6` (blue)
  • Interview: `#f59e0b` (amber)
  • Offer: `#8b5cf6` (purple)
  • Hired: `#22c55e` (green)
  • Rejected/Closed: `#ef4444` (red)
  • **Font:** system-ui stack, no web fonts
  • **Stat cards:** white background, subtle shadow, large bold number, label below, left border in status colour
  • **Charts:** contained in a 2-column grid on wide screens, stacked on narrow
  • **Table:**
  • Alternating row shading
  • Status column uses a coloured pill/badge
  • `source` column renders as
Read more
@madslorentzen
Ships withai-job-search

The job search that runs on your machine. AI job application framework built on Claude Code: evaluate postings, tailor CVs, write cover letters, prep interviews. Fork it and own it.

68k3 skills3 agents
Get the whole plugin

Stars on GitHub

Momentum

stars in total
68.5k
Star history of MadsLorentzen/ai-job-search on GitHub, a chart by star-history.com

Other commands on ai-job-search.

add-portal

add-portal

Command

You are helping the user build a job-portal search skill for a job board in their market. The repo ships worked examples of the…

add-template

add-template

Command

You are helping the user register their own CV or cover letter template with the AI Job Search framework — LaTeX, Typst, or any…

apply

apply

Command

You are orchestrating a two-agent job application workflow. The job posting is provided below as `$ARGUMENTS` (either a URL or…

expand

expand

Command

You are enriching the candidate profile by discovering competencies hidden in documents and public online presence. This command…

gmail-sync

gmail-sync

Command

You are scanning the user's Gmail for status signals on tracked job applications (interview invites, assessment links, offers,…

interview

interview

Command

You are preparing the user for a real, scheduled interview on one of their applications. The frameworks for this already exist -…

More commands from other plugins.

setup

setup

Command

Setup wizard for lockedin. Walks the user through: (1) wiring the HUD into Claude Code's statusLine, (2) default Q&A interview…

lockedin5mo127
@daypunk@daypunkInstall Command