"""Configuration centralisée de Monitorink, chargée depuis l'environnement. Toutes les valeurs sensibles (token Claude, identifiants trackers) viennent de variables d'environnement / `.env` et ne sont jamais versionnées. """ from __future__ import annotations import os from dataclasses import dataclass, field from dotenv import load_dotenv load_dotenv() def _get(name: str, default: str = "") -> str: return os.environ.get(name, default).strip() def _get_list(name: str) -> list[str]: raw = _get(name) return [item.strip() for item in raw.split(",") if item.strip()] @dataclass(frozen=True) class TrackerSpec: """Un tracker torrent privé dont on affiche le ratio. `type` choisit le fetcher (v1 : `unit3d_nuxt`). Identifiants jamais versionnés (lus depuis `.env`).""" key: str label: str base_url: str username: str password: str type: str = "unit3d_nuxt" @dataclass(frozen=True) class Config: # --- Affichage --- timezone: str = field(default_factory=lambda: _get("MONITORINK_TZ", "Europe/Paris")) locale: str = field(default_factory=lambda: _get("MONITORINK_LOCALE", "fr_FR")) # Canevas de RENDU en paysage (1680x1264). Le PNG est ensuite pivoté de 90° dans # render.py pour le panneau e-ink physiquement en portrait (1264x1680). width: int = field(default_factory=lambda: int(_get("MONITORINK_WIDTH", "1680"))) height: int = field(default_factory=lambda: int(_get("MONITORINK_HEIGHT", "1264"))) # Sens de rotation pour l'affichage Kobo : "cw" (bouton à droite) ou "ccw". rotate: str = field(default_factory=lambda: _get("MONITORINK_ROTATE", "cw").lower()) # --- Claude --- # Chemin du fichier .credentials.json d'un login Claude ISOLÉ dédié à Monitorink # (CLAUDE_CONFIG_DIR séparé). Le backend y lit/écrit (refresh) sans toucher le # ~/.claude partagé. L'endpoint /usage exige le scope user:profile -> login complet # requis (le token `claude setup-token` ne suffit pas, scope insuffisant). claude_creds_path: str = field( default_factory=lambda: _get("MONITORINK_CLAUDE_CREDS", "/creds/.credentials.json") ) claude_ua: str = field( default_factory=lambda: _get("MONITORINK_CLAUDE_UA", "claude-code/2.1.172") ) 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"))) weather_lon: float = field(default_factory=lambda: float(_get("MONITORINK_LON", "2.3522"))) # --- NAS (moniteur maison nas_monitor, endpoint /api/status) --- nas_url: str = field(default_factory=lambda: _get("MONITORINK_NAS_URL")) # --- Codex (usage ChatGPT/Codex via wham/usage) --- # Fichier auth.json de Hermes monté en lecture seule : Hermes y maintient un token # openai-codex frais. Monitorink le relit à chaque rendu (aucun refresh côté Monitorink). codex_token_file: str = field( default_factory=lambda: _get("MONITORINK_CODEX_TOKEN_FILE", "/hermes/auth.json") ) # --- Trackers torrent privés (ratio du compte) --- # Le ratio change lentement (~30 min/1 h côté trackers) et le login est coûteux # (CSRF + session) : on cache le résultat plus longtemps que le reste (défaut 1 h). tracker_ttl_seconds: int = field( default_factory=lambda: int(_get("MONITORINK_TRACKER_TTL", "3600")) ) # Cache persistant sur disque : survit aux redéploiements du conteneur, évite de # reloguer les 4 trackers d'un coup après chaque rebuild. Monté via le volume /data. tracker_cache_file: str = field( default_factory=lambda: _get("MONITORINK_TRACKER_CACHE_FILE", "/data/trackers.json") ) # --- Cache / rafraîchissement serveur --- cache_ttl_seconds: int = field( default_factory=lambda: int(_get("MONITORINK_CACHE_TTL", "120")) ) # Intervalle mini entre deux appels réels à l'endpoint /usage de Claude (rate-limité). # Indépendant de la cadence de rendu : protège du 429 quand on rend souvent (dev 30 s). usage_ttl_seconds: int = field( default_factory=lambda: int(_get("MONITORINK_USAGE_TTL", "120")) ) # --- Refresh partiel e-ink (endpoints /frame.*) --- # Un full refresh (flash, efface le ghosting) est forcé au 1er lancement/reset puis toutes les # N minutes — indépendamment du cycle Kobo. Entre deux, on ne fait que des partiels serrés sur # les zones réellement modifiées. En dev on descend à 1-2 min pour tester rapidement. full_refresh_interval_minutes: int = field( 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 trackers(self) -> list[TrackerSpec]: """Trackers actifs : `MONITORINK_TRACKERS=c411,autre` + un bloc d'env par clé, ex. `MONITORINK_TRACKER_C411_{URL,USER,PASS,LABEL,TYPE}`.""" specs: list[TrackerSpec] = [] for key in _get_list("MONITORINK_TRACKERS"): k = key.upper().replace("-", "_") base = _get(f"MONITORINK_TRACKER_{k}_URL").rstrip("/") if not base: continue specs.append(TrackerSpec( key=key, label=_get(f"MONITORINK_TRACKER_{k}_LABEL", key), base_url=base, username=_get(f"MONITORINK_TRACKER_{k}_USER"), password=_get(f"MONITORINK_TRACKER_{k}_PASS"), type=_get(f"MONITORINK_TRACKER_{k}_TYPE", "unit3d_nuxt"), )) return specs config = Config()