# -*- coding: utf-8 -*- """ AZA Systemstatus / Selbsttest – Praxiscomputer-Validierung. Zeigt den Zustand aller kritischen Komponenten in einem Fenster. """ import os import sys import time import json import uuid import platform import urllib.error import tkinter as tk from tkinter import messagebox from pathlib import Path from aza_config import get_writable_data_dir from aza_style import FONT_FAMILY, ACCENT, TEXT, SUBTLE, BORDER def _get_device_id() -> str: p = Path(get_writable_data_dir()) / "device_id.txt" if p.exists(): try: v = p.read_text(encoding="utf-8").strip() if v: return v except Exception: pass v = f"aza-{platform.system()}-{platform.machine()}-{uuid.uuid4()}" try: p.write_text(v, encoding="utf-8") except Exception: pass return v _BG = "#FFFFFF" _OK = "#22C55E" _WARN = "#F59E0B" _ERR = "#EF4444" def _status_icon(ok, warn=False): if ok is None: return "\u2014", SUBTLE if warn: return "\u26A0", _WARN if ok: return "\u2713", _OK return "\u2717", _ERR def collect_status() -> list[dict]: """Sammelt alle Statuspunkte. Gibt Liste von dicts mit name, value, ok, detail.""" checks = [] try: from aza_version import APP_VERSION except Exception: APP_VERSION = "?" checks.append({"name": "App-Version", "value": APP_VERSION, "ok": True}) frozen = getattr(sys, "frozen", False) checks.append({ "name": "Installationsmodus", "value": "Installiert" if frozen else "Entwicklung", "ok": True, }) if frozen: exe_dir = os.path.dirname(sys.executable) else: exe_dir = os.path.dirname(os.path.abspath(__file__)) checks.append({"name": "Installationspfad", "value": exe_dir, "ok": True}) data_dir = get_writable_data_dir() writable = False try: test_file = os.path.join(data_dir, ".write_test") with open(test_file, "w") as f: f.write("ok") os.remove(test_file) writable = True except Exception: pass checks.append({ "name": "APPDATA-Verzeichnis", "value": data_dir, "ok": writable, "detail": "" if writable else "Nicht beschreibbar", }) key_ok = False try: from security_vault import has_vault_key key_ok = has_vault_key() except Exception: pass if not key_ok: key_ok = bool(os.getenv("OPENAI_API_KEY", "").strip()) _key_detail = "" if key_ok: _key_detail = "(verschlüsselt, benutzerbezogen)" else: _key_detail = ("Nicht eingerichtet \u2013 jeder Windows-Benutzer muss " "seinen eigenen Schlüssel unter Einstellungen hinterlegen") checks.append({ "name": "OpenAI-Schlüssel", "value": ("Eingerichtet " + _key_detail) if key_ok else _key_detail, "ok": key_ok, }) runtime_cfg = False cfg_path = os.path.join(data_dir, "config", "aza_runtime.env") if not os.path.isfile(cfg_path): alt = os.path.join(exe_dir, "config", "aza_runtime.env") if frozen else "" if alt and os.path.isfile(alt): runtime_cfg = True else: runtime_cfg = True checks.append({ "name": "Runtime-Config", "value": "Vorhanden" if runtime_cfg else "Nicht gefunden", "ok": runtime_cfg, "warn": not runtime_cfg, }) backend_ok = False backend_url = "" backend_detail = "" try: backend_url = (os.getenv("MEDWORK_BACKEND_URL") or "").strip().rstrip("/") if not backend_url: url_file = os.path.join(exe_dir, "backend_url.txt") if frozen else os.path.join( os.path.dirname(os.path.abspath(__file__)), "backend_url.txt" ) if os.path.isfile(url_file): with open(url_file, "r") as f: backend_url = f.read().strip().rstrip("/") if not backend_url: try: from desktop_backend_autostart import BACKEND_HOST, BACKEND_PORT backend_url = f"http://{BACKEND_HOST}:{BACKEND_PORT}" except Exception: backend_url = "http://127.0.0.1:8000" if backend_url: import urllib.request req = urllib.request.Request(f"{backend_url}/health", method="GET") resp = urllib.request.urlopen(req, timeout=5) backend_ok = resp.status == 200 except Exception: pass if backend_ok: backend_detail = backend_url[:60] elif backend_url: backend_detail = f"{backend_url[:60]} (nicht erreichbar)" else: backend_detail = "URL nicht konfiguriert" checks.append({ "name": "Backend erreichbar", "value": "Ja" if backend_ok else "Nein", "ok": backend_ok, "detail": backend_detail, }) license_ok = None license_detail = "" try: if backend_ok and backend_url: import urllib.request token = "" script_dir = os.path.dirname(os.path.abspath(__file__)) search_dirs = [script_dir, exe_dir] if frozen: internal_dir = os.path.join(exe_dir, "_internal") if os.path.isdir(internal_dir): search_dirs.insert(0, internal_dir) for d in search_dirs: tf = os.path.join(d, "backend_token.txt") if os.path.isfile(tf): try: with open(tf, "r", encoding="utf-8-sig") as f: token = (f.read() or "").replace("\ufeff", "").strip() except Exception: pass if token: break if not token: tokens_env = os.getenv("MEDWORK_API_TOKENS", "").strip() if tokens_env: token = tokens_env.split(",")[0].strip() if not token: token = os.getenv("MEDWORK_API_TOKEN", "").strip() if token: lic_headers = {"X-API-Token": token} try: lic_headers["X-Device-Id"] = _get_device_id() except Exception: pass req = urllib.request.Request( f"{backend_url}/license/status", headers=lic_headers, ) resp = urllib.request.urlopen(req, timeout=5) data = json.loads(resp.read().decode("utf-8")) license_ok = data.get("valid", False) if license_ok: license_detail = "Gueltig" else: license_detail = "Ungueltig" else: license_detail = "Kein Token" except urllib.error.HTTPError as e: license_detail = f"HTTP {e.code}" if hasattr(e, "code") else "HTTP-Fehler" except urllib.error.URLError: license_detail = "Backend nicht erreichbar" except Exception: license_detail = "Pruefung fehlgeschlagen" checks.append({ "name": "Lizenz", "value": license_detail or ("Gueltig" if license_ok else "Unbekannt"), "ok": license_ok, }) activation_ok = None try: from aza_activation import check_app_access result = check_app_access() if isinstance(result, tuple) and len(result) >= 2: activation_ok = bool(result[0]) act_detail = str(result[1]) if result[1] else ("Aktiv" if activation_ok else "Abgelaufen") elif isinstance(result, dict): activation_ok = bool(result.get("allowed", False)) act_detail = result.get("reason", "") or ("Aktiv" if activation_ok else "Abgelaufen") else: act_detail = "Unbekanntes Format" except Exception: act_detail = "Nicht pruefbar" checks.append({ "name": "Aktivierung", "value": act_detail, "ok": activation_ok, }) login_ok = None try: from aza_persistence import load_user_profile profile = load_user_profile() last_ts = profile.get("last_login_ts") if isinstance(last_ts, (int, float)): elapsed_days = (time.time() - float(last_ts)) / 86400 login_ok = elapsed_days < 7 if login_ok: checks.append({ "name": "Wochenlogin", "value": f"OK (vor {elapsed_days:.0f} Tagen)", "ok": True, }) else: checks.append({ "name": "Wochenlogin", "value": f"Ausstehend (vor {elapsed_days:.0f} Tagen)", "ok": False, }) else: checks.append({"name": "Wochenlogin", "value": "Noch nie", "ok": False}) except Exception: checks.append({"name": "Wochenlogin", "value": "Nicht pruefbar", "ok": None}) try: from aza_persistence import load_launcher_prefs prefs = load_launcher_prefs() has_pref = bool(prefs.get("default_module")) checks.append({ "name": "Launcher-Startpraeferenz", "value": prefs.get("default_module", "Keine") if has_pref else "Keine", "ok": True, "warn": not has_pref, }) except Exception: checks.append({"name": "Launcher-Startpraeferenz", "value": "?", "ok": None}) notizen_dir = os.path.join(data_dir, "notizen") notizen_ok = False try: os.makedirs(notizen_dir, exist_ok=True) test_f = os.path.join(notizen_dir, ".write_test") with open(test_f, "w") as f: f.write("ok") os.remove(test_f) notizen_ok = True except Exception: pass checks.append({ "name": "Notizen-Verzeichnis", "value": "Beschreibbar" if notizen_ok else "Nicht beschreibbar", "ok": notizen_ok, }) try: from aza_firewall import check_firewall_rule fw = check_firewall_rule() if fw["exists"]: all_ok = all( fw.get(k) is True for k in ("protocol_ok", "port_ok", "remote_ok", "program_ok") ) if all_ok: checks.append({ "name": "Firewall-Regel", "value": "OK", "ok": True, "detail": fw["detail"], }) else: checks.append({ "name": "Firewall-Regel", "value": "Teilweise", "ok": True, "warn": True, "detail": fw["detail"], }) else: detail = fw.get("detail", "") if "nicht moeglich" in detail.lower(): checks.append({ "name": "Firewall-Regel", "value": "Hinweis", "ok": None, "detail": detail, }) else: checks.append({ "name": "Firewall-Regel", "value": "Nicht vorhanden", "ok": False, "detail": "Regel wird bei Neuinstallation automatisch angelegt", }) except Exception: checks.append({ "name": "Firewall-Regel", "value": "Hinweis", "ok": None, "detail": "Firewall-Status konnte nicht vollstaendig geprueft werden", }) return checks def format_status_text(checks: list[dict]) -> str: """Formatiert den Status als kopierbaren Text (ohne Secrets).""" lines = ["AZA Desktop – Systemstatus", "=" * 40] for c in checks: icon = "OK" if c.get("ok") else ("?" if c.get("ok") is None else "FEHLER") if c.get("warn"): icon = "HINWEIS" line = f"[{icon:>7}] {c['name']}: {c['value']}" if c.get("detail"): line += f" ({c['detail']})" lines.append(line) lines.append("=" * 40) return "\n".join(lines) def show_systemstatus(parent): """Zeigt das Systemstatus-Fenster.""" win = tk.Toplevel(parent) win.title("AZA \u2013 Systemstatus") win.configure(bg=_BG) win.resizable(True, True) win.minsize(500, 400) win.attributes("-topmost", True) outer = tk.Frame(win, bg=_BG) outer.pack(fill="both", expand=True, padx=28, pady=20) header = tk.Frame(outer, bg=_BG) header.pack(fill="x", side="top") tk.Label(header, text="\U0001F6E0 Systemstatus", font=(FONT_FAMILY, 16, "bold"), fg=TEXT, bg=_BG ).pack(anchor="w", pady=(0, 4)) tk.Label(header, text="Pruefung der wichtigsten Komponenten", font=(FONT_FAMILY, 10), fg=SUBTLE, bg=_BG ).pack(anchor="w", pady=(0, 12)) btn_frame = tk.Frame(outer, bg=_BG) btn_frame.pack(fill="x", side="bottom", pady=(14, 0)) mid = tk.Frame(outer, bg=_BG) mid.pack(fill="both", expand=True, side="top") canvas = tk.Canvas(mid, bg=_BG, highlightthickness=0) scrollbar = tk.Scrollbar(mid, orient="vertical", command=canvas.yview) scroll_frame = tk.Frame(canvas, bg=_BG) scroll_frame.bind("", lambda e: canvas.configure(scrollregion=canvas.bbox("all"))) canvas.create_window((0, 0), window=scroll_frame, anchor="nw") canvas.configure(yscrollcommand=scrollbar.set) canvas.pack(side="left", fill="both", expand=True) scrollbar.pack(side="right", fill="y") status_data = [None] def _populate(checks): for widget in scroll_frame.winfo_children(): widget.destroy() for c in checks: row = tk.Frame(scroll_frame, bg=_BG) row.pack(fill="x", pady=3) is_warn = c.get("warn", False) ok = c.get("ok") icon_text, icon_color = _status_icon(ok, warn=is_warn) tk.Label(row, text=icon_text, font=(FONT_FAMILY, 12, "bold"), fg=icon_color, bg=_BG, width=3).pack(side="left") name_lbl = tk.Label(row, text=c["name"] + ":", font=(FONT_FAMILY, 10), fg=SUBTLE, bg=_BG, anchor="w", width=22) name_lbl.pack(side="left") val_text = c.get("value", "") detail = c.get("detail", "") if detail: val_text += f" ({detail})" tk.Label(row, text=val_text, font=(FONT_FAMILY, 9), fg=TEXT, bg=_BG, anchor="w", wraplength=280, justify="left" ).pack(side="left", fill="x", expand=True) def _run_checks(): checks = collect_status() status_data[0] = checks _populate(checks) def _copy_status(): if status_data[0]: txt = format_status_text(status_data[0]) try: win.clipboard_clear() win.clipboard_append(txt) messagebox.showinfo("Kopiert", "Diagnose wurde in die Zwischenablage kopiert.", parent=win) except Exception: pass btn_refresh = tk.Button( btn_frame, text="\u21BB Neu pruefen", font=(FONT_FAMILY, 10, "bold"), bg=ACCENT, fg="white", activebackground="#0067C0", activeforeground="white", relief="flat", bd=0, padx=16, pady=7, cursor="hand2", command=_run_checks, ) btn_refresh.pack(side="left", padx=(0, 8)) btn_copy = tk.Button( btn_frame, text="Diagnose kopieren", font=(FONT_FAMILY, 10), bg="#EBEDF0", fg="#374151", activebackground="#D1D5DB", relief="flat", bd=0, padx=16, pady=7, cursor="hand2", command=_copy_status, ) btn_copy.pack(side="left") btn_close = tk.Button( btn_frame, text="Schliessen", font=(FONT_FAMILY, 10), bg=_BG, fg=SUBTLE, activebackground="#F0F0F0", relief="solid", bd=1, padx=16, pady=6, cursor="hand2", highlightbackground=BORDER, command=win.destroy, ) btn_close.pack(side="right") win.grab_set() _run_checks() win.update_idletasks() req_w = max(win.winfo_reqwidth(), 600) req_h = win.winfo_reqheight() + 20 sw = win.winfo_screenwidth() sh = win.winfo_screenheight() w = min(req_w, sw - 40) h = min(req_h, sh - 80) win.geometry(f"{w}x{h}+{(sw - w) // 2}+{(sh - h) // 2}")