148 lines
6.6 KiB
Python
148 lines
6.6 KiB
Python
"""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 (endpoint HTTP exposant l'état du NAS au format /api/status) ---
|
|
nas_url: str = field(default_factory=lambda: _get("MONITORINK_NAS_URL"))
|
|
|
|
# --- Codex (usage ChatGPT/Codex via backend-api/usage) ---
|
|
# Fichier auth.json monté en lecture seule : un processus externe 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", "/codex/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()
|