diff --git a/app/main.py b/app/main.py index 6ddf76b..296c97f 100644 --- a/app/main.py +++ b/app/main.py @@ -3131,9 +3131,25 @@ async def admin_overview(request: Request): total_spend += spend total_all_time += member_all_time + # Bot and sandbox teams: real spend, but not members. Surfaced in their own + # category so the co-op's own usage is accountable without inflating member + # counts — the same reasoning as the operator accounts above. + agents = [ + { + "alias": alias, + "spend": float(t.get("spend") or 0.0), + "budget": t.get("max_budget"), + "team_members": len(t.get("members_with_roles") or []), + } + for alias, t in teams.items() + if alias and not alias.startswith("member:") + ] + agents.sort(key=lambda a: a["spend"], reverse=True) + return { "members": members, "admins": admins, + "agents": agents, "totals": { "members": len(members), "active": sum(1 for m in members if m["active"]), diff --git a/metrics_snapshot.py b/metrics_snapshot.py index 133a168..c06effb 100644 --- a/metrics_snapshot.py +++ b/metrics_snapshot.py @@ -143,21 +143,32 @@ def listmonk_joins() -> list[str] | None: return joins or None +# Utility/operator accounts are not members. The panel has always excluded them +# (OPERATOR_EMAILS in app/main.py) while the metrics counted every row, so the two +# disagreed. Same variable name, same default, so they cannot drift apart again. +OPERATOR_EMAILS = { + e.strip().lower() + for e in os.environ.get("OPERATOR_EMAILS", "info@inference.coop,bot@inference.coop").split(",") + if e.strip() +} + + def from_sqlite() -> dict: - """Members, plus the provisioning dates that give the growth curve.""" + """Members, plus the provisioning dates that give the growth curve. + + Operator/utility accounts are excluded, matching the panel's member count. + """ conn = connect() try: - active = conn.execute("SELECT COUNT(*) FROM members WHERE active=1").fetchone()[0] - total = conn.execute("SELECT COUNT(*) FROM members").fetchone()[0] - joins = [ - row[0] - for row in conn.execute( - "SELECT welcome_sent_at FROM members WHERE welcome_sent_at IS NOT NULL AND welcome_sent_at != ''" - ) - ] + rows = conn.execute("SELECT email, active, welcome_sent_at FROM members").fetchall() finally: conn.close() - return {"active": active, "total": total, "joins": joins} + real = [r for r in rows if (r[0] or "").strip().lower() not in OPERATOR_EMAILS] + return { + "active": sum(1 for r in real if r[1]), + "total": len(real), + "joins": [r[2] for r in real if r[2]], + } def from_listmonk() -> int | None: