from fastapi import APIRouter, Depends, HTTPException from sqlalchemy import select, func from sqlalchemy.ext.asyncio import AsyncSession from app.dependencies import get_current_user, get_db from app.models.device import Device from app.models.object import Object from app.models.snapshot import ConfigSnapshot from app.models.snapshot_policy import SnapshotPolicy from app.models.user import User from app.schemas.snapshot_policy import ( ObjectSnapshotSummary, PolicySnapshotStatus, SnapshotPolicyCreate, SnapshotPolicyRead, SnapshotPolicyUpdate, ) from app.services import task as task_service router = APIRouter(prefix="/api/v1", tags=["snapshot-policies"]) def _policy_snapshot_label(policy: SnapshotPolicy) -> str: if policy.action_type == "mikrotik_get_config": return "routeros-export" return policy.params.get("path", "") # ─── Dashboard ─────────────────────────────────────────────────────────────── @router.get("/snapshots/dashboard", response_model=list[ObjectSnapshotSummary]) async def snapshot_dashboard( db: AsyncSession = Depends(get_db), _: User = Depends(get_current_user), ): # 1. Load all active global policies policies_result = await db.execute( select(SnapshotPolicy).where(SnapshotPolicy.is_active == True).order_by(SnapshotPolicy.label) ) policies = policies_result.scalars().all() if not policies: return [] # Collect categories we care about categories = list({p.category for p in policies}) # 2. Find all active objects that have devices in relevant categories obj_q = ( select(Object.id, Object.name, Object.city, Object.client) .where(Object.is_active == True) .order_by(Object.city, Object.name) ) obj_result = await db.execute(obj_q) all_objects = obj_result.all() if not all_objects: return [] # 3. Count eligible devices per (object_id, category) eligible_q = ( select( Device.object_id, Device.category, func.count(Device.id).label("total"), ) .where(Device.is_active == True, Device.category.in_(categories)) .group_by(Device.object_id, Device.category) ) eligible_result = await db.execute(eligible_q) eligible_map: dict[tuple[int, str], int] = { (r.object_id, r.category): r.total for r in eligible_result.all() } # 4. Snapshot stats per (object_id, category, label) stats_q = ( select( Device.object_id, Device.category, ConfigSnapshot.label, func.count(func.distinct(ConfigSnapshot.device_id)).label("success_count"), func.max(ConfigSnapshot.created_at).label("last_collected_at"), ) .join(Device, ConfigSnapshot.device_id == Device.id) .where(Device.is_active == True, Device.category.in_(categories)) .group_by(Device.object_id, Device.category, ConfigSnapshot.label) ) stats_result = await db.execute(stats_q) stats_map: dict[tuple[int, str, str], tuple[int, object]] = { (r.object_id, r.category, r.label): (r.success_count, r.last_collected_at) for r in stats_result.all() } # 5. Assemble: for each object, build policy statuses result = [] for obj_id, obj_name, obj_city, obj_client in all_objects: obj_policies = [] for policy in policies: total = eligible_map.get((obj_id, policy.category), 0) if total == 0: continue snap_label = _policy_snapshot_label(policy) stats = stats_map.get((obj_id, policy.category, snap_label)) success_count = stats[0] if stats else 0 last_collected_at = stats[1] if stats else None obj_policies.append(PolicySnapshotStatus( policy_id=policy.id, policy_label=policy.label, category=policy.category, action_type=policy.action_type, params=policy.params, last_collected_at=last_collected_at, success_count=success_count, total_devices=total, )) if obj_policies: result.append(ObjectSnapshotSummary( object_id=obj_id, object_name=obj_name, city=obj_city, client=obj_client, policies=obj_policies, )) return result # ─── Policies CRUD (global) ────────────────────────────────────────────────── @router.get("/snapshot-policies", response_model=list[SnapshotPolicyRead]) async def list_policies( db: AsyncSession = Depends(get_db), _: User = Depends(get_current_user), ): result = await db.execute( select(SnapshotPolicy).order_by(SnapshotPolicy.label) ) return result.scalars().all() @router.post("/snapshot-policies", response_model=SnapshotPolicyRead, status_code=201) async def create_policy( body: SnapshotPolicyCreate, db: AsyncSession = Depends(get_db), _: User = Depends(get_current_user), ): policy = SnapshotPolicy( category=body.category, action_type=body.action_type, params=body.params, label=body.label, is_active=body.is_active, ) db.add(policy) await db.flush() await db.refresh(policy) return policy @router.put("/snapshot-policies/{policy_id}", response_model=SnapshotPolicyRead) async def update_policy( policy_id: int, body: SnapshotPolicyUpdate, db: AsyncSession = Depends(get_db), _: User = Depends(get_current_user), ): policy = await db.get(SnapshotPolicy, policy_id) if policy is None: raise HTTPException(status_code=404, detail="Policy not found") for field, value in body.model_dump(exclude_unset=True).items(): setattr(policy, field, value) await db.flush() await db.refresh(policy) return policy @router.delete("/snapshot-policies/{policy_id}", status_code=204) async def delete_policy( policy_id: int, db: AsyncSession = Depends(get_db), _: User = Depends(get_current_user), ): policy = await db.get(SnapshotPolicy, policy_id) if policy is None: raise HTTPException(status_code=404, detail="Policy not found") await db.delete(policy) await db.flush() # ─── Collect ───────────────────────────────────────────────────────────────── @router.post("/snapshot-policies/{policy_id}/collect", status_code=202) async def collect_all( policy_id: int, db: AsyncSession = Depends(get_db), current_user: User = Depends(get_current_user), ): """Collect snapshots for ALL objects matching this policy's category.""" policy = await db.get(SnapshotPolicy, policy_id) if policy is None: raise HTTPException(status_code=404, detail="Policy not found") if not policy.is_active: raise HTTPException(status_code=400, detail="Policy is disabled") task = await task_service.create_task( action_type=policy.action_type, target_scope={ "type": "category", "category": policy.category, }, params=policy.params, user_id=current_user.id, ) return {"task_id": task.id} @router.post("/snapshot-policies/{policy_id}/collect/{obj_id}", status_code=202) async def collect_for_object( policy_id: int, obj_id: int, db: AsyncSession = Depends(get_db), current_user: User = Depends(get_current_user), ): """Collect snapshots for a specific object matching this policy's category.""" policy = await db.get(SnapshotPolicy, policy_id) if policy is None: raise HTTPException(status_code=404, detail="Policy not found") if not policy.is_active: raise HTTPException(status_code=400, detail="Policy is disabled") obj = await db.get(Object, obj_id) if obj is None: raise HTTPException(status_code=404, detail="Object not found") task = await task_service.create_task( action_type=policy.action_type, target_scope={ "type": "category", "object_id": obj_id, "category": policy.category, }, params=policy.params, user_id=current_user.id, ) return {"task_id": task.id}