"""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)}