""" 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)}