Welle 1 · Live-Zahlen je Dienst auf den Kacheln, AdGuard-Zugang in den Einstellungen
Jede Gerätekachel (Homelab-Cockpit und Übersicht) zeigt eine Live-Zahl, wo es eine gibt: Proxmox-Host „8 von 8 Gästen an“, PBS „letzte Sicherung vor 13 h“, NPMplus „10 Zertifikate gültig“ (aus der Zertifikats-Wache), Arcane „11 von 12 Containern an“ (Arcane-API mit dem vorhandenen Schlüssel) und AdGuard „12 % geblockt“. Für AdGuard gibt es unter Einstellungen → Homelab einen Zugang, der vor dem Speichern gegen AdGuard geprüft wird und nur im Homelab-Teil liegt (0600, das Passwort steht in keiner Antwort). Arcane und AdGuard werden höchstens alle zwei Minuten gefragt. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
53ea0cd9b6
commit
ad8dbbc1b3
@@ -5,6 +5,8 @@
|
||||
<Datenordner>/arcane.key (0600); MC_ARCANE_KEY in /etc/mc2/homelab.env geht vor (arcane.py).
|
||||
• Docker echt echte Docker-Updates über Arcane statt Probelauf (<Datenordner>/homelab-einstellungen.json);
|
||||
MC_ARCANE_ECHT in der Umgebung gewinnt.
|
||||
• AdGuard-Zugang (seit 25.09.2026) Benutzer und Passwort für die Statistik („% geblockt“ auf der Kachel). Vor
|
||||
dem Speichern fragt AdGuard einmal damit; lehnt es ab, wird nichts gespeichert (adguard.py).
|
||||
stand() zeigt dazu die Wartezeit nach Skriptänderungen (karenz.py) und den Ausführer (verbunden, Nur-Lesen,
|
||||
Sicherungsspeicher). Der Schlüssel verlässt diesen Teil nie: Keine Antwort, kein Protokolleintrag und keine
|
||||
Fehlermeldung enthält ihn, auch kein Stück davon.
|
||||
@@ -15,7 +17,7 @@ import logging
|
||||
import re
|
||||
import time
|
||||
|
||||
from services.homelab import apps, arcane, inventar, kanal, karenz
|
||||
from services.homelab import adguard, apps, arcane, inventar, kanal, karenz
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
@@ -42,6 +44,8 @@ def stand() -> dict:
|
||||
return {
|
||||
"arcane": {"url": arcane_url() or "", "schluessel": arcane.schluessel_quelle(), "echt": arcane.echt(),
|
||||
"echt_quelle": arcane.echt_quelle()},
|
||||
"adguard": {"url": adguard.url() or "", "zugang": "hinterlegt" if (z := adguard.zugang()) else "fehlt",
|
||||
"benutzer": z[0] if z else None},
|
||||
"karenz_h": round(karenz.karenz_s() / 3600),
|
||||
"ausfuehrer": {"verbunden": bool(zuletzt and time.time() - zuletzt < inventar.BERICHT_ALT_S),
|
||||
"zuletzt": zuletzt, "nur_lesen": bool(host.get("nur_lesen")),
|
||||
@@ -127,3 +131,61 @@ def arcane_echt_setzen(an: bool) -> dict:
|
||||
return {"ok": True, "text": ("Docker-Updates über Arcane laufen ab jetzt echt: Arcane holt die neuen Images und "
|
||||
"startet die Container neu." if an else
|
||||
"Docker-Updates über Arcane laufen wieder nur als Probelauf; geändert wird nichts.")}
|
||||
|
||||
|
||||
# --- AdGuard-Zugang (seit 25.09.2026) --------------------------------------------------------------------------------
|
||||
|
||||
def zugang_aus_body(roh: bytes) -> tuple[str, str] | None:
|
||||
"""(Benutzer, Passwort) aus dem Body {"benutzer", "passwort"} — ohne Pydantic, dessen 422 die Eingabe wiederholte."""
|
||||
try:
|
||||
daten = json.loads(roh.decode("utf-8")) if roh else None
|
||||
except (UnicodeDecodeError, ValueError):
|
||||
return None
|
||||
if not isinstance(daten, dict):
|
||||
return None
|
||||
benutzer, passwort = daten.get("benutzer"), daten.get("passwort")
|
||||
return (benutzer, passwort) if isinstance(benutzer, str) and isinstance(passwort, str) else None
|
||||
|
||||
|
||||
def _adguard_abgelehnt(ergebnis: dict, adresse: str) -> str:
|
||||
art, status = ergebnis.get("art"), ergebnis.get("status")
|
||||
if art == "abgelehnt":
|
||||
return f"AdGuard lehnt Benutzer oder Passwort ab (HTTP {status}). Nichts gespeichert."
|
||||
if art == "http":
|
||||
return f"AdGuard antwortet mit HTTP {status}. Nichts gespeichert."
|
||||
if art == "netz":
|
||||
return f"AdGuard ist nicht erreichbar ({adresse}). Nichts gespeichert."
|
||||
return "AdGuard antwortet nicht wie erwartet. Nichts gespeichert."
|
||||
|
||||
|
||||
def adguard_zugang_setzen(werte: tuple[str, str] | None) -> dict:
|
||||
"""POST /api/homelab/einstellungen/adguard-zugang: erst bei AdGuard die Statistik holen, dann speichern."""
|
||||
benutzer, passwort = (werte[0].strip(), werte[1]) if werte else ("", "")
|
||||
if not benutzer or not passwort or len(benutzer) > 200 or len(passwort) > 500:
|
||||
return {"ok": False, "detail": "Benutzer und Passwort eintragen. Nichts gespeichert."}
|
||||
adresse = adguard.url()
|
||||
if not adresse:
|
||||
return {"ok": False, "detail": "AdGuard steht nicht im Bericht des Ausführers, die Adresse ist unbekannt. "
|
||||
"Nichts gespeichert."}
|
||||
ergebnis = adguard.statistik(adresse, benutzer, passwort)
|
||||
if not ergebnis["ok"]:
|
||||
log.info("homelab: AdGuard-Zugang nicht übernommen (%s, HTTP %s)", ergebnis.get("art"), ergebnis.get("status"))
|
||||
return {"ok": False, "detail": _adguard_abgelehnt(ergebnis, adresse)}
|
||||
try:
|
||||
adguard.zugang_speichern(benutzer, passwort)
|
||||
except OSError as exc:
|
||||
return {"ok": False, "detail": f"AdGuard nimmt den Zugang an, aber er ließ sich nicht speichern "
|
||||
f"({exc.__class__.__name__})."}
|
||||
anteil = ergebnis["anteil"]
|
||||
return {"ok": True, "text": (f"AdGuard nimmt den Zugang an: {ergebnis['anfragen']} Anfragen"
|
||||
+ (f", davon {str(anteil).replace('.', ',')} % geblockt" if anteil is not None else "")
|
||||
+ ". Die Kachel zeigt den Anteil ab sofort.")}
|
||||
|
||||
|
||||
def adguard_zugang_loeschen() -> dict:
|
||||
"""DELETE /api/homelab/einstellungen/adguard-zugang."""
|
||||
try:
|
||||
da = adguard.zugang_loeschen()
|
||||
except OSError as exc:
|
||||
return {"ok": False, "detail": f"Der Zugang ließ sich nicht löschen ({exc.__class__.__name__})."}
|
||||
return {"ok": True, "text": "Der AdGuard-Zugang ist gelöscht." if da else "Es war kein AdGuard-Zugang hinterlegt."}
|
||||
|
||||
Reference in New Issue
Block a user