Files
aza/AzA march 2026/tools/diagnose_empfang_practice_inventory.py
2026-05-16 20:33:36 +02:00

190 lines
6.9 KiB
Python

# -*- coding: utf-8 -*-
"""
Offline-Diagnose: Empfang-Praxen, Konten, Sessions, Geräte, externe Links.
Nur lesend. Keine Datenänderung. Für Betrieb: auf dem Server im Projektroot
oder mit --data-dir ausführen.
Beispiel:
python tools/diagnose_empfang_practice_inventory.py
python tools/diagnose_empfang_practice_inventory.py --data-dir /pfad/zum/data
Ausgabe: stdout (UTF-8). Keine vollen Lizenzschlüssel; Konten ohne pw_hash-Inhalt.
"""
from __future__ import annotations
import argparse
import hashlib
import json
from collections import defaultdict
from pathlib import Path
from typing import Any
def _load(path: Path, default):
if not path.is_file():
return default
try:
return json.loads(path.read_text(encoding="utf-8"))
except Exception as exc:
print(f"# FEHLER beim Lesen {path}: {exc}")
return default
def _short_uid(uid: str, keep: int = 10) -> str:
s = (uid or "").strip()
if len(s) <= keep:
return s
return s[:keep] + ""
def _license_tail(acc: dict) -> str:
for key in ("license_key", "activation_key", "stored_license"):
raw = (acc.get(key) or "").strip()
if raw:
return "***" + raw[-4:] if len(raw) >= 4 else "****"
return ""
def main() -> int:
ap = argparse.ArgumentParser()
ap.add_argument(
"--data-dir",
type=Path,
help="Ordner mit empfang_*.json (Default: ../data relativ zu Repo)",
)
args = ap.parse_args()
here = Path(__file__).resolve().parent
repo = here.parent
data_dir = args.data_dir or (repo / "data")
paths = {
"practices": data_dir / "empfang_practices.json",
"accounts": data_dir / "empfang_accounts.json",
"sessions": data_dir / "empfang_sessions.json",
"devices": data_dir / "empfang_devices.json",
"links": data_dir / "empfang_practice_links.json",
"connections": data_dir / "empfang_connections.json",
}
print("=== Empfang Practice Inventory (read-only) ===")
print(f"data_dir={data_dir.resolve()}")
for name, p in paths.items():
print(f" {name}: {'OK' if p.is_file() else '— fehlt —'} {p.name}")
practices: dict[str, Any] = _load(paths["practices"], {})
accounts_raw = _load(paths["accounts"], {})
sessions_raw = _load(paths["sessions"], {})
devices_raw = _load(paths["devices"], {})
links_raw = _load(paths["links"], {"links": []})
connections_raw = _load(paths["connections"], [])
accounts: dict[str, dict] = accounts_raw if isinstance(accounts_raw, dict) else {}
sessions_dict: dict = sessions_raw if isinstance(sessions_raw, dict) else {}
devices_dict: dict = devices_raw if isinstance(devices_raw, dict) else {}
links: list = links_raw.get("links") or [] if isinstance(links_raw, dict) else []
# Accounts by practice
by_practice: dict[str, list[dict]] = defaultdict(list)
for uid, a in accounts.items():
if not isinstance(a, dict):
continue
pid = (a.get("practice_id") or "").strip()
if pid:
by_practice[pid].append(a)
# Session counts by practice_id (token -> session record)
sess_by_practice: dict[str, int] = defaultdict(int)
for _tok, s in sessions_dict.items():
if isinstance(s, dict):
pid = (s.get("practice_id") or "").strip()
if pid:
sess_by_practice[pid] += 1
dev_by_practice: dict[str, int] = defaultdict(int)
for _dk, d in devices_dict.items():
if not isinstance(d, dict):
continue
pid = (d.get("practice_id") or "").strip()
if pid:
dev_by_practice[pid] += 1
ext_count: dict[str, int] = defaultdict(int)
for L in links:
if not isinstance(L, dict):
continue
for k in ("source_practice_id", "target_practice_id"):
pid = (L.get(k) or "").strip()
if pid:
ext_count[pid] += 1
print("\n--- Praxis-Übersicht ---")
all_pids = sorted(set(practices.keys()) | set(by_practice.keys()))
dup_name_hints: dict[str, list[str]] = defaultdict(list)
for pid, pdata in practices.items():
if isinstance(pdata, dict):
nm = (pdata.get("name") or "").strip().lower()
if nm:
dup_name_hints[nm].append(pid)
for pid in all_pids:
pdata = practices.get(pid, {}) if isinstance(practices.get(pid), dict) else {}
name = (pdata.get("name") or "").strip() or "(ohne Eintrag in empfang_practices.json)"
invite = (pdata.get("invite_code") or "").strip()
invite_h = hashlib.sha256(invite.encode()).hexdigest()[:10] if invite else ""
members = by_practice.get(pid, [])
admins = [m for m in members if str(m.get("role") or "").lower() == "admin"]
print(f"\n* practice_id: {pid}")
print(f" practice_name: {name}")
print(f" invite_code: {invite or ''} (sha256-prefix: {invite_h or ''})")
print(f" user_count: {len(members)} admin_count: {len(admins)}")
if admins:
print(" admins: " + ", ".join((a.get("display_name") or "?") for a in admins[:20]))
print(f" sessions_count: {sess_by_practice.get(pid, 0)}")
print(f" devices_count: {dev_by_practice.get(pid, 0)}")
print(f" external_link_rows_touching_practice: {ext_count.get(pid, 0)}")
print(" users (gekürzt):")
for m in sorted(members, key=lambda x: str(x.get("display_name") or "").lower())[:40]:
dn = (m.get("display_name") or "").strip()
ln = (m.get("login_name") or "").strip()
role = (m.get("role") or "").strip()
uid = _short_uid(str(m.get("user_id") or ""))
lt = _license_tail(m)
print(f" - {dn!r} login={ln!r} role={role} uid={uid} lic_hint={lt or ''}")
if len(members) > 40:
print(f" … ({len(members) - 40} weitere)")
print("\n--- Hinweise Dubletten (gleicher Praxisname, verschiedene IDs) ---")
found = False
for nm, pids in dup_name_hints.items():
if len(pids) > 1:
found = True
print(f" name_low={nm!r} -> {pids}")
if not found:
print(" keine Namens-Kollisionen in empfang_practices.json")
print("\n--- Konten ohne Praxis-Eintrag in practices.json ---")
orphan_accounts = []
for uid, a in accounts.items():
if not isinstance(a, dict):
continue
pid = (a.get("practice_id") or "").strip()
if pid and pid not in practices:
orphan_accounts.append((pid, a))
if not orphan_accounts:
print(" keine")
else:
for pid, a in orphan_accounts[:50]:
print(f" practice_id={pid} user={a.get('display_name')} (Praxis-Stammsatz fehlt)")
print("\n--- Ende (keine Änderungen vorgenommen) ---")
print(
"Reparatur: nicht automatisch. STRATEGIE: siehe Nutzer-Anforderung BLOCK 5 / Arzt-Admin entscheidet nach Backup."
)
return 0
if __name__ == "__main__":
raise SystemExit(main())