JAA-ATS-Tool / HISTORY.md
saitejatirunagari's picture
feat(ui): merge legacy platform checkboxes into unified platform selector
53c490d
|
Raw
History Blame
25.5 kB

Project History β€” Job Automation Agent

A running log of everything built, fixed, and changed. Most recent first.


2026-06-13 β€” Unified Platform Selector + ATS + HTML Rendering Fixes

Changes

  • Unified platform selector: Merged the 6 legacy checkboxes ("🌐 Job Platforms") and the grouped ever-jobs selector ("🌐 ever-jobs Platforms") into a single "🌐 Job Platforms" section. One place to search all 170 platforms. Selecting LinkedIn/Indeed/Glassdoor/Remotive/WeWorkRemotely/Naukri still routes to their dedicated high-quality scrapers; everything else goes through EverJobsScraper.
  • ATS min_score default: Changed slider default from 6 to 1 β€” LLM resumes now generated for ALL jobs regardless of score.
  • HTML rendering fix: Switched all 5 st.markdown(..., unsafe_allow_html=True) calls to st.html() β€” fixes raw <span>/<a> tags showing as plain text in job cards (Streamlit 1.45+ regression).

Modified Files

  • ui.py β€” removed 6 legacy checkboxes, renamed section label, updated platforms_cfg, updated pipeline routing to use unified all_platforms key

2026-06-13 β€” Phase 1: ever-jobs Integration (160+ Platforms)

New Features

  • 160+ job platforms via ever-jobs REST API integration (was 5 platforms)
  • Grouped platform selector in UI: Search Boards / ATS Platforms / Company Pages with st.multiselect search
  • India-focused defaults: 10 platforms pre-selected (LinkedIn, Naukri, Indeed, Glassdoor, Google, BDJobs, Internshala, Bayt, IIMJobs, Foundit)
  • Content fingerprint dedup: SHA-256 of (title+company) catches cross-platform duplicates where same job appears on LinkedIn AND Greenhouse with different URLs
  • Performance warning: UI shows warning when >30 platforms selected

New Files

  • src/ever_jobs_bridge/__init__.py β€” package init
  • src/ever_jobs_bridge/server.py β€” Docker/npm server lifecycle (start/stop/health)
  • src/ever_jobs_bridge/client.py β€” HTTP client for POST /api/jobs/search
  • src/ever_jobs_bridge/mapper.py β€” IJob JSON β†’ Job dataclass field mapper
  • src/ever_jobs_bridge/platforms.py β€” 170 platform catalog with group metadata
  • src/scrapers/ever_jobs.py β€” EverJobsScraper extending BaseScraper
  • vendor/ever-jobs/ β€” ever-jobs NestJS monorepo (cloned, gitignored)

Modified Files

  • src/job_history.py β€” added content_fp column + is_duplicate_by_content() function
  • config.py β€” added EVER_JOBS config block
  • ui.py β€” grouped platform selector + EverJobsScraper pipeline wiring + ever_jobs step
  • requirements.txt β€” added rapidfuzz>=3.0
  • .gitignore β€” added vendor/

R3 ATS Finding (Definitive)

ever-jobs "ATS" = Applicant Tracking System platforms that companies use to POST jobs (Greenhouse, Lever, Workday). This is NOT resume scoring. Our src/ats_scorer.py (70% JD keyword match + 30% resume quality) is the correct resume ATS scoring system and is UNCHANGED. No modifications to ats_scorer.py are needed.

Backward Compatibility

All existing scrapers (LinkedIn, Indeed, Glassdoor, Remotive, WeWorkRemotely) are UNTOUCHED. Pipeline flow is unchanged β€” ever-jobs is an additive parallel path.


Session 10 β€” 2026-06-13

New: 2 additional job platforms (Remotive + We Work Remotely)

  • src/scrapers/remotive.py β€” Remotive.io public JSON API. No auth needed. Fetches WFH/remote PM jobs globally (India-eligible: "Worldwide" / APAC filter).
  • src/scrapers/weworkremotely.py β€” We Work Remotely RSS feed scraper. Free-to-scrape, good volume of remote PM roles.
  • Both expose get_details_bulk() (no-op, descriptions come with the listing).
  • Both appear as checkboxes in the new UI; step-skip if unchecked.

Fixed: max_resumes slider removed β€” all jobs now get a resume

Previously max_resumes slider (default 15) silently capped LLM resumes even when 30–40 jobs were fetched. Fixed by passing max_llm_resumes=len(assessed_jobs) (effectively no cap). Every eligible job now gets an LLM-tailored resume.

Fixed: platform cap is now total-per-platform, not per-query

Old code applied max_results=N per roleΓ—location query. With 3 roles Γ— 3 locations you could get 9 Γ— 15 = 135 from one platform β€” far more than the user intended. New code: the outer loop breaks once platform_jobs reaches max_jobs_per_platform, and the per-query max_results is set to remaining = cap - len(platform_jobs).

Fixed: Google Sheets error messages are now informative

  • FileNotFoundError (no credentials) now emits a clear "run setup_google.py" hint
  • Full error text (up to 120 chars) logged to the live UI log, not just the file log
  • A "Google Sheet status" indicator (βœ“/⚠) shown in the Configure section before run

New: run history (save + load past runs)

  • src/run_history.py β€” saves each completed run as JSON in data/output/run_history/run_YYYY-MM-DD_HH-MM-SS.json. Summary fields stored without jobs for fast listing; full jobs on load.
  • History is auto-saved at the end of every pipeline run.
  • UI "Load" button restores any past run's results to the active session without rerunning the pipeline.

New: complete UI redesign (ui.py)

  • No sidebar β€” all controls now live inline in the main area.
  • History panel β€” top-right "πŸ“œ History" button opens a panel listing all past runs with stats (jobs, high-priority count, ATS before/after). Click "Load" to restore any run.
  • Configure section β€” expandable card with resume upload, roles, locations, platform checkboxes, days, max-per-platform, and min score. Google Sheet status shown inline.
  • Start button β€” centered, prominent, full-width.
  • Step timeline β€” CSS grid layout (auto-fill columns), fits all platforms.
  • Results tab β€” job cards β€” top 10 shown as visual cards (title, company, ATS before/after, salary, apply link). Switch to "Full Table" for all jobs.
  • Download fix β€” zip now contains only the current run's date subfolder (not all historical date folders). Eliminates the "90 files for 30 jobs" confusion (per run: 30 DOCX + 30 PDF = 60 files as expected).
  • Metrics row β€” Total | High | Medium | LLM Resumes | PDFs | Avg ATS After.
  • Welcome state shown when no results are loaded yet.

Fixed: test_mode β†’ False in config.py

Was accidentally left True, capping the pipeline at 10 jobs per test run.


Session 9 β€” 2026-06-13

Fixed: UI stuck at "0% β€” Starting…" while pipeline ran fine in background

Symptom: Click Start β†’ UI shows 0% and all steps "Waiting…" forever, but the console/logs show the pipeline scraping, assessing 41 jobs, and generating resumes at 91–94% ATS. Users clicked Start again thinking it was dead β†’ duplicate pipeline threads (Thread-8 + Thread-17 in the logs).

Root cause: _progress_q = queue.Queue() was created at MODULE level in ui.py with a comment claiming module globals survive reruns. They do NOT β€” Streamlit re-executes the entry script top-to-bottom on EVERY rerun, creating a brand-new empty Queue each time. The background thread kept writing progress to the original queue; the UI drain loop polled the new empty one. Nothing ever arrived.

Fix (ui.py):

  • Queue now lives in st.session_state["progress_q"] β€” the only store that survives reruns within a session
  • run_pipeline receives the queue as an explicit default arg (_q=_progress_q) and shadows the module helpers, so the thread always writes to the queue the drain loop reads β€” even across reruns and multiple sessions
  • st.session_state["current_log_file"] was being set FROM the background thread (the "missing ScriptRunContext" warning, silently broken) β€” now sent through the queue as a ("logfile", path) message handled by the drain loop

Verified with Streamlit AppTest: queue identity preserved across reruns; clicked Start in the test harness β€” UI received 7 log messages, step cards updated (resume βœ… β†’ profile βœ… β†’ linkedin ⏳), progress bar at 15%.

Files changed: ui.py, HISTORY.md


Session 8 β€” 2026-06-12

Major performance + quality overhaul: parallel resumes, PDF output, full JD fetching

Root causes of "taking lot of time, not going forward":

  1. LLM resumes generated ONE at a time (50–150s each Γ— 30 = up to an hour, UI frozen)
  2. Indeed launched a full Chromium browser PER job description (~10s overhead each)
  3. Glassdoor NEVER fetched descriptions (no detail method existed)
  4. LinkedIn job_id regex broken β€” LinkedIn switched to slug URLs (/jobs/view/title-at-company-4423634421), so ALL detail fetches 404'd β†’ no JDs
  5. UI capped search to 3 roles Γ— 2 locations

Fixes:

  • src/resume_customizer.py β€” LLM resumes now generated IN PARALLEL via ThreadPoolExecutor (6 workers, round-robin across phase2 model API keys). Per-resume progress_cb streams live status to the UI.
  • src/scrapers/linkedin.py β€” fixed job_id extraction (slug URLs); new get_details_bulk() fetches ALL descriptions with 4 parallel HTTP workers
  • src/scrapers/indeed.py β€” new get_details_bulk(): ONE browser session for all job descriptions instead of one browser per job
  • src/scrapers/glassdoor.py β€” new get_details_bulk() with Cloudflare-challenge wait + JSON-LD JobPosting parsing (Glassdoor still intermittent β€” bot-hostile)
  • ui.py β€” searches ALL selected roles Γ— locations (caps removed); cross-platform dedup by (title, company) in addition to URL; live per-resume progress

ATS quality fixes (tailored resumes were sometimes scoring LOWER than original):

  • src/llm_client.py β€” validates LLM customization (summary >50 chars, β‰₯5 skills), retries once, unwraps JSON arrays, max_tokens 3000β†’4000
  • resume_customizer.py β€” optimization loop now: scores with same extra_kw as final report Β· skips empty customizations Β· retries fall back to Kimi Β· rewrites BEST attempt to disk (was keeping last) Β· GUARANTEE: if LLM result scores below the original resume, ships keyword-injected template instead (After β‰₯ Before always)
  • _inject_missing_keywords() rewritten β€” now injects the ACTUAL missing JD keywords (was injecting generic PM keywords that didn't move the JD-match score)

PDF output (new):

  • src/pdf_writer.py β€” DOCXβ†’PDF: one Word COM session per batch on Windows (perfect fidelity), reportlab re-render fallback on Linux/HF Spaces
  • Every resume now saved as both .docx and .pdf in data/output/resumes/YYYY-MM-DD/
  • UI: PDF + DOCX download buttons per job; zip download includes PDFs
  • requirements.txt: + reportlab, docx2pdf (win32 only)

Files changed: src/pdf_writer.py (new), src/resume_customizer.py, src/llm_client.py, src/scrapers/linkedin.py, src/scrapers/indeed.py, src/scrapers/glassdoor.py, ui.py, requirements.txt, README.md, HISTORY.md


Session 7 β€” 2026-06-12

File-based logging system + Logs tab in UI

Problem: Pipeline was failing on HF Spaces with no way to see why. Queue-based live log only showed last 30 messages and swallowed full tracebacks.

What was built:

src/app_logger.py β€” New centralized logger:

  • Writes every run to data/logs/run_YYYY-MM-DD_HH-MM-SS.log
  • Captures ALL Python logging output (INFO, WARNING, ERROR, DEBUG)
  • Redirects stdout/stderr via _TeeStream so print() and Playwright output are also captured
  • In-memory ring buffer (500 lines) for UI access without file I/O
  • list_log_files() returns all previous runs, newest first

ui.py changes:

  • New πŸ“‹ Logs tab (5th tab)
    • Color-coded viewer: errors=red, warnings=yellow, INFO done=green, info=blue
    • Slider to show 50–500 lines
    • Toggle to show/hide DEBUG lines
    • Auto-refresh every 2s while pipeline is running
    • Download button for raw .log file
    • Previous run selector to load any past log
    • Error/warning counts in footer
  • Pipeline thread now calls app_logger.setup() at start β†’ creates timestamped log file
  • Every scrape attempt logged with role + location + raw result count
  • Full tracebacks on scrape errors (logging.error(..., traceback))
  • Fatal pipeline exceptions logged in full, not truncated to 400 chars
  • current_log_file added to session state defaults

Dockerfile β€” Added data/logs to mkdir -p list

Files changed: src/app_logger.py (new), ui.py, Dockerfile, HISTORY.md, README.md


Session 6 β€” 2026-06-11

GitHub push + Hugging Face Spaces deployment prep

Code pushed to GitHub: https://github.com/saitejatiru/JAA-ATS-Tool

HF Spaces files added:

  • README.md β€” prepended YAML frontmatter (sdk: streamlit, app_file: ui.py)
  • packages.txt β€” Chromium system dependencies for Playwright on Linux
  • .gitignore β€” excludes secrets (google_token.json, .env, resumes, output data)
  • .env.example β€” documents all 9 NVIDIA API keys + Google Sheet ID
  • requirements.txt β€” added gspread, google-auth, google-auth-oauthlib, google-api-python-client

ui.py changes for HF Spaces:

  • Playwright install: @st.cache_resource function installs Chromium once per server lifetime
  • Google credentials bootstrap: reads GOOGLE_CREDENTIALS_JSON env var and writes to google_credentials.json on startup

Files changed: README.md, requirements.txt, packages.txt, .gitignore, .env.example, ui.py


Session 5 β€” 2026-06-11

ATS Before/After in Excel + Verbose Resume Error Logging

Excel reporter fixed:

  • Added ATS Before (%), ATS After (%), ATS Improvement columns to all sheets (was completely missing)
  • Column order: Relevance Score β†’ ATS Before β†’ ATS After β†’ ATS Improvement β†’ Skills Match β†’ …
  • _pct() helper: shows "45%" or "β€”" for null; improvement shows "+37pp" or "β€”"
  • Column indices for score badge (9), URL hyperlink (23), priority color (15) updated to match new order

Resume error visibility:

  • Added explicit tqdm.write() on success: "βœ“ LLM resume: Google β†’ ATS 45% β†’ 82% (+37pp)"
  • Added traceback.format_exc() on failure so exact error is visible in the terminal
  • Fallback ATS scoring (original resume score) always runs on failure so sheet never shows blank

Confirmed working (run completed 2026-06-11 11:16):

  • 7 LLM-tailored + 2 template resumes generated in data/output/resumes/2026-06-11/
  • Google Sheet updated with all 10 jobs
  • Files: Google_Product Manager I Ads.docx, Instagram, Workday, Giga, Denave, Tessera, Latinem

Files changed: src/excel_reporter.py, src/resume_customizer.py


Session 4 β€” 2026-06-11

ATS Before/After Fix + Best Resume Prompt

ATS Before/After not showing β€” root causes fixed:

  1. score_resume() was calling Kimi AGAIN (via fast_model_cfg) during ATS scoring β€” after already using Kimi for 9 resume generations, rate limits caused silent failures and blank scores. Fixed: removed fast_model_cfg from scoring calls; use pre-extracted keywords from assessment phase only.
  2. On resume generation failure, ats_score_before/after was never set at all. Fixed: fallback block now always computes and stores ATS scores even if DOCX generation fails.

Best ATS resume β€” prompt redesigned:

  • Old prompt: generic instructions, 1500 char JD limit, 2000 token output
  • New prompt:
    • Explicit mandatory keyword list with instruction "MUST include ALL of these"
    • Rules enforce: exact JD language mirroring, action verbs on every bullet, quantified metrics required
    • JD limit raised to 2000 chars, resume to 2500 chars
    • Output tokens raised to 3000 (room for full detailed resume)
    • 15 core competencies (was 12)
    • More specific bullet format: "β€’ Led X resulting in Y% improvement"

Profile extraction speed fix:

  • Step 2 was blocked on GLM 5.1 (234s). Now tries Kimi-K2.6 (5s) first via extract_profile_summary_fast(cfg, ...) with fallback to GLM.
  • Added LLMClient.extract_profile_summary_fast(cfg, resume_text) method.

Files changed: src/llm_client.py, src/resume_customizer.py, main.py


Session 3 β€” 2026-06-11

Streamlit UI Fixes + LLM Resume Root-Cause Fix

4 issues addressed:

Issue Fix
LLM resumes = 0 Root cause: ATSScorer class imported but never existed β†’ silent ImportError. Fixed by replacing with score_resume() function. Also fixed PM_DOMAIN_KEYWORDS β†’ PM_BASE_KEYWORDS + PM_TOOLS
Fast model for resume generation Added LLMClient._call_with_cfg() + customize_resume_fast(cfg, ...). Now uses Kimi-K2.6 (5s) instead of GLM (234s)
Date-based local resume folders Resumes now save to data/output/resumes/YYYY-MM-DD/. No more Google Drive upload
Sheet headers missing gsheets.py now detects missing header row and inserts at row 1 using ws.insert_row() even when data already exists
Test limit 5 β†’ 10 jobs

Streamlit UI updated:

  • Fixed customize_for_jobs() parameter mismatch (min_score β†’ min_score_for_llm, max_count β†’ max_llm_resumes)
  • Resume zip download now scans all date subfolders (Path.rglob("*.docx"))
  • Results table now shows ATS Before, ATS After, ATS Gain columns
  • Job Details tab shows ATS before/after inline
  • fast_model_cfg wired into UI pipeline (Kimi-K2.6 for LLM keywords + resume tailoring)

To launch UI:

streamlit run ui.py
# Opens at http://localhost:8501

Session 2 β€” 2026-06-11

Test Run Completed Successfully βœ…

Results:

  • LinkedIn 60 + Indeed 18 + Glassdoor 13 jobs scraped (capped to 5 in test mode)
  • Assessment: 16 seconds for 5 jobs (Kimi K2.6, single batch)
  • Top job: Associate Product Manager (Adtech) at MakeMyTrip β€” Score 8/10
  • Google Sheet updated: https://docs.google.com/spreadsheets/d/1Ehxt3eortehbtySdtgcSrMhCqmxIMUAmvRqSkII0HJk/edit
  • Excel saved: data/output/reports/job_report.xlsx
  • 5 jobs marked in dedup store (SQLite) β€” won't reappear next run

Bugs found during test run:

  1. bulk_mark_seen AttributeError β€” Job dataclass doesn't have .get(). Fixed with isinstance(job, dict) + getattr().
  2. Drive upload: 'Client' object has no attribute 'auth' β€” gspread doesn't expose Drive API directly. Still pending fix.
  3. LLM resumes = 0 β€” resume customization calling GLM (234s), timing out silently. Still pending fix (need to switch to Kimi/Step).

ATS Scoring β€” Rebuilt from Scratch

Problem: Original ATS scored resume quality (structural), not job-description match. A generic resume scored the same for any job.

Solution: Resume-Matcher approach

  • extract_jd_keywords(jd_text) β€” pulls keywords from the specific JD
  • jd_match_score(resume_text, jd_text) β€” word-boundary regex matching (not substring)
  • Final score: 70% JD match + 30% resume quality
  • Benchmark: EdTech JD β†’ 90%, SAP/ERP JD β†’ 53% (correctly differentiates)

Files changed: src/ats_scorer.py (full rewrite)


Speed Optimization β€” 10-Model Parallel Pool

Problem: GLM 5.1 alone = 234s/job. 110 jobs = 6+ hours.

Solution: ModelPool with worker queue

  • Phase 1 (keyword scoring): instant, no LLM
  • Phase 2 (LLM assessment): 7 fast models compete for batches of 8 jobs
  • Kimi K2.6 handles most work at ~5s/batch
  • Wall clock for 110 jobs: ~3–5 minutes

Files changed: src/model_pool.py, src/job_assessor.py


Added Models (cumulative)

Model API Key Env Speed Phase 2
GLM-5.1 NVIDIA_API_KEY ~234s No
Kimi-K2.6 NVIDIA_API_KEY_3 ~5s Yes
Step-3.7-Flash NVIDIA_API_KEY_8 ~8-35s Yes
Qwen3.5-397b NVIDIA_API_KEY_7 ~9s Yes
Qwen3.5-122b-v2 NVIDIA_API_KEY_7 ~12s Yes
GPT-OSS-120b NVIDIA_API_KEY_5 ~11s Yes
Qwen3.5-122b NVIDIA_API_KEY_4 ~40s Yes
DeepSeek-v4-Pro NVIDIA_API_KEY_2 ~42s Yes
DeepSeek-v4-Flash NVIDIA_API_KEY_6 ~229s No
MiniMax-M2.7 NVIDIA_API_KEY_2 ~908s No

Odysseus Deep Research Engine

Integrated the Odysseus IterResearch engine for company research.

Architecture: Think β†’ Search β†’ Extract β†’ Synthesize loop

  • DuckDuckGo search with Bing fallback
  • 12h page content cache (data/research_cache/)
  • GLM 5.1 for all LLM steps
  • asyncio.to_thread + OpenAI SDK (not raw httpx) for proper timeout handling

Files: src/research/deep_researcher.py, src/research/search.py, src/odysseus_llm_core.py


Google Sheets Integration

Sheet columns: Batch Date, Rank, Job Title, Company, Location, Platform, Salary, Experience, Relevance Score, ATS Before (%), ATS After (%), ATS Improvement, Resume Quality, Priority, Matching Skills, Missing Skills, AI Recommendation, Apply Link, Resume Link, Application Status, Date Applied, Notes

Auth approach: OAuth (user login via browser, token saved to google_token.json)

File: src/gsheets.py


PM-Only Filter

All scrapers enforce BaseScraper.is_pm_role(title) at scrape time:

  • Title must contain "product"
  • Must match PM patterns: product manager, product owner, APM, senior PM, etc.
  • Blocked: engineer, developer, teacher, sales, marketing manager, project manager, data analyst, etc.
  • Test result: 16/16 accuracy on mixed title set

File: src/scrapers/base.py


Job Deduplication

SQLite store at data/job_history.db:

  • is_duplicate(url, days=30) β€” skip jobs seen in last 30 days
  • bulk_mark_seen(jobs) β€” handles both dict and Job dataclass objects
  • Stats: get_stats(), housekeep: clear_old_entries(days=90)

File: src/job_history.py


Bugs Fixed (Session 2)

Bug Fix
Kimi returns ' ["[7,6,8]"]' (wrapped string) _parse_score_array() unwraps ["[string]"] format
score_resume_against_jd ImportError Added backward-compat alias in ats_scorer.py
bulk_mark_seen AttributeError on Job dataclass isinstance(job, dict) check + getattr() for dataclass
GLM timeout in research engine Switched to OpenAI SDK via asyncio.to_thread(), timeout=300s
Windows UnicodeEncodeError on box-drawing chars sys.stdout = io.TextIOWrapper(encoding="utf-8", errors="replace")
Google OAuth "Access blocked" (403) Add email as test user in GCP OAuth consent screen

Session 1 β€” Initial Build

Project Created

Goal: Automate PM job search β†’ AI assessment β†’ ATS resume β†’ Google Sheet.

Stack chosen:

  • Scraping: requests + BeautifulSoup for LinkedIn; Playwright for Indeed/Glassdoor (JS-rendered)
  • AI: NVIDIA API (OpenAI-compatible endpoint), starting with GLM 5.1
  • Resume: pdfplumber (parse) + python-docx (generate DOCX)
  • Storage: SQLite (dedup), gspread (Google Sheets), Google Drive API
  • UI: Streamlit

Scrapers Built

Platform Method Status
LinkedIn requests + BeautifulSoup βœ… Working
Indeed Playwright (JS rendering) βœ… Working
Glassdoor Playwright βœ… Working
Naukri Attempted Playwright + requests ❌ Blocked by Akamai (returns 406 / "Access Denied")

Key fixes during scraper development:

  • LinkedIn: company from span[data-testid=company-name], title from aria-label (strip "full details of" prefix)
  • Indeed: div.job_seen_beacon via BS4 on page.content() after wait_until="networkidle"
  • Glassdoor: li[data-jobid] cards, span[class*="compactEmployerName"] for company
  • Playwright sync_playwright conflict: two scrapers fighting over one context β†’ fixed by creating context per search() call

Resume Parsing + Customization

  • ResumeParser β€” pdfplumber extracts text from PDF
  • LLMClient β€” GLM 5.1 extracts structured profile JSON + compact profile string
  • ResumeCustomizer β€” iterative LLM optimizer:
    1. LLM tailors resume to JD
    2. Score it β†’ if < 95%, feed gap report back to LLM
    3. Up to 3 attempts
    4. Fallback: _inject_missing_keywords() to force 95%+
  • Resume filename: {Company}_{JobTitle}.docx (no score in filename, per user request)
  • Score stored in Google Sheet, not filename

Streamlit UI

Four tabs:

  1. Search β€” configure roles/locations, toggle platforms, run pipeline
  2. Results β€” table view of all jobs with color-coded scores
  3. Job Details β€” expand any job for full AI breakdown + resume download
  4. Deep Research β€” Odysseus engine with quick-preset buttons from top jobs

Live progress via _progress_q queue + st.rerun() polling loop.

File: ui.py


Pending (as of 2026-06-11)

Task Priority Notes
Fix Google Drive upload 'Client' object has no attribute 'auth' High gspread doesn't expose Drive auth directly
Fix LLM resume generation = 0 (GLM timeout) High Switch ResumeCustomizer to use Kimi/Step instead of GLM
Set test_mode: False in config.py High For full 100+ job production run
LLM-extracted JD keywords in ATS scoring Medium Use Kimi/Step to semantically extract required skills from each JD β†’ upgrade ATS from 7.5/10 to ~9/10 accuracy
Add saitejatirunagari@gmail.com as GCP test user Done (user action) https://console.cloud.google.com/apis/credentials/consent