"""GitHub-Abfragen mit Zwischenspeicher — für beide Rollen (Box: Motor und llama-swap, Homelab: die Apps). GitHub erlaubt ohne Anmeldung 60 Anfragen pro Stunde. Jeder Blick auf die Update-Details fragte früher neu; jetzt gilt eine Antwort 15 Minuten. Fehler (auch das Anfragelimit) werden nicht gemerkt. """ import os import re import threading import time import httpx GITHUB_CACHE_S = int(os.environ.get("MC_GITHUB_CACHE_S", "900")) _cache: dict[str, tuple[float, object]] = {} _lock = threading.Lock() def github_json(pfad: str, timeout: float = 8) -> object: """GET https://api.github.com/, 15 Minuten gemerkt. Wirft bei HTTP-Fehlern.""" jetzt = time.time() with _lock: if (eintrag := _cache.get(pfad)) and jetzt - eintrag[0] < GITHUB_CACHE_S: return eintrag[1] r = httpx.get(f"https://api.github.com/{pfad}", timeout=timeout, headers={"User-Agent": "MissionControl2"}) r.raise_for_status() daten = r.json() with _lock: _cache[pfad] = (jetzt, daten) return daten _VERSION = re.compile(r"^v?(\d+(?:\.\d+)+|\d{4}-\d{2}-\d{2}(?:-r\d+)?)", re.IGNORECASE) def neueste_version(repo: str) -> dict | None: """Neueste Veröffentlichung eines Repos: {"tag", "version", "datum"} oder None. Manche Projekte veröffentlichen „untagged“ und tragen die Version nur im Namen (PVE Scripts Local, 09/2026).""" daten = github_json(f"repos/{repo}/releases/latest") if not isinstance(daten, dict) or not daten.get("tag_name"): return None tag, name = str(daten["tag_name"]), str(daten.get("name") or "") roh = tag if _VERSION.match(tag) or not _VERSION.match(name) else name return {"tag": roh, "tag_roh": tag, "version": roh.lstrip("vV"), "datum": str(daten.get("published_at") or "")[:10] or None}