diff --git a/pipe/dashboard.py b/pipe/dashboard.py index 04b9956..f13dd4e 100644 --- a/pipe/dashboard.py +++ b/pipe/dashboard.py @@ -13,8 +13,8 @@ from pathlib import Path from . import config, kategorien -RANG = {"notfall": 0, "hoch": 1, "normal": 2, "niedrig": 3} -FARBE = {"notfall": "#E9322D", "hoch": "#E0A339", "normal": "#31CC7C", "niedrig": "#8A93A3"} +RANG = kategorien.DRINGLICHKEIT_RANG +FARBE = {k: f"#{v}" for k, v in kategorien.DRINGLICHKEIT_FARBE.items()} KAT_LABEL = kategorien.KATEGORIE_LABEL diff --git a/pipe/deck.py b/pipe/deck.py index 12d5d6f..c1b117e 100644 --- a/pipe/deck.py +++ b/pipe/deck.py @@ -22,13 +22,8 @@ from . import config, kategorien _API = "/index.php/apps/deck/api/v1.1" -# Farben (6-stelliges Hex ohne #) je Dringlichkeit. -DRINGLICHKEIT_FARBE = { - "niedrig": "8A93A3", # grau - "normal": "31CC7C", # grün - "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 = kategorien.KATEGORIE_LABEL diff --git a/pipe/kategorien.py b/pipe/kategorien.py index ec8de04..4365a04 100644 --- a/pipe/kategorien.py +++ b/pipe/kategorien.py @@ -6,9 +6,10 @@ Label) dieselbe Quelle nutzen - ein neues Profil ändert nur die JSON-Datei, kein Python-Code. Dringlichkeitsstufen sind bewusst NICHT Teil des Profils: die vierstufige -Eskalationsskala (niedrig/normal/hoch/notfall) ist einsatzzweck-unabhängig -und steckt fest in Farben/Reihenfolge in dashboard.py, deck.py und der -Leitstand-Oberfläche - nur die Kategorien selbst unterscheiden sich fachlich. +Eskalationsskala (niedrig/normal/hoch/notfall) ist einsatzzweck-unabhängig. +Farbe und Sortierrang je Stufe stehen deshalb ebenfalls hier - eine Quelle +für dashboard.py, deck.py und den Leitstand (server.py), statt an drei +Stellen von Hand synchron zu halten. """ from __future__ import annotations @@ -18,6 +19,17 @@ from functools import lru_cache from . import config DRINGLICHKEITEN = ["niedrig", "normal", "hoch", "notfall"] +# Farben (6-stelliges Hex ohne "#" - so will es Nextcloud Deck; dashboard.py +# und server.py stellen sich das "#" selbst voran). +DRINGLICHKEIT_FARBE = { + "niedrig": "8A93A3", # grau - unwichtig + "normal": "31CC7C", # grün + "hoch": "E0A339", # orange + "notfall": "E9322D", # rot +} +# Sortierrang (0 = dringlichst zuerst) - aus DRINGLICHKEITEN abgeleitet statt +# separat gepflegt. +DRINGLICHKEIT_RANG = {d: i for i, d in enumerate(reversed(DRINGLICHKEITEN))} @lru_cache(maxsize=1) diff --git a/pipe/server.py b/pipe/server.py index 80f1e62..e927781 100644 --- a/pipe/server.py +++ b/pipe/server.py @@ -26,7 +26,26 @@ from urllib.parse import parse_qs, unquote, urlparse from . import bewertung, config, dashboard, dienste, export, kategorien, modellwahl, protokoll, stapel, testzugang +# Anrufe() liest alle meta.json unter ablage/ (auch bereits archivierte, die +# erst danach rausgefiltert werden) - waechst mit der Gesamtzahl je +# empfangener Anrufe. Mehrere angemeldete Nutzer pollen das alle 3s parallel +# (siehe takt() im Frontend) - kurz zwischenspeichern, damit das nicht mit +# der Nutzerzahl skaliert. +_ANRUFE_CACHE: dict[str, tuple[float, list[dict]]] = {} +ANRUFE_CACHE_S = 2.0 + + def anrufe() -> list[dict]: + jetzt = time.time() + gepuffert = _ANRUFE_CACHE.get("anrufe") + if gepuffert and jetzt - gepuffert[0] < ANRUFE_CACHE_S: + return gepuffert[1] + liste = _anrufe_berechnen() + _ANRUFE_CACHE["anrufe"] = (jetzt, liste) + return liste + + +def _anrufe_berechnen() -> list[dict]: liste = [] for d in dashboard._anrufe(): e = d["auswertung"] @@ -138,11 +157,24 @@ def _telefon_ereignisse(anzahl: int = 60) -> list[dict]: return ereignisse[-anzahl:] +# _telefon_ereignisse() liest bis zu 300 KB aus dem FreeSWITCH-Log und scannt +# bis zu 4000 Zeilen per Regex - bei mehreren Nutzern, die alle 3s pollen, +# unnoetig oft. Gleicher kurzer Cache wie bei anrufe(). +_VERLAUF_CACHE: dict[str, tuple[float, list[dict]]] = {} +VERLAUF_CACHE_S = 2.0 + + def verlauf(anzahl: int = 150) -> list[dict]: """Pipe-Protokoll und Telefonie-Ereignisse, zeitlich zusammengeführt.""" + jetzt = time.time() + gepuffert = _VERLAUF_CACHE.get("verlauf") + if gepuffert and jetzt - gepuffert[0] < VERLAUF_CACHE_S: + return gepuffert[1] alle = protokoll.lies(anzahl) + _telefon_ereignisse() alle.sort(key=lambda e: e.get("zeit") or "") - return alle[-anzahl:] + ergebnis = alle[-anzahl:] + _VERLAUF_CACHE["verlauf"] = (jetzt, ergebnis) + return ergebnis # Bei Testzugang (kein Passwort, oeffentlicher Link) werden Rufnummern nicht @@ -259,14 +291,18 @@ class Handler(BaseHTTPRequestHandler): daten["nutzer"] = self._nutzer self._json(daten) elif pfad == "/api/anrufe": + # anrufe() ist gecacht (mehrere Nutzer pollen alle 3s parallel) - + # bei Testzugang auf einer Kopie maskieren, sonst wuerden die + # ersetzten Rufnummern die naechste (nicht maskierte) Antwort + # verseuchen. liste = anrufe() if getattr(self, "_via_token", False): - liste = _maskiere_anrufe(liste) + liste = _maskiere_anrufe([dict(a) for a in liste]) self._json({"stapel": stapel.erlaubt(), "anrufe": liste}) elif pfad == "/api/verlauf": eintraege = verlauf(150) if getattr(self, "_via_token", False): - eintraege = _maskiere_verlauf(eintraege) + eintraege = _maskiere_verlauf([dict(e) for e in eintraege]) self._json(eintraege) elif pfad == "/api/modell": self._json({"aktuell": modellwahl.aktuelle_id(), "optionen": modellwahl.auswahl()}) @@ -621,7 +657,7 @@ color:var(--gut);border-radius:999px;padding:1px 8px;font-size:11px;font-weight: