"""Statuts du NAS via le moniteur maison `nas_monitor` (`GET /api/status`). Expose : occupation des disques de données (`/mnt/*`), santé des conteneurs Docker, et l'état du port forwardé entre gluetun (VPN) et qBittorrent. """ from __future__ import annotations from dataclasses import dataclass, field import httpx from config import config @dataclass class NasDisk: label: str percent: float free_gb: float @property def free_human(self) -> str: if self.free_gb >= 1024: return f"{self.free_gb / 1024:.1f}".replace(".", ",") + " To" return f"{self.free_gb:.0f} Go" @dataclass class NasStatus: ok: bool error: str | None = None disks: list[NasDisk] = field(default_factory=list) docker_running: int = 0 docker_total: int = 0 docker_unhealthy: int = 0 vpn_port: int | None = None vpn_ok: bool = False def _disk_label(mount: str) -> str: """`/mnt/disk1` -> `Disque 1`, sinon le dernier segment capitalisé.""" base = mount.rstrip("/").split("/")[-1] if base.startswith("disk"): return "Disque " + base[len("disk"):] return base.capitalize() or mount def _as_int(value: object) -> int | None: try: return int(value) # type: ignore[arg-type] except (TypeError, ValueError): return None async def fetch_status() -> NasStatus: if not config.nas_url: return NasStatus(ok=False, error="NAS désactivé") try: async with httpx.AsyncClient(timeout=15) as client: resp = await client.get(config.nas_url) if resp.status_code != 200: return NasStatus(ok=False, error=f"HTTP {resp.status_code}") data = resp.json() except httpx.HTTPError: return NasStatus(ok=False, error="injoignable") metrics = (data.get("host") or {}).get("metrics") or {} disks: list[NasDisk] = [] for disk in metrics.get("disks") or []: mount = str(disk.get("mount", "")) if not mount.startswith("/mnt"): # on ne garde que les disques de données continue disks.append(NasDisk( label=_disk_label(mount), percent=float(disk.get("percent", 0) or 0), free_gb=float(disk.get("free_gb", 0) or 0), )) summary = (data.get("docker") or {}).get("summary") or {} # Port VPN : OK quand le port forwardé par gluetun == port d'écoute qBittorrent. qb = data.get("qbittorrent") or {} listen = _as_int((qb.get("prefs") or {}).get("listen_port")) fwd = _as_int(qb.get("forwarded_port_file")) vpn_ok = listen is not None and fwd is not None and listen == fwd return NasStatus( ok=True, disks=disks, docker_running=int(summary.get("running", 0) or 0), docker_total=int(summary.get("total", 0) or 0), docker_unhealthy=int(summary.get("unhealthy", 0) or 0), vpn_port=fwd if fwd is not None else listen, vpn_ok=vpn_ok, )