update
This commit is contained in:
309
AzA march 2026/aza_admin.py
Normal file
309
AzA march 2026/aza_admin.py
Normal file
@@ -0,0 +1,309 @@
|
||||
# -*- 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"
|
||||
Reference in New Issue
Block a user