190 lines
6.9 KiB
Python
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())
|