"""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