update
This commit is contained in:
484
AzA march 2026/aza_systemstatus.py
Normal file
484
AzA march 2026/aza_systemstatus.py
Normal file
@@ -0,0 +1,484 @@
|
||||
# -*- 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}")
|
||||
Reference in New Issue
Block a user