Files
wehub-resource-sync fed8b2eed7
Build and push multi-arch DocsGPT Docker image / build (linux/amd64, ubuntu-latest, amd64) (push) Has been cancelled
Backend release / release (push) Has been cancelled
Bandit Security Scan / bandit_scan (push) Has been cancelled
Build and push multi-arch DocsGPT Docker image / build (linux/arm64, ubuntu-24.04-arm, arm64) (push) Has been cancelled
Build and push multi-arch DocsGPT Docker image / manifest (push) Has been cancelled
Build and push DocsGPT FE Docker image for development / build (linux/amd64, ubuntu-latest, amd64) (push) Has been cancelled
Build and push DocsGPT FE Docker image for development / build (linux/arm64, ubuntu-24.04-arm, arm64) (push) Has been cancelled
Build and push DocsGPT FE Docker image for development / manifest (push) Has been cancelled
Python linting / ruff (push) Has been cancelled
Run python tests with pytest / Run tests and count coverage (3.12) (push) Has been cancelled
React Widget Build / build (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:28:29 +08:00

138 lines
5.5 KiB
Python

"""Read-only cross-table aggregations for the admin dashboard.
Unlike the per-table repositories, this one answers operator-level questions
that span tables (counts, engagement, spend). All methods are read-only and
time-bounded where they touch large tables, so they are safe to call from an
admin-gated dashboard but should not be put on any hot path.
"""
from __future__ import annotations
from datetime import datetime, timedelta, timezone
from typing import Optional
from sqlalchemy import Connection, text
from application.storage.db.base_repository import row_to_dict
class AdminStatsRepository:
"""Global aggregates for the admin overview and per-user drill-down."""
def __init__(self, conn: Connection) -> None:
self._conn = conn
def _scalar(self, sql: str, params: dict | None = None) -> int:
return int(self._conn.execute(text(sql), params or {}).scalar() or 0)
def overview(self) -> dict:
"""Top-line counts + recent-window engagement for the dashboard home."""
now = datetime.now(timezone.utc)
cutoff_7d = now - timedelta(days=7)
cutoff_30d = now - timedelta(days=30)
users_total = self._scalar("SELECT count(*) FROM users")
users_active = self._scalar("SELECT count(*) FROM users WHERE active")
return {
"users": {
"total": users_total,
"active": users_active,
"inactive": users_total - users_active,
},
"admins": self._scalar(
"SELECT count(DISTINCT user_id) FROM user_roles WHERE role = 'admin'"
),
"agents": self._scalar("SELECT count(*) FROM agents"),
"sources": self._scalar("SELECT count(*) FROM sources"),
"conversations": self._scalar("SELECT count(*) FROM conversations"),
"new_users_7d": self._scalar(
"SELECT count(*) FROM users WHERE created_at >= :c", {"c": cutoff_7d}
),
"active_users_30d": self._scalar(
"SELECT count(DISTINCT user_id) FROM token_usage "
"WHERE timestamp >= :c AND user_id IS NOT NULL",
{"c": cutoff_30d},
),
"failed_logins_7d": self._scalar(
"SELECT count(*) FROM auth_events "
"WHERE event = 'oidc_login_denied' AND created_at >= :c",
{"c": cutoff_7d},
),
"tokens_30d": self._scalar(
"SELECT COALESCE(SUM(prompt_tokens + generated_tokens), 0) "
"FROM token_usage WHERE timestamp >= :c",
{"c": cutoff_30d},
),
}
def top_token_users(self, *, since: datetime, limit: int = 10) -> list[dict]:
"""Highest token consumers since ``since`` (admin usage view)."""
result = self._conn.execute(
text(
"""
SELECT user_id, SUM(prompt_tokens + generated_tokens) AS tokens
FROM token_usage
WHERE timestamp >= :since AND user_id IS NOT NULL
GROUP BY user_id
ORDER BY tokens DESC
LIMIT :limit
"""
),
{"since": since, "limit": int(limit)},
)
return [{"user_id": r[0], "tokens": int(r[1])} for r in result.fetchall()]
def list_users(
self, user_id_filter: Optional[str], offset: int, limit: int
) -> tuple[int, list[dict]]:
"""Paginated users with their last auth-event time, most-recently-seen first.
``last_seen`` is max(auth_events.created_at) for the user (NULL = never).
Ordering surfaces active accounts first, dormant ones last — the natural
triage order for an operator.
"""
where = ""
count_params: dict = {}
if user_id_filter is not None:
where = "WHERE u.user_id ILIKE '%' || :uid || '%'"
count_params["uid"] = user_id_filter
total = self._conn.execute(
text(f"SELECT count(*) FROM users u {where}"), count_params
).scalar_one()
result = self._conn.execute(
text(
f"""
SELECT u.user_id, u.active, u.created_at,
max(ae.created_at) AS last_seen
FROM users u
LEFT JOIN auth_events ae ON ae.user_id = u.user_id
{where}
GROUP BY u.user_id, u.active, u.created_at
ORDER BY max(ae.created_at) DESC NULLS LAST, u.created_at DESC
LIMIT :limit OFFSET :offset
"""
),
{**count_params, "limit": int(limit), "offset": int(offset)},
)
return int(total), [row_to_dict(row) for row in result.fetchall()]
def user_counts(self, user_id: str) -> dict:
"""Per-user resource counts for the drill-down view."""
cutoff_30d = datetime.now(timezone.utc) - timedelta(days=30)
return {
"agents": self._scalar(
"SELECT count(*) FROM agents WHERE user_id = :u", {"u": user_id}
),
"sources": self._scalar(
"SELECT count(*) FROM sources WHERE user_id = :u", {"u": user_id}
),
"conversations": self._scalar(
"SELECT count(*) FROM conversations WHERE user_id = :u", {"u": user_id}
),
"tokens_30d": self._scalar(
"SELECT COALESCE(SUM(prompt_tokens + generated_tokens), 0) "
"FROM token_usage WHERE user_id = :u AND timestamp >= :c",
{"u": user_id, "c": cutoff_30d},
),
}