diff --git a/app/main.py b/app/main.py index a435163..fde633c 100644 --- a/app/main.py +++ b/app/main.py @@ -3054,22 +3054,14 @@ async def admin_overview(request: Request): } -@app.get("/admin/metrics/series") -@limiter.limit("30/minute") -async def admin_metrics_series(request: Request, days: int = 90): - """Daily metrics series for charts. +def metrics_series(days: int = 90) -> list[dict]: + """Daily metrics rows, oldest first. - Authenticated by the X-Admin-Token header. Returns oldest-first rows from - `metrics_daily` for the last `days` days, for the admin panel and the member - dashboard to chart from one source of truth. A NULL column means that source - could not be read on that day: render a gap, not a zero. - - Cumulative columns (members_*, newsletter_subs, matrix_space_members) are - counts as of that day; spend_usd/requests are that day's traffic only. - Backfilled days carry a LOWER BOUND for members_total (members provisioned - without a welcome email are absent from the history). + One query, two callers: the admin panel (via /admin/metrics/series) and the + member dashboard (via /broker/metrics). Columns are cumulative as of that + day except spend_usd/requests, which are that day only; NULL means the + source could not be read that day. """ - _verify_admin_token(request) limit = max(1, min(int(days or 90), 730)) conn = get_db() try: @@ -3092,8 +3084,27 @@ async def admin_metrics_series(request: Request, days: int = 90): "requests", "recorded_at", ] - series = [dict(zip(cols, row)) for row in reversed(rows)] - return {"series": series, "count": len(series), "days": limit} + return [dict(zip(cols, row)) for row in reversed(rows)] + + +@app.get("/admin/metrics/series") +@limiter.limit("30/minute") +async def admin_metrics_series(request: Request, days: int = 90): + """Daily metrics series for charts. + + Authenticated by the X-Admin-Token header. Returns oldest-first rows from + `metrics_daily` for the last `days` days, for the admin panel and the member + dashboard to chart from one source of truth. A NULL column means that source + could not be read on that day: render a gap, not a zero. + + Cumulative columns (members_*, newsletter_subs, matrix_space_members) are + counts as of that day; spend_usd/requests are that day's traffic only. + Backfilled days carry a LOWER BOUND for members_total (members provisioned + without a welcome email are absent from the history). + """ + _verify_admin_token(request) + series = metrics_series(days) + return {"series": series, "count": len(series), "days": max(1, min(int(days or 90), 730))} @app.get("/admin/model-usage") @@ -3469,6 +3480,21 @@ async def broker_usage(request: Request, days: int = 0): return await broker_get_usage(email, days=days) +@app.get("/broker/model-leaderboard") +@limiter.limit("30/minute") +async def broker_metrics(request: Request, days: int = 90): + """Co-op metrics over time, for the member dashboard. + + The same series the admin panel reads. It comes through the broker because + it is AGGREGATE and non-personal — co-op size, requests served, spend — the + same class of social-proof data as /broker/model-leaderboard. No per-member + figure passes through here, and members asked to see how the co-op is doing. + """ + _verify_broker_secret(request) + series = metrics_series(days) + return {"series": series, "count": len(series), "days": max(1, min(int(days or 90), 730))} + + @app.get("/broker/model-leaderboard") @limiter.limit("30/minute") async def broker_model_leaderboard(request: Request, days: int = 0):