# Changelog All notable changes to Peregrine are documented here. Format follows [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). --- ## [Unreleased] --- ## [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`