mirror of
https://github.com/Jeuners/Logpy-AgentOne.git
synced 2026-09-09 15:02:33 +02:00
perf: /api/anrufe und /api/verlauf cachen, Dringlichkeit-Farben zentralisiert
Audit auf Optimierungspotenzial ergab: - /api/anrufe las bei jeder Anfrage ALLE meta.json unter ablage/ neu (auch bereits archivierte, erst danach rausgefiltert) - waechst mit der Gesamtzahl je empfangener Anrufe, jetzt von bis zu 3 Logins alle 3s parallel gepollt. - /api/verlauf las bei jeder Anfrage bis zu 300 KB aus dem FreeSWITCH-Log und scannte 4000 Zeilen per Regex, ebenfalls ungecacht. Beide bekommen jetzt einen kurzen 2s-Cache (gleiches Muster wie dienste.status()). Maskierung bei Testzugang arbeitet jetzt auf einer Kopie (dict(a) je Eintrag) statt die gecachten Objekte in-place zu veraendern - sonst haetten maskierte Rufnummern in die naechste, nicht maskierte Antwort durchsickern koennen. Dringlichkeit-Farben (rot/orange/gruen/grau) und der Sortierrang standen in drei Dateien dupliziert (dashboard.py, deck.py, server.py-JS) - bei der letzten Farbaenderung mussten alle drei von Hand synchron gehalten werden. Jetzt eine Quelle in pipe/kategorien.py (DRINGLICHKEIT_FARBE, DRINGLICHKEIT_RANG - Rang aus DRINGLICHKEITEN abgeleitet statt separat gepflegt), server.py injiziert die Farben serverseitig wie schon die Kategorie-Labels. Getestet: RANG/FARBE identisch zum alten Stand, /api/anrufe und /api/verlauf liefern bei zwei Anfragen innerhalb 2s dasselbe (gecachte) Ergebnis, Maskierung veraendert nachweislich nur die Kopie. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EbdWYnDtyMgxsq4eN1WnrQ
This commit is contained in:
parent
ab2bd949be
commit
5c9f891c7e
4 changed files with 61 additions and 16 deletions
|
|
@ -13,8 +13,8 @@ from pathlib import Path
|
||||||
|
|
||||||
from . import config, kategorien
|
from . import config, kategorien
|
||||||
|
|
||||||
RANG = {"notfall": 0, "hoch": 1, "normal": 2, "niedrig": 3}
|
RANG = kategorien.DRINGLICHKEIT_RANG
|
||||||
FARBE = {"notfall": "#E9322D", "hoch": "#E0A339", "normal": "#31CC7C", "niedrig": "#8A93A3"}
|
FARBE = {k: f"#{v}" for k, v in kategorien.DRINGLICHKEIT_FARBE.items()}
|
||||||
KAT_LABEL = kategorien.KATEGORIE_LABEL
|
KAT_LABEL = kategorien.KATEGORIE_LABEL
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -22,13 +22,8 @@ from . import config, kategorien
|
||||||
|
|
||||||
_API = "/index.php/apps/deck/api/v1.1"
|
_API = "/index.php/apps/deck/api/v1.1"
|
||||||
|
|
||||||
# Farben (6-stelliges Hex ohne #) je Dringlichkeit.
|
# Farben je Dringlichkeit - siehe pipe/kategorien.py.
|
||||||
DRINGLICHKEIT_FARBE = {
|
DRINGLICHKEIT_FARBE = kategorien.DRINGLICHKEIT_FARBE
|
||||||
"niedrig": "8A93A3", # grau
|
|
||||||
"normal": "31CC7C", # grün
|
|
||||||
"hoch": "E0A339", # orange
|
|
||||||
"notfall": "E9322D", # rot
|
|
||||||
}
|
|
||||||
# Lesbare Namen der Kategorien für den Kartentitel - der Stapel sagt jetzt,
|
# Lesbare Namen der Kategorien für den Kartentitel - der Stapel sagt jetzt,
|
||||||
# wie weit die Bearbeitung ist, nicht mehr worum es geht.
|
# wie weit die Bearbeitung ist, nicht mehr worum es geht.
|
||||||
KATEGORIE_LABEL = kategorien.KATEGORIE_LABEL
|
KATEGORIE_LABEL = kategorien.KATEGORIE_LABEL
|
||||||
|
|
|
||||||
|
|
@ -6,9 +6,10 @@ Label) dieselbe Quelle nutzen - ein neues Profil ändert nur die JSON-Datei,
|
||||||
kein Python-Code.
|
kein Python-Code.
|
||||||
|
|
||||||
Dringlichkeitsstufen sind bewusst NICHT Teil des Profils: die vierstufige
|
Dringlichkeitsstufen sind bewusst NICHT Teil des Profils: die vierstufige
|
||||||
Eskalationsskala (niedrig/normal/hoch/notfall) ist einsatzzweck-unabhängig
|
Eskalationsskala (niedrig/normal/hoch/notfall) ist einsatzzweck-unabhängig.
|
||||||
und steckt fest in Farben/Reihenfolge in dashboard.py, deck.py und der
|
Farbe und Sortierrang je Stufe stehen deshalb ebenfalls hier - eine Quelle
|
||||||
Leitstand-Oberfläche - nur die Kategorien selbst unterscheiden sich fachlich.
|
für dashboard.py, deck.py und den Leitstand (server.py), statt an drei
|
||||||
|
Stellen von Hand synchron zu halten.
|
||||||
"""
|
"""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
|
@ -18,6 +19,17 @@ from functools import lru_cache
|
||||||
from . import config
|
from . import config
|
||||||
|
|
||||||
DRINGLICHKEITEN = ["niedrig", "normal", "hoch", "notfall"]
|
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)
|
@lru_cache(maxsize=1)
|
||||||
|
|
|
||||||
|
|
@ -26,7 +26,26 @@ from urllib.parse import parse_qs, unquote, urlparse
|
||||||
from . import bewertung, config, dashboard, dienste, export, kategorien, modellwahl, protokoll, stapel, testzugang
|
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]:
|
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 = []
|
liste = []
|
||||||
for d in dashboard._anrufe():
|
for d in dashboard._anrufe():
|
||||||
e = d["auswertung"]
|
e = d["auswertung"]
|
||||||
|
|
@ -138,11 +157,24 @@ def _telefon_ereignisse(anzahl: int = 60) -> list[dict]:
|
||||||
return ereignisse[-anzahl:]
|
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]:
|
def verlauf(anzahl: int = 150) -> list[dict]:
|
||||||
"""Pipe-Protokoll und Telefonie-Ereignisse, zeitlich zusammengeführt."""
|
"""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 = protokoll.lies(anzahl) + _telefon_ereignisse()
|
||||||
alle.sort(key=lambda e: e.get("zeit") or "")
|
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
|
# Bei Testzugang (kein Passwort, oeffentlicher Link) werden Rufnummern nicht
|
||||||
|
|
@ -259,14 +291,18 @@ class Handler(BaseHTTPRequestHandler):
|
||||||
daten["nutzer"] = self._nutzer
|
daten["nutzer"] = self._nutzer
|
||||||
self._json(daten)
|
self._json(daten)
|
||||||
elif pfad == "/api/anrufe":
|
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()
|
liste = anrufe()
|
||||||
if getattr(self, "_via_token", False):
|
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})
|
self._json({"stapel": stapel.erlaubt(), "anrufe": liste})
|
||||||
elif pfad == "/api/verlauf":
|
elif pfad == "/api/verlauf":
|
||||||
eintraege = verlauf(150)
|
eintraege = verlauf(150)
|
||||||
if getattr(self, "_via_token", False):
|
if getattr(self, "_via_token", False):
|
||||||
eintraege = _maskiere_verlauf(eintraege)
|
eintraege = _maskiere_verlauf([dict(e) for e in eintraege])
|
||||||
self._json(eintraege)
|
self._json(eintraege)
|
||||||
elif pfad == "/api/modell":
|
elif pfad == "/api/modell":
|
||||||
self._json({"aktuell": modellwahl.aktuelle_id(), "optionen": modellwahl.auswahl()})
|
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:
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<script>
|
<script>
|
||||||
const FARBE={notfall:'#d93a34',hoch:'#e08a2a',normal:'#1f9d5c',niedrig:'#8a93a3'};
|
const FARBE=__FARBE_JSON__;
|
||||||
const KAT=__KAT_JSON__;
|
const KAT=__KAT_JSON__;
|
||||||
const esc=s=>String(s??'').replace(/[&<>"]/g,c=>({'&':'&','<':'<','>':'>','"':'"'}[c]));
|
const esc=s=>String(s??'').replace(/[&<>"]/g,c=>({'&':'&','<':'<','>':'>','"':'"'}[c]));
|
||||||
let STAPEL=[], ANRUFE=[], offeneTranskripte=new Set(), pausiert=false;
|
let STAPEL=[], ANRUFE=[], offeneTranskripte=new Set(), pausiert=false;
|
||||||
|
|
@ -1041,6 +1077,8 @@ def _seite() -> str:
|
||||||
zusammengesetzt.
|
zusammengesetzt.
|
||||||
"""
|
"""
|
||||||
seite = SEITE.replace("__KAT_JSON__", json.dumps(kategorien.KATEGORIE_LABEL, ensure_ascii=False))
|
seite = SEITE.replace("__KAT_JSON__", json.dumps(kategorien.KATEGORIE_LABEL, ensure_ascii=False))
|
||||||
|
farben = {k: f"#{v}" for k, v in kategorien.DRINGLICHKEIT_FARBE.items()}
|
||||||
|
seite = seite.replace("__FARBE_JSON__", json.dumps(farben, ensure_ascii=False))
|
||||||
return seite.replace("__PROFIL_NAME__", html.escape(kategorien.PROFIL_NAME))
|
return seite.replace("__PROFIL_NAME__", html.escape(kategorien.PROFIL_NAME))
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue