Claude: refresh OAuth proactif + erreurs fatales distinctes (fini les reconnexions manuelles)

Le refresh token est rotatif : la chaîne se régénère seule indéfiniment tant que
le nouveau token est persisté. La reconnexion manuelle n'était requise que lorsque
cet invariant cassait. Trois correctifs :

- Refresh PROACTIF : on rafraîchit dès qu'il reste < 2h sur le token (~8h de vie)
  au lieu des 2 dernières minutes. Un échec transitoire a des heures de marge avant
  que l'access token meure ; la fenêtre où un kill/timeout perd le token rotatif
  fraîchement rotaté passe de ~8h à quelques ms. Réglable via
  MONITORINK_CLAUDE_REFRESH_LEAD_MIN (défaut 120).
- Distinction FATAL vs TRANSITOIRE : 400 invalid_grant / 401 sur l'endpoint token
  -> _RefreshFatal, sans backoff ni re-soumission en boucle (évite la reuse-detection
  qui révoque toute la famille). 429/5xx/réseau gardent le backoff exponentiel.
- Visibilité + auto-réparation : le cas fatal affiche "Reconnexion Claude requise"
  (pas de repli cache silencieux) et l'alerte se referme seule dès qu'un token frais
  réapparaît sur disque (re-login isolé), sans redémarrer le conteneur.

Timeout du POST de refresh porté à 45s (réglable, MONITORINK_CLAUDE_REFRESH_TIMEOUT)
pour réduire la fenêtre de perte du token après rotation serveur.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-18 10:46:21 +02:00
parent 2c5acc1e36
commit 53fefd4654
3 changed files with 113 additions and 14 deletions

View File

@@ -78,6 +78,18 @@ class Config:
ccusage_enabled: bool = field(
default_factory=lambda: _get("MONITORINK_CCUSAGE", "0") in ("1", "true", "yes")
)
# Marge proactive de refresh OAuth : on rafraîchit dès qu'il reste MOINS que ça sur le token
# (~8 h de vie), au lieu d'attendre les toutes dernières minutes. 2 h par défaut -> refresh
# ~6 h avant l'échéance, ce qui laisse des heures de marge pour retenter un échec transitoire
# (429/réseau) AVANT que l'access token meure, et fait tourner le refresh token rotatif tôt.
claude_refresh_lead_minutes: int = field(
default_factory=lambda: int(_get("MONITORINK_CLAUDE_REFRESH_LEAD_MIN", "120"))
)
# Timeout du POST de refresh. Généreux : réduit la fenêtre où le serveur a rotaté le refresh
# token sans qu'on ait pu le persister (cause n°1 des reconnexions manuelles). Secondes.
claude_refresh_timeout: int = field(
default_factory=lambda: int(_get("MONITORINK_CLAUDE_REFRESH_TIMEOUT", "45"))
)
# --- Météo (Open-Meteo, sans clé) ---
weather_lat: float = field(default_factory=lambda: float(_get("MONITORINK_LAT", "48.8566")))
@@ -127,6 +139,11 @@ class Config:
default_factory=lambda: int(_get("MONITORINK_FULL_INTERVAL_MIN", "120"))
)
@property
def claude_refresh_lead_ms(self) -> int:
"""Marge proactive de refresh en millisecondes (cf. claude_refresh_lead_minutes)."""
return self.claude_refresh_lead_minutes * 60_000
@property
def ha_entities(self) -> list[HAEntity]:
return [HAEntity.parse(s) for s in _get_list("MONITORINK_HA_ENTITIES")]