"""Update job routes: trigger, list, logs, cancel.""" from fastapi import APIRouter, Depends, Query, Request from sqlalchemy import func, select from sqlalchemy.ext.asyncio import AsyncSession from app.api.deps import client_ip, get_current_user from app.core.database import get_db from app.core.exceptions import JobNotCancellableError, NotFoundError from app.models.server import Server from app.models.update_job import JobStatus, UpdateJob, UpdateLog from app.models.user import User from app.schemas.update import JobTriggerRequest, UpdateJobRead, UpdateLogRead from app.services.audit import AuditService from app.services.job_runner import job_runner router = APIRouter() @router.post("/trigger", response_model=UpdateJobRead, status_code=201) async def trigger_update( payload: JobTriggerRequest, request: Request, db: AsyncSession = Depends(get_db), user: User = Depends(get_current_user), ) -> UpdateJob: server = await db.get(Server, payload.server_id) if not server: raise NotFoundError("Server nicht gefunden") job = UpdateJob( server_id=server.id, type=payload.type, started_by=user.username, ) db.add(job) await db.flush() await AuditService(db).log( username=user.username, action="update.trigger", target=server.name, details={"job_id": job.id, "type": payload.type.value}, ip_address=client_ip(request), ) await db.commit() await job_runner.start(job.id) return job @router.get("", response_model=list[UpdateJobRead]) async def list_jobs( status: JobStatus | None = None, limit: int = Query(default=50, le=200), db: AsyncSession = Depends(get_db), _user: User = Depends(get_current_user), ) -> list[UpdateJob]: stmt = select(UpdateJob).order_by(UpdateJob.id.desc()).limit(limit) if status: stmt = stmt.where(UpdateJob.status == status) result = await db.execute(stmt) return list(result.scalars().all()) @router.get("/{job_id}", response_model=UpdateJobRead) async def get_job( job_id: int, db: AsyncSession = Depends(get_db), _user: User = Depends(get_current_user), ) -> UpdateJob: job = await db.get(UpdateJob, job_id) if not job: raise NotFoundError("Job nicht gefunden") return job @router.get("/{job_id}/logs", response_model=list[UpdateLogRead]) async def get_job_logs( job_id: int, after_id: int = 0, limit: int = Query(default=500, le=2000), db: AsyncSession = Depends(get_db), _user: User = Depends(get_current_user), ) -> list[UpdateLog]: stmt = ( select(UpdateLog) .where(UpdateLog.job_id == job_id, UpdateLog.id > after_id) .order_by(UpdateLog.id) .limit(limit) ) result = await db.execute(stmt) return list(result.scalars().all()) @router.post("/{job_id}/cancel", response_model=UpdateJobRead) async def cancel_job( job_id: int, request: Request, db: AsyncSession = Depends(get_db), user: User = Depends(get_current_user), ) -> UpdateJob: job = await db.get(UpdateJob, job_id) if not job: raise NotFoundError("Job nicht gefunden") if job.status not in (JobStatus.PENDING, JobStatus.RUNNING): raise JobNotCancellableError() cancelled = await job_runner.cancel(job_id) if not cancelled: job.status = JobStatus.CANCELLED await AuditService(db).log( username=user.username, action="update.cancel", target=f"job:{job_id}", ip_address=client_ip(request), ) return job @router.get("/stats/summary") async def job_stats( db: AsyncSession = Depends(get_db), _user: User = Depends(get_current_user), ) -> dict: total = await db.scalar(select(func.count(UpdateJob.id))) running = await db.scalar( select(func.count(UpdateJob.id)).where(UpdateJob.status == JobStatus.RUNNING) ) failed = await db.scalar( select(func.count(UpdateJob.id)).where(UpdateJob.status == JobStatus.FAILED) ) return {"total": total or 0, "running": running or 0, "failed": failed or 0}