Files
aza/AzA march 2026 - Kopie (13)/aza_systemstatus.py
2026-04-19 20:41:37 +02:00

485 lines
16 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# -*- 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("<Configure>", 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}")