- fix: "Log leeren" without account filter used a plain DELETE over millions of rows. That held the sqlite write lock long enough that the scheduler's own log writes started failing with "database is locked", and the request itself eventually 500'd back as HTML "Internal Server Error" — which the frontend then tried to JSON.parse, producing the popup the user reported. Now uses DROP+CREATE for full clears (near-instant) and wal_checkpoint to reclaim space; per-account clear still uses a normal DELETE. - Raise sqlite busy_timeout from 10s to 60s so a legitimately slow writer no longer starves smaller ones. - retry-failed: single bulk DELETE with mail_uid IN (...) per account instead of one query per uid. - Both endpoints now return proper HTTPException(500, ...) with detail instead of letting the exception bubble as plain text. - Frontend: parseResponse() reads text first, then tries JSON. Both clearLogs() and retryFailed() surface the actual server message instead of crashing on non-JSON responses. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
110 lines
4.2 KiB
Python
110 lines
4.2 KiB
Python
import logging
|
|
|
|
from fastapi import APIRouter, Depends, HTTPException, Query
|
|
from sqlalchemy import text
|
|
from sqlalchemy.orm import Session
|
|
|
|
from app.database import engine, get_db
|
|
from app.models.db_models import FilterLog
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
router = APIRouter(prefix="/api/logs", tags=["logs"])
|
|
|
|
|
|
@router.get("/")
|
|
def get_logs(
|
|
account_id: int | None = None,
|
|
level: str | None = None,
|
|
search: str | None = None,
|
|
search_subject: str | None = None,
|
|
search_from: str | None = None,
|
|
search_rule: str | None = None,
|
|
search_message: str | None = None,
|
|
search_details: str | None = None,
|
|
search_folder: str | None = None,
|
|
limit: int = Query(default=100, le=500),
|
|
offset: int = 0,
|
|
db: Session = Depends(get_db),
|
|
):
|
|
query = db.query(FilterLog).order_by(FilterLog.created_at.desc())
|
|
if account_id:
|
|
query = query.filter(FilterLog.account_id == account_id)
|
|
if level:
|
|
query = query.filter(FilterLog.level == level)
|
|
# Einfache Suche (ODER über alle Felder)
|
|
if search:
|
|
term = f"%{search}%"
|
|
query = query.filter(
|
|
FilterLog.message.ilike(term)
|
|
| FilterLog.mail_subject.ilike(term)
|
|
| FilterLog.mail_from.ilike(term)
|
|
| FilterLog.rule_name.ilike(term)
|
|
| FilterLog.details.ilike(term)
|
|
| FilterLog.folder.ilike(term)
|
|
)
|
|
# Erweiterte Suche (UND pro Feld)
|
|
if search_subject:
|
|
query = query.filter(FilterLog.mail_subject.ilike(f"%{search_subject}%"))
|
|
if search_from:
|
|
query = query.filter(FilterLog.mail_from.ilike(f"%{search_from}%"))
|
|
if search_rule:
|
|
query = query.filter(FilterLog.rule_name.ilike(f"%{search_rule}%"))
|
|
if search_message:
|
|
query = query.filter(FilterLog.message.ilike(f"%{search_message}%"))
|
|
if search_details:
|
|
query = query.filter(FilterLog.details.ilike(f"%{search_details}%"))
|
|
if search_folder:
|
|
query = query.filter(FilterLog.folder.ilike(f"%{search_folder}%"))
|
|
total = query.count()
|
|
logs = query.offset(offset).limit(limit).all()
|
|
return {
|
|
"total": total,
|
|
"logs": [
|
|
{
|
|
"id": log.id,
|
|
"account_id": log.account_id,
|
|
"account_name": log.account_name,
|
|
"level": log.level.value if log.level else "info",
|
|
"message": log.message,
|
|
"rule_name": log.rule_name,
|
|
"action_type": log.action_type,
|
|
"mail_uid": log.mail_uid,
|
|
"mail_subject": log.mail_subject,
|
|
"mail_from": log.mail_from,
|
|
"folder": log.folder,
|
|
"details": log.details,
|
|
"created_at": log.created_at.isoformat() if log.created_at else None,
|
|
}
|
|
for log in logs
|
|
],
|
|
}
|
|
|
|
|
|
@router.delete("/")
|
|
def clear_logs(account_id: int | None = None, db: Session = Depends(get_db)):
|
|
"""Löscht Logs. Ohne account_id: schnelles Drop+Recreate der Tabelle
|
|
(sekundenschnell auch bei Millionen Zeilen). Mit account_id: normales DELETE."""
|
|
try:
|
|
if account_id is None:
|
|
# Vollständiges Leeren: DROP+CREATE statt DELETE.
|
|
# DELETE FROM filter_logs würde jede Zeile einzeln ins WAL schreiben,
|
|
# bei Millionen Einträgen dauert das ewig und blockiert die DB.
|
|
db.close() # Sitzung freigeben, sonst holds sie noch Locks
|
|
row_count = 0
|
|
with engine.begin() as conn:
|
|
row_count = conn.execute(text("SELECT COUNT(*) FROM filter_logs")).scalar() or 0
|
|
FilterLog.__table__.drop(conn, checkfirst=True)
|
|
FilterLog.__table__.create(conn, checkfirst=True)
|
|
# PRAGMA wal_checkpoint(TRUNCATE) räumt die WAL-Datei auf
|
|
conn.execute(text("PRAGMA wal_checkpoint(TRUNCATE)"))
|
|
logger.info("Log vollständig geleert (%d Einträge, Tabelle neu angelegt)", row_count)
|
|
return {"deleted": row_count}
|
|
|
|
count = db.query(FilterLog).filter(FilterLog.account_id == account_id).delete()
|
|
db.commit()
|
|
return {"deleted": count}
|
|
except Exception as e:
|
|
logger.exception("clear_logs fehlgeschlagen")
|
|
raise HTTPException(500, f"Fehler beim Leeren des Logs: {e}")
|