Add usage stats script and /api/admin/stats endpoint

scripts/usage_stats.py: standalone script (PEP 723, runs via uv run)
that parses all nginx access.log files, filters bots, maps Referer
headers to feature labels, and produces a 3-panel matplotlib figure:
daily logins + 7-day rolling mean, hour×weekday API heatmap, and
weekly feature usage stacked area. Output saved to
/var/bincio/stats/latest.png. Intended for a weekly cron job.

bincio/serve/routers/admin.py: GET /api/admin/stats serves the PNG
via the existing _require_admin() check — no new auth logic or nginx
changes needed.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Davide Scaini
2026-05-18 20:54:17 +02:00
parent bbfab72138
commit adaa075e6e
2 changed files with 305 additions and 1 deletions
+11 -1
View File
@@ -10,7 +10,7 @@ from pathlib import Path
from typing import Any
from fastapi import APIRouter, Cookie, HTTPException, Request
from fastapi.responses import JSONResponse, StreamingResponse
from fastapi.responses import FileResponse, JSONResponse, StreamingResponse
from bincio.serve import deps, tasks
from bincio.serve.models import ResetPasswordCodeResponse
@@ -58,6 +58,16 @@ def _wipe_user_activities(user_dir: Path) -> int:
return deleted
@router.get("/api/admin/stats")
async def admin_stats(bincio_session: str | None = Cookie(default=None)) -> FileResponse:
"""Serve the latest usage stats figure. Admin only."""
deps._require_admin(bincio_session)
path = deps._get_data_dir().parent / "stats" / "latest.png"
if not path.exists():
raise HTTPException(404, "Stats not yet generated — run scripts/usage_stats.py first")
return FileResponse(path, media_type="image/png", headers={"Cache-Control": "no-cache, no-store"})
@router.get("/api/admin/users")
async def admin_users(bincio_session: str | None = Cookie(default=None)) -> JSONResponse:
deps._require_admin(bincio_session)