From 77024d443fe725d832a25d1fd75e612ec7ba8421 Mon Sep 17 00:00:00 2001 From: nickpons666 Date: Sat, 21 Mar 2026 15:15:38 -0600 Subject: [PATCH] =?UTF-8?q?Feat:=20A=C3=B1adir=20panel=20de=20m=C3=A9trica?= =?UTF-8?q?s=20con=20estad=C3=ADsticas=20por=20idioma,=20plataforma=20y=20?= =?UTF-8?q?servidor?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Crear página dedicada /metrics con gráficos usando Chart.js - Implementar función get_translation_stats() en database.py - Añadir endpoint /api/stats en panel/main.py - Mostrar métricas de traducciones por idioma, plataforma y servidor Discord - Agregar tarjeta de acceso rápido a Métricas en el Dashboard - Actualizar action_plan_pro.md con el progreso completado --- action_plan_pro.md | 36 +++-- botdiscord/database.py | 112 +++++++++++++++ panel/main.py | 21 ++- panel/templates/dashboard.html | 10 ++ panel/templates/metrics.html | 244 +++++++++++++++++++++++++++++++++ 5 files changed, 408 insertions(+), 15 deletions(-) create mode 100644 panel/templates/metrics.html diff --git a/action_plan_pro.md b/action_plan_pro.md index 4ca067c..3382c02 100644 --- a/action_plan_pro.md +++ b/action_plan_pro.md @@ -2,24 +2,32 @@ ## 🎯 Progreso de Mejoras -- [ ] **1. Panel de Métricas (Analytics & Estadísticas)** - - Agregar gráficos en el Dashboard (FastAPI/Jinja). - - Contabilizar traducciones totales, por idioma y por plataforma. +- [x] **1. Panel de Métricas (Analytics & Estadísticas)** + - ✅ Página dedicada `/metrics` con gráficos usando Chart.js. + - ✅ Métricas de traducciones totales, por idioma y por plataforma. + - ✅ Métricas por servidor de Discord. + - ✅ Función `get_translation_stats()` en `botdiscord/database.py`. + - ✅ Endpoint `/api/stats` en `panel/main.py`. + - ✅ Tarjeta de acceso rápido a Métricas en el Dashboard. -- [ ] **2. Rate Limiting (Prevención de Spam y Abusos)** - - Limitar botones de Discord/Telegram a X usos por minuto por usuario. - - Implementar mensajes efímeros de advertencia por spam. +- [x] **2. Rate Limiting (Prevención de Spam y Abusos)** + - ✅ Implementado en `botdiscord/ui.py`: máximo 1 clic por usuario/idioma cada 3 segundos. + - ✅ Aviso efímero automático si el usuario excede el límite. -- [ ] **3. Sistema de Logging Real y Monitoreo (Observabilidad)** - - Reemplazar `print()` por la librería estandar `logging` con guardado en disco rotatorio (archivos diarios). - - Enmascarar errores y alertas. +- [x] **3. Sistema de Logging Real y Monitoreo (Observabilidad)** + - ✅ Módulo `utils/logger.py` con rotación de archivos diaria (14 días de historial). + - ✅ Reemplazados todos los `print()` por `log.info()`, `log.warning()`, `log.error()`. -- [ ] **4. Reemplazo de Caché en RAM por Redis** - - Configurar contenedor oficial de Redis en OMV (`docker-compose-redis.yaml`). - - Adaptar `botdiscord/database.py` y `panel/main.py` para usar Redis si está disponible. +- [x] **4. Reemplazo de Caché en RAM por Redis** + - ✅ `utils/cache.py` creado con fallback a RAM si Redis no está disponible. + - ✅ Caché en cascada en `panel/main.py` y `translate.py`: Redis → RAM → DB → LibreTranslate. + - ✅ `docker-compose-redis.yaml` creado y Redis instalado en OMV. + - ✅ Variables de entorno configuradas en `docker-compose.yml` y `.env.example`. -- [ ] **5. Sistema de "Health Check" de LibreTranslate** - - Comprobar que el endpoint de traducción está vivo antes de lanzar errores, devolviendo mensaje de mantenimiento. +- [x] **5. Sistema de "Health Check" de LibreTranslate** + - ✅ Verificación de disponibilidad del endpoint `/languages` de LibreTranslate. + - ✅ Resultado cacheado 30s para no sobrecargar el servidor. + - ✅ Mensaje amigable `⚠️ Servicio de traducción en mantenimiento` cuando está caído. --- *Este documento guiará la transformación del ecosistema de bots a un entorno de producción masiva.* diff --git a/botdiscord/database.py b/botdiscord/database.py index aa35309..463f5e7 100644 --- a/botdiscord/database.py +++ b/botdiscord/database.py @@ -974,3 +974,115 @@ def delete_discord_server(server_id: int): c.execute("DELETE FROM discord_servers WHERE server_id = ?", (server_id,)) conn.commit() conn.close() + +def get_translation_stats() -> dict: + """Obtiene estadísticas de traducciones totales, por idioma y por plataforma""" + db_type = get_db_type() + + if db_type == "mysql": + conn = get_connection() + cursor = conn.cursor(dictionary=True) + + cursor.execute("SELECT COUNT(*) as total FROM translations") + total_result = cursor.fetchone() + total_translations = total_result['total'] if total_result else 0 + + cursor.execute(""" + SELECT target_lang, COUNT(*) as count + FROM translations + GROUP BY target_lang + ORDER BY count DESC + """) + by_language = {row['target_lang']: row['count'] for row in cursor.fetchall()} + + cursor.execute(""" + SELECT bot_type, COUNT(*) as count + FROM translations t + JOIN messages m ON t.message_id = m.message_id + GROUP BY bot_type + """) + by_platform = {row['bot_type']: row['count'] for row in cursor.fetchall()} + + cursor.execute(""" + SELECT DATE(created_at) as date, COUNT(*) as count + FROM translations + WHERE created_at >= DATE_SUB(NOW(), INTERVAL 30 DAY) + GROUP BY DATE(created_at) + ORDER BY date + """) + by_day = [{'date': str(row['date']), 'count': row['count']} for row in cursor.fetchall()] + + cursor.execute(""" + SELECT COALESCE(s.server_name, CONCAT('Servidor ', m.guild_id)) as server_name, + COUNT(*) as count + FROM translations t + JOIN messages m ON t.message_id = m.message_id + LEFT JOIN discord_servers s ON m.guild_id = s.server_id + WHERE m.guild_id IS NOT NULL + GROUP BY m.guild_id + ORDER BY count DESC + """) + by_server = [{'server': row['server_name'], 'count': row['count']} for row in cursor.fetchall()] + + cursor.close() + + return { + 'total': total_translations, + 'by_language': by_language, + 'by_platform': by_platform, + 'by_day': by_day, + 'by_server': by_server + } + else: + conn = get_connection() + c = conn.cursor() + + c.execute("SELECT COUNT(*) FROM translations") + total_translations = c.fetchone()[0] or 0 + + c.execute(""" + SELECT target_lang, COUNT(*) as count + FROM translations + GROUP BY target_lang + ORDER BY count DESC + """) + by_language = {row[0]: row[1] for row in c.fetchall()} + + c.execute(""" + SELECT m.bot_type, COUNT(*) as count + FROM translations t + JOIN messages m ON t.message_id = m.message_id + GROUP BY m.bot_type + """) + by_platform = {row[0]: row[1] for row in c.fetchall()} + + c.execute(""" + SELECT DATE(created_at) as date, COUNT(*) as count + FROM translations + WHERE created_at >= DATE('now', '-30 days') + GROUP BY DATE(created_at) + ORDER BY date + """) + by_day = [{'date': row[0], 'count': row[1]} for row in c.fetchall()] + + c.execute(""" + SELECT COALESCE(s.server_name, 'Servidor ' || m.guild_id) as server_name, + COUNT(*) as count + FROM translations t + JOIN messages m ON t.message_id = m.message_id + LEFT JOIN discord_servers s ON m.guild_id = s.server_id + WHERE m.guild_id IS NOT NULL + GROUP BY m.guild_id + ORDER BY count DESC + """) + by_server = [{'server': row[0], 'count': row[1]} for row in c.fetchall()] + + conn.close() + + return { + 'total': total_translations, + 'by_language': by_language, + 'by_platform': by_platform, + 'by_day': by_day, + 'by_server': by_server + } diff --git a/panel/main.py b/panel/main.py index b492f91..b8699ec 100644 --- a/panel/main.py +++ b/panel/main.py @@ -24,7 +24,7 @@ load_config() # Cargamos configuración inmediatamente from botdiscord.database import ( init_db, get_ui_translation, save_ui_translation, get_admins, get_admin_by_username, add_admin, delete_admin, - _normalize_text + _normalize_text, get_translation_stats ) init_db() # Aseguramos que las tablas existan antes de que FastAPI atienda peticiones from botdiscord.translate import translate_text @@ -684,6 +684,25 @@ async def diagnosis_page(request: Request): "config": config }) +@app.get("/api/stats") +async def get_stats(request: Request): + if request.cookies.get("auth") != "ok": + raise HTTPException(status_code=401) + + stats = get_translation_stats() + return stats + +@app.get("/metrics") +async def metrics_page(request: Request): + if request.cookies.get("auth") != "ok": + return RedirectResponse(url="/login") + + username = request.cookies.get("username", "") + return templates.TemplateResponse("metrics.html", { + "request": request, + "username": username + }) + if __name__ == "__main__": import uvicorn web_config = get_web_config() diff --git a/panel/templates/dashboard.html b/panel/templates/dashboard.html index e701ad4..a5a8e20 100644 --- a/panel/templates/dashboard.html +++ b/panel/templates/dashboard.html @@ -87,6 +87,16 @@ + +
+
+
+
{{ "Métricas" | translate(lang) }}
+

{{ "Ver estadísticas y análisis de traducciones" | translate(lang) }}

+ {{ "Ver Métricas" | translate(lang) }} +
+
+
diff --git a/panel/templates/metrics.html b/panel/templates/metrics.html new file mode 100644 index 0000000..4a882d6 --- /dev/null +++ b/panel/templates/metrics.html @@ -0,0 +1,244 @@ +{% set lang = request.cookies.get('panel_lang', 'es') %} + + + + + + {{ "Métricas - Bots de Traducción" | translate(lang) }} + + + + + + +
+

📊 {{ "Métricas y Estadísticas" | translate(lang) }}

+ +
+
+
+
+

-

+ {{ "Total Traducciones" | translate(lang) }} +
+
+
+
+
+
+

-

+ {{ "Discord" | translate(lang) }} +
+
+
+
+
+
+

-

+ {{ "Telegram" | translate(lang) }} +
+
+
+
+
+
+

-

+ {{ "Idiomas usados" | translate(lang) }} +
+
+
+
+ +
+
+
+
+
{{ "Traducciones por Idioma" | translate(lang) }}
+
+
+ +
+
+
+
+
+
+
{{ "Traducciones por Servidor" | translate(lang) }}
+
+
+ +
+
+
+
+ +
+
+
+
+
{{ "Detalle por Idioma" | translate(lang) }}
+
+
+ + + + + + + + + + +
{{ "Idioma" | translate(lang) }}{{ "Cantidad" | translate(lang) }}{{ "Porcentaje" | translate(lang) }}
+
+
+
+
+
+
+
{{ "Detalle por Servidor" | translate(lang) }}
+
+
+ + + + + + + + + + +
{{ "Servidor" | translate(lang) }}{{ "Cantidad" | translate(lang) }}{{ "Porcentaje" | translate(lang) }}
+
+
+
+
+ +
+
+
{{ "Actividad (últimos 30 días)" | translate(lang) }}
+
+
+ +
+
+
+ + + + +