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"
|