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
bcc4371ac7
commit
f255728f75
15 changed files with 451 additions and 26 deletions
|
|
@ -3,7 +3,7 @@ import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
|||
import { Bell, Check, History, Monitor, Pause, Play, RefreshCw, User, UserPlus, X } from "lucide-react";
|
||||
import { SCHEMES, type Scheme, type ThemePrefs } from "../lib/theme";
|
||||
import { api, type Invite, type Me } from "../lib/api";
|
||||
import { formatEta } from "../lib/format";
|
||||
import { formatEta, quotaActionLabel } from "../lib/format";
|
||||
import {
|
||||
configureNotifications,
|
||||
getNotifSettings,
|
||||
|
|
@ -322,6 +322,7 @@ function Notifications() {
|
|||
function Sync({ me }: { me: Me }) {
|
||||
const qc = useQueryClient();
|
||||
const status = useQuery({ queryKey: ["my-status"], queryFn: api.myStatus });
|
||||
const usage = useQuery({ queryKey: ["my-usage"], queryFn: api.myUsage });
|
||||
const s = status.data;
|
||||
const syncSubs = useMutation({
|
||||
mutationFn: () => api.syncSubscriptions(),
|
||||
|
|
@ -392,6 +393,36 @@ function Sync({ me }: { me: Me }) {
|
|||
)}
|
||||
</Section>
|
||||
|
||||
<Section title="Your API usage">
|
||||
{usage.data ? (
|
||||
<>
|
||||
<div className="space-y-1.5 text-sm">
|
||||
<Row label="Today" hint="YouTube API units your own actions used today (resets midnight US Pacific). Background sync isn't counted here.">
|
||||
{usage.data.today.toLocaleString()}
|
||||
</Row>
|
||||
<Row label="Last 7 days">{usage.data.last_7d.toLocaleString()}</Row>
|
||||
<Row label="Last 30 days">{usage.data.last_30d.toLocaleString()}</Row>
|
||||
<Row label="All time">{usage.data.all_time.toLocaleString()}</Row>
|
||||
</div>
|
||||
{Object.keys(usage.data.by_action).length > 0 && (
|
||||
<div className="mt-2 pt-2 border-t border-border space-y-1 text-xs text-muted">
|
||||
<div className="uppercase tracking-wide">By action (30d)</div>
|
||||
{Object.entries(usage.data.by_action)
|
||||
.sort((a, b) => b[1] - a[1])
|
||||
.map(([action, units]) => (
|
||||
<div key={action} className="flex justify-between gap-2">
|
||||
<span>{quotaActionLabel(action)}</span>
|
||||
<span className="tabular-nums">{units.toLocaleString()}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<div className="text-muted text-sm">No usage yet.</div>
|
||||
)}
|
||||
</Section>
|
||||
|
||||
<Section title="Actions">
|
||||
<Tooltip hint="Re-import your YouTube subscriptions and pull in any newly-followed channels.">
|
||||
<button
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue