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 0dad71cf6e
commit d610647d13
15 changed files with 451 additions and 26 deletions

View file

@ -1,5 +1,5 @@
import { useRef, useState } from "react";
import { Home, LogOut, Search, Settings, Shield, Tv } from "lucide-react";
import { BarChart3, Home, LogOut, Search, Settings, Shield, Tv } from "lucide-react";
import type { FeedFilters, Me } from "../lib/api";
import type { Page } from "../lib/urlState";
import SyncStatus from "./SyncStatus";
@ -48,7 +48,9 @@ export default function Header({
/>
</div>
) : (
<div className="flex-1 text-center text-sm font-semibold">Channel manager</div>
<div className="flex-1 text-center text-sm font-semibold">
{page === "stats" ? "Usage & stats" : "Channel manager"}
</div>
)}
<div className="flex items-center gap-1">
@ -134,17 +136,24 @@ function AccountMenu({
)}
<div className="mt-2 flex flex-col">
{page === "channels" ? (
{page !== "feed" && (
<button onClick={() => { setPage("feed"); setOpen(false); }} className={itemClass}>
<Home className="w-4 h-4" />
Feed
</button>
) : (
)}
{page !== "channels" && (
<button onClick={() => { setPage("channels"); setOpen(false); }} className={itemClass}>
<Tv className="w-4 h-4" />
Channels
</button>
)}
{me.role === "admin" && page !== "stats" && (
<button onClick={() => { setPage("stats"); setOpen(false); }} className={itemClass}>
<BarChart3 className="w-4 h-4" />
Stats
</button>
)}
<button onClick={() => { onOpenSettings(); setOpen(false); }} className={itemClass}>
<Settings className="w-4 h-4" />
Settings