Commit graph

227 commits

Author SHA1 Message Date
d138b27619 fix(vue-spa): suppress spring snap-back on processed cards
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.
2026-03-17 22:39:06 -07:00
1f5ab2df37 chore(vue-spa): dev API + Vite proxy for live data during development
- 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
2026-03-17 22:36:45 -07:00
75cc0760e1 feat(vue-spa): JobReviewView card stack with swipe gestures
- stores/review.ts: Pinia setup store — pending queue, undo stack,
  stoop-speed session timer (easter egg 9.2: 10 cards/60s)
- components/JobCard.vue: card content with match-score badge (colored
  pill), keyword-gap pills, expand/collapse description, footer with
  job URL + relative date; shimmer animation for ≥95% matches (ee 9.4)
- components/JobCardStack.vue: pointer-event drag with setPointerCapture,
  rolling 50ms velocity buffer for fling detection (600px/s + cos45°
  alignment), left/right color-tint overlay (red/green), spring snap-back
  on no-action, buffered exit animation before emitting approve/reject
- views/JobReviewView.vue: segmented status tabs, card stack for pending,
  list view for other statuses, action buttons, keyboard shortcuts
  (←/J reject, →/L approve, S skip, Z undo, ? help), help overlay,
  undo toast (5s), falcon stoop empty state (easter egg 9.3)
2026-03-17 22:30:33 -07:00
f3ce46e252 feat(web): implement design spec — peregrine.css, sidebar nav, HomeView
Applies the full design spec from circuitforge-plans/peregrine/2026-03-03-nuxt-design-system.md:

