"""Campaign logs and audit log endpoints: send attempts, events, errors with filters, CSV export, per-recipient history. Combined logs view for the Logs page.""" import csv import io from datetime import datetime, timezone from fastapi import APIRouter, Depends, HTTPException, Query from fastapi.responses import StreamingResponse from sqlalchemy import func, or_, select from sqlalchemy.ext.asyncio import AsyncSession from app.api.deps import get_current_user, get_db from app.models import AuditLog, Campaign, CampaignRecipient, EmailEvent, SendAttempt, User router = APIRouter(prefix="/logs", tags=["logs"]) def _ser_attempt(a: SendAttempt) -> dict: return { "id": a.id, "campaign_id": a.campaign_id, "campaign_recipient_id": a.campaign_recipient_id, "account_id": a.gmail_account_id, "attempt_number": a.attempt_number, "outcome": a.outcome, "error_class": a.error_class, "error_message": (a.error_message or "")[:300], "message_id": a.message_id, "provider_response": (a.provider_response or "")[:300], "created_at": a.created_at.isoformat() if a.created_at else None, } @router.get("/list") async def list_logs(campaign_id: int = Query(0), outcome: str = Query("", description="submitted|accepted|temp_failed|perm_failed|skipped"), search: str = Query("", description="email or message id fragment"), page: int = Query(1, ge=1), page_size: int = Query(50, ge=1, le=200), db: AsyncSession = Depends(get_db), user: User = Depends(get_current_user)): """Unified send log: SendAttempt rows joined with CampaignRecipient for context.""" q = (select(SendAttempt, CampaignRecipient) .join(CampaignRecipient, SendAttempt.campaign_recipient_id == CampaignRecipient.id) .join(Campaign, Campaign.id == SendAttempt.campaign_id) .where(Campaign.user_id == user.id)) if campaign_id: q = q.where(SendAttempt.campaign_id == campaign_id) if outcome: q = q.where(SendAttempt.outcome == outcome) if search: like = f"%{search}%" q = q.where(or_(CampaignRecipient.email.ilike(like), SendAttempt.message_id.ilike(like))) total = (await db.execute( select(func.count()).select_from(q.subquery()))).scalar() or 0 rows = (await db.execute( q.order_by(SendAttempt.id.desc()).offset((page - 1) * page_size).limit(page_size) )).all() out = [] for a, cr in rows: d = _ser_attempt(a) d["email"] = cr.email d["recipient_status"] = cr.status out.append(d) return {"logs": out, "total": total, "page": page, "page_size": page_size} @router.get("/events") async def list_events(campaign_id: int = Query(0), event_type: str = Query(""), page: int = Query(1, ge=1), page_size: int = Query(50, ge=1, le=200), db: AsyncSession = Depends(get_db), user: User = Depends(get_current_user)): q = (select(EmailEvent, CampaignRecipient) .join(CampaignRecipient, EmailEvent.campaign_recipient_id == CampaignRecipient.id) .join(Campaign, Campaign.id == EmailEvent.campaign_id) .where(Campaign.user_id == user.id)) if campaign_id: