Files
imap-mail-filter-service/app/main.py
T
duffyduck b77b192b56 fix UI 500 + slow backup with millions of processed_mails
- fix: Starlette 1.0 changed TemplateResponse signature — request is now
  the first positional argument, not nested inside the context dict.
  All HTML routes returned 500 (unhashable dict in jinja2 template cache)
  after the image rebuild picked up the new starlette version.
- fix: processed_mails (1.7M rows in production: 1 account × 12 rules ×
  141k inbox mails) made backup export hit 558 MB / 90s. Moved to opt-in
  checkbox in the UI alongside the logs option, default off.
- yield_per for streaming the processed_mails query when included.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 20:37:32 +02:00

130 lines
4.1 KiB
Python

import logging
from contextlib import asynccontextmanager
from fastapi import FastAPI
from fastapi.staticfiles import StaticFiles
from fastapi.templating import Jinja2Templates
from app.config import settings
from app.database import Base, engine
logging.basicConfig(
level=getattr(logging, settings.log_level.upper(), logging.INFO),
format="%(asctime)s [%(levelname)s] %(name)s: %(message)s",
)
logger = logging.getLogger(__name__)
@asynccontextmanager
async def lifespan(app: FastAPI):
logger.info("Starte IMAP Mail Filter Service...")
# Datenbank-Migration mit Alembic
from alembic.config import Config
from alembic import command
alembic_cfg = Config("alembic.ini")
command.upgrade(alembic_cfg, "head")
logger.info("Datenbank-Migration abgeschlossen.")
if settings.yaml_sync_on_startup:
from app.services.yaml_service import import_from_file
result = import_from_file()
logger.info("YAML-Startup-Import: %s", result)
from app.services.scheduler import start_scheduler, stop_scheduler
start_scheduler()
yield
stop_scheduler()
logger.info("Service wird beendet.")
app = FastAPI(title="IMAP Mail Filter Service", lifespan=lifespan)
app.mount("/static", StaticFiles(directory="app/static"), name="static")
templates = Jinja2Templates(directory="app/templates")
from fastapi import Depends, Request # noqa: E402
from sqlalchemy.orm import Session # noqa: E402
from app.database import get_db # noqa: E402
from app.models.db_models import Account # noqa: E402
from app.routers import accounts, filters, logs, yaml_sync # noqa: E402
app.include_router(accounts.router)
app.include_router(filters.router)
app.include_router(yaml_sync.router)
app.include_router(logs.router)
# --- API: Scheduler-Status ---
@app.get("/api/scheduler/status")
def scheduler_status():
from app.services.scheduler import scheduler
jobs = []
for job in scheduler.get_jobs():
jobs.append({
"id": job.id,
"next_run": job.next_run_time.isoformat() if job.next_run_time else None,
"interval": str(job.trigger),
})
return {
"running": scheduler.running,
"jobs": jobs,
}
# --- Web-UI Routen ---
@app.get("/")
def dashboard(request: Request, db: Session = Depends(get_db)):
accs = db.query(Account).order_by(Account.name).all()
account_list = []
for acc in accs:
account_list.append({
"id": acc.id,
"name": acc.name,
"username": acc.username,
"imap_host": acc.imap_host,
"enabled": acc.enabled,
"poll_interval_seconds": acc.poll_interval_seconds,
"last_poll_at": acc.last_poll_at,
"filter_rule_count": len(acc.filter_rules),
})
return templates.TemplateResponse(request, "dashboard.html", {"accounts": account_list})
@app.get("/accounts")
def accounts_page(request: Request, db: Session = Depends(get_db)):
accs = db.query(Account).order_by(Account.name).all()
return templates.TemplateResponse(request, "accounts.html", {"accounts": accs})
@app.get("/accounts/new")
def new_account_page(request: Request):
return templates.TemplateResponse(request, "account_form.html", {"account": None})
@app.get("/accounts/{account_id}/edit")
def edit_account_page(account_id: int, request: Request, db: Session = Depends(get_db)):
account = db.get(Account, account_id)
return templates.TemplateResponse(request, "account_form.html", {"account": account})
@app.get("/filters")
def filters_page(request: Request, account_id: int = 0, db: Session = Depends(get_db)):
accs = db.query(Account).order_by(Account.name).all()
return templates.TemplateResponse(request, "filters.html", {
"accounts": accs,
"selected_account_id": account_id,
})
@app.get("/yaml")
def yaml_page(request: Request):
return templates.TemplateResponse(request, "yaml.html", {})
@app.get("/logs")
def logs_page(request: Request, db: Session = Depends(get_db)):
accs = db.query(Account).order_by(Account.name).all()
return templates.TemplateResponse(request, "logs.html", {"accounts": accs})