186:@router.get("/list") 187:async def list_campaigns(status: str = Query(""), 201: return {"campaigns": [_ser(c) for c in rows], "total": total, 205:@router.post("/create") 221:@router.get("/{cid}") 227:@router.post("/{cid}/patch") 249:@router.post("/{cid}/delete") 262:@router.post("/clear-all") 285:@router.post("/{cid}/preview") 355: out.append({ 374:@router.post("/{cid}/start") 406:@router.post("/{cid}/pause") 418:@router.post("/{cid}/resume") 432:@router.post("/{cid}/cancel") 474:@router.get("/{cid}/progress") 481:@router.get("/{cid}/stream") 546:@router.get("/{cid}/recipients") 581: out.append({ 596:@router.get("/{cid}/export") 634:@router.get("/wizard/options") 659: list_out.append({"id": rl.id, "name": rl.name, "row_count": rl.row_count, 678:@router.post("/validate-step") === async def list_campaigns(status: str = Query(""), search: str = Query(""), page: int = Query(1, ge=1), page_size: int = Query(20, ge=1, le=100), db: AsyncSession = Depends(get_db), user: User = Depends(get_current_user)): q = select(Campaign).where(Campaign.user_id == user.id) if status: q = q.where(Campaign.status == status) if search: q = q.where(Campaign.name.ilike(f"%{search}%")) total = (await db.execute(select(func.count()).select_from(q.subquery()))).scalar() or 0 rows = (await db.execute(q.order_by(Campaign.id.desc()) .offset((page - 1) * page_size).limit(page_size))).scalars().all() return {"campaigns": [_ser(c) for c in rows], "total": total, "page": page, "page_size": page_size} @router.post("/create") async def create_campaign(payload: CampaignIn, db: AsyncSession = Depends(get_db), user: User = Depends(get_current_user), _: None = Depends(require_csrf)): """Create a new campaign (wizard step 12 / Save). Validates all references.""" errors = await _validate_refs(db, user, payload) if errors: raise HTTPException(status_code=400, detail={"message": "Validation failed", "errors": errors}) c = Campaign(user_id=user.id, **payload.model_dump()) db.add(c) await db.commit() await log_audit(db, user.id, "campaign.create", "campaign", c.id, {"name": c.name}) return _ser(c) @router.get("/{cid}") async def get_campaign(cid: int, user: User = Depends(get_current_user), db: AsyncSession = Depends(get_db)): return _ser(await _get_own(db, user, cid)) @router.post("/{cid}/patch") async def patch_campaign(cid: int, payload: CampaignIn, db: AsyncSession = Depends(get_db), user: User = Depends(get_current_user), _: None = Depends(require_csrf)): """Full update (wizard edit). Only allowed while draft/paused/queued."""