diff --git a/backend/api/competitors.py b/backend/api/competitors.py index 3379f33..7439ca5 100644 --- a/backend/api/competitors.py +++ b/backend/api/competitors.py @@ -361,6 +361,16 @@ async def enable_scraper( # MANUAL SCRAPE TRIGGER # ============================================ +def _run_scheduled_sync(): + """Run the full scheduled scrape job in a background task.""" + _SCRAPE_PENDING.clear() # Task is now running — allow new submissions to queue + try: + from jobs.scrape_booking_rates import run_scheduled_booking_scrape + run_scheduled_booking_scrape() + except Exception as e: + logger.error(f"Triggered scheduled scrape failed: {e}", exc_info=True) + + def run_scrape_sync(from_date: date, to_date: date): """Run scrape in sync context for background task.""" import asyncio @@ -434,6 +444,34 @@ async def trigger_manual_scrape( } +@router.post("/scrape/scheduled") +async def trigger_scheduled_scrape( + background_tasks: BackgroundTasks, + db: AsyncSession = Depends(get_db), + current_user: dict = Depends(get_current_user), +): + """Manually trigger the full scheduled scrape job. + Repopulates the queue with today's high/medium/low priority dates and processes it. + Use after an interrupted scheduled run.""" + if not (current_user.get('is_admin') or 'manage_scraper' in (current_user.get('caps') or [])): + raise HTTPException(status_code=403, detail="manage_scraper capability required") + + enabled_row = await db.execute( + text("SELECT config_value FROM system_config WHERE config_key = 'booking_scraper_enabled'") + ) + enabled = (enabled_row.fetchone() or [None])[0] + if enabled != 'true': + raise HTTPException(status_code=400, detail="Scraper is disabled. Enable it first.") + + from services.booking_scraper import get_lock_status + if get_lock_status()["locked"] or _SCRAPE_PENDING.is_set(): + raise HTTPException(status_code=409, detail="A scrape is already running. Try again when it finishes.") + + _SCRAPE_PENDING.set() + background_tasks.add_task(_run_scheduled_sync) + return {"status": "started", "message": "Scheduled scrape triggered. Check status for progress."} + + @router.post("/scrape/reset") async def reset_scraper( db: AsyncSession = Depends(get_db), diff --git a/frontend/src/pages/MarketView.tsx b/frontend/src/pages/MarketView.tsx index 988ee11..306f91b 100644 --- a/frontend/src/pages/MarketView.tsx +++ b/frontend/src/pages/MarketView.tsx @@ -386,6 +386,14 @@ const SettingsTab: React.FC = () => { }, }) + const scheduledScrapeMutation = useMutation({ + mutationFn: async () => (await api.post('/competitors/scrape/scheduled')).data, + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['scraper-status'] }) + queryClient.invalidateQueries({ queryKey: ['scrape-history'] }) + }, + }) + return (
+ Re-runs today's full scheduled job — repopulates the queue with all high/medium/low priority + dates and processes it. Use after an interrupted scheduled scrape. +
+ + {scheduledScrapeMutation.isSuccess && ( ++ Scheduled scrape triggered. Check status for progress. +
+ )} + {scheduledScrapeMutation.isError && ( ++ {(scheduledScrapeMutation.error as any)?.response?.data?.detail || 'Failed to trigger scrape'} +
+ )} +- Trigger a one-off competitor rate scrape for a date range. Runs in background. + Scrape a specific date range on demand. Uses the{' '} + {status?.backend === 'playwright_hotel_page' ? 'hotel-page' : 'search-results'}{' '} + backend (same as scheduled) — {status?.backend === 'playwright_hotel_page' + ? 'fetches all rate plans for each tracked hotel.' + : 'searches the configured location and discovers market hotels.' + }