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."""
