""" 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.
" "Le systeme Google limite actuellement les requetes pour eviter la surcharge.
" 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.
") return f"""
[ AVERTISSEMENT TACTIQUE : CODE 429 ]
SATURATION TEMPORAIRE DES TRANSMISSIONS VERS LE CENTRE DE DONNEES.
{explication}> ACTION REQUISE : Maintenez la position et patientez environ 60 secondes avant de relancer.
""" 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('''
[ SERVICE INDISPONIBLE ]
Une erreur temporaire est survenue. Réessayez dans quelques instants.
''', unsafe_allow_html=True)