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

@ -22,6 +22,7 @@ import Header from "./components/Header";
import Sidebar from "./components/Sidebar";
import Feed from "./components/Feed";
import Channels from "./components/Channels";
import Stats from "./components/Stats";
import SettingsPanel from "./components/SettingsPanel";
import Toaster from "./components/Toaster";
@ -157,9 +158,7 @@ export default function App() {
/>
)}
<main className="flex-1 min-w-0 overflow-y-auto">
{page === "feed" ? (
<Feed filters={filters} setFilters={setFilters} view={view} />
) : (
{page === "channels" ? (
<Channels
canWrite={meQuery.data!.can_write}
onViewChannel={(id, name) => {
@ -167,6 +166,10 @@ export default function App() {
setPage("feed");
}}
/>
) : page === "stats" && meQuery.data!.role === "admin" ? (
<Stats />
) : (
<Feed filters={filters} setFilters={setFilters} view={view} />
)}
</main>
</div>