Neuer Reiter zwischen Audit-Log und Verbrauchsrechner, Anlass war die Dashboard-Kachel API-Kosten gesamt, die nirgendwo hinführte (sie springt jetzt hierher). Kennzahlen-Set nach Betreiber-Best-Practices für nutzungsbasierte KI-Produkte (Kostenverlauf, Marge je Kunde, Betrieb). - GET /api/statistik?months=N liefert alles in einem JSON. Kosten und Läufe je Monat und Lagentyp (führende Quelle refresh_log mit lückenlosem incidents-Join), Quellen-Split aus token_usage_monthly, Erfolgsquote, normalisiertes Fehler-Ranking, Dauer je Typ, Wachstum, MAU/DAU aus user_activity_days und je Organisation Ist-Kosten plus Margen-Schätzung zu heutigen Sätzen (Recherche-Anlage zählt dreifach, weil der Multi-Pass nur eine Log-Zeile erzeugt). Aggregation als pure, unit-getestete Funktionen, Monatsachse lückenlos gefüllt, Mediane nur über Läufe mit Kosten (Erfassung begann erst 03/2026). - statistik.js injiziert sich selbst nach dem rechner.js-Muster, Platzierung doppelt gesichert (Ladereihenfolge mit async=false plus insertBefore). Charts als Inline-SVG ohne Bibliothek, eine Achse je Chart, Hover-Titel mit großen Trefferflächen, Legenden, Serienfarben je Theme mit dem dataviz-Palette-Validator geprüft (alle Checks PASS). Zeitraum 6/12/Alles, Datenstand-Hinweis, ehrliche Fußnote unter der Margen-Tabelle, Leerzustand für die junge Aktivitäts-Erfassung. - Migration user_activity_days für die Portal-Staging-DB (auf Live legt sie der Monitor an, dort läuft die Erfassung in get_current_user). - 10 neue Unit-Tests plus Auth-Smoke für den Endpoint, 96 Tests grün (die zwei bekannten shared-Import-Fehlschläge bestehen unverändert). Gegenstück im Monitor: Commit d367c60 (Aktivitäts-Erfassung). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
87 Zeilen
2.7 KiB
Python
87 Zeilen
2.7 KiB
Python
"""Verwaltungsportal - FastAPI Anwendung.
|
|
|
|
Auth: Magic-Link (analog Monitor). Passwort-Login wurde mit Migration
|
|
2026-05-09 entfernt. Erlaubte Email-Adresse(n) sind in config.ALLOWED_EMAIL.
|
|
"""
|
|
import logging
|
|
from contextlib import asynccontextmanager
|
|
|
|
from fastapi import FastAPI
|
|
from fastapi.staticfiles import StaticFiles
|
|
from fastapi.responses import FileResponse
|
|
|
|
from config import STATIC_DIR, PORT
|
|
from routers import auth, organizations, licenses, users, dashboard, sources, token_usage, audit, translation, x_scraper, pricing, statistik
|
|
|
|
logging.basicConfig(
|
|
level=logging.INFO,
|
|
format="%(asctime)s %(levelname)s [%(name)s] %(message)s",
|
|
)
|
|
logger = logging.getLogger("verwaltung")
|
|
|
|
|
|
@asynccontextmanager
|
|
async def lifespan(app: FastAPI):
|
|
# Schutz gegen falsche Deploy-Reihenfolge: Die Credits-Spalten in licenses
|
|
# legt die Monitor-Migration an. Fehlen sie, scheitern Lizenz-Anlage und
|
|
# Token-Tab mit "no such column". Hier nur ein deutlicher Log-Hinweis,
|
|
# kein Hard-Fail.
|
|
try:
|
|
from database import get_db
|
|
db = await get_db()
|
|
try:
|
|
cursor = await db.execute("PRAGMA table_info(licenses)")
|
|
cols = [row[1] for row in await cursor.fetchall()]
|
|
if "credits_period" not in cols:
|
|
logger.error(
|
|
"licenses-Tabelle ohne Credits-Spalten (credits_period fehlt)! "
|
|
"Zuerst den Monitor deployen (dessen Migration legt sie an) "
|
|
"oder migrations/2026-07-25_credits_period.py gegen diese DB fahren."
|
|
)
|
|
finally:
|
|
await db.close()
|
|
except Exception as exc:
|
|
logger.warning("Startup-Schema-Check uebersprungen: %s", exc)
|
|
logger.info("Verwaltungsportal gestartet auf Port %s", PORT)
|
|
yield
|
|
logger.info("Verwaltungsportal beendet")
|
|
|
|
|
|
app = FastAPI(
|
|
title="AegisSight Verwaltungsportal",
|
|
version="2.0.0",
|
|
lifespan=lifespan,
|
|
)
|
|
|
|
# --- Routen ---
|
|
app.include_router(auth.router)
|
|
app.include_router(organizations.router)
|
|
app.include_router(licenses.router)
|
|
app.include_router(users.router)
|
|
app.include_router(dashboard.router)
|
|
app.include_router(sources.router)
|
|
app.include_router(token_usage.router)
|
|
app.include_router(audit.router)
|
|
app.include_router(translation.router)
|
|
app.include_router(x_scraper.router)
|
|
app.include_router(pricing.router)
|
|
app.include_router(statistik.router)
|
|
|
|
# --- Statische Dateien ---
|
|
app.mount("/static", StaticFiles(directory=STATIC_DIR), name="static")
|
|
|
|
|
|
@app.get("/")
|
|
async def index():
|
|
return FileResponse(f"{STATIC_DIR}/index.html")
|
|
|
|
|
|
@app.get("/dashboard")
|
|
async def dashboard_page():
|
|
return FileResponse(f"{STATIC_DIR}/dashboard.html")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
import uvicorn
|
|
uvicorn.run("main:app", host="0.0.0.0", port=PORT, reload=True)
|