Files
2026-07-20 11:05:07 +02:00

44 lines
1.9 KiB
Python

"""Agrégats jail/pays partagés entre views.py (calcul local, toujours
disponible) et les commandes management master_listen.py (publication du
roster) / master_client.py (cache Redis du roster reçu, cf. constantes
ROSTER_* ci-dessous) — une seule requête à maintenir plutôt que deux
copies qui divergent."""
from django.db.models import Count
from .models import BanEvent
TOP_N = 5
# Cache Redis du roster reçu par un noeud client (master_client.py) — DB
# dédiée (db=2) : db=0 déjà pris par Channels (CHANNEL_LAYERS), db=1 par
# Celery (CELERY_BROKER_URL). Expiration alignée sur NODE_OFFLINE_THRESHOLD_SECONDS
# (views.py) : une donnée périmée doit disparaître plutôt que mentir si le
# roster cesse d'arriver.
ROSTER_REDIS_DB = 2
ROSTER_REDIS_KEY = 'roster:global_stats'
ROSTER_TTL_SECONDS = 180
def with_bar_pct(rows: list[dict]) -> list[dict]:
"""Ajoute `pct` = largeur de barre relative au plus grand compte de la
liste — pas un vrai pourcentage (la somme ne fait pas 100), juste une
échelle visuelle pour la barre du widget sidebar."""
max_count = max((row['count'] for row in rows), default=0)
for row in rows:
row['pct'] = round(row['count'] / max_count * 100) if max_count else 0
return rows
def top_jails_and_countries() -> dict[str, list[dict]]:
"""Compteurs globaux (toutes dates, tous noeuds connus de CE process)
— sur le master, "tous les noeuds" ; sur un client, seulement lui-même
(d'où le roster, qui republie la version calculée côté master)."""
top_jails = list(
BanEvent.objects.values('jail_name').annotate(count=Count('id')).order_by('-count')[:TOP_N]
)
top_countries = list(
BanEvent.objects.exclude(country='')
.values('country').annotate(count=Count('id')).order_by('-count')[:TOP_N]
)
return {'top_jails': with_bar_pct(top_jails), 'top_countries': with_bar_pct(top_countries)}