Files
aza/AzA march 2026 - Kopie (17)/aza_admin.py

310 lines
11 KiB
Python
Raw Normal View History

2026-04-19 20:41:37 +02:00
# -*- 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("<Return>", 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("<Enter>", lambda e: btn.configure(bg=ACCENT_HOVER))
btn.bind("<Leave>", 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("<Enter>", lambda e: btn_reset.configure(bg=ACCENT_HOVER))
btn_reset.bind("<Leave>", 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"