"""JA4 SOC Dashboard — FastAPI application.""" from __future__ import annotations import logging from fastapi import FastAPI, Request from fastapi.middleware.cors import CORSMiddleware from fastapi.responses import HTMLResponse, JSONResponse from fastapi.staticfiles import StaticFiles from fastapi.templating import Jinja2Templates from backend.database import ClickHouseUnavailable, is_available from backend.routes.api import router as api_router from backend.routes.pages import router as pages_router logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(name)s: %(message)s") _templates = Jinja2Templates(directory="backend/templates") _PAGE_MAP = { "/": "overview", "/detections": "detections", "/scores": "scores", "/traffic": "traffic", "/classify": "classify", "/features": "features", "/models": "models", "/network": "network", "/campaigns": "campaigns", "/tactics": "tactics", "/reflists": "reflists", "/fleet": "fleet", "/health": "health", "/browsers": "browsers", "/fingerprints": "fingerprints", } app = FastAPI(title="JA4 SOC Dashboard", version="1.0.0") # CORS — allow all origins for dashboard access app.add_middleware( CORSMiddleware, allow_origins=["*"], allow_credentials=True, allow_methods=["*"], allow_headers=["*"], ) @app.exception_handler(ClickHouseUnavailable) async def ch_unavailable_handler(request: Request, exc: ClickHouseUnavailable): """Return 503 for API calls, render degraded pages for HTML requests.""" accept = request.headers.get("accept", "") path = request.url.path # If the client expects JSON (API call), return 503 JSON if "application/json" in accept or path.startswith("/api/"): return JSONResponse( status_code=503, content={"detail": "ClickHouse unavailable", "error": str(exc)}, ) # For HTML pages, render the template with ch_available=False page_name = _PAGE_MAP.get(path, "overview") return _templates.TemplateResponse( f"{page_name}.html", {"request": request, "active_page": page_name, "ch_available": False}, status_code=503, ) # Static assets app.mount("/static", StaticFiles(directory="backend/static"), name="static") # Routers — API first so /api/* paths match before page catch-all app.include_router(api_router) app.include_router(pages_router) @app.get("/api/healthcheck") async def healthcheck(): ch = is_available() return {"status": "ok" if ch else "degraded", "clickhouse": "up" if ch else "down"}