CSS tokens:
- Falcon Blue (#2B6CB0 / #68A8D8 dark) — was incorrectly using forest green
- Talon Orange (#E06820 / #F6872A dark) with --app-accent-text dark navy (never white)
- Full pipeline status token set (--status-pending/approve/reject/applied/synced/...)
- Match score tokens, motion tokens, type scale tokens
- Dark mode + hacker mode overrides

AppNav: sidebar layout (replaces top bar)
- Desktop ≥1024px: persistent sidebar with brand, links, hacker-exit, settings footer
- Mobile <1024px: bottom tab bar with 5 primary destinations
- Click-the-bird easter egg (9.6): 5 rapid clicks → ruffle animation
- Heroicons via @heroicons/vue/24/outline

App.vue:
- Skip-to-content link (a11y)
- Sidebar margin-left layout (desktop) / tab bar clearance (mobile)

HomeView: full dashboard implementation
- Pipeline metric cards (Pending/Approved/Applied/Synced/Rejected) with status colors
- Primary workflow buttons (Run Discovery, Sync Emails, Score Unscored) + sync banner
- Auto-enrichment status row
- Backlog management (conditionally visible)
- Add Jobs by URL / CSV upload tabs
- Advanced/danger zone in collapsible <details>
- Stoop speed toast easter egg (9.2)
- Midnight mode greeting easter egg (9.7)

WorkflowButton component with loading spinner, proper touch targets (min-height 44px)
Pinia jobs store (setup form) with counts + system status

Build: clean 2.28s, 0 errors
2026-03-17 22:00:42 -07:00
ae6021ceeb feat(web): Vue 3 SPA scaffold with avocet lessons applied
Sets up web/ Vue 3 SPA skeleton for issue #8, synthesizing all 15 gotchas
from avocet's Vue port testbed. Key fixes baked in before any component work:

- App.vue root uses .app-root class (not id="app") — gotcha #1
- overflow-x: clip on html (not hidden) — gotcha #3
- UnoCSS presetAttributify with prefixedOnly: true — gotcha #4
- peregrine.css alias map for theme variable names — gotcha #5
- useHaptics guards navigator.vibrate — gotcha #9
- Pinia setup store pattern documented — gotcha #10
- test-setup.ts stubs matchMedia, vibrate, ResizeObserver — gotcha #12
- min-height: 100dvh throughout — gotcha #13

Includes:
- All 7 Peregrine views as stubs (ready to port from Streamlit)
- AppNav with all routes
- useApi (fetch + SSE), useMotion, useHaptics, useEasterEgg composables
- Konami hacker mode easter egg + confetti + cursor trail
- docs/vue-spa-migration.md: full migration guide + implementation order
- Build verified clean (0 errors)
- .gitleaks.toml: allowlist web/package-lock.json (sha512 integrity hashes)
2026-03-17 21:24:00 -07:00
bf1dc39f14 fix(tests): update mock from inner_text() to text_content() in e2e helpers
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.
2026-03-17 20:33:55 -07:00
6e480c56ad chore(release): v0.6.2
Bugfix release — demo mode error fixes + Playwright E2E test harness.
See CHANGELOG.md for full details.
2026-03-17 20:08:55 -07:00
167fa8d84a fix(e2e): cloud auth via cookie, local port, Playwright WebSocket gotcha
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.
2026-03-17 20:01:42 -07:00
0758b70306 feat(e2e): add smoke + interaction tests; fix two demo mode errors
- 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.
2026-03-17 07:00:54 -07:00
c746acd89f feat(e2e): add conftest with Streamlit helpers, browser fixtures, console filter 2026-03-16 23:14:24 -07:00
4844c55292 feat(e2e): add BasePage and 7 page objects
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.
2026-03-16 23:14:20 -07:00
3be63f4a81 feat(e2e): add mode configs (demo/cloud/local) with Directus JWT auth 2026-03-16 23:07:34 -07:00
4d58d33567 feat(e2e): add ErrorRecord, ModeConfig, diff_errors models with tests 2026-03-16 23:06:02 -07:00
0317b9582a chore(e2e): scaffold E2E harness directory and install deps
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.
2026-03-16 22:58:47 -07:00
251f463522 chore(e2e): add xvfb-run wrapper for headed debugging sessions
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>/.
2026-03-16 22:57:21 -07:00
378c614d2f chore(e2e): add .env.e2e.example and gitignore .env.e2e
Committed credential template for E2E harness setup.
Directus e2e test user provisioned: e2e@circuitforge.tech (user ID: e2c224f7-a2dd-481f-bb3e-e2a5674f8337).
2026-03-16 22:41:24 -07:00
e1496f7827 chore: update CHANGELOG for v0.6.0 and v0.6.1 2026-03-16 21:48:52 -07:00
b51a4c9141 fix: keyword suggestions visibility, wizard identity autofill, dynamic sync label
- 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)
2026-03-16 21:47:37 -07:00
b6e16eb6e9 Merge pull request 'feat: push interview events to connected calendar integrations' (#20) from feature/calendar-push into main
Reviewed-on: #20
2026-03-16 21:45:06 -07:00
37d151725e feat: push interview events to connected calendar integrations (#19)
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
2026-03-16 21:31:22 -07:00
a60cf9ea8c fix: bootstrap resume_keywords.yaml on first cloud session
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.
2026-03-16 12:01:25 -07:00
f3e547dcd7 fix: auto-provision free license on first cloud session, fix score button in Docker
- 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
2026-03-16 11:51:15 -07:00
ed175e9fb4 feat: add pre-commit sensitive file blocker and support request issue template
Completes issue #7 (public mirror setup):
- .githooks/pre-commit: blocks sensitive filenames (.env, *.key, *.pem,
  id_rsa, credentials.json, etc.) and credential content patterns (private
  key headers, AWS keys, GitHub tokens, Stripe secret keys, generic API
  key assignments) from being committed
- .github/ISSUE_TEMPLATE/support_request.md: third issue template for
  usage questions alongside existing bug report and feature request
2026-03-16 11:30:11 -07:00
cd564c7abc fix: get_config_dir had one extra .parent, resolved to /config not /app/config 2026-03-15 17:14:48 -07:00
d8549bd356 Merge pull request 'feat: LLM queue optimizer — resource-aware batch scheduler (closes #2)' (#15) from feature/llm-queue-optimizer into main 2026-03-15 16:48:37 -07:00
922ede9258 fix: _trim_to_letter_end matches full name when no profile set
When _profile is None the fallback pattern \w+ only matched the first
word of a two-word sign-off (e.g. 'Alex' from 'Alex Rivera'), silently
dropping the last name. Switch fallback to \w+(?:\s+\w+)? so a full
first+last sign-off is preserved in no-config environments (CI, first run).
2026-03-15 16:43:27 -07:00
062f249ef9 ci: apt-get update before installing libsqlcipher-dev 2026-03-15 16:37:46 -07:00
5527fe9bf8 ci: install libsqlcipher-dev before pip install 2026-03-15 16:36:50 -07:00
722661058a ci: re-trigger after actions enabled 2026-03-15 15:54:27 -07:00
fd02c11441 ci: enable forgejo actions 2026-03-15 15:48:35 -07:00
1400a396ae ci: trigger runner 2026-03-15 15:39:45 -07:00
22696b4e50 docs: add Jobgether non-headless Playwright scraper to backlog
Xvfb-based Playwright can bypass Cloudflare Turnstile on jobgether.com.
Live inspection confirmed selectors; deferred pending Xvfb integration.
2026-03-15 11:59:48 -07:00
9c36c578ef feat: add Jobgether recruiter framing to cover letter generation
When source == "jobgether", build_prompt() injects a recruiter context
note directing the LLM to address the Jobgether recruiter using
"Your client [at {company}] will appreciate..." framing rather than
addressing the employer directly. generate() and task_runner both
thread the is_jobgether flag through automatically.
2026-03-15 09:45:51 -07:00
b3893e9ad9 feat: add Jobgether URL detection and scraper to scrape_url.py 2026-03-15 09:45:50 -07:00
ee054408ea feat: filter Jobgether listings via blocklist 2026-03-15 09:45:50 -07:00
d0987abc0e Merge pull request 'feat: LLM queue optimizer — resource-aware batch scheduler (closes #2)' (#13) from feature/llm-queue-optimizer into main
Reviewed-on: #13
2026-03-15 05:11:29 -07:00
c6fea9b3e2 feat: LLM queue optimizer complete — closes #2
Resource-aware batch scheduler for LLM tasks:
- scripts/task_scheduler.py (new): TaskScheduler singleton with VRAM-aware
  batch scheduling, durability, thread-safe singleton, memory safety
- scripts/task_runner.py: submit_task() routes LLM types through scheduler
- scripts/db.py: reset_running_tasks() for durable restart behavior
- app/app.py: _startup() preserves queued tasks on restart
- config/llm.yaml.example: scheduler VRAM budget config documented
- tests/test_task_scheduler.py (new): 24 tests covering all behaviors

Pre-existing failure: test_generate_calls_llm_router (issue #12, unrelated)
2026-03-15 05:01:24 -07:00
6e0105b0e8 feat(app): use reset_running_tasks() on startup to preserve queued tasks 2026-03-15 04:57:49 -07:00
690a1ccf93 feat(task_runner): route LLM tasks through scheduler in submit_task()
Replaces the spawn-per-task model for LLM task types with scheduler
routing: cover_letter, company_research, and wizard_generate are now
enqueued via the TaskScheduler singleton for VRAM-aware batching.
Non-LLM tasks (discovery, email_sync, etc.) continue to spawn daemon
threads directly. Adds autouse clean_scheduler fixture to
test_task_runner.py to prevent singleton cross-test contamination.
2026-03-15 04:52:42 -07:00
3e3c6f1fc5 feat(scheduler): add durability — re-queue surviving LLM tasks on startup 2026-03-15 04:24:11 -07:00
9b96c45b63 feat(scheduler): implement thread-safe singleton get_scheduler/reset_scheduler 2026-03-15 04:19:23 -07:00
a53a03d593 feat(scheduler): implement scheduler loop and batch worker with VRAM-aware scheduling 2026-03-15 04:14:56 -07:00
68d257d278 feat(scheduler): implement enqueue() with depth guard and ghost-row cleanup 2026-03-15 04:05:22 -07:00
46b229094a refactor(scheduler): use module-level _get_gpus directly in __init__ 2026-03-15 04:01:01 -07:00
415e98d401 feat(scheduler): implement TaskScheduler.__init__ with budget loading and VRAM detection 2026-03-15 03:32:11 -07:00
fe8da36e00 feat(scheduler): add task_scheduler.py skeleton with constants and TaskSpec 2026-03-15 03:28:43 -07:00
e3547cd998 docs(config): add scheduler VRAM budget config to llm.yaml.example 2026-03-15 03:28:26 -07:00
1616858729 refactor(tests): remove unused imports from test_task_scheduler 2026-03-15 03:27:17 -07:00
376e028af5 feat(db): add reset_running_tasks() for durable scheduler restart 2026-03-15 03:22:45 -07:00
12974f030c chore: add .worktrees/ to .gitignore
Prevents worktree directories from being tracked.
2026-03-14 16:30:38 -07:00