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:
parent
0dad71cf6e
commit
d610647d13
15 changed files with 451 additions and 26 deletions
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue