From 9da67a902111f29d817537edd5ecb225b4208c3d Mon Sep 17 00:00:00 2001 From: inference-bot Date: Mon, 14 Sep 2026 16:39:54 -0600 Subject: [PATCH] Add /admin/overview endpoint (member list + spend + co-op aggregates) --- app/main.py | 59 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 59 insertions(+) diff --git a/app/main.py b/app/main.py index b10dd7b..c032ff2 100644 --- a/app/main.py +++ b/app/main.py @@ -1080,6 +1080,65 @@ async def admin_migrate_teams(token: str): return {"status": "migrated", "results": results} +@app.get("/admin/overview/{token}") +async def admin_overview(token: str): + """Return a co-op-wide overview for the admin dashboard. + + Protected by the same secret token as the other /admin endpoints. Returns + every member (active + inactive) with their balance, spend, remaining, and + team reset date, plus co-op aggregates (total members, total spend). + """ + if not WEBHOOK_TOKEN or not secrets.compare_digest(token, WEBHOOK_TOKEN): + raise HTTPException(401, "Invalid token") + + conn = get_db() + rows = conn.execute( + "SELECT email, active, balance, cloudron_user_id FROM members ORDER BY email" + ).fetchall() + conn.close() + + # Fetch team spend/budget from LiteLLM once. + teams = {} + try: + async with httpx.AsyncClient(timeout=15.0) as client: + r = await client.get( + f"{LITELLM_BASE}/team/list", + headers={"Authorization": f"Bearer {LITELLM_MASTER_KEY}"}, + ) + r.raise_for_status() + for t in r.json(): + teams[t.get("team_alias")] = t + except Exception as e: + logger.warning("admin overview: team/list failed: %s", e) + + members = [] + total_spend = 0.0 + for email, active, balance, user_id in rows: + team = teams.get(f"member:{email}", {}) + spend = float(team.get("spend") or 0.0) + total_spend += spend + members.append( + { + "email": email, + "active": bool(active), + "balance": float(balance) if balance is not None else MEMBER_BUDGET, + "spend": spend, + "remaining": max((balance or MEMBER_BUDGET) - spend, 0.0), + "reset_at": team.get("budget_reset_at"), + "cloudron_user_id": user_id, + } + ) + + return { + "members": members, + "totals": { + "members": len(members), + "active": sum(1 for m in members if m["active"]), + "total_spend": total_spend, + }, + } + + async def litellm_delete_keys_by_alias(email: str) -> None: """Delete all LiteLLM keys whose alias matches member:.