# Changelog

All notable changes to Peregrine are documented here.
Format follows [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).

---

## [Unreleased]

---

## [0.9.5] — 2026-05-08

### Fixed

- **Theme: dark/explicit themes show correct page background** — `index.html` inline style
  set `html, body { background: #eaeff8 }` hardcoded. `body` paints on top of `html`, so
  even when `html { background: var(--color-surface) }` correctly resolved to `#16202e` in
  dark mode, the body's hardcoded light background covered it — producing dark cards on a
  light page. Fixed by: (1) removing body background from the inline style; (2) adding a
  tiny blocking `<script>` that reads `cf-theme` / `cf-hacker-mode` from localStorage and
  sets `data-theme` on `<html>` before first paint; (3) adding
  `html[data-theme="dark"|"solarized-dark"|"hacker"]` rules so FOUT prevention fires the
  right background immediately on load.

---

## [0.9.4] — 2026-05-08

### Added

- **Messages view — expandable email timeline** — click any email item to lazy-load
  and read the full body inline (HTML stripped to plain text via `DOMParser`).
  Bodies are fetched on-demand via the new `GET /api/contacts/{id}` endpoint to avoid
  loading 50KB+ email bodies on every page view.
- **Messages view — compose bar** — action buttons (Log call, Log note, Use template,
  Draft reply with LLM, Call via Osprey) moved from the always-visible header into a
  sticky bottom compose bar triggered by a ＋ New toggle. Reduces visual clutter when
  just reading the thread.
- **Home view — "Skip review" checkbox** — when adding jobs by URL, a checkbox (default
  on) sends them directly to the Apply queue, bypassing Job Review.
- **ContactsView — sync status** — shows last completed sync time and a spinner when
  an email sync is running.
- **imap_sync: Indeed alert parser** — `parse_indeed_alert()` extracts job title,
  company, location, salary, and canonical URL from Indeed Job Alert digest emails.
- **scrape_url: Oracle HCM support** — Playwright-based scraper for Oracle HCM
  CandidateExperience portals (React SPAs requiring JS execution).
- **manage.sh** — compose engine auto-detection (docker compose / podman compose /
  podman-compose), `build` command, and cloud/demo stack shortcuts.
- **theme.css** — `--color-overlay` token for modal/dialog backdrops.

### Fixed

- **Messages view layout** — changed `height: 100%` to `height: 100dvh` with a mobile
  override for the 56px tab bar. `height: 100%` was resolving to "shrink-wrap" because
  `.app-main` has no explicit height; compose bar is now correctly pinned to the bottom.
- **Accessibility: danger button contrast** — `btn--danger` used `color: white` on
  `--app-accent` (Talon Orange), yielding 2.8:1 contrast (fails WCAG AA 4.5:1 for
  normal text). Fixed to `color: var(--app-accent-text)` (dark navy, 5.5:1).
- **Accessibility: warning badge contrast** — `tab-badge` in Job Review used `color: white`
  on `--color-warning` (amber). Same fix applied.
- **Theme: Interviews signal banners** — hardcoded `rgba(245,158,11,…)` / `rgba(39,174,…)`
  / `rgba(192,57,…)` replaced with `color-mix()` against `--color-warning/success/error`.
- **Theme: Interviews signal count** — `color: #e67e22` hardcode replaced with
  `var(--app-accent)`.
- **Theme: References academic tag chip** — `color: #7c3aed` hardcode replaced with
  `var(--status-synced)`; background uses `color-mix()` with the same token.
- **Theme: Interviews signal-move button** — `color: #fff` on `--color-primary` fails
  in dark mode (light green bg); fixed to `var(--color-text-inverse)`.
- **Modal backdrops** — `rgba(0,0,0,0.5)` replaced with `var(--color-overlay)` for
  theme consistency.

---

## [0.9.3] — 2026-05-05

### Added

- **Editable resume review** — proposed summary and experience bullets in the review modal
  are now editable text areas. Edits flow through `apply_review_decisions()` and override
  the LLM output in the final resume struct. Preview textarea in Apply Workspace is also
  editable, with manual changes preserved through the approve step via `preview_text_override`.

### Fixed

- **Double bullets in resume optimizer** — `_section_text_for_prompt` now strips existing
  bullet characters before prefixing with `•`, and `_reparse_experience_bullets` uses a
  greedy strip regex so `• •` patterns can no longer survive parsing.
- **Asterisk markup in summary** — added `_clean_summary_markup()` to strip LLM-generated
  markdown bullet chars (`*`, `-`, etc.) from career summary output; injected no-markdown
  rule into the LLM prompt's CRITICAL RULES list.
- **Light theme dark CSS bleed** — `peregrine.css` media dark override now scoped to
  `:root:not([data-theme])` (auto mode only) instead of `:root:not([data-theme="hacker"])`.
  Fixes dark navy `--app-primary-light`/`--app-accent-light` bleeding into light themes
  (light, solarized-light, colorblind) on dark-OS machines.

---

## [0.9.2] — 2026-05-02

### Added

- **Cover letter training export** (#111) — opt-in consent gate (`training_export_opt_in`
  in `user.yaml`, default off) lets users export applied-job cover letters as Alpaca-format
  JSONL for local fine-tuning. Per-job exclude/restore curation in Settings → Fine-Tune.
  Streaming JSONL download merges DB pairs with any previously uploaded file pairs.
  Cloud fine-tune Phase 2 stub (501) reserved for cf-orch integration.
- **WizardTrainingStep** — new onboarding consent step inserted between Resume and Identity;
  skippable, opt-in default off, cloud-aware privacy copy.
- **a11y:** confirmed-state toggle (no optimistic DOM divergence), visible Premium tier gate
  with upgrade link, `aria-live` region on pairs list, cloud-aware consent copy.

---

## [0.9.0] — 2026-04-20

### Added

- **Messaging tab** (#74) — per-job communication timeline replacing `/contacts`.
  Unified view of IMAP emails (`job_contacts`) and manually logged entries (`messages`).
  Log calls and in-person notes with timestamp. Message template library with 4 built-in
  templates (follow-up, thank-you, accommodation request, withdrawal) and user-created
  templates with `{{token}}` substitution. LLM draft reply for inbound emails (BYOK-unlockable,
  BSL 1.1). Draft approval flow with inline editing and one-click clipboard copy. Osprey
  IVR stub button (Phase 2 placeholder with easter egg). `migrations/008_messaging.sql`.
- **Public demo experience** (#103) — full read-only demo mode at `demo.circuitforge.tech/peregrine`.
  `IS_DEMO=true` write-blocks all mutating API endpoints with a toast notification.
  Ephemeral seed data via tmpfs + `demo/seed.sql` (resets on container start). WelcomeModal
  on first visit (localStorage-gated). Per-view HintChips guiding new users through the
  job search flow (localStorage-dismissed). DemoBanner with accessible CTA buttons
  (WCAG-compliant contrast in light and dark themes). `migrations/006_missing_columns.sql`.
- **References tracker and recommendation letter system** (#96) — track professional
  references and generate LLM-drafted recommendation request letters.
- **Shadow listing detector** — flags duplicate or aggregator-reposted job listings.
- **Hired feedback widget** — capture post-hire notes and retrospective feedback on jobs.
- **Interview prep Q&A** — LLM-generated practice questions for the selected job.
- **Resume library ↔ profile sync** — `POST /api/resumes/{id}/apply-to-profile` pushes
  a library resume into the active profile; `PUT /api/settings/resume` syncs edits back
  to the default library entry. `ResumeSyncConfirmModal` shows a before/after diff.
  `ResumeProfileView` extended with career summary, education, and achievements sections.
  `migrations/007_resume_sync.sql` adds `synced_at` to `resumes`.
- **Plausible analytics** — lightweight privacy-preserving analytics in Vue SPA and docs.
- **cf_text / cf_voice LLM backends** — wire trunk service backends in `llm.yaml`.
- **Mission alignment domains** — load preferred company domains from
  `config/mission_domains.yaml` rather than hardcoded values.
- **GitHub Actions CI** — workflow for public credibility badge (`ci.yml`).
- **`CF_APP_NAME` cloud annotation** — coordinator pipeline attribution for multi-product
  cloud deployments.

### Changed

- `/contacts` route now redirects to `/messages`; nav item renamed "Messages" → "Contacts"
  label removed. `ContactsView.vue` preserved for reference, router points to `MessagingView`.
- Survey `/analyze` endpoint is now fully async via the task queue (no blocking LLM call
  on the request thread).
- nginx config adds `/peregrine/` base-path routing for subdirectory deployments.
- `compose.demo.yml` updated for Vue/FastAPI architecture with tmpfs demo volume.

### Fixed

- Tier bypass and draft body persistence after page navigation.
- `canDraftLlm` cleanup and message list `limit` cap.
- DemoBanner button contrast — semantic surface token instead of hardcoded white.
- Period split in `profile_to_library` now handles ISO date strings containing hyphens.
- Cloud startup sweeps all user DBs for pending migrations on deploy.
- Resume import strips CID glyph references via `resume_parser` extractors.
- Survey and interview tests updated for `hired_feedback` column and async analyze flow.

---

## [0.8.6] — 2026-04-12

### Added

- **Resume Review Modal** — paged tabbed dialog replaces the inline resume review
  section in the Apply workspace. Pages through Skills diff, Summary diff, one page
  per experience entry, and a Confirm summary. Color-coded tab status: unvisited
  (gray), in-progress (indigo), accepted (green), partial (amber), skipped (slate).
  Full ARIA tabs pattern with focus trap and `Teleport to body`.
- **Resume Library** — new `/resumes` page for managing saved resumes. Two-column
  layout: list sidebar + full-text preview pane. Supports import (.txt, .pdf, .docx,
  .odt, .yaml), rename (Edit), set as default, download (txt/pdf/yaml), and delete
  (guarded: disabled when only resume or is default). 5 MB upload limit.
- **ResumeLibraryCard** — compact widget shown above the ATS Resume Optimizer in the
  Apply workspace. Displays the currently active resume for the job (job-specific or
  global default), with Switch and Manage deep links.
- **Resume library API** — `GET/POST /api/resumes`, `GET/PATCH/DELETE /api/resumes/{id}`,
  `POST /api/resumes/{id}/set-default`, `POST /api/resumes/import`,
  `GET/PATCH /api/jobs/{job_id}/resume`. `approve_resume` extended with
  `save_to_library` + `resume_name` params to save optimized resumes directly.
- **`resumes` DB migration** — `migrations/005_resumes_table.sql` adds `resumes` table
  (10 columns) and `resume_id` FK on `jobs`.
- **Resumes nav link** — Document icon entry added after Apply in the main nav.

### Changed

- Resume optimizer "Awaiting review" state now triggers the Review Modal instead of
  rendering an inline diff; save-to-library checkbox and name input surfaced on the
  preview confirmation step.

---

## [0.8.5] — 2026-04-02

### Added

- **Vue onboarding wizard** — 7-step first-run setup replaces the Streamlit wizard
  in the Vue SPA: Hardware detection → Tier → Resume upload/build → Identity →
  Inference & API keys → Search preferences → Integrations. Progress saves to
  `user.yaml` on every step; crash-recovery resumes from the last completed step.
- **Wizard API endpoints** — `GET /api/wizard/status`, `POST /api/wizard/step`,
  `GET /api/wizard/hardware`, `POST /api/wizard/inference/test`,
  `POST /api/wizard/complete`. Inference test always soft-fails so Ollama being
  unreachable never blocks setup completion.
- **Cloud auto-skip** — cloud instances automatically complete steps 1 (hardware),
  2 (tier), and 5 (inference) and drop the user directly on the Resume step.
- **`wizardGuard` router gate** — all Vue routes require wizard completion; completed
  users are bounced away from `/setup` to `/`.
- **Chip-input search step** — job titles and locations entered as press-Enter/comma
  chips; validates at least one title before advancing.
- **Integrations tile grid** — optional step 7 shows Notion, Calendar, Slack, Discord,
  Drive with paid-tier badges; skippable on Finish.

### Fixed

- **User config isolation: dangerous fallback removed** — `_user_yaml_path()` fell
  back to `/devl/job-seeker/config/user.yaml` (legacy profile) when `user.yaml`
  didn't exist at the expected path; new users now get an empty dict instead of
  another user's data. Affects profile, resume, search, and all wizard endpoints.
- **Resume path not user-isolated** — `RESUME_PATH = Path("config/plain_text_resume.yaml")`
  was a relative CWD path shared across all users. Replaced with `_resume_path()`
  derived from `_user_yaml_path()` / `STAGING_DB`.
- **Resume upload silently returned empty data** — `upload_resume` was passing a
  file path string to `structure_resume()` which expects raw text; now reads bytes
  and dispatches to the correct extractor (`extract_text_from_pdf` / `_docx` / `_odt`).
- **Wizard resume step read wrong envelope field** — `WizardResumeStep.vue` read
  `data.experience` but the upload response wraps parsed data under `data.data`.

---

## [0.8.4] — 2026-04-02

### Fixed

- **Cloud: cover letter used wrong user's profile** — `generate_cover_letter.generate()`
  loaded `_profile` from the global `config/user.yaml` at module import time, so all
  cloud users got the default user's name, voice, and mission preferences in their
  generated letters. `generate()` now accepts a `user_yaml_path` parameter; `task_runner`
  derives it from the per-user config directory (`db_path/../config/user.yaml`) and
  passes it through. `_build_system_context`, `_build_mission_notes`, `detect_mission_alignment`,
  `build_prompt`, and `_trim_to_letter_end` all accept a `profile` override so the
  per-call profile is used end-to-end without breaking CLI mode.
- **Apply Workspace: hardcoded config paths in cloud mode** — `4_Apply.py` was loading
  `_USER_YAML` and `RESUME_YAML` from the repo-root `config/` before `resolve_session()`
  ran, so cloud users saw the global (Meg's) resume in the Apply tab. Both paths now
  derive from `get_config_dir()` after session resolution.

### Changed

- **Vue SPA open to all tiers** — Vue 3 frontend is no longer gated behind the beta
  flag; all tier users can switch to the Vue UI from Settings.
- **LLM model candidates** — vllm backend now tries Qwen2.5-3B first, Phi-4-mini
  as fallback (was reversed). cf_orch allocation block added to vllm config.
- **Preflight** — removed `vllm` from Docker adoption list; vllm is now managed
  entirely by cf-orch and should not be stubbed by preflight.

---

## [0.8.3] — 2026-04-01

### Fixed
- **CI: Forgejo auth** — GitHub Actions `pip install` was failing to fetch
  `circuitforge-core` from the private Forgejo VCS URL. Added `FORGEJO_TOKEN`
  repository secret and a `git config insteadOf` step to inject credentials
  before `pip install`.
- **CI: settings API tests** — 6 `test_dev_api_settings` PUT/POST tests were
  returning HTTP 500 in CI because `_user_yaml_path()` read the module-level
  `DB_PATH` constant (frozen at import time), so `monkeypatch.setenv("STAGING_DB")`
  had no effect. Fixed by reading `os.environ` at call time.

---

## [0.8.2] — 2026-04-01

### Fixed
- **CI pipeline** — `pip install -r requirements.txt` was failing in GitHub Actions
  because `-e ../circuitforge-core` requires a sibling directory that doesn't exist
  in a single-repo checkout. Replaced with a `git+https://` VCS URL fallback;
  `Dockerfile.cfcore` still installs from the local `COPY` to avoid redundant
  network fetches during Docker builds.
- **Vue-nav reload loop** — `sync_ui_cookie()` was calling
  `window.parent.location.reload()` on every render when `user.yaml` has
  `ui_preference: vue` but no Caddy proxy is in the traffic path (test instances,
  bare Docker). Gated the reload on `PEREGRINE_CADDY_PROXY=1`; instances without
  the env var set the cookie silently and skip the reload.

### Changed
- **cfcore VRAM lease integration** — the task scheduler now acquires a VRAM lease
  from the cf-orch coordinator before running a batch of LLM tasks and releases it
  when the batch completes. Visible in the coordinator dashboard at `:7700`.
- **`CF_ORCH_URL` env var** — scheduler reads coordinator address from
  `CF_ORCH_URL` (default `http://localhost:7700`); set to
  `http://host.docker.internal:7700` in Docker compose files so containers can
  reach the host coordinator.
- **All compose files on `Dockerfile.cfcore`** — `compose.yml`, `compose.cloud.yml`,
  and `compose.test-cfcore.yml` all use the parent-context build. `build: .` is
  removed from `compose.yml`.

---

## [0.8.1] — 2026-04-01

### Fixed
- **Job title suggester silent failure** — when the LLM returned empty arrays or
  non-JSON text, the spinner would complete with zero UI feedback. Now shows an
  explicit "No new suggestions found" info message with a resume-upload hint for
  new users who haven't uploaded a resume yet.
- **Suggester exception handling** — catch `Exception` instead of only
  `RuntimeError` so connection errors and `FileNotFoundError` (missing llm.yaml)
  surface as error messages rather than crashing the page silently.

### Added
- **`Dockerfile.cfcore`** — parent-context Dockerfile that copies
  `circuitforge-core/` alongside `peregrine/` before `pip install`, resolving
  the `-e ../circuitforge-core` editable requirement inside Docker.
- **`compose.test-cfcore.yml`** — single-user test instance on port 8516 for
  smoke-testing cfcore shim integration before promoting to the cloud instance.

---

## [0.8.0] — 2026-04-01

### Added
- **ATS Resume Optimizer** (gap report free; LLM rewrite paid+)
  - `scripts/resume_optimizer.py` — full pipeline: TF-IDF gap extraction →
    `prioritize_gaps` → `rewrite_for_ats` → hallucination guard (anchor-set
    diffing on employers, institutions, and dates)
  - `scripts/db.py` — `optimized_resume` + `ats_gap_report` columns;
    `save_optimized_resume` / `get_optimized_resume` helpers
  - `GET /api/jobs/{id}/resume_optimizer` — fetch gap report + rewrite
  - `POST /api/jobs/{id}/resume_optimizer/generate` — queue rewrite task
  - `GET /api/jobs/{id}/resume_optimizer/task` — poll task status
  - `web/src/components/ResumeOptimizerPanel.vue` — gap report (all tiers),
    LLM rewrite section (paid+), hallucination warning badge, `.txt` download
  - `ResumeOptimizerPanel` integrated into `ApplyWorkspace`

- **Vue SPA full merge** (closes #8) — `feature/vue-spa` merged to `main`
  - `dev-api.py` — full FastAPI backend (settings, jobs, interviews, prep,
    survey, digest, resume optimizer); cloud session middleware (JWT → per-user
    SQLite); BYOK credential store
  - `dev_api.py` — symlink → `dev-api.py` for importable module alias
  - `scripts/job_ranker.py` — two-stage ranking for `/api/jobs/stack`
  - `scripts/credential_store.py` — per-user BYOK API key management
  - `scripts/user_profile.py` — `load_user_profile` / `save_user_profile`
  - `web/src/components/TaskIndicator.vue` + `web/src/stores/tasks.ts` —
    live background task queue display
  - `web/public/` — peregrine logo assets (SVG + PNG)

- **API test suite** — 5 new test modules (622 tests total)
  - `tests/test_dev_api_settings.py` (38 tests)
  - `tests/test_dev_api_interviews.py`, `test_dev_api_prep.py`,
    `test_dev_api_survey.py`, `test_dev_api_digest.py`

### Fixed
- **Cloud DB routing** — `app/pages/1_Job_Review.py`, `5_Interviews.py`,
  `6_Interview_Prep.py`, `7_Survey.py` were hardcoding `DEFAULT_DB`; now
  use `get_db_path()` for correct per-user routing in cloud mode (#24)
- **Test isolation** — `importlib.reload(dev_api)` in digest/interviews
  fixtures reset all module globals, silently breaking `monkeypatch.setattr`
  in subsequent test files; replaced with targeted `monkeypatch.setattr(dev_api,
  "DB_PATH", tmp_db)` (#26)

---

## [0.7.0] — 2026-03-22

### Added
- **Vue 3 SPA — beta access for paid tier** — The new Vue 3 frontend (built with
  Vite + UnoCSS) is now merged into `main` and available to paid-tier subscribers
  as an opt-in beta. The Streamlit UI remains the default and will continue to
  receive full support.
  - `web/` — full Vue 3 SPA source (components, stores, router, composables,
    views) from `feature/vue-spa`
  - `web/src/components/ClassicUIButton.vue` — one-click switch back to the
    Classic (Streamlit) UI; sets `prgn_ui=streamlit` cookie and appends
    `?prgn_switch=streamlit` so `user.yaml` stays in sync
  - `web/src/composables/useFeatureFlag.ts` — reads `prgn_demo_tier` cookie for
    demo toolbar visual consistency (display-only, not an authoritative gate)

- **UI switcher** — Reddit-style opt-in to the Vue SPA with durable preference
  persistence and graceful fallback.
  - `app/components/ui_switcher.py` — `sync_ui_cookie()`, `switch_ui()`,
    `render_banner()`, `render_settings_toggle()`
  - `scripts/user_profile.py` — `ui_preference` field (`streamlit` | `vue`,
    default: `streamlit`) with round-trip `save()`
  - `app/wizard/tiers.py` — `vue_ui_beta: "paid"` feature key; `demo_tier`
    keyword arg on `can_use()` for thread-safe demo mode simulation
  - Banner (dismissible, paid tier only) + Settings → System → Deployment toggle
  - Caddy cookie routing: `prgn_ui=vue` → nginx Vue SPA; absent/`streamlit` →
    Streamlit. 502 fallback clears cookie and redirects with `?ui_fallback=1`

- **Demo toolbar** — slim full-width tier-simulation bar for `DEMO_MODE`
  instances. Free / Paid / Premium pills let demo visitors explore all feature
  tiers without an account. Persists via `prgn_demo_tier` cookie. Default: Paid
  (most compelling first impression). `app/components/demo_toolbar.py`

- **Docker `web` service** — multi-stage nginx container serving the Vue SPA
  `dist/` build. Added to `compose.yml` (port 8506), `compose.demo.yml`
  (port 8507), `compose.cloud.yml` (port 8508). `manage.sh build` now includes
  the `web` service alongside `app`.

### Changed
- **Caddy routing** — `menagerie.circuitforge.tech` and
  `demo.circuitforge.tech` peregrine blocks now inspect the `prgn_ui` cookie
  and fan-out to the Vue SPA service or Streamlit accordingly.

---

## [0.6.2] — 2026-03-18

### Added
- **Playwright E2E test harness** — smoke + interaction test suite covering all
  three Peregrine instances (demo / cloud / local). Navigates every page, checks
  for DOM errors on load, clicks every interactable element, diffs errors
  before/after each click, and XFAIL-marks expected demo-mode failures so
  neutering-guard regressions are surfaced as XPASSes. Screenshots on failure.
  - `tests/e2e/test_smoke.py` — page-load error detection
  - `tests/e2e/test_interactions.py` — full click-through with XFAIL/XPASS bucketing
  - `tests/e2e/conftest.py` — Streamlit-aware wait helpers, error scanner, fixtures
  - `tests/e2e/models.py` — `ErrorRecord`, `ModeConfig`, `diff_errors`
  - `tests/e2e/modes/` — per-mode configs (demo / cloud / local)
  - `tests/e2e/pages/` — page objects for all 7 pages including Settings tabs

### Fixed
- **Demo: "Discovery failed" error on Home page load** — `task_runner.py` now
  checks `DEMO_MODE` before importing `discover.py`; returns a friendly error
  immediately instead of crashing on missing `search_profiles.yaml` (#21)
- **Demo: silent `st.error()` in collapsed Practice Q&A expander** — Interview
  Prep no longer auto-triggers the LLM on page render in demo mode; shows an
  `st.info` placeholder instead, eliminating the hidden error element (#22)
- **Cloud: auth wall shown to E2E test browser** — `cloud_session.py` now falls
  back to the `Cookie` header when `X-CF-Session` is absent (direct access
  without Caddy). Playwright's `set_extra_http_headers()` does not propagate to
  WebSocket handshakes; cookies do. Test harness uses `ctx.add_cookies()`.
- **E2E error scanner returned empty text for collapsed expanders** — switched
  from `inner_text()` (respects CSS `display:none`) to `text_content()` so
  errors inside collapsed Streamlit expanders are captured with their full text.

---

## [0.6.1] — 2026-03-16

### Fixed
- **Keyword suggestions not visible on first render** — `✨ Suggest` in
  Settings → Search now calls `st.rerun()` after storing results; chips appear
  immediately without requiring a tab switch (#18)
- **Wizard identity step required manual re-entry of resume data** — step 4
  (Identity) now prefills name, email, and phone from the parsed resume when
  those fields are blank; existing saved values are not overwritten (#17)
- **"Send to Notion" hardcoded on Home dashboard** — sync section now shows the
  connected provider name, or a "Set up a sync integration" prompt with a
  Settings link when no integration is configured (#16)
- **`test_generate_calls_llm_router` flaky in full suite** — resolved by queue
  optimizer merge; mock state pollution eliminated (#12)

---

## [0.6.0] — 2026-03-16

### Added
- **Calendar integration** — push interview events to Apple Calendar (CalDAV) or
  Google Calendar directly from the Interviews kanban. Idempotent: a second push
  updates the existing event rather than creating a duplicate. Button shows
  "📅 Add to Calendar" on first push and "🔄 Update Calendar" thereafter.
  Event title: `{Stage}: {Job Title} @ {Company}`; 1hr duration at noon UTC;
  job URL and company research brief included in event description.
  - `scripts/calendar_push.py` — push/update orchestration
  - `scripts/integrations/apple_calendar.py` — `create_event()` / `update_event()`
    via `caldav` + `icalendar`
  - `scripts/integrations/google_calendar.py` — `create_event()` / `update_event()`
    via `google-api-python-client` (service account); `test()` now makes a real API call
  - `scripts/db.py` — `calendar_event_id TEXT` column (auto-migration) +
    `set_calendar_event_id()` helper
  - `environment.yml` — pin `caldav>=1.3`, `icalendar>=5.0`,
    `google-api-python-client>=2.0`, `google-auth>=2.0`

---

## [0.4.1] — 2026-03-13

### Added
- **LinkedIn profile import** — one-click import from a public LinkedIn profile URL
  (Playwright headless Chrome, no login required) or from a LinkedIn data export zip.
  Staged to `linkedin_stage.json` so the profile is parsed once and reused across
  sessions without repeated network requests. Available on all tiers including Free.
  - `scripts/linkedin_utils.py` — HTML parser with ordered CSS selector fallbacks;
    extracts name, experience, education, skills, certifications, summary
  - `scripts/linkedin_scraper.py` — Playwright URL scraper + export zip CSV parser;
    atomic staging file write; URL validation; robust error handling
  - `scripts/linkedin_parser.py` — staging file reader; re-runs HTML parser on stored
    raw HTML so selector improvements apply without re-scraping
  - `app/components/linkedin_import.py` — shared Streamlit widget (status bar, preview,
    URL import, advanced zip upload) used by both wizard and Settings
  - Wizard step 3: new "🔗 LinkedIn" tab alongside Upload and Build Manually
  - Settings → Resume Profile: collapsible "Import from LinkedIn" expander
  - Dockerfile: Playwright Chromium install added to Docker image

### Fixed
- **Cloud mode perpetual onboarding loop** — wizard gate in `app.py` now reads
  `get_config_dir()/user.yaml` (per-user in cloud, repo-level locally) instead of a
  hardcoded repo path; completing the wizard now correctly exits it in cloud mode
- **Cloud resume YAML path** — wizard step 3 writes resume to per-user `CONFIG_DIR`
  instead of the shared repo `config/` (would have merged all cloud users' data)
- **Cloud session redirect** — missing/invalid session token now JS-redirects to
  `circuitforge.tech/login` instead of showing a raw error message
- Removed remaining AIHawk UI references (`Home.py`, `4_Apply.py`, `migrate.py`)

---

## [0.3.0] — 2026-03-06

### Added
- **Feedback button** — in-app issue reporting with screenshot paste support; posts
  directly to Forgejo as structured issues; available from sidebar on all pages
  (`app/feedback.py`, `scripts/feedback_api.py`, `app/components/paste_image.py`)
- **BYOK cloud backend detection** — `scripts/byok_guard.py`: pure Python detection
  engine with full unit test coverage (18 tests); classifies backends as cloud or local
  based on type, `base_url` heuristic, and opt-out `local: true` flag
- **BYOK activation warning** — one-time acknowledgment required in Settings when a
  new cloud LLM backend is enabled; shows data inventory (what leaves your machine,
  what stays local), provider policy links; ack state persisted to `config/user.yaml`
  under `byok_acknowledged_backends`
- **Sidebar cloud LLM indicator** — amber badge on every page when any cloud backend
  is active; links to Settings; disappears when reverted to local-only config
- **LLM suggest: search terms** — three-angle analysis from resume (job titles,
  skills keywords, and exclude terms to filter irrelevant listings)
- **LLM suggest: resume keywords** — skills gap analysis against job descriptions
- **LLM Suggest button** in Settings → Search → Skills & Keywords section
- **Backup/restore script** (`scripts/backup.py`) — multi-instance and legacy support
- `PRIVACY.md` — short-form privacy notice linked from Settings

### Changed
- Settings save button for LLM Backends now gates on cloud acknowledgment before
  writing `config/llm.yaml`

### Fixed
- Settings widget crash on certain rerun paths
- Docker service controls in Settings → System tab
- `DEFAULT_DB` now respects `STAGING_DB` environment variable (was silently ignoring it)
- `generate()` in cover letter refinement now correctly passes `max_tokens` kwarg

### Security / Privacy
- Full test suite anonymized — fictional "Alex Rivera" replaces all real personal data
  in test fixtures (`tests/test_cover_letter.py`, `test_imap_sync.py`,
  `test_classifier_adapters.py`, `test_db.py`)
- Complete PII scrub from git history: real name, email address, and phone number
  removed from all 161 commits across both branches via `git filter-repo`

---

## [0.2.0] — 2026-02-26

### Added
- Cover letter iterative refinement: "Refine with Feedback" expander in Apply Workspace;
  `generate()` accepts `previous_result`/`feedback`; task params passed through `submit_task`
- Expanded first-run wizard: 7-step onboarding with GPU detection, tier selection,
  resume upload/parsing, LLM inference test, search profile builder, integration cards
- Tier system: free / paid / premium feature gates (`app/wizard/tiers.py`)
- 13 integration drivers: Notion, Google Sheets, Airtable, Google Drive, Dropbox,
  OneDrive, MEGA, Nextcloud, Google Calendar, Apple Calendar, Slack, Discord,
  Home Assistant — with auto-discovery registry
- Resume parser: PDF (pdfplumber) and DOCX (python-docx) + LLM structuring
- `wizard_generate` background task type with iterative refinement (feedback loop)
- Dismissible setup banners on Home page (13 contextual prompts)
- Developer tab in Settings: tier override selectbox and wizard reset button
- Integrations tab in Settings: connect / test / disconnect all 12 non-Notion drivers
- HuggingFace token moved to Developer tab
- `params` column in `background_tasks` for wizard task payloads
- `wizard_complete`, `wizard_step`, `tier`, `dev_tier_override`, `dismissed_banners`,
  `effective_tier` added to UserProfile
- MkDocs documentation site (Material theme, 20 pages)
- `LICENSE-MIT` and `LICENSE-BSL`, `CONTRIBUTING.md`, `CHANGELOG.md`

### Changed
- `app.py` wizard gate now checks `wizard_complete` flag in addition to file existence
- Settings tabs reorganised: Integrations tab added, Developer tab conditionally shown
- HF token removed from Services tab (now Developer-only)

### Removed
- Dead `app/pages/3_Resume_Editor.py` (functionality lives in Settings → Resume Profile)

---

## [0.1.0] — 2026-02-01

### Added
- Initial release: JobSpy discovery pipeline, SQLite staging, Streamlit UI
- Job Review, Apply Workspace, Interviews kanban, Interview Prep, Survey Assistant
- LLM router with fallback chain (Ollama, vLLM, Claude Code wrapper, Anthropic)
- Notion sync, email sync with IMAP classifier, company research with SearXNG
- Background task runner with daemon threads
- Vision service (moondream2) for survey screenshot analysis
- Adzuna, The Ladders, and Craigslist custom board scrapers
- Docker Compose profiles: remote, cpu, single-gpu, dual-gpu
- `setup.sh` cross-platform dependency installer
- `scripts/preflight.py` and `scripts/migrate.py`
