"""Notification tracker management API routes.""" import logging from fastapi import APIRouter, Depends, HTTPException, Query, status from pydantic import BaseModel from sqlmodel import select from sqlmodel.ext.asyncio.session import AsyncSession from ..auth.dependencies import get_current_user from ..database.engine import get_session from ..database.models import ( EventLog, NotificationTracker, NotificationTrackerState, NotificationTrackerTarget, ServiceProvider, User, ) from ..services.scheduler import schedule_tracker, unschedule_tracker from .helpers import get_owned_entity from .notification_tracker_targets import _tt_response _LOGGER = logging.getLogger(__name__) router = APIRouter(prefix="/api/notification-trackers", tags=["notification-trackers"]) class NotificationTrackerCreate(BaseModel): provider_id: int name: str icon: str = "" collection_ids: list[str] = [] scan_interval: int = 60 batch_duration: int = 0 default_tracking_config_id: int | None = None default_template_config_id: int | None = None enabled: bool = True class NotificationTrackerUpdate(BaseModel): name: str | None = None icon: str | None = None collection_ids: list[str] | None = None scan_interval: int | None = None batch_duration: int | None = None default_tracking_config_id: int | None = None default_template_config_id: int | None = None enabled: bool | None = None @router.get("") async def list_notification_trackers( user: User = Depends(get_current_user), session: AsyncSession = Depends(get_session), ): result = await session.exec( select(NotificationTracker).where(NotificationTracker.user_id == user.id) ) trackers = result.all() return [await _tracker_response(session, t) for t in trackers] @router.post("", status_code=status.HTTP_201_CREATED) async def create_notification_tracker( body: NotificationTrackerCreate, user: User = Depends(get_current_user), session: AsyncSession = Depends(get_session), ): provider = await session.get(ServiceProvider, body.provider_id) if not provider or provider.user_id != user.id: raise HTTPException(status_code=404, detail="Provider not found") tracker = NotificationTracker(user_id=user.id, **body.model_dump()) session.add(tracker) await session.commit() await session.refresh(tracker) if tracker.enabled: await schedule_tracker(tracker.id, tracker.scan_interval) return await _tracker_response(session, tracker) @router.get("/{tracker_id}") async def get_notification_tracker( tracker_id: int, user: User = Depends(get_current_user), session: AsyncSession = Depends(get_session), ): tracker = await _get_user_tracker(session, tracker_id, user.id) return await _tracker_response(session, tracker) @router.put("/{tracker_id}") async def update_notification_tracker( tracker_id: int, body: NotificationTrackerUpdate, user: User = Depends(get_current_user), session: AsyncSession = Depends(get_session), ): tracker = await _get_user_tracker(session, tracker_id, user.id) for field, value in body.model_dump(exclude_unset=True).items(): setattr(tracker, field, value) session.add(tracker) await session.commit() await session.refresh(tracker) if tracker.enabled: await schedule_tracker(tracker.id, tracker.scan_interval) else: await unschedule_tracker(tracker.id) return await _tracker_response(session, tracker) @router.delete("/{tracker_id}", status_code=status.HTTP_204_NO_CONTENT) async def delete_notification_tracker( tracker_id: int, user: User = Depends(get_current_user), session: AsyncSession = Depends(get_session), ): tracker = await _get_user_tracker(session, tracker_id, user.id) # Delete associated tracker-target links result = await session.exec( select(NotificationTrackerTarget).where(NotificationTrackerTarget.tracker_id == tracker_id) ) for tt in result.all(): await session.delete(tt) # Delete associated tracker state state_result = await session.exec( select(NotificationTrackerState).where(NotificationTrackerState.tracker_id == tracker_id) ) for ts in state_result.all(): await session.delete(ts) # Nullify event log references event_result = await session.exec( select(EventLog).where(EventLog.tracker_id == tracker_id) ) for el in event_result.all(): el.tracker_id = None session.add(el) await session.delete(tracker) await session.commit() await unschedule_tracker(tracker_id) @router.post("/{tracker_id}/trigger") async def trigger_notification_tracker( tracker_id: int, user: User = Depends(get_current_user), session: AsyncSession = Depends(get_session), ): tracker = await _get_user_tracker(session, tracker_id, user.id) from ..services.watcher import check_tracker result = await check_tracker(tracker.id) return {"triggered": True, "result": result} @router.get("/{tracker_id}/history") async def notification_tracker_history( tracker_id: int, limit: int = Query(default=20, ge=1, le=500), user: User = Depends(get_current_user), session: AsyncSession = Depends(get_session), ): await _get_user_tracker(session, tracker_id, user.id) result = await session.exec( select(EventLog) .where(EventLog.tracker_id == tracker_id) .order_by(EventLog.created_at.desc()) .limit(limit) ) return [ { "id": e.id, "event_type": e.event_type, "collection_id": e.collection_id, "collection_name": e.collection_name, "details": e.details, "created_at": e.created_at.isoformat() + ("Z" if not e.created_at.tzinfo else ""), } for e in result.all() ] async def _tracker_response(session: AsyncSession, t: NotificationTracker) -> dict: """Build tracker response with nested tracker_targets.""" result = await session.exec( select(NotificationTrackerTarget).where(NotificationTrackerTarget.tracker_id == t.id) ) tracker_targets = [await _tt_response(session, tt) for tt in result.all()] return { "id": t.id, "name": t.name, "icon": t.icon, "provider_id": t.provider_id, "collection_ids": t.collection_ids, "scan_interval": t.scan_interval, "batch_duration": t.batch_duration, "default_tracking_config_id": t.default_tracking_config_id, "default_template_config_id": t.default_template_config_id, "enabled": t.enabled, "tracker_targets": tracker_targets, "created_at": t.created_at.isoformat(), } async def _get_user_tracker( session: AsyncSession, tracker_id: int, user_id: int ) -> NotificationTracker: return await get_owned_entity( session, NotificationTracker, tracker_id, user_id, not_found_msg="Tracker not found", )