Files
cx-ui/backend/app/main.py
Parham Monfared 1262690276
Some checks failed
build-and-deploy / test (push) Has been cancelled
build-and-deploy / image (push) Has been cancelled
build-and-deploy / deploy (push) Has been cancelled
Split into a FastAPI backend and a React frontend, add case state and SSO
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>
2026-08-06 07:11:28 +01:00

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)