-
Anruf-Übersicht
+
Anruf-Übersicht · {html.escape(kategorien.PROFIL_NAME)}
{len(anrufe)} Anrufe · {zusammenfassung or '—'} · lokal erzeugt aus der Ablage
{karten}
"""
diff --git a/pipe/deck.py b/pipe/deck.py
index 394e302..c1b117e 100644
--- a/pipe/deck.py
+++ b/pipe/deck.py
@@ -18,24 +18,15 @@ import urllib.error
import urllib.request
from functools import lru_cache
-from . import config
+from . import config, kategorien
_API = "/index.php/apps/deck/api/v1.1"
-# Farben (6-stelliges Hex ohne #) je Dringlichkeit.
-DRINGLICHKEIT_FARBE = {
- "niedrig": "31CC7C", # grün
- "normal": "F1DB50", # gelb
- "hoch": "E0A339", # orange
- "notfall": "E9322D", # rot
-}
+# Farben je Dringlichkeit - siehe pipe/kategorien.py.
+DRINGLICHKEIT_FARBE = kategorien.DRINGLICHKEIT_FARBE
# Lesbare Namen der Kategorien für den Kartentitel - der Stapel sagt jetzt,
# wie weit die Bearbeitung ist, nicht mehr worum es geht.
-KATEGORIE_LABEL = {
- "termin": "Termin", "rezept": "Rezept", "ueberweisung": "Überweisung",
- "befund": "Befund", "verwaltung": "Verwaltung", "beschwerden": "Beschwerden", "notfall": "Notfall",
- "rueckruf": "Rückruf", "sonstiges": "Sonstiges",
-}
+KATEGORIE_LABEL = kategorien.KATEGORIE_LABEL
@lru_cache(maxsize=1)
diff --git a/pipe/dienste.py b/pipe/dienste.py
new file mode 100644
index 0000000..c17342f
--- /dev/null
+++ b/pipe/dienste.py
@@ -0,0 +1,96 @@
+"""Zustand der beteiligten Dienste (Telefon, Verarbeitung, Spracherkennung,
+Nextcloud) — geteilt zwischen Leitstand (pipe.server) und Störungswache
+(pipe.monitor), damit beide dieselbe Sicht auf "läuft/steht" haben.
+"""
+from __future__ import annotations
+
+import subprocess
+import time
+import urllib.error
+import urllib.request
+from datetime import datetime
+
+from . import config, store
+
+# Statusabfragen sind teuer (Netz, Unterprozesse) - kurz zwischenspeichern,
+# damit haeufiges Nachfragen den Rechner nicht belastet.
+_CACHE: dict[str, tuple[float, dict]] = {}
+CACHE_S = 4.0
+
+
+def _laeuft(muster: str) -> bool:
+ try:
+ return subprocess.run(["pgrep", "-f", muster], capture_output=True).returncode == 0
+ except Exception:
+ return False
+
+
+def _http_ok(url: str, timeout: float = 5.0) -> bool:
+ """Erreichbarkeitstest. Mit eigenem User-Agent - ein WAF vor Nextcloud
+ blockt den Standardnamen von urllib und meldete fälschlich 'nicht
+ erreichbar'."""
+ req = urllib.request.Request(url, headers={"User-Agent": "praxis-telefon-agent/1.0"})
+ try:
+ with urllib.request.urlopen(req, timeout=timeout, context=store._ssl_kontext()):
+ return True
+ except urllib.error.HTTPError:
+ return True # antwortet - reicht als Lebenszeichen
+ except Exception:
+ return False
+
+
+def _trunk() -> tuple[str, str]:
+ """(zustand, text) des SIP-Trunks."""
+ if not _laeuft("[f]reeswitch"):
+ return "aus", "FreeSWITCH läuft nicht"
+ try:
+ roh = subprocess.run(
+ ["fs_cli", "-P", config.FS_PORT, "-x", "sofia status gateway plusnet"],
+ capture_output=True, text=True, timeout=8).stdout
+ for zeile in roh.splitlines():
+ if zeile.startswith("Status"):
+ wert = zeile.split()[-1]
+ return ("gut", "registriert") if wert == "UP" else ("schlecht", f"Trunk {wert}")
+ return "unklar", "Gateway unbekannt"
+ except Exception:
+ return "unklar", "fs_cli nicht erreichbar"
+
+
+def status() -> dict:
+ jetzt = time.time()
+ gepuffert = _CACHE.get("status")
+ if gepuffert and jetzt - gepuffert[0] < CACHE_S:
+ return gepuffert[1]
+
+ trunk_zustand, trunk_text = _trunk()
+ watcher = _laeuft("[p]ipe.watch")
+ ollama = _http_ok(f"{config.OLLAMA_URL}/api/version")
+ nextcloud = (_http_ok(f"{config.NEXTCLOUD_URL}/status.php")
+ if config.nextcloud_aktiv() else None)
+
+ offen = 0
+ if config.TELEFON_EINGANG.is_dir():
+ offen = sum(1 for d in config.TELEFON_EINGANG.iterdir()
+ if d.is_file() and not d.name.startswith("."))
+ fehler = 0
+ if config.TELEFON_FEHLER.is_dir():
+ fehler = sum(1 for d in config.TELEFON_FEHLER.iterdir() if d.is_file())
+
+ daten = {
+ "dienste": [
+ {"name": "Telefon", "zustand": trunk_zustand, "text": trunk_text},
+ {"name": "Verarbeitung", "zustand": "gut" if watcher else "aus",
+ "text": "beobachtet Eingang" if watcher else "Watcher läuft nicht"},
+ {"name": "Spracherkennung", "zustand": "gut" if ollama else "schlecht",
+ "text": "Ollama bereit" if ollama else "Ollama nicht erreichbar"},
+ {"name": "Nextcloud", "zustand": "unklar" if nextcloud is None
+ else ("gut" if nextcloud else "schlecht"),
+ "text": "nicht konfiguriert" if nextcloud is None
+ else ("erreichbar" if nextcloud else "nicht erreichbar")},
+ ],
+ "eingang_offen": offen,
+ "fehler": fehler,
+ "stand": datetime.now().strftime("%H:%M:%S"),
+ }
+ _CACHE["status"] = (jetzt, daten)
+ return daten
diff --git a/pipe/kategorien.py b/pipe/kategorien.py
new file mode 100644
index 0000000..4365a04
--- /dev/null
+++ b/pipe/kategorien.py
@@ -0,0 +1,48 @@
+"""Kategorien-Schema des aktiven Branchen-Profils (profile/