initial commit (backend)
This commit is contained in:
@@ -0,0 +1,148 @@
|
||||
"""
|
||||
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)}
|
||||
Reference in New Issue
Block a user