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

310 lines
11 KiB
Python
Raw 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 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"