First commit

This commit is contained in:
unknown
2026-06-09 21:18:13 -03:00
commit 5bff6b938b
66 changed files with 10922 additions and 0 deletions
+320
View File
@@ -0,0 +1,320 @@
"""
Stats.py
Contiene la lógica de los endpoints utilizados para crear y consultar datos estadísticos.
"""
from fastapi import APIRouter, Depends, HTTPException
from sqlalchemy.orm import Session
from sqlalchemy import func, cast, Integer, extract
from database import get_db
from typing import Optional
from datetime import datetime, timedelta
import models
from auth import get_current_user
router = APIRouter()
@router.get("/stats/", tags=["Stats"])
def get_stats(
date_from: Optional[datetime] = None,
date_to: Optional[datetime] = None,
db: Session = Depends(get_db),
current_user: str = Depends(get_current_user),
):
"""
Devuelve todas las estadísticas agregadas en una sola llamada.
Si no se pasan fechas, usa los últimos 30 días.
"""
if date_to is None:
date_to = datetime.utcnow()
if date_from is None:
date_from = date_to - timedelta(days=30)
# ------------------------------------------------------------------
# 1. Resumen ejecutivo
# ------------------------------------------------------------------
total_alertas = db.query(models.Alert).filter(
models.Alert.created_at >= date_from,
models.Alert.created_at <= date_to
).count()
alertas_abiertas = db.query(models.Alert).filter(
models.Alert.created_at >= date_from,
models.Alert.created_at <= date_to,
models.Alert.status == "open"
).count()
alertas_en_progreso = db.query(models.Alert).filter(
models.Alert.created_at >= date_from,
models.Alert.created_at <= date_to,
models.Alert.status == "in_progress"
).count()
alertas_cerradas = total_alertas - alertas_abiertas - alertas_en_progreso
total_mensajes = db.query(models.Message).filter(
models.Message.date >= date_from,
models.Message.date <= date_to
).count()
grupos_activos = db.query(models.Group).filter(
models.Group.type != "P"
).count()
reglas_activas = db.query(models.Rule).filter(
models.Rule.is_active == True
).count()
# ------------------------------------------------------------------
# 2. Alertas por día (para gráfico de línea)
# ------------------------------------------------------------------
alertas_por_dia_raw = (
db.query(
func.date(models.Alert.created_at).label("dia"),
models.Alert.status,
func.count(models.Alert.id).label("total")
)
.filter(
models.Alert.created_at >= date_from,
models.Alert.created_at <= date_to
)
.group_by(
func.date(models.Alert.created_at),
models.Alert.status
)
.order_by(func.date(models.Alert.created_at))
.all()
)
alertas_por_dia = {}
for row in alertas_por_dia_raw:
dia = str(row.dia)
if dia not in alertas_por_dia:
alertas_por_dia[dia] = {"abiertas": 0, "cerradas": 0}
if row.status == "open":
alertas_por_dia[dia]["abiertas"] = row.total
else:
alertas_por_dia[dia]["cerradas"] = row.total
# ------------------------------------------------------------------
# 3. Distribución por severidad
# ------------------------------------------------------------------
sev_raw = (
db.query(
models.Rule.severity,
func.count(models.Alert.id).label("total")
)
.join(models.Alert, models.Alert.rule_id == models.Rule.id)
.filter(
models.Alert.created_at >= date_from,
models.Alert.created_at <= date_to
)
.group_by(models.Rule.severity)
.all()
)
distribucion_severidad = {row.severity: row.total for row in sev_raw}
# ------------------------------------------------------------------
# 4. Alertas por grupo
# ------------------------------------------------------------------
alertas_por_grupo_raw = (
db.query(
models.Alert.group_id,
func.count(models.Alert.id).label("total")
)
.filter(
models.Alert.created_at >= date_from,
models.Alert.created_at <= date_to
)
.group_by(models.Alert.group_id)
.order_by(func.count(models.Alert.id).desc())
.limit(10)
.all()
)
# Resolver nombres de grupos
group_ids = [r.group_id for r in alertas_por_grupo_raw]
grupos_map = {
g.id_telegram: g.name
for g in db.query(models.Group).filter(
models.Group.id_telegram.in_(group_ids)
).all()
}
alertas_por_grupo = [
{
"grupo": grupos_map.get(r.group_id, str(r.group_id)),
"total": r.total
}
for r in alertas_por_grupo_raw
]
# ------------------------------------------------------------------
# 5. Reglas más disparadas (top 10)
# ------------------------------------------------------------------
reglas_top_raw = (
db.query(
models.Rule.id,
models.Rule.description,
models.Rule.severity,
func.count(models.Alert.id).label("total")
)
.join(models.Alert, models.Alert.rule_id == models.Rule.id)
.filter(
models.Alert.created_at >= date_from,
models.Alert.created_at <= date_to
)
.group_by(models.Rule.id, models.Rule.description, models.Rule.severity)
.order_by(func.count(models.Alert.id).desc())
.limit(10)
.all()
)
reglas_top = [
{
"id": row.id,
"description": row.description,
"severity": row.severity,
"total": row.total
}
for row in reglas_top_raw
]
# ------------------------------------------------------------------
# 6. Volumen de mensajes por día
# ------------------------------------------------------------------
mensajes_por_dia_raw = (
db.query(
func.date(models.Message.date).label("dia"),
func.count(models.Message.id_mess_g).label("total")
)
.filter(
models.Message.date >= date_from,
models.Message.date <= date_to
)
.group_by(func.date(models.Message.date))
.order_by(func.date(models.Message.date))
.all()
)
mensajes_por_dia = {str(row.dia): row.total for row in mensajes_por_dia_raw}
# ------------------------------------------------------------------
# 7. Heatmap: mensajes por hora × día de semana
# ------------------------------------------------------------------
# MariaDB usa DAYOFWEEK() (1=Dom..7=Sab) y HOUR() en lugar de extract("dow"/"hour")
from sqlalchemy import text
heatmap_raw = (
db.query(
func.dayofweek(models.Message.date).label("dia_semana"),
func.hour(models.Message.date).label("hora"),
func.count(models.Message.id_mess_g).label("total")
)
.filter(
models.Message.date >= date_from,
models.Message.date <= date_to
)
.group_by(
func.dayofweek(models.Message.date),
func.hour(models.Message.date)
)
.all()
)
# MariaDB DAYOFWEEK: 1=Dom, 2=Lun, ..., 7=Sáb
DIAS = {1: "Dom", 2: "Lun", 3: "Mar", 4: "Mié", 5: "Jue", 6: "Vie", 7: "Sáb"}
heatmap = [
{
"dia": DIAS.get(int(row.dia_semana), str(row.dia_semana)),
"hora": int(row.hora),
"total": row.total
}
for row in heatmap_raw
]
# ------------------------------------------------------------------
# 8. Top senders por actividad
# ------------------------------------------------------------------
top_senders_raw = (
db.query(
models.Message.sender_id,
func.count(models.Message.id_mess_g).label("mensajes")
)
.filter(
models.Message.date >= date_from,
models.Message.date <= date_to
)
.group_by(models.Message.sender_id)
.order_by(func.count(models.Message.id_mess_g).desc())
.limit(10)
.all()
)
sender_ids = [r.sender_id for r in top_senders_raw]
senders_map = {
s.id_telegram: s
for s in db.query(models.Sender).filter(
models.Sender.id_telegram.in_(sender_ids)
).all()
}
# Contar alertas por sender
alertas_sender_raw = (
db.query(
models.Message.sender_id,
func.count(models.Alert.id).label("alertas")
)
.join(models.Alert, (
models.Alert.message_id == models.Message.id_mess_g) & (
models.Alert.group_id == models.Message.group_id)
)
.filter(
models.Message.date >= date_from,
models.Message.date <= date_to,
models.Message.sender_id.in_(sender_ids)
)
.group_by(models.Message.sender_id)
.all()
)
alertas_por_sender = {r.sender_id: r.alertas for r in alertas_sender_raw}
top_senders = []
for row in top_senders_raw:
s = senders_map.get(row.sender_id)
nombre = ""
if s:
nombre = f"{s.first_name or ''} {s.last_name or ''}".strip() or s.username or str(row.sender_id)
else:
nombre = str(row.sender_id)
top_senders.append({
"id_telegram": row.sender_id,
"nombre": nombre,
"username": s.username if s else None,
"mensajes": row.mensajes,
"alertas": alertas_por_sender.get(row.sender_id, 0),
})
# ------------------------------------------------------------------
# Respuesta final
# ------------------------------------------------------------------
return {
"periodo": {
"desde": date_from.isoformat(),
"hasta": date_to.isoformat(),
},
"resumen": {
"total_alertas": total_alertas,
"alertas_abiertas": alertas_abiertas,
"alertas_cerradas": alertas_cerradas,
"total_mensajes": total_mensajes,
"grupos_activos": grupos_activos,
"reglas_activas": reglas_activas,
},
"alertas_por_dia": alertas_por_dia,
"distribucion_severidad": distribucion_severidad,
"alertas_por_grupo": alertas_por_grupo,
"reglas_top": reglas_top,
"mensajes_por_dia": mensajes_por_dia,
"heatmap": heatmap,
"top_senders": top_senders,
}