"""Background scheduler — runs run_check_all() every N hours. Run as a separate process: `python -m app.scheduler`. """ from __future__ import annotations import logging import time from apscheduler.schedulers.blocking import BlockingScheduler from apscheduler.triggers.interval import IntervalTrigger from app.config import settings from app.db import init_db from app.services.monitor import run_check_all logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(name)s: %(message)s") logger = logging.getLogger(__name__) def job() -> None: logger.info("Scheduled scan starting…") start = time.time() summary = run_check_all() elapsed = time.time() - start total_changes = sum(c for c in summary.values() if c > 0) logger.info( "Scan done in %.1fs. Projects: %d, total changes: %d", elapsed, len(summary), total_changes, ) def main() -> None: init_db() hours = max(1, settings.scrape_interval_hours) scheduler = BlockingScheduler(timezone="UTC") scheduler.add_job( job, trigger=IntervalTrigger(hours=hours), # Omit next_run_time so APScheduler defaults the first run to now+interval # (i.e. don't fire immediately at startup, fire after one interval, then # every interval). Passing next_run_time=None instead creates the job in a # PAUSED state and it never fires — that was the bug. id="periodic-scan", max_instances=1, coalesce=True, ) logger.info("Scheduler started — interval %d hour(s).", hours) try: scheduler.start() except (KeyboardInterrupt, SystemExit): logger.info("Scheduler stopped.") if __name__ == "__main__": main()