From 8e8cb90d1161560d3db576221d9e13ead8a02ca0 Mon Sep 17 00:00:00 2001 From: Inferencebot Date: Wed, 28 Oct 2026 04:15:42 +0000 Subject: [PATCH] dashboard: "The co-op over time" chart in the Co-op usage view Members asked for charts to share with the co-op. This is the member-facing half of it, in the view where co-op-wide data already lives. - /api/metrics proxies the portal's new /broker/metrics (aggregate, non-personal: co-op size, requests served, spend). No per-member figure is fetched. - Same chart as the admin panel's Trends tab: inline SVG, no library, no CDN, colours from the shared palette vars. Members / Requests / Spend, and 30 / 90 / all time, re-rendered client-side without refetching. - Lazy: loads the first time someone opens the co-op view, so the dashboard's initial load is unchanged. - Gaps stay gaps. A day never measured breaks the line rather than reading as zero members, with the sub-line saying how many days are missing. - Integer y-axis for people and requests; 1/2/5 money ticks, per the panel fix. Verified with the same node harness used for the panel (shared, so it asserts the same invariants against the live series). --- app/main.py | 196 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 196 insertions(+) diff --git a/app/main.py b/app/main.py index 4e31b95..5bfb6c6 100644 --- a/app/main.py +++ b/app/main.py @@ -133,6 +133,23 @@ async def api_usage(request: Request): return JSONResponse({"error": e.detail}, status_code=e.status_code) +@app.get("/api/metrics") +async def api_metrics(request: Request): + """Co-op metrics over time, for the Co-op usage view. + + Aggregate and non-personal (co-op size, requests served, spend), the same + class as the model leaderboard. Fetched at the widest range so the metric + and period toggles re-render client-side without refetching. + """ + identity = get_user_identity(request) + if not identity: + return JSONResponse({"error": "unauthenticated"}, status_code=401) + try: + return await broker_get(identity, "/broker/metrics?days=730") + except HTTPException as e: + return JSONResponse({"error": e.detail}, status_code=e.status_code) + + @app.post("/api/keys") async def api_create_key(request: Request): identity = get_user_identity(request) @@ -363,6 +380,15 @@ PAGE_HTML = """ /* Leaderboard */ .board-note { color: var(--muted); font-size: 12px; margin-bottom: 10px; } + /* The co-op over time: same chart as the admin panel's Trends tab, sized for + members. Inline SVG, no library, colours from the shared palette vars. */ + .coop-trend { border: 1px solid var(--border, #e4e0d8); border-radius: 12px; padding: 14px 16px 12px; margin-bottom: 20px; } + .coop-trend-head { display: flex; align-items: baseline; gap: 10px; flex-wrap: wrap; margin-bottom: 8px; } + .coop-trend-title { font-weight: 700; font-size: 15px; color: var(--ink, #2d3327); } + .coop-trend-sub { color: var(--muted); font-size: 12px; } + .coop-trend-controls { display: flex; gap: 18px; flex-wrap: wrap; margin-bottom: 10px; } + .trend-group { display: flex; align-items: center; gap: 6px; flex-wrap: wrap; } + .coop-trend-body svg { display: block; width: 100%; height: auto; overflow: visible; } .board-row { display: flex; align-items: flex-start; gap: 14px; padding: 12px 0; border-top: 1px solid #f0ede6; @@ -533,6 +559,28 @@ PAGE_HTML = """