205 lines
5.9 KiB
Python
205 lines
5.9 KiB
Python
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)
|