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
|
|
@ -265,6 +265,25 @@ class ApiQuotaUsage(Base):
|
|||
units_used: Mapped[int] = mapped_column(Integer, default=0, server_default="0")
|
||||
|
||||
|
||||
class QuotaEvent(Base):
|
||||
"""Per-spend audit log for YouTube API quota attribution. `user_id` is the end-user
|
||||
whose action caused the spend; NULL means background/system work (scheduler backfill,
|
||||
enrichment, resync). The aggregate budget still lives in ApiQuotaUsage; this is detail."""
|
||||
|
||||
__tablename__ = "quota_events"
|
||||
|
||||
id: Mapped[int] = mapped_column(primary_key=True)
|
||||
# SET NULL on user delete: keep the audit trail (it just becomes system-attributed).
|
||||
user_id: Mapped[int | None] = mapped_column(
|
||||
ForeignKey("users.id", ondelete="SET NULL"), index=True
|
||||
)
|
||||
action: Mapped[str] = mapped_column(String(40), index=True)
|
||||
units: Mapped[int] = mapped_column(Integer)
|
||||
created_at: Mapped[datetime] = mapped_column(
|
||||
DateTime(timezone=True), server_default=func.now(), index=True
|
||||
)
|
||||
|
||||
|
||||
class AppState(Base):
|
||||
"""Single-row global app state (admin-controlled)."""
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue