Logpy-AgentOne/pipe/deck.py
Jeuner 87621e5bec feat: Branchen-Profile - Projekt fuer andere Einsatzzwecke konfigurierbar machen
Kategorien, Prompt und Notfall-Sicherheitsnetz waren fest auf eine
Hausarztpraxis zugeschnitten (categorize.py: KATEGORIEN-Liste, _NOTFALL_MUSTER-
Regex). Fuer einen anderen Einsatzzweck (z.B. 24h-Aufzug-Notdienst) musste
bisher Python-Code geaendert werden.

Neu: profile/<name>/ buendelt prompt.txt, kategorien.json, sicherheitsnetz.txt
und ansage.txt - alles, was fachlich vom Einsatzzweck abhaengt, an einer
Stelle. PROFIL in der .env waehlt den aktiven Ordner (Default "praxis", altes
Verhalten bleibt exakt gleich - per Test verifiziert: identische Kategorien-
Liste und Sicherheitsnetz-Muster). profile/aufzug-notdienst/ liegt als
ausformuliertes Beispielprofil bei (eigene Kategorien, eigenes
Sicherheitsnetz, eigene Ansage).

Dringlichkeitsstufen (niedrig/normal/hoch/notfall) bleiben bewusst fest -
nur die Kategorien sind einsatzzweck-spezifisch. Neues pipe/kategorien.py
laedt das Profil-Schema einmal fuer categorize.py, dashboard.py, deck.py und
server.py (Leitstand-JS: Kategorie-Label kommen jetzt vom Server statt fest
im Frontend zu stehen).

prompts/categorize_de.txt und telefon/ansage.txt sind nach profile/praxis/
umgezogen (git erkennt es als Rename).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EbdWYnDtyMgxsq4eN1WnrQ
2026-08-28 17:07:29 +02:00

158 lines
5.8 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 (6-stelliges Hex ohne #) je Dringlichkeit.
DRINGLICHKEIT_FARBE = {
"niedrig": "31CC7C", # grün
"normal": "F1DB50", # gelb
"hoch": "E0A339", # orange
"notfall": "E9322D", # rot
}
# 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