# -*- coding: utf-8 -*- """ AZA Admin-Panel – Verstecktes Administrations-Fenster. Zugang ausschliesslich über Doppelklick auf das AZA-Logo im Launcher. """ import hashlib import os import sys import threading import tkinter as tk from tkinter import messagebox from aza_config import get_writable_data_dir, DEFAULT_TOKEN_QUOTA from aza_persistence import ( load_token_usage, reset_token_allowance, get_remaining_tokens, get_location_display, log_installation_location, load_installation_location, get_install_count, ) from aza_style import ( ACCENT, ACCENT_HOVER, TEXT, SUBTLE, BORDER, FONT_FAMILY, format_number_de, ) _BG = "#FFFFFF" _SECTION_BG = "#F8FAFC" _ADMIN_PW_HASH = "0fa4e974f520d0419a2f6c5a03c5d64bdf8f97097a506ff8857bd3072f29c72d" def _check_password(pw: str) -> bool: return hashlib.sha256(pw.encode("utf-8")).hexdigest() == _ADMIN_PW_HASH def show_admin_login(parent) -> bool: """Zeigt den Admin-Login-Dialog. Gibt True zurück bei erfolgreichem Login.""" result = {"ok": False} dlg = tk.Toplevel(parent) dlg.title("AZA Administration") dlg.configure(bg=_BG) dlg.resizable(False, False) w, h = 400, 240 dlg.geometry(f"{w}x{h}") dlg.attributes("-topmost", True) try: dlg.update_idletasks() sw = dlg.winfo_screenwidth() sh = dlg.winfo_screenheight() dlg.geometry(f"{w}x{h}+{(sw - w) // 2}+{(sh - h) // 2}") except Exception: pass content = tk.Frame(dlg, bg=_BG) content.pack(fill="both", expand=True, padx=36, pady=24) tk.Label(content, text="\U0001F6E0", font=(FONT_FAMILY, 24), fg=ACCENT, bg=_BG).pack(anchor="w") tk.Label(content, text="Admin-Zugang", font=(FONT_FAMILY, 16, "bold"), fg=TEXT, bg=_BG ).pack(anchor="w", pady=(4, 12)) pw_frame = tk.Frame(content, bg=BORDER) pw_frame.pack(fill="x", pady=(0, 6)) pw_entry = tk.Entry(pw_frame, font=(FONT_FAMILY, 12), bg="white", fg=TEXT, relief="flat", bd=0, show="\u2022") pw_entry.pack(fill="x", ipady=7, padx=2, pady=2) status = tk.Label(content, text="", font=(FONT_FAMILY, 9), fg="#E05050", bg=_BG) status.pack(anchor="w", pady=(0, 10)) def do_login(event=None): if _check_password(pw_entry.get()): result["ok"] = True dlg.destroy() else: status.configure(text="\u26A0 Falsches Passwort.") pw_entry.delete(0, "end") pw_entry.bind("", do_login) btn = tk.Button(content, text="Anmelden", font=(FONT_FAMILY, 11, "bold"), bg=ACCENT, fg="white", activebackground=ACCENT_HOVER, activeforeground="white", relief="flat", bd=0, padx=22, pady=8, cursor="hand2", command=do_login) btn.pack(anchor="w") btn.bind("", lambda e: btn.configure(bg=ACCENT_HOVER)) btn.bind("", lambda e: btn.configure(bg=ACCENT)) dlg.protocol("WM_DELETE_WINDOW", dlg.destroy) pw_entry.focus_set() dlg.grab_set() parent.wait_window(dlg) return result["ok"] def show_admin_panel(parent): """Öffnet das Admin-Panel (nach erfolgreichem Login).""" win = tk.Toplevel(parent) win.title("AZA \u2013 MedWork Administration") win.configure(bg=_BG) win.resizable(True, True) w, h = 580, 640 win.minsize(500, 520) win.geometry(f"{w}x{h}") win.attributes("-topmost", True) try: win.update_idletasks() sw = win.winfo_screenwidth() sh = win.winfo_screenheight() win.geometry(f"{w}x{h}+{(sw - w) // 2}+{(sh - h) // 2}") except Exception: pass outer = tk.Frame(win, bg=_BG) outer.pack(fill="both", expand=True, padx=32, pady=24) tk.Label(outer, text="\U0001F6E0 AZA \u2013 MedWork Administration", font=(FONT_FAMILY, 18, "bold"), fg=TEXT, bg=_BG ).pack(anchor="w", pady=(0, 16)) # ── KI-Guthaben ────────────────────────────────────────────────────────── sec1 = tk.LabelFrame(outer, text=" KI-Guthaben ", font=(FONT_FAMILY, 11, "bold"), fg=TEXT, bg=_SECTION_BG, bd=1, relief="solid", highlightbackground=BORDER, highlightthickness=1) sec1.pack(fill="x", pady=(0, 14), ipady=8) inner1 = tk.Frame(sec1, bg=_SECTION_BG) inner1.pack(fill="x", padx=20, pady=8) data = load_token_usage() used = data.get("used", 0) total = data.get("total", DEFAULT_TOKEN_QUOTA) remaining = get_remaining_tokens() info_lines = [ ("Gesamt-Budget:", format_number_de(total)), ("Verbraucht:", format_number_de(used)), ("Verbleibend:", format_number_de(remaining)), ] value_labels = {} for label_text, value_text in info_lines: row = tk.Frame(inner1, bg=_SECTION_BG) row.pack(fill="x", pady=2) tk.Label(row, text=label_text, font=(FONT_FAMILY, 10), fg=SUBTLE, bg=_SECTION_BG, width=16, anchor="w").pack(side="left") vl = tk.Label(row, text=value_text, font=(FONT_FAMILY, 10, "bold"), fg=TEXT, bg=_SECTION_BG, anchor="w") vl.pack(side="left") value_labels[label_text] = vl btn_frame = tk.Frame(inner1, bg=_SECTION_BG) btn_frame.pack(fill="x", pady=(10, 0)) def do_reset(): answer = messagebox.askyesno( "Guthaben aufladen", f"KI-Guthaben auf {format_number_de(DEFAULT_TOKEN_QUOTA)} Einheiten aufladen?\n\n" "Der bisherige Verbrauch wird auf 0 gesetzt.", parent=win, ) if answer: reset_token_allowance(DEFAULT_TOKEN_QUOTA) value_labels["Gesamt-Budget:"].configure(text=format_number_de(DEFAULT_TOKEN_QUOTA)) value_labels["Verbraucht:"].configure(text=format_number_de(0)) value_labels["Verbleibend:"].configure(text=format_number_de(DEFAULT_TOKEN_QUOTA)) messagebox.showinfo( "Erledigt", f"Guthaben wurde auf {format_number_de(DEFAULT_TOKEN_QUOTA)} Einheiten aufgeladen.", parent=win, ) btn_reset = tk.Button( btn_frame, text=f"\u21BB Guthaben auf {format_number_de(DEFAULT_TOKEN_QUOTA)} Einheiten aufladen", font=(FONT_FAMILY, 10, "bold"), bg=ACCENT, fg="white", activebackground=ACCENT_HOVER, activeforeground="white", relief="flat", bd=0, padx=18, pady=7, cursor="hand2", command=do_reset, ) btn_reset.pack(side="left") btn_reset.bind("", lambda e: btn_reset.configure(bg=ACCENT_HOVER)) btn_reset.bind("", lambda e: btn_reset.configure(bg=ACCENT)) # ── Installation & Standort ────────────────────────────────────────────── sec2 = tk.LabelFrame(outer, text=" Installation ", font=(FONT_FAMILY, 11, "bold"), fg=TEXT, bg=_SECTION_BG, bd=1, relief="solid", highlightbackground=BORDER, highlightthickness=1) sec2.pack(fill="x", pady=(0, 14), ipady=8) inner2 = tk.Frame(sec2, bg=_SECTION_BG) inner2.pack(fill="x", padx=20, pady=8) data_dir = get_writable_data_dir() exe_dir = (os.path.dirname(os.path.abspath(sys.executable)) if getattr(sys, "frozen", False) else os.path.dirname(os.path.abspath(__file__))) device_id = _get_device_id_short() vault_status = "Nicht eingerichtet" try: from security_vault import has_vault_key, get_masked_key if has_vault_key(): vault_status = f"Aktiviert ({get_masked_key()})" except Exception: pass location_text = get_location_display() install_lines = [ ("Geräte-ID:", device_id), ("Standort:", location_text), ("Datenverzeichnis:", data_dir), ("Installationsordner:", exe_dir), ("API-Key Tresor:", vault_status), ] location_value_label = None for label_text, value_text in install_lines: row = tk.Frame(inner2, bg=_SECTION_BG) row.pack(fill="x", pady=2) tk.Label(row, text=label_text, font=(FONT_FAMILY, 10), fg=SUBTLE, bg=_SECTION_BG, width=18, anchor="w").pack(side="left") vl = tk.Label(row, text=value_text, font=(FONT_FAMILY, 9), fg=TEXT, bg=_SECTION_BG, anchor="w", wraplength=300, justify="left") vl.pack(side="left", fill="x") if label_text == "Standort:": location_value_label = vl if location_text == "Nicht ermittelt" and location_value_label: def _fetch_location(): loc = log_installation_location() if loc and location_value_label.winfo_exists(): display = get_location_display() try: location_value_label.configure(text=display) except Exception: pass threading.Thread(target=_fetch_location, daemon=True).start() # ── AZA Netzwerk ───────────────────────────────────────────────────────── sec3 = tk.LabelFrame(outer, text=" AZA Netzwerk ", font=(FONT_FAMILY, 11, "bold"), fg=TEXT, bg=_SECTION_BG, bd=1, relief="solid", highlightbackground=BORDER, highlightthickness=1) sec3.pack(fill="x", pady=(0, 14), ipady=8) inner3 = tk.Frame(sec3, bg=_SECTION_BG) inner3.pack(fill="x", padx=20, pady=8) net_row = tk.Frame(inner3, bg=_SECTION_BG) net_row.pack(fill="x", pady=2) tk.Label(net_row, text="Aktive Praxen:", font=(FONT_FAMILY, 10), fg=SUBTLE, bg=_SECTION_BG, width=18, anchor="w").pack(side="left") count_label = tk.Label(net_row, text="Wird geladen\u2026", font=(FONT_FAMILY, 10, "bold"), fg=TEXT, bg=_SECTION_BG, anchor="w") count_label.pack(side="left") def _load_count(): count, is_live = get_install_count() if not count_label.winfo_exists(): return suffix = "" if is_live else " (lokal)" try: count_label.configure( text=f"{format_number_de(count)} Computer{suffix}") except Exception: pass threading.Thread(target=_load_count, daemon=True).start() # ── Schliessen ─────────────────────────────────────────────────────────── tk.Button(outer, text="Schliessen", font=(FONT_FAMILY, 10), bg=_BG, fg=SUBTLE, activebackground="#F0F0F0", relief="solid", bd=1, padx=18, pady=6, cursor="hand2", highlightbackground=BORDER, command=win.destroy).pack(anchor="e", pady=(10, 0)) win.grab_set() def _get_device_id_short() -> str: """Kurzform der Geräte-ID (anonymisiert).""" try: import platform raw = f"{platform.node()}-{platform.machine()}-{os.getlogin()}" return hashlib.sha256(raw.encode()).hexdigest()[:12].upper() except Exception: return "unbekannt"