Commit Graph

3 Commits

Author SHA1 Message Date
JasonFraser a25909a459 Add per-seat trip watchlist, fix cross-URL caching bug, and improve results table UX
The biggest fix: change-detection and the fast-path cache lookup were
scoped only by competitor_id, not by which specific trip page was
scraped. Since trip-intent matching lets a PM scrape a different URL per
run, get_last_hash_for_url() compared each new scrape against whatever
page happened to be hashed most recently for that competitor — almost
always a different page — so change_detected never settled back to
False and the true no-scrape fast path was unreachable in practice.
Scoped scrape_runs/products lookups by (competitor_id, url) instead, and
threaded a cleaned confirmed_url consistently through needs_refresh(),
detect_change(), and analyse_from_cache(). Verified end-to-end with a
regression test reproducing the exact reported scenario.

New: a per-seat, tier-capped trip watchlist (1 trip/seat at Analyse).
The nightly cron now re-scrapes only the specific trips a PM chose to
track instead of every competitor's homepage (a page already flagged
low-signal by is_generic_homepage()) — bounded, predictable volume, and
every resulting alert is about a trip someone actually cares about.
Reuses the existing confirmed_url/per-url caching machinery with zero
changes needed to jobs.py's job loop. Also fixes a real gap:
auth_service.py resolved the caller's active seat only to validate the
JWT and then discarded it — every route before this only ever needed
tenant_id. get_seat_from_request() now returns it.

Also fixed the Firecrawl /map timeout (15s → 30s; a large real site's
sitemap fetch took ~21s and was silently read as "no match found"), a
job left stuck at "Stopping…" forever after a worker process was killed
mid-run (status endpoint now self-heals via the underlying RQ job
state), and several results-table issues: hardcoded "G Adventures" in
the extraction prompt and a literal "G" in the comments column label
(replaced with the real tenant name), non-clickable trip links, a
cache-source badge that was silently clipped on most columns, a missing
view into the client's own scraped trip data, and bolding for
Strengths/Weaknesses/Differences/Pricing in the comments text.

370+ new/updated backend tests; full suite green.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-04 22:03:54 -04:00
JasonFraser ad1d7f1815 Fix critical scrape-targeting bug and harden proxy, matching, and dashboard reliability
The highest-impact fix: run_research_job() only ever read entry['id'] from
confirmed competitors, silently discarding the Firecrawl-matched or
manually-swapped URL — every scrape fell back to the competitor's bare
root domain regardless of what was confirmed. This explains most of the
"unpredictable/wrong data" behavior seen against the old app.py. Threaded
confirmed_url through jobs.py -> analysis_service.py so the actually
confirmed page is what gets scraped.

Other fixes bundled in this pass:
- Proxy layer: Webshare rotating-gateway country code lowercased (was
  silently failing auth), Bright Data port corrected to 33335 (current
  cert), dedicated-IP candidate rotation added before falling back to the
  shared gateway/Bright Data escalation.
- Trip-intent matching: score_and_rank_urls() now hard-gates on
  destination keyword and excludes bare root-domain candidates, instead
  of letting duration/category signals alone push a wrong-country or
  homepage URL over the confidence threshold.
- force_refresh cache-bypass toggle added (opt-in, collapsed "Advanced
  options"), plus a "Skip this competitor" affordance for unmatched URLs.
- De-hardcoded "G Adventures" (the hackathon reference client) out of the
  extraction prompt — now uses the real tenant's company name.
- RunHistory/Apps-Script "latest results" queries filtered to
  triggered_by = "manual" so per-competitor content-hash bookkeeping rows
  no longer drown out real batch runs.
- Dashboard fixes: stuck-state bug on revisiting the Analyse page,
  AccountPage/BattlecardsPage restacked to single-column layout, Alerts
  moved to a header bell dropdown, invisible Export/Run Analysis icons
  (fill -> stroke), tour popup width mismatch, horizontal-scroll bug on
  cards (missing min-width: 0 in flex layout).
- New PocketBase migrations for scrape_runs (content_hash fields,
  tab_type, created_by_email) and products (departures, start_days).

335 backend tests passing.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-04 17:33:05 -04:00
JasonFraser 6a259a5012 Phase 2: refactor legacy app.py into core/industries/config structure
Implements CLAUDE.md Build Order Phase 2 — same scraping/extraction
logic as the legacy app.py, reorganized into the three-layer structure
and upgraded per §10's explicit instructions:
- core/scraper.py — playwright-stealth replaces the manual
  add_init_script stealth block; smart proxy (Webshare→Bright Data)
  wired via core/proxy_service.py with block-detection + auto-escalation;
  check_robots_txt() gate added per §30
- core/extractor.py — OpenRouter model cycling, merging the legacy
  JSON-repair fallback chain with the spec's env-driven free/paid
  model list and Sentry breadcrumb on paid-fallback use
- core/ai_fetch.py — AI web fetch fallback, ported unchanged
- industries/adventure_travel/{fields,prompt}.py ported unchanged;
  schema.py added as the future V2 custom-fields attachment point
- config/client.py — gspread Sheet write-back, ported unchanged
- services/analysis_service.py now sources CACHE_STALENESS_HOURS from
  core/scraper.py, its canonical location per §7

Dropped the legacy's commented-out Groq/Gemini alternates — OpenRouter
is the sole active provider and the dead code added nothing.

requirements.txt: relaxed playwright to >=1.55.0 and pinned
playwright-stealth==1.0.6 + setuptools<81 — Python 3.14 has no
prebuilt wheels for the originally-listed pins, and playwright-stealth
2.x replaced the stealth_async(page) API §10 documents verbatim with a
class-based one.

192 tests, 94% coverage. Also fixes a real bug surfaced by this phase:
several test helpers mutated core.extractor / industries.adventure_travel.prompt
via raw setattr instead of monkeypatch.setattr, silently corrupting
shared module state across tests once those modules became real.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-01 12:44:35 -04:00