148 lines
5.5 KiB
Python
148 lines
5.5 KiB
Python
"""
|
|
Admin Router — админ-панель с реальной статистикой проекта.
|
|
1. /admin/dashboard — общая статистика (юзеры, онлайн, по уровням)
|
|
2. /admin/sentry-errors — ошибки из Sentry (через API)
|
|
"""
|
|
import os
|
|
import logging
|
|
import httpx
|
|
from fastapi import APIRouter, Request, HTTPException
|
|
from auth import require_auth
|
|
from database import get_pool
|
|
from config import ADMIN_IDS
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
router = APIRouter(prefix="/admin", tags=["admin"])
|
|
|
|
# ============ Настройки Sentry ============
|
|
SENTRY_AUTH_TOKEN = os.getenv("SENTRY_AUTH_TOKEN", "")
|
|
SENTRY_ORG_SLUG = os.getenv("SENTRY_ORG_SLUG", "drobysh")
|
|
SENTRY_PROJECT_SLUG = os.getenv("SENTRY_PROJECT_SLUG", "filwords-backend")
|
|
SENTRY_API_BASE = os.getenv("SENTRY_API_BASE", "https://de.sentry.io/api/0")
|
|
|
|
|
|
def _require_admin(telegram_id: int):
|
|
"""Проверка, что пользователь — админ."""
|
|
if telegram_id not in ADMIN_IDS:
|
|
raise HTTPException(403, "Access denied")
|
|
|
|
|
|
@router.get("/dashboard")
|
|
async def get_admin_dashboard(request: Request):
|
|
"""
|
|
Общая статистика проекта:
|
|
- Всего пользователей / новых сегодня / онлайн
|
|
- Активных за 24 часа
|
|
- Распределение по уровням языка
|
|
"""
|
|
telegram_id, _ = await require_auth(request)
|
|
_require_admin(telegram_id)
|
|
|
|
pool = await get_pool()
|
|
async with pool.acquire() as conn:
|
|
# Всего пользователей
|
|
total_users = await conn.fetchval("SELECT COUNT(*) FROM users") or 0
|
|
|
|
# Новых сегодня
|
|
new_today = await conn.fetchval(
|
|
"SELECT COUNT(*) FROM users WHERE created_at::date = CURRENT_DATE"
|
|
) or 0
|
|
|
|
# Онлайн за последние 30 минут
|
|
online_now = await conn.fetchval("""
|
|
SELECT COUNT(DISTINCT user_id) FROM (
|
|
SELECT user_id FROM user_progress WHERE completed_at > NOW() - INTERVAL '30 minutes'
|
|
UNION
|
|
SELECT user_id FROM sentence_progress WHERE completed_at > NOW() - INTERVAL '30 minutes'
|
|
UNION
|
|
SELECT user_id FROM daily_logins WHERE login_date = CURRENT_DATE
|
|
) active
|
|
""") or 0
|
|
|
|
# Активных за 24 часа
|
|
active_24h = await conn.fetchval("""
|
|
SELECT COUNT(DISTINCT user_id) FROM (
|
|
SELECT user_id FROM user_progress WHERE completed_at > NOW() - INTERVAL '24 hours'
|
|
UNION
|
|
SELECT user_id FROM sentence_progress WHERE completed_at > NOW() - INTERVAL '24 hours'
|
|
UNION
|
|
SELECT user_id FROM user_learning_answers WHERE created_at > NOW() - INTERVAL '24 hours'
|
|
UNION
|
|
SELECT user_id FROM daily_logins WHERE login_date = CURRENT_DATE
|
|
) active
|
|
""") or 0
|
|
|
|
# Распределение по уровням
|
|
level_counts = {}
|
|
for lvl in ["A1", "A2", "B1", "B2", "C1", "C2"]:
|
|
count = await conn.fetchval(
|
|
"SELECT COUNT(*) FROM users WHERE current_level = $1", lvl
|
|
) or 0
|
|
level_counts[lvl] = count
|
|
|
|
return {
|
|
"total_users": total_users,
|
|
"new_today": new_today,
|
|
"online_now": online_now,
|
|
"active_24h": active_24h,
|
|
"level_counts": level_counts,
|
|
}
|
|
|
|
|
|
@router.get("/sentry-errors")
|
|
async def get_sentry_errors(request: Request):
|
|
"""
|
|
Последние ошибки из Sentry.
|
|
Возвращает только unresolved (нерешённые) за последние 24 часа.
|
|
"""
|
|
telegram_id, _ = await require_auth(request)
|
|
_require_admin(telegram_id)
|
|
|
|
if not SENTRY_AUTH_TOKEN:
|
|
return {"errors": [], "error": "SENTRY_AUTH_TOKEN not configured"}
|
|
|
|
url = f"{SENTRY_API_BASE}/projects/{SENTRY_ORG_SLUG}/{SENTRY_PROJECT_SLUG}/issues/"
|
|
params = {
|
|
"query": "is:unresolved",
|
|
"statsPeriod": "24h",
|
|
"limit": 20,
|
|
}
|
|
headers = {
|
|
"Authorization": f"Bearer {SENTRY_AUTH_TOKEN}",
|
|
}
|
|
|
|
try:
|
|
async with httpx.AsyncClient(timeout=10.0) as client:
|
|
response = await client.get(url, params=params, headers=headers)
|
|
|
|
if response.status_code != 200:
|
|
logger.error(f"Sentry API error: {response.status_code} — {response.text[:200]}")
|
|
return {"errors": [], "error": f"Sentry API: {response.status_code}"}
|
|
|
|
raw_issues = response.json()
|
|
|
|
errors = []
|
|
for issue in raw_issues:
|
|
errors.append({
|
|
"id": issue.get("id"),
|
|
"short_id": issue.get("shortId"),
|
|
"title": issue.get("title"),
|
|
"culprit": issue.get("culprit"),
|
|
"level": issue.get("level"),
|
|
"count": int(issue.get("count", 0)),
|
|
"user_count": issue.get("userCount", 0),
|
|
"first_seen": issue.get("firstSeen"),
|
|
"last_seen": issue.get("lastSeen"),
|
|
"permalink": issue.get("permalink"),
|
|
"priority": issue.get("priority"),
|
|
})
|
|
|
|
return {"errors": errors, "count": len(errors)}
|
|
|
|
except httpx.TimeoutException:
|
|
logger.error("Sentry API timeout")
|
|
return {"errors": [], "error": "Sentry API timeout"}
|
|
except Exception as e:
|
|
logger.error(f"Sentry API unexpected error: {e}")
|
|
return {"errors": [], "error": str(e)} |