import json import os from typing import List from fastapi import Depends, FastAPI, HTTPException, Request from fastapi.responses import HTMLResponse, RedirectResponse from fastapi.staticfiles import StaticFiles from fastapi.templating import Jinja2Templates from sqlalchemy.orm import Session from ai import analyze_thread from database import ( Message, SessionLocal, Thread, create_db_tables, get_thread_messages, ) def get_db(): db = SessionLocal() try: yield db finally: db.close() create_db_tables() app = FastAPI(title="Email Alerts UI") # Static and templates os.makedirs("templates", exist_ok=True) os.makedirs("static", exist_ok=True) templates = Jinja2Templates(directory="templates") app.mount("/static", StaticFiles(directory="static"), name="static") @app.get("/", response_class=HTMLResponse) def home(request: Request, db: Session = Depends(get_db), account: str | None = None): q = db.query(Thread).order_by(Thread.updated_at.desc()) if account: q = q.filter(Thread.account_email == account.lower()) threads = q.limit(100).all() return templates.TemplateResponse( "threads.html", { "request": request, "threads": threads, "account": account or "", }, ) @app.get("/thread/{thread_id}", response_class=HTMLResponse) def show_thread(thread_id: int, request: Request, db: Session = Depends(get_db)): thread = db.query(Thread).filter(Thread.id == thread_id).one_or_none() if not thread: raise HTTPException(status_code=404, detail="Thread not found") messages: List[Message] = get_thread_messages(db, thread_id) # Convert for AI analyzer and template msg_dicts = [ { "id": m.id, "date_sent": m.date_sent, "subject": m.subject, "from_email": m.from_email, "to_email": m.to_email, "body": m.body, "is_incoming": m.is_incoming, } for m in messages ] ai = analyze_thread(thread.subject or "", msg_dicts) return templates.TemplateResponse( "thread_detail.html", { "request": request, "thread": thread, "messages": messages, "ai": ai, }, ) @app.get("/health") def health(): return {"status": "ok"} # --------------------- # Config editor routes # --------------------- CONFIG_PATH = os.path.join(os.path.dirname(__file__), "config.json") def load_config() -> dict: if not os.path.exists(CONFIG_PATH): return { "email_address": "", "time_frames": [ {"name": "1-24 hours", "hours": 24, "alert_level": 1}, {"name": "24-48 hours", "hours": 48, "alert_level": 2}, {"name": "48+ hours", "hours": 72, "alert_level": 3}, ], "email_days_back": 7, "agency_domains": [], "zoho_email": "", "zoho_app_password": "", "auto_process": False, "auto_process_interval": 30, } with open(CONFIG_PATH, "r", encoding="utf-8") as f: return json.load(f) def save_config(cfg: dict) -> None: with open(CONFIG_PATH, "w", encoding="utf-8") as f: json.dump(cfg, f, indent=2, ensure_ascii=False) @app.get("/config", response_class=HTMLResponse) def config_form(request: Request, saved: int | None = None): cfg = load_config() # Render up to existing frames or at least 3 rows frames = cfg.get("time_frames") or [] min_rows = max(len(frames), 3) return templates.TemplateResponse( "config.html", { "request": request, "cfg": cfg, "rows": list(range(min_rows)), "saved": bool(saved), }, ) @app.post("/config") async def config_save(request: Request): form = await request.form() cfg = load_config() # Basic fields cfg["email_address"] = (form.get("email_address") or "").strip() # email_days_back try: cfg["email_days_back"] = int( form.get("email_days_back") or cfg.get("email_days_back", 7) ) except Exception: cfg["email_days_back"] = 7 # agency domains (comma or newline separated) domains_raw = (form.get("agency_domains") or "").replace("\r", "") parts = [p.strip() for p in domains_raw.replace(",", "\n").split("\n") if p.strip()] cfg["agency_domains"] = parts # auto_process cfg["auto_process"] = form.get("auto_process") == "on" # auto_process_interval try: cfg["auto_process_interval"] = int( form.get("auto_process_interval") or cfg.get("auto_process_interval", 30) ) except Exception: cfg["auto_process_interval"] = 30 # Zoho (optional - note: current client reads env vars) cfg["zoho_email"] = (form.get("zoho_email") or cfg.get("zoho_email", "")).strip() cfg["zoho_app_password"] = ( form.get("zoho_app_password") or cfg.get("zoho_app_password", "") ).strip() # Time frames: collect indexed rows frames: list[dict] = [] # find indices present indices = set() for k in form.keys(): if k.startswith("time_name_"): try: indices.add(int(k.split("_")[-1])) except Exception: pass for i in sorted(indices): name = (form.get(f"time_name_{i}") or "").strip() hrs_raw = form.get(f"time_hours_{i}") or "" lvl_raw = form.get(f"time_alert_{i}") or "" if not name and not hrs_raw and not lvl_raw: continue try: hours = int(hrs_raw) except Exception: hours = 0 try: level = int(lvl_raw) except Exception: level = 0 if name: frames.append({"name": name, "hours": hours, "alert_level": level}) if frames: cfg["time_frames"] = frames save_config(cfg) return RedirectResponse(url="/config?saved=1", status_code=303)