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