Setup-form Pinia store with per-stage computed lanes, optimistic status
mutation on move, and API-error rollback. Shallow-copies API response
objects on fetch to prevent shared-reference mutation across tests.
Adds GET /api/interviews to fetch all pipeline-stage jobs in one call,
and POST /api/jobs/:id/move to transition a job between kanban statuses
with automatic timestamp stamping (or rejection_stage capture).
- router: add /apply/:id → ApplyWorkspaceView (lazy-loaded)
- ApplyView.vue: approved job list sorted by match score; badges for
match %, remote, and cover-letter draft status; links to workspace
- ApplyWorkspaceView.vue: two-panel desktop layout (sticky job details
left, editor right); cover letter state machine (none/queued/running/
ready/failed); auto-resize textarea; word count toolbar; Save button
with dirty tracking; Download PDF (programmatic <a> click, named file);
Generate with AI + Retry; Mark as Applied + Reject Listing actions;
polling loop for in-flight generation tasks; toast on action
- HomeView.vue: split combined "Archive Pending + Rejected" into three
separate per-status archive buttons (only shown when count > 0)
- dev-api.py: add GET /api/jobs/:id, POST /api/jobs/:id/applied,
PATCH /api/jobs/:id/cover_letter, POST .../cover_letter/generate
(wires submit_task), GET .../cover_letter/task (poll), GET .../pdf
(reportlab); has_cover_letter field on list + detail responses
When a new job prop arrives after approve/reject, the watch cleared the
exit-transform inline style while the spring transition was still active,
causing the card to animate from offscreen back to center before the new
card rendered. Fix: set transition:none before clearing the style, then
restore it on the next rAF — browser paints the new position first.
- dev-api.py: minimal FastAPI on :8601 reading /devl/job-seeker/staging.db
Endpoints: GET /api/jobs, /api/jobs/counts, POST /api/jobs/{id}/approve|reject|revert,
GET /api/system/status, /api/config/user
- vite.config.ts: server.proxy /api/* → localhost:8601; host: 0.0.0.0 for LAN access
get_page_errors() was switched to text_content() to capture errors in
CSS-hidden elements (collapsed Streamlit expanders). Two unit test mocks
still stubbed inner_text() — causing CI failures because MagicMock()
returned a non-string from text_content(), breaking the "boom" in message
content assertion.
E2E harness fixes to get all three modes (demo/cloud/local) passing:
- conftest.py: use ctx.add_cookies() for cloud auth instead of
ctx.route() or set_extra_http_headers(). Playwright's route() only
intercepts HTTP; set_extra_http_headers() explicitly excludes
WebSocket handshakes. Streamlit reads st.context.headers from the
WebSocket upgrade, so cookies are the only vehicle that reaches it
without a reverse proxy.
- cloud_session.py: fall back to Cookie header when X-CF-Session is
absent — supports direct access (E2E tests, dev without Caddy).
In production Caddy sets X-CF-Session; in tests the cf_session cookie
is set on the browser context and arrives in the Cookie header.
- modes/cloud.py: add /peregrine base URL path (STREAMLIT_SERVER_BASE_URL_PATH=peregrine)
- modes/local.py: correct port from 8502 → 8501 and add /peregrine path
All three modes now pass smoke + interaction tests clean.
- Add tests/e2e/test_smoke.py: page-load error check for all pages
- Add tests/e2e/test_interactions.py: click every interactable, diff
errors, XFAIL expected demo failures, flag regressions as XPASS
- Fix conftest get_page_errors() to use text_content() instead of
inner_text() so errors inside collapsed expanders are captured with
their actual message text (inner_text respects CSS display:none)
- Fix tests/e2e/modes/demo.py base_url to include /peregrine path prefix
(STREAMLIT_SERVER_BASE_URL_PATH=peregrine set in demo container)
App fixes surfaced by the harness:
- task_runner.py: add DEMO_MODE guard for discovery task — previously
crashed with FileNotFoundError on search_profiles.yaml before any
demo guard could fire; now returns friendly error immediately
- 6_Interview_Prep.py: stop auto-triggering LLM session on page load
in demo mode; show "AI features disabled" info instead, preventing
a silent st.error() inside the collapsed Practice Q&A expander
Both smoke and interaction tests now pass clean against demo mode.
BasePage provides navigation, error capture, and interactable discovery
with fnmatch-based expected_failure matching. SettingsPage extends it
with tab-aware discovery. All conftest imports are deferred to method
bodies so the module loads without a live browser fixture.
Add pytest-playwright and pytest-json-report to requirements.txt; create
tests/e2e/ skeleton (modes/, pages/, results/) with __init__.py files and
.gitkeep; add results subdirs to .gitignore.
Adds `e2e` subcommand to manage.sh supporting headless (default) and
headed (E2E_HEADLESS=false via xvfb-run) Playwright test runs with
per-mode JSON report output under tests/e2e/results/<mode>/.
- Settings: add st.rerun() after storing _kw_suggestions so chips appear
immediately without requiring a tab switch (#18)
- Setup wizard step 4: prefill name/email/phone from parsed resume when
identity fields are blank; saved values take precedence on re-visit (#17)
- Home dashboard: sync section shows provider name when Notion is connected,
or 'Set up a sync integration' with a settings link when not configured (#16)
Implements idempotent calendar push for Apple Calendar (CalDAV) and
Google Calendar from the Interviews kanban.
- db: add calendar_event_id column (migration) + set_calendar_event_id helper
- integrations/apple_calendar: create_event / update_event via caldav + icalendar
- integrations/google_calendar: create_event / update_event via google-api-python-client;
test() now makes a real API call instead of checking file existence
- scripts/calendar_push: orchestrates push/update, builds event title from stage +
job title + company, attaches job URL and company brief to description,
defaults to noon UTC / 1hr duration
- app/pages/5_Interviews: "Add to Calendar" / "Update Calendar" button shown
when interview date is set and a calendar integration is configured
- environment.yml: pin caldav, icalendar, google-api-python-client, google-auth
- tests/test_calendar_push: 9 tests covering create, update, error handling,
event timing, idempotency, and missing job/date guards
New cloud users got a "resume_keywords.yaml not found" warning in
Settings → Skills & Keywords because the file was never created during
account provisioning. resolve_session() now writes an empty scaffold
(skills/domains/keywords: []) to the user's config dir on first visit
if the file doesn't exist, consistent with how config/ and data/ dirs
are already created. Never overwrites an existing file.
- cloud_session.py: add _ensure_provisioned() called in resolve_session() so
new Google OAuth signups get a free Heimdall key created on first page load;
previously resolve returned "free" tier but no key was ever written to
Heimdall, leaving users in an untracked state
- Home.py: replace conda run invocation in "Score All Unscored Jobs" with
sys.executable so the button works inside Docker where conda is not present