The single-file stdlib server became the limit: no way to track what had been done about an alert, no accounts, and a UI that had to be hand-rolled in template strings. This restructures it into something deployable. Backend (FastAPI) - app/ holds config, database, auth, delivery and the routers; triagelib keeps the triage engine unchanged, so the validated screening and runbook logic is untouched. - Cases persist per alert fingerprint with a status workflow (investigating, customer contacted, escalated to Infra, waiting, remediated, resolved, won't fix, false positive), an assignee, notes and an append-only history. An alert that stops and re-fires lands back on the same case and counts as a reopen. - Suppression rules move from a JSON file into the database. Auth - Signed session cookies over PBKDF2 local accounts, plus an OIDC flow ready for Authentik: users are created on first login and admin follows a group claim. Local login can be switched off entirely once SSO is live. Zendesk and Jira - Delivery is now implemented, behind three gates: the integration must be configured, its feature flag on, and CX_FEATURE_SEND_ENABLED on. A demo instance leaves the last off and cannot mail anyone. Both search before creating, so re-diagnosing an alert updates one ticket rather than opening several, and a rolling daily cap stops a loop mailing everybody. Deployment - Multi-stage Dockerfile builds the bundle and serves it from the API origin. - docker-compose for local and single-host use; Gitea Actions runs the tests, builds the image and renders deploy/k8s with envsubst. Two fixes found while testing: assigning a case returned a null assignee, and add_event could leave an already-loaded history collection stale. Known gap: the engine reaches OpenStack via `docker exec <region>-osc`, which does not work in a pod without the CX-Tools containers alongside it. docs/DEPLOYMENT.md sets out the three ways to close that. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
67 lines
2.3 KiB
Python
67 lines
2.3 KiB
Python
"""FastAPI application entry point."""
|
|
from __future__ import annotations
|
|
|
|
import os
|
|
from contextlib import asynccontextmanager
|
|
|
|
from fastapi import FastAPI
|
|
from fastapi.responses import FileResponse, JSONResponse
|
|
from fastapi.staticfiles import StaticFiles
|
|
|
|
from . import VERSION
|
|
from .auth import ensure_bootstrap_admin
|
|
from .config import get_settings
|
|
from .db import SessionLocal, init_db
|
|
from .routers import (actions_router, alerts_router, auth_router, cases_router,
|
|
linkage_router, settings_router)
|
|
|
|
settings = get_settings()
|
|
|
|
|
|
@asynccontextmanager
|
|
async def lifespan(app: FastAPI):
|
|
init_db()
|
|
db = SessionLocal()
|
|
try:
|
|
message = ensure_bootstrap_admin(db)
|
|
if message:
|
|
print(f"[auth] {message}")
|
|
finally:
|
|
db.close()
|
|
print(f"[startup] {settings.app_name} {VERSION}")
|
|
print(f"[startup] prometheus: {settings.prometheus_base}")
|
|
print(f"[startup] sso: {'on' if settings.oidc_enabled else 'off'} | "
|
|
f"zendesk: {'ready' if settings.zendesk_ready else 'off'} | "
|
|
f"jira: {'ready' if settings.jira_ready else 'off'} | "
|
|
f"sending: {'ENABLED' if settings.feature_send_enabled else 'disabled'}")
|
|
alerts_router.engine.warm()
|
|
yield
|
|
|
|
|
|
app = FastAPI(title=settings.app_name, version=VERSION, lifespan=lifespan)
|
|
|
|
for module in (auth_router, alerts_router, cases_router, settings_router,
|
|
linkage_router, actions_router):
|
|
app.include_router(module.router)
|
|
|
|
|
|
@app.get("/api/health")
|
|
def health():
|
|
return {"status": "ok", "version": VERSION, "config": settings.public_flags()}
|
|
|
|
|
|
# The built React bundle is copied into the image; in development Vite serves it
|
|
# instead and proxies /api here, so a missing directory is not an error.
|
|
if os.path.isdir(settings.static_dir):
|
|
app.mount("/assets", StaticFiles(directory=os.path.join(settings.static_dir, "assets")),
|
|
name="assets")
|
|
|
|
@app.get("/{full_path:path}")
|
|
def spa(full_path: str):
|
|
if full_path.startswith("api/"):
|
|
return JSONResponse({"detail": "Not found"}, status_code=404)
|
|
index = os.path.join(settings.static_dir, "index.html")
|
|
if os.path.isfile(index):
|
|
return FileResponse(index)
|
|
return JSONResponse({"detail": "Frontend not built"}, status_code=404)
|