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)