1: return {"events": out, "total": total, "page": page, "page_size": page_size} 4:@router.get("/audit") 5:async def list_audit(action: str = Query(""), 26: "detail": r.detail if isinstance(r.detail, dict) else {}, 30: return {"audit": out, "total": total, "page": page, "page_size": page_size} 33:@router.get("/dlq") 34:async def list_dlq(page: int = Query(1, ge=1), page_size: int = Query(50, ge=1, le=200), 43: return {"dlq": [], "total": 0, "error": f"Redis unavailable: {e}"} 53: return {"dlq": out[start:start + page_size], "total": total, 57:@router.get("/export") 58:async def export_logs(campaign_id: int = Query(0), 72: def generate(): 88: return StreamingResponse(generate(), media_type="text/csv", 93:@router.get("/recipient/{cr_id}/history") 94:async def recipient_history(cr_id: int, db: AsyncSession = Depends(get_db), 99: raise HTTPException(status_code=404, detail="Recipient not found") 102: raise HTTPException(status_code=404, detail="Recipient not found") 111: return { 121: "id": e.id, "event_type": e.event_type, "detail": (e.detail or "")[:300], ===SUPP-IMPORT-TAIL=== lines = text.splitlines() looks_csv = any("," in ln for ln in lines[:50]) if looks_csv: for row in csv.reader(io.StringIO(text)): if not row or not (row[0] or "").strip(): continue email = row[0].strip().lower() r = (row[1].strip() if len(row) > 1 else reason) or reason if email.lower().startswith(("email", "address")): continue if is_valid_email(email): entries.append((email, _norm_reason(r))) else: invalid.append(email) else: for line in lines: email = line.strip().lower() if not email or email.lower().startswith(("email", "address", "#")): continue if is_valid_email(email): entries.append((email, reason)) else: invalid.append(email) if not entries: raise HTTPException(status_code=400, detail=f"No valid emails found. Invalid: {invalid[:10]}") existing = set((await db.execute( select(SuppressionEntry.email).where(SuppressionEntry.user_id == user.id))).scalars()) added = 0 for email, r in entries: if email in existing: continue db.add(SuppressionEntry(user_id=user.id, email=email, reason=r, source="import")) existing.add(email) added += 1 await db.commit() await log_audit(db, user.id, "suppression.import", "suppression", None, {"added": added, "invalid": len(invalid)}) return {"added": added, "duplicates": len(entries) - added, "invalid": invalid[:20], "total_valid": len(entries)} @router.post("/bulk") ===RESET=== @router.post("/reset") async def reset_setting(payload: SettingIn, db: AsyncSession = Depends(get_db), user: User = Depends(get_current_user), _: None = Depends(require_csrf)): """Reset one setting to its default value (deletes the DB row).""" if payload.key not in SETTING_DEFS: raise HTTPException(status_code=400, detail="Unknown setting key") row = await db.execute( select(ApplicationSettings).where(ApplicationSettings.key == payload.key)) row = row.scalar() if row: await db.delete(row) await db.commit() await log_audit(db, user.id, "settings.reset", "application_setting", None, {"key": payload.key}) return {"key": payload.key, "value": SETTING_DEFS[payload.key]["default"]} @router.post("/reset-all") async def reset_all_settings(db: AsyncSession = Depends(get_db), user: User = Depends(get_current_user), _: None = Depends(require_csrf)): rows = (await db.execute( select(ApplicationSettings).where(ApplicationSettings.key.in_(SETTING_DEFS.keys())) )).scalars().all() for r in rows: await db.delete(r) await db.commit() await log_audit(db, user.id, "settings.reset_all", "application_setting", None, {"reset": len(rows)}) ===BATCH=== @router.post("/save-batch") async def save_batch(payload: BatchSettingsIn, db: AsyncSession = Depends(get_db), user: User = Depends(get_current_user), _: None = Depends(require_csrf)): """Save multiple settings at once (Settings page form).""" applied, errors = {}, {} for key, value in payload.values.items(): if key not in SETTING_DEFS: errors[key] = "Unknown setting key" continue ok, v = _validate_value(key, value) if not ok: errors[key] = v continue row = await db.execute( select(ApplicationSettings).where(ApplicationSettings.key == key)) row = row.scalar() if row: row.value = v else: db.add(ApplicationSettings(key=key, value=v, description=SETTING_DEFS[key]["label"], category=SETTING_DEFS[key]["category"])) applied[key] = v await db.commit() if applied: await log_audit(db, user.id, "settings.save_batch", "application_setting", None, {"keys": list(applied.keys())}) return {"applied": applied, "errors": errors} @router.post("/reset")