Logpy-AgentOne/pipe/deck.py
Jeuner 5c9f891c7e 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
2026-08-28 19:14:21 +02:00

153 lines
5.7 KiB
Python

"""Nextcloud-Deck-Anbindung: pro Anruf eine Triage-Karte.
Aufbau:
- Ein Board (Standard: "Anrufe").
- Ein Stapel je Kategorie (termin, rezept, …) — wird bei Bedarf angelegt.
- Ein Label je Dringlichkeit (farbig) — wird bei Bedarf angelegt und der Karte
zugewiesen.
- Karte je Anruf: Titel kompakt, Beschreibung = die Zusammenfassung (Markdown).
Nur aktiv, wenn NEXTCLOUD_DECK=true. Alles über die Deck-REST-API (v1.1).
"""
from __future__ import annotations
import base64
import json
import ssl
import urllib.error
import urllib.request
from functools import lru_cache
from . import config, kategorien
_API = "/index.php/apps/deck/api/v1.1"
# 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
@lru_cache(maxsize=1)
def _ssl_kontext() -> ssl.SSLContext:
try:
import certifi
return ssl.create_default_context(cafile=certifi.where())
except Exception:
return ssl.create_default_context()
def _api(method: str, pfad: str, koerper: dict | None = None):
roh = f"{config.NEXTCLOUD_USER}:{config.NEXTCLOUD_PASS}".encode("utf-8")
headers = {
"Authorization": "Basic " + base64.b64encode(roh).decode("ascii"),
"OCS-APIRequest": "true",
"Content-Type": "application/json",
"User-Agent": "praxis-telefon-agent/1.0",
}
daten = json.dumps(koerper).encode("utf-8") if koerper is not None else None
req = urllib.request.Request(
f"{config.NEXTCLOUD_URL}{_API}{pfad}", data=daten, headers=headers, method=method
)
with urllib.request.urlopen(req, timeout=30, context=_ssl_kontext()) as antwort:
text = antwort.read().decode("utf-8")
return json.loads(text) if text.strip() else None
# --- Board / Stapel / Labels sicherstellen -----------------------------------
def _freigabe(board_id: int, acl: list | None) -> None:
"""Teilt das Board mit den Nutzern aus DECK_TEILEN, falls noch nicht geschehen.
Das Board gehört dem Konto, mit dem die Pipe arbeitet. Ohne Freigabe legt
sie zwar Karten an, die Praxis sieht aber ein leeres Deck.
"""
if not config.DECK_TEILEN:
return
schon = {(a.get("participant") or {}).get("uid") for a in (acl or [])}
for nutzer in config.DECK_TEILEN:
if nutzer in schon:
continue
try:
_api("POST", f"/boards/{board_id}/acl",
{"type": 0, "participant": nutzer, "permissionEdit": True,
"permissionShare": False, "permissionManage": False})
print(f" → Deck: Board mit '{nutzer}' geteilt")
except Exception as fehler:
print(f" → Deck: Freigabe für '{nutzer}' fehlgeschlagen ({fehler})")
def _board_id() -> int:
for board in _api("GET", "/boards") or []:
if board.get("title") == config.DECK_BOARD and not board.get("archived"):
_freigabe(board["id"], board.get("acl"))
return board["id"]
neu = _api("POST", "/boards", {"title": config.DECK_BOARD, "color": "0082C9"})
_freigabe(neu["id"], [])
return neu["id"]
def _stapel(board_id: int) -> dict[str, int]:
"""Stellt die Stapel des Arbeitsablaufs sicher und gibt sie zurück."""
vorhanden = {s["title"]: s["id"] for s in (_api("GET", f"/boards/{board_id}/stacks") or [])}
for i, name in enumerate(config.DECK_STAPEL):
if name not in vorhanden:
neu = _api("POST", f"/boards/{board_id}/stacks", {"title": name, "order": i})
vorhanden[name] = neu["id"]
return vorhanden
def _labels(board_id: int) -> dict[str, int]:
board = _api("GET", f"/boards/{board_id}")
vorhanden = {l["title"]: l["id"] for l in (board.get("labels") or [])}
for name, farbe in DRINGLICHKEIT_FARBE.items():
if name not in vorhanden:
neu = _api("POST", f"/boards/{board_id}/labels",
{"title": name, "color": farbe})
vorhanden[name] = neu["id"]
return vorhanden
# --- Karte anlegen ------------------------------------------------------------
def _kartentitel(datensatz: dict) -> str:
"""Zeit, Kategorie, Anrufer - in dieser Reihenfolge lesbar auf der Karte."""
e = datensatz["auswertung"]
wer = e.get("anrufer_name") or datensatz.get("anrufer_nummer") or "unbekannt"
zeit = datensatz["empfangen"][11:16]
kategorie = KATEGORIE_LABEL.get(e["kategorie"], e["kategorie"])
return f"{zeit} · {kategorie} · {wer}"
def karte_anlegen(datensatz: dict, beschreibung: str) -> int | None:
"""Legt eine Deck-Karte für den Anruf an. Gibt die Karten-ID zurück.
Wirft nie — Deck ist ein Zusatz, kein Muss.
"""
if not (config.nextcloud_aktiv() and config.NEXTCLOUD_DECK):
return None
try:
board_id = _board_id()
stapel = _stapel(board_id)
labels = _labels(board_id)
e = datensatz["auswertung"]
eingang = config.DECK_STAPEL[0]
stack_id = stapel[eingang]
karte = _api(
"POST", f"/boards/{board_id}/stacks/{stack_id}/cards",
{"title": _kartentitel(datensatz), "type": "plain", "order": 0,
"description": beschreibung},
)
karten_id = karte["id"]
label_id = labels.get(e.get("dringlichkeit"))
if label_id is not None:
_api("PUT",
f"/boards/{board_id}/stacks/{stack_id}/cards/{karten_id}/assignLabel",
{"labelId": label_id})
return karten_id
except Exception as fehler:
print(f" → Deck: FEHLGESCHLAGEN ({fehler})")
return None