Emit a rotating progress message every ~5s while the Apify scrape + Opus curation run,
so the long (30-40s) silent gap can't idle the response relay and improves the wait UX.
- OpportunityState model + repo: persist applied / dismissed / restored status and the
generated resume-builder doc ids (never re-pay to re-tailor). annotate_and_filter drops
dismissed + annotates applied/docs on read; handlers for dismiss/restore/mark_applied/
save_apply_docs.
- Unique decks per run (spend only for NEW jobs, no cache, no re-fetch+dedup band-aid):
per-(user,query) search cursor → LinkedIn page++ and Naukri incremental+stateKey (async
run path in apify_client, since the actor's crawl exceeds the run-sync window); seen-net
excludes already-shown ids (covers Foundit, which can't paginate).
- normalize: every board offsite-first (apply_url prefers the employer/ATS redirect over the
board listing) + offsite_apply flag; real company logos (_logo_url across Naukri logoPath /
LinkedIn company.logo / etc.).
- suggest.py: stage-aware engine (broad -> narrow -> role) — LLM (claude-haiku-4-5)
generates bubbles per stage from the picks so far. Profile-grounded, generalizes
to any field (English professor -> Senior Instructional Designer, etc.); None on
no input -> frontend keeps seeded bubbles
- handle_suggest_bubbles action (run off the event loop) + SUGGEST_MODEL config
- bundled: LLM work (sift/curate/suggest) now via asyncio.to_thread so the Redis
response can publish (fixes the loader hang); get_feed handler -> get_scout_feed
(avoids the course-service action collision)
- A1: fire the hard filter (Apify is precise); embedding vibe-ranker (embed.py)
blends with the floor-free white-box (rank.py) to sift ~90 -> top 18
- A2: Opus curator (curate.py) reads the 18 -> honest <=count + Gemini-voiced
report cards; graceful fallback to the white-box + templated cards
- LLM via opencode.ai/zen gateway (llm.py): Opus chat + direct-OpenAI embeddings
- Run LLM work off the event loop (asyncio.to_thread) so the Redis response publishes
- C1: dedicated Postgres (app/db/) persists the per-user feed; get_scout_feed
replays it so matches survive navigation/refresh
- match contract + report-card fields (schema.py); skills.py; tests/