Spaces:
Running
Running
File size: 7,679 Bytes
397b23d 56277f7 4d00d77 a62a48f 001eb74 a62a48f 001eb74 a62a48f 001eb74 a62a48f 001eb74 a62a48f 4d00d77 a62a48f 001eb74 4d00d77 001eb74 4d00d77 001eb74 4d00d77 001eb74 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 | """
JOURNALISATION DES ERREURS AVALEES
=====================================
Beaucoup de blocs try/except de l'app convertissent une donnee illisible
(revenu, date, montant...) en valeur par defaut (0, 1, 30 jours...) pour ne
pas interrompre l'ecran en cours. C'est un choix legitime pour ne pas
bloquer un gerant sur une ligne Google Sheets mal saisie, MAIS un `except:`
nu qui ne log rien rend ces incidents invisibles : personne ne sait qu'un
revenu n'a pas pu etre lu, et le calcul en aval (endettement, taux...) se
poursuit avec une valeur par defaut sans que quiconque soit prevenu.
log_swallowed() ne change pas le comportement de repli (l'app continue),
mais ecrit l'erreur dans les logs du process (visibles sur Hugging Face
Space -> Logs) avec le contexte, pour qu'un incident recurrent soit
detectable au lieu de disparaitre silencieusement.
"""
import logging
_logger = logging.getLogger("vortex")
if not _logger.handlers:
_handler = logging.StreamHandler()
_handler.setFormatter(logging.Formatter("[VORTEX] %(levelname)s %(message)s"))
_logger.addHandler(_handler)
_logger.setLevel(logging.WARNING)
def log_swallowed(context: str, exc: Exception):
"""A appeler dans un except pour tracer une erreur avant de retomber sur
une valeur par defaut. `context` doit identifier precisement l'endroit
et, si possible, la donnee en cause (ex: "revenu client CLI-2026-0004")."""
_logger.warning("%s : %s: %s", context, type(exc).__name__, exc)
try:
from core import audit_log
audit_log.evenement("ERREUR", "ERREUR_AVALEE", cible=context, statut="ECHEC",
detail=f"{type(exc).__name__}: {exc}")
except Exception:
pass
_MARQUEURS_QUOTA = ("quota exceeded", "resource_exhausted", "too many requests", "rate limit", "ratelimit")
def est_quota(erreur) -> bool:
"""Vrai si `erreur` (exception ou texte) est un depassement de quota : code HTTP 429
(« [429] », « code 429 », « error 429 ») ou formulation Google/LLM equivalente.
Un simple nombre contenant 429 (ex. « 1 429 000 XOF ») n'est PAS un quota."""
import re
texte = str(erreur or "")
if re.search(r"\[429\]|(?:code|error|erreur|status|http)\W{0,3}429|429\W{0,3}(?:too many|quota|resource)",
texte, re.IGNORECASE):
return True
return any(m in texte.lower() for m in _MARQUEURS_QUOTA)
def _html_bandeau_quota(texte=""):
sheets = "sheets.googleapis" in str(texte) or "Read requests" in str(texte) or not str(texte).strip()
explication = (
"Quota Google Sheets depasse : trop de lectures par minute (limite partagee entre tous les utilisateurs "
"de l'application). Vos donnees ne sont pas perdues.<br>"
"Le systeme Google limite actuellement les requetes pour eviter la surcharge.<br>"
if sheets else
"Un service externe (IA, email ou donnees) limite temporairement les requetes : trop d'appels en peu de temps. "
"Vos donnees ne sont pas perdues.<br>")
return f"""
<div style="background: rgba(243, 156, 18, 0.08); border: 1px solid rgba(243, 156, 18, 0.3); border-left: 4px solid #f39c12; padding: 14px 18px; margin-bottom: 16px; font-family: 'JetBrains Mono', monospace; border-radius: 0;">
<div style="color: #f39c12; font-weight: 800; font-size: 0.75rem; letter-spacing: 2px; text-transform: uppercase; margin-bottom: 6px;">
[ AVERTISSEMENT TACTIQUE : CODE 429 ]
</div>
<div style="color: #a8b8c8; font-size: 0.75rem; line-height: 1.6;">
SATURATION TEMPORAIRE DES TRANSMISSIONS VERS LE CENTRE DE DONNEES.<br>
{explication}<span style="color:#f39c12; font-weight: 700;">> ACTION REQUISE :</span> Maintenez la position et patientez environ 60 secondes avant de relancer.
</div>
</div>
"""
def _admin_connecte() -> bool:
"""Le detail technique n'est montre qu'a un admin authentifie, jamais au public."""
import streamlit as st
try:
return bool(st.session_state.get("authenticated"))
except Exception:
return False
def installer_bandeau_429():
"""Partout dans l'app : un st.error / st.warning (y compris st.sidebar.error, container.error...)
dont le message est un depassement de quota 429 s'affiche en bandeau JAUNE explicatif
au lieu d'un cadre rouge brut. Idempotent. A appeler une fois, tot dans streamlit_app.py."""
import streamlit as st
from streamlit.delta_generator import DeltaGenerator
if getattr(DeltaGenerator, "_vortex_429_installe", False):
return
def _envelopper(original, incidents=False):
def _alerte(self, body, *args, **kwargs):
if est_quota(body):
try:
return self.markdown(_html_bandeau_quota(body), unsafe_allow_html=True)
except Exception:
pass
if incidents and _admin_connecte():
from core import incidents as _inc
if _inc.est_technique(body):
# Panne technique (pas une consigne de saisie) : bloc explique par l'IA.
return _inc.afficher_incident(body, contexte=str(st.session_state.get("_audit_menu", "") or ""))
return original(self, body, *args, **kwargs)
_alerte.__wrapped__ = original
return _alerte
DeltaGenerator.error = _envelopper(DeltaGenerator.error, incidents=True)
DeltaGenerator.warning = _envelopper(DeltaGenerator.warning)
# st.error / st.warning sont des methodes deja liees au DeltaGenerator principal : on les re-lie.
st.error = lambda body, *a, **k: DeltaGenerator.error(st._main, body, *a, **k)
st.warning = lambda body, *a, **k: DeltaGenerator.warning(st._main, body, *a, **k)
DeltaGenerator._vortex_429_installe = True
def afficher_erreur_sheets(exc: Exception, contexte: str = ""):
"""Affiche une erreur Google Sheets de maniere lisible plutot qu'une
traceback brute : bandeau JAUNE si c'est un quota depasse (429/"Quota
exceeded" — l'utilisateur doit juste patienter), bandeau ROUGE sinon
(erreur reelle). Meme gabarit que celui deja utilise dans
modules/repayments.py et modules/request_loans.py, factorise ici pour
etre reutilise partout ou une lecture/ecriture Google Sheets peut echouer.
`contexte` est un libelle optionnel (ex: "chargement des remboursements")
insere dans le message rouge pour aider a localiser l'incident."""
import streamlit as st
erreur_str = str(exc)
if est_quota(erreur_str):
st.markdown(_html_bandeau_quota(erreur_str), unsafe_allow_html=True)
elif _admin_connecte():
from core.incidents import afficher_incident
afficher_incident(exc, contexte=contexte or str(st.session_state.get("_audit_menu", "") or ""))
else:
# Public : jamais de detail technique (peut contenir noms de feuilles, identifiants...).
st.markdown('''
<div style="background: rgba(88, 166, 255, 0.05); border: 1px solid rgba(88, 166, 255, 0.28); border-left: 4px solid #58a6ff; padding: 14px 18px; margin-bottom: 16px; font-family: 'JetBrains Mono', monospace; border-radius: 0;">
<div style="color: #58a6ff; font-weight: 800; font-size: 0.75rem; letter-spacing: 2px; text-transform: uppercase; margin-bottom: 6px;">
[ SERVICE INDISPONIBLE ]
</div>
<div style="color: #a8b8c8; font-size: 0.75rem; line-height: 1.6;">
Une erreur temporaire est survenue. Réessayez dans quelques instants.
</div>
</div>
''', unsafe_allow_html=True)
|