initial commit (backend)

This commit is contained in:
2026-09-20 11:45:58 +00:00
commit 033adf023e
65 changed files with 28292 additions and 0 deletions
+148
View File
@@ -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)}