Files
bf-infra/adguard/ha-self-service.md
Christoph Emsenhuber ae87e9f511 TP23 Stufe 5 Redesign: pro-Person Self-Service (agh-exception.py + Bau-Spec)
Stufe 5 reopened — DAU-Requirements (pro Person, "zuletzt geblockt -> 1 Klick",
temp+dauerhaft umschaltbar, scoped HA-Dashboard) nicht erfuellt → Stufe 6 gated.
agh-exception.py: $client-scoped allow/unallow, list-exceptions, recent-blocked
(selftest OK, reine Logik netzunabhaengig getestet). HA-Bau-Spec + Personen-
Client-Modell dokumentiert. Bau/Abnahme on-network offen.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-09-12 14:32:13 +02:00

7.0 KiB

TP23 Stufe 5 (Redesign 2026-09-12) — DAU-tauglicher, personenbezogener DNS-Self-Service

🔴 Status: Bau-Spec, noch nicht deployt. Die alte Fassung (single-domain, temp-only, HA-Neustart bei neuer Domain) war „funktional ein kastriertes Pi-hole" (User-Befund 2026-09-12) und ist ersetzt. Bau + Abnahme laufen on-network (HAOS 10.40.20.105, AGH .36/.37) — beim Anlegen den home-assistant-best-practices-Skill konsultieren und via ha-mcp in .storage anlegen (native Helper, entity_id statt device_id). Diese Datei ist die Repro-/SSoT-Quelle.

Ziel (User-Requirements 2026-09-12)

Ein einfaches, klar verständliches End-User-UI zum Entsperren:

  • alles was den Nutzer betrifft, nichts von anderen / Admin / Power-User;
  • Policy nur sichtbar, wo sie für den einfachen Nutzer Relevanz hat.
# Requirement Umsetzung
1 Pro Person (Login), Sicht über alle Geräte HA-User = Person; Person → AGH-Client (Name), dessen ids = ihre Geräte-IPs
2 „zuletzt geblockt" → 1 Klick recent-blocked --client <person> aus dem AGH-Query-Log, auf die Geräte der Person gefiltert
3 temporär UND dauerhaft, Modus umschaltbar Dauer-Select inkl. „dauerhaft"; input_boolean self-permanent ⇄ Anfrage. Start: self-permanent
4 HA-Dashboard, scoped pro Person ein View je Person, nur für ihre HA-User-ID sichtbar

Identitätsmodell — AGH-Client je Person ist die SSoT

  • Pro Person ein AGH-Client (/control/clients), name = Personenname, ids = alle Geräte-IPs der Person (die per DHCP faktisch statisch sind). Damit ist Person→Gerät an einem Ort gepflegt (kein zweites Mapping-File), und Ausnahmen sind stabil bei Gerätewechsel.
  • Ausnahme-Regel = @@||domain^$client='Personenname' (statt globalem @@||domain^). 🔴 Vor Scharfschalten on-network verifizieren, dass AGH $client= auf @@-Regeln wirkt (s. agh-exception.py Kopf). Fällt der Test negativ aus: Fallback = $client=<ip>|<ip> mit den Geräte-IPs statt dem Namen (Code kann beides, scoped_rule()).
  • Die bestehenden VLAN-CIDR-Gruppen bleiben die Policy-Grenze (Filter an/aus je VLAN); Personen-Clients sind spezifischer und tragen nur die Ausnahmen.

CLI (deployt nach /config/agh-exception.py, wirkt auf .36+.37)

allow-domain   <domain> --client <person>     # Ausnahme setzen (temp: HA revertiert; perm: kein Timer)
unallow-domain <domain> --client <person>     # dieselbe Ausnahme entfernen
list-exceptions --client <person> --json      # „meine Ausnahmen" (für Übersicht + Entfernen)
recent-blocked  --client <person> --json      # „zuletzt geblockt" (nur Geräte der Person)

--client nimmt einen AGH-Client-Namen (→ IPs via /control/clients) ODER eine Komma-IP/CIDR-Liste.

configuration.yaml — shell_command (einziger YAML-Teil, Backup .bak-tp23-*)

Domain und Client sind Parameter → kein HA-Neustart mehr bei neuer Domain (das war der alte DAU-Killer: pro Domain ein eigener shell_command).

shell_command:
  agh_allow:   "python3 /config/agh-exception.py allow-domain '{{ domain }}' --client '{{ client }}'"
  agh_unallow: "python3 /config/agh-exception.py unallow-domain '{{ domain }}' --client '{{ client }}'"

Helfer / Entities (via ha-mcp in .storage)

Pro Person (Beispiel christoph) — bei mehreren Personen je einmal, Suffix = Personen-Slug:

Typ entity_id Zweck
command_line sensor sensor.dns_recent_blocked_<slug> recent-blocked --client <person> --json → json_attributes: items (Liste {domain,count,last}); Scan-Intervall ~60 s
command_line sensor sensor.dns_exceptions_<slug> list-exceptions --client <person> --json → aktive Ausnahmen der Person
input_select input_select.dns_dauer_<slug> 1 Stunde / 4 Stunden / dauerhaft
timer timer.dns_ausnahme_<slug> JIT-Countdown je temporärer Freigabe (restore=false)
input_boolean input_boolean.dns_permanent_ohne_anfrage global: an = „dauerhaft" wirkt sofort (Start); aus = erzeugt Admin-Genehmigung
script script.dns_freigeben_<slug> nimmt domain als Feld → agh_allow; bei „dauerhaft"+Switch-aus → todo/Notification statt Freigabe; sonst Timer starten
automation automation.dns_ablauf_<slug> timer.finished/.cancelled → agh_unallow, Aufräumen

Dashboard — ein scoped View je Person

  • Eigener storage-Dashboard-View, nur für die HA-User-ID der Person sichtbar (visible: mit der user-id; require_admin:false). Kleiner Haushalt → ein View je Person ist praktikabel und umgeht das „Template kennt den aktuellen Betrachter nicht"-Problem.
  • Karte 1 „Zuletzt geblockt (antippen zum Freigeben)": rendert sensor.dns_recent_blocked_<slug> Attribut items; jede Zeile hat eine tap_action → script.dns_freigeben_<slug> mit domain der Zeile + aktueller input_select-Dauer. (Dynamische Per-Zeilen-tap_action braucht i.d.R. auto-entities + template/button-card oder ein kleines Custom-List-Template — beim Bau on-network entscheiden, das ist der Teil, der Live-Verifikation braucht.)
  • Karte 2 „Meine Ausnahmen": sensor.dns_exceptions_<slug> als Liste, je Eintrag Entfernen (agh_unallow).
  • Karte 3 „Dauer": input_select.dns_dauer_<slug> (inkl. „dauerhaft").
  • Kein globaler Status, keine fremden Clients, keine Policy-Interna. Nur wenn relevant ein kurzer Hinweis („durch den Familien-Filter blockiert").

Genehmigungs-Variante (Modus-Switch aus)

input_boolean.dns_permanent_ohne_anfrage = off → „dauerhaft"-Klick legt statt der Freigabe einen Eintrag in einer todo-Liste „DNS-Freigaben zu prüfen" an (+ Notification an Christoph). Beim Abhaken führt eine Admin-Automation agh_allow ohne Timer aus. Beide Varianten sind gebaut; der Switch entscheidet zur Laufzeit.

Alt-Reste, die beim Bau wegfallen/fixen

  • Single-domain-Flow (input_text.dns_ausnahme_domain/_aktiv, input_button…_starten, automation…_starten/_ablauf) und der agh_allow_domain/_unallow_domain-pro-Domain-Zwang.
  • Statuskachel-Sensor binary_sensor.adguard_bf1_status hing am toten .16 → auf .36/.37 neu.

Abnahme (on-network, = neue echte Stufe-6-Vorbedingung)

Mit einem echten Nicht-Admin-Login: geblockte Seite → erscheint in ihrer Fläche → 1 Klick temporär (revertiert nach Ablauf) und 1 Klick dauerhaft (bleibt) → wirkt nur für diese Person (Gegentest: anderer Client sieht die Domain weiter geblockt). .36 == .37 (agh-sync.py check). Modus-Switch beide Wege einmal durchgespielt.

Verweise

  • CLI: adguard/agh-exception.py (Subcommands + selftest). Deploy-Weg unverändert (base64→scp, s. unten). Creds /config/agh-exception.secrets (chmod 600).
  • Redeploy: base64 < adguard/agh-exception.py | ssh admin@10.40.20.105 "base64 -d | sudo tee /config/agh-exception.py >/dev/null".
  • Personen-Client-Modell: adguard/README.md. Kontext/Reihenfolge: TP23 in docs/teilprojekte.md.