"""History sync background task. Periodically copies new records from the fail2ban sqlite database into the BanGUI application archive table to prevent gaps when fail2ban purges old rows. """ from __future__ import annotations import datetime from typing import TYPE_CHECKING import structlog from app.db import open_db from app.services import history_service from app.utils.runtime_state import get_effective_settings if TYPE_CHECKING: from fastapi import FastAPI from app.config import Settings log: structlog.stdlib.BoundLogger = structlog.get_logger() #: Stable APScheduler job id. JOB_ID: str = "history_sync" #: Interval in seconds between sync runs. HISTORY_SYNC_INTERVAL: int = 300 #: Backfill window when archive is empty (seconds). BACKFILL_WINDOW: int = 648000 async def _run_sync_with_settings(settings: Settings) -> None: socket_path: str = settings.fail2ban_socket db = await open_db(settings.database_path) try: synced = await history_service.sync_from_fail2ban_db(db, socket_path) log.info("history_sync_complete", synced=synced) except Exception: log.exception("history_sync_failed") finally: await db.close() async def _run_sync(app: FastAPI) -> None: await _run_sync_with_settings(get_effective_settings(app)) def register(app: FastAPI) -> None: """Register the history sync periodic job. Should be called after scheduler startup, from the lifespan handler. """ settings = get_effective_settings(app) app.state.scheduler.add_job( _run_sync_with_settings, trigger="interval", seconds=HISTORY_SYNC_INTERVAL, kwargs={"settings": settings}, id=JOB_ID, replace_existing=True, next_run_time=datetime.datetime.now(tz=datetime.UTC), ) log.info("history_sync_scheduled", interval_seconds=HISTORY_SYNC_INTERVAL)