# Changelog

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

---

## [Unreleased]

---

## [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`
