feat(stats): per-user API quota attribution + admin usage page

Track who burned how much YouTube API quota. A QuotaEvent audit log (migration
0009) records every spend with the triggering user (NULL = background/system) and
an action label, set via a request/job-scoped contextvar (quota.attribute) so no
call signatures change. User-initiated work (sync subscriptions, unsubscribe,
opt-in recent backfill, manual enrich) attributes to the user; scheduler work to
System, split by action.

- backend: QuotaEvent model + migration 0009; quota.attribute() contextvar;
  record_usage logs events; entry points wrapped (routes/sync, routes/channels,
  scheduler); GET /api/quota/my-usage + GET /api/quota/admin
- frontend: admin-only Stats page (header nav, page=stats) with daily bars +
  per-user breakdown by action and range picker; 'Your API usage' in Settings ->
  Sync for every user

Verified: attribution + endpoints compute correctly; events are per-user vs System.
This commit is contained in:
npeter83 2026-06-12 02:47:55 +02:00
parent 80cae63811
commit f6375a097e
15 changed files with 451 additions and 26 deletions

View file

@ -26,7 +26,7 @@ from starlette.middleware.sessions import SessionMiddleware
from app import auth
from app.config import settings
from app.routes import admin, channels, feed, health, me, sync, tags
from app.routes import admin, channels, feed, health, me, quota, sync, tags
from app.scheduler import shutdown_scheduler, start_scheduler
@ -67,6 +67,7 @@ app.include_router(feed.router)
app.include_router(me.router)
app.include_router(channels.router)
app.include_router(admin.router)
app.include_router(quota.router)
# The built SPA (populated by the Docker frontend build stage).
STATIC_DIR = Path(__file__).parent / "static_spa"