bifroest/app/usage/routes.py
2026-10-27 08:34:17 +01:00

127 lines
5.9 KiB
Python

"""Nutzung und Kosten, Runs (→ Konzept 13, Phase 5 Administration)."""
import csv
import io
from datetime import date, datetime, time, timedelta
from fastapi import APIRouter, Request
from fastapi.responses import HTMLResponse, Response
from app import web
from app.core.errors import Invalid
from app.feedback import service as feedback
from app.identity import admin
from app.tasks import states
from app.usage import service as usage
from app.workspaces import service as workspaces
PERIODS = {"month": "Dieser Monat", "last_month": "Letzter Monat", "7d": "Letzte 7 Tage", "30d": "Letzte 30 Tage",
"custom": "Zeitraum"}
def period(params: dict[str, str]) -> tuple[str, date, date]:
"""Gewählter Zeitraum als [start, end] in Tagen (end einschließlich)."""
today = date.today()
key = params.get("period", "month")
if key == "last_month":
end = today.replace(day=1) - timedelta(days=1)
return key, end.replace(day=1), end
if key == "7d":
return key, today - timedelta(days=6), today
if key == "30d":
return key, today - timedelta(days=29), today
if key == "custom":
try:
start, end = date.fromisoformat(params.get("start", "")), date.fromisoformat(params.get("end", ""))
except ValueError:
raise Invalid("Bitte Start- und Enddatum angeben.") from None
if end < start:
raise Invalid("Das Enddatum liegt vor dem Start.")
return key, start, end
return "month", today.replace(day=1), today
def bounds(start: date, end: date) -> tuple[datetime, datetime]:
return (datetime.combine(start, time.min).astimezone(),
datetime.combine(end + timedelta(days=1), time.min).astimezone())
def router() -> APIRouter:
r = APIRouter()
def overview(request: Request, workspace=None) -> HTMLResponse:
params = dict(request.query_params)
key, start, end = period(params)
group = params.get("group", "team" if workspace is None else "user")
if group not in usage.GROUPS or (workspace is not None and group in ("team", "workspace")):
group = "user"
since, until = bounds(start, end)
ws_id = workspace.id if workspace else None
with web.db(request).session() as session:
rows = usage.report(session, since, until, group, workspace_id=ws_id)
context = {"rows": rows, "total": usage.total(rows), "group": group, "groups": usage.GROUPS,
"feedback": feedback.report(session, since, until, workspace_id=ws_id),
"period": key, "periods": PERIODS, "start": start, "end": end,
"stats": usage.run_stats(session, since, until, workspace_id=ws_id),
"labels": states.LABELS, "workspace": workspace, "current_workspace": workspace,
"base": f"/workspaces/{ws_id}" if workspace else "/admin"}
return web.render(request, "usage.html", context)
def run_list(request: Request, workspace=None) -> HTMLResponse:
params = dict(request.query_params)
key, start, end = period(params)
status = params.get("status", "")
since, until = bounds(start, end)
with web.db(request).session() as session:
rows = usage.runs(session, since, until, status=status if status in states.LABELS else "",
workspace_id=workspace.id if workspace else None)
return web.render(request, "runs.html", {
"rows": rows, "status": status, "labels": states.LABELS, "period": key, "periods": PERIODS,
"start": start, "end": end, "workspace": workspace, "current_workspace": workspace,
"base": f"/workspaces/{workspace.id}" if workspace else "/admin"})
def feedback_csv(request: Request, workspace=None) -> Response:
key, start, end = period(dict(request.query_params))
since, until = bounds(start, end)
with web.db(request).session() as session:
found = feedback.rows(session, since, until, workspace_id=workspace.id if workspace else None)
buffer = io.StringIO()
csv.writer(buffer, delimiter=";").writerows(feedback.csv_rows(found))
name = f"rueckmeldungen-{start}-{end}.csv"
return Response("\ufeff" + buffer.getvalue(), media_type="text/csv; charset=utf-8",
headers={"Content-Disposition": f'attachment; filename="{name}"'})
@r.get("/admin/usage/feedback.csv")
def platform_feedback(request: Request, principal: web.User) -> Response:
admin.require(principal)
return feedback_csv(request)
@r.get("/workspaces/{workspace_id}/usage/feedback.csv")
def workspace_feedback(request: Request, principal: web.User, workspace_id: int) -> Response:
with web.db(request).session() as session:
ws = workspaces.require_admin(session, principal, workspace_id).workspace
return feedback_csv(request, ws)
@r.get("/admin/usage")
def platform_usage(request: Request, principal: web.User) -> HTMLResponse:
admin.require(principal)
return overview(request)
@r.get("/admin/runs")
def platform_runs(request: Request, principal: web.User) -> HTMLResponse:
admin.require(principal)
return run_list(request)
@r.get("/workspaces/{workspace_id}/usage")
def workspace_usage(request: Request, principal: web.User, workspace_id: int) -> HTMLResponse:
with web.db(request).session() as session:
ws = workspaces.require_admin(session, principal, workspace_id).workspace
return overview(request, ws)
@r.get("/workspaces/{workspace_id}/runs")
def workspace_runs(request: Request, principal: web.User, workspace_id: int) -> HTMLResponse:
with web.db(request).session() as session:
ws = workspaces.require_admin(session, principal, workspace_id).workspace
return run_list(request, ws)
return r