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

757 lines
70 KiB
Plaintext

diff --git a/AzA march 2026/basis14.py b/AzA march 2026/basis14.py
index 60c684d..1ccfe0d 100644
--- a/AzA march 2026/basis14.py
+++ b/AzA march 2026/basis14.py
@@ -468,165 +468,176 @@ class KGDesktopApp(tk.Tk, TodoMixin, TextWindowsMixin, AzaDiktatMixin, AzaSettin
if self._autotext_data.get("kommentare_auto_open", False):
self.after(1000, self._open_kommentare_fenster)
if not self.api_key_present:
try:
_show_openai_key_setup_dialog()
if has_openai_api_key():
api_key = get_openai_api_key()
self.client = OpenAI(api_key=api_key) if api_key else None
self.api_key_present = True
except Exception:
pass
def _dispatch_start_module(self):
"""Öffnet das per Launcher gewählte Modul."""
mod = self._start_module
if not mod or mod == "kg":
return
try:
dispatch = {
"ki": lambda: None,
"notizen": lambda: self._start_audio_notiz_addon(silent=False),
"translator": self._open_uebersetzer,
"medwork_chat": self._open_docapp,
"praxis_chat": self._open_docapp,
}
handler = dispatch.get(mod)
if handler is None:
messagebox.showinfo(
"Modul nicht verf├╝gbar",
f"Das Modul '{mod}' ist noch nicht vollständig integriert.\n"
"Sie k├Ânnen es manuell ├╝ber die Seitenleiste starten.",
)
return
if mod in ("notizen", "translator", "medwork_chat", "praxis_chat"):
if not self._check_ai_consent():
return
if mod in ("translator", "notizen", "medwork_chat", "praxis_chat"):
self.iconify()
handler()
except Exception as e:
messagebox.showerror("Modul-Start", f"Fehler beim Öffnen von '{mod}':\n{e}")
_LOGIN_INTERVAL_DAYS = 7
def _login_needed(self) -> bool:
"""True wenn der letzte Login länger als _LOGIN_INTERVAL_DAYS her ist."""
last_ts = self._user_profile.get("last_login_ts")
if not isinstance(last_ts, (int, float)):
return True
elapsed = time.time() - float(last_ts)
return elapsed > (self._LOGIN_INTERVAL_DAYS * 86400)
def _record_login(self):
self._user_profile["last_login_ts"] = int(time.time())
save_user_profile(self._user_profile)
def check_license_status(self):
self.license_mode = "demo"
license_mode = "DEMO"
license_reason = "unknown"
valid_until = None
cache = _load_license_cache()
if cache and _cache_is_fresh(cache) and _cache_license_valid(cache):
license_mode = "ACTIVE"
license_reason = "offline_cache"
valid_until = cache.get("valid_until")
self.license_mode = "active"
print(f"[LICENSE] mode={license_mode} reason={license_reason} valid_until={_format_valid_until_ts(valid_until)}")
return
try:
backend_url = self.get_backend_url()
api_token = self.get_backend_token()
print(f"[LICENSE] status_url={backend_url}/license/status")
response = None
status_code = None
last_exc = None
device_id = _get_or_create_device_id()
headers = {"X-API-Token": api_token, "X-Device-Id": device_id}
+ _profile_email = ""
+ try:
+ _prof = load_user_profile()
+ _profile_email = (_prof.get("email") or "").strip()
+ except Exception:
+ pass
+ _lic_params: dict = {}
+ if _profile_email:
+ _lic_params["email"] = _profile_email
+ print(f"[LICENSE] customer_email={_profile_email}")
for attempt in range(1, 7):
try:
response = requests.get(
f"{backend_url}/license/status",
headers=headers,
+ params=_lic_params,
timeout=5,
)
status_code = response.status_code
response.raise_for_status()
print(f"[LICENSE] status_code={status_code} attempt={attempt}/6")
break
except requests.HTTPError as http_exc:
last_exc = http_exc
print(f"[LICENSE] status_code={status_code} attempt={attempt}/6")
break
except requests.RequestException as req_exc:
last_exc = req_exc
print(f"[LICENSE] retry={attempt}/6 reason={req_exc}")
if attempt < 6:
time.sleep(1.0)
if isinstance(last_exc, requests.HTTPError):
if status_code in (401, 403):
license_mode = "DEMO"
license_reason = "unauthorized"
else:
if cache and _cache_license_valid(cache):
license_mode = "ACTIVE"
license_reason = "offline_cache"
valid_until = cache.get("valid_until")
else:
license_mode = "DEMO"
license_reason = "no_backend"
valid_until = cache.get("valid_until") if isinstance(cache, dict) else None
self.license_mode = "active" if license_mode == "ACTIVE" else "demo"
print(f"[LICENSE] mode={license_mode} reason={license_reason} valid_until={_format_valid_until_ts(valid_until)}")
return
if response is None:
raise RuntimeError(last_exc or "license status request failed")
try:
data = response.json()
except Exception:
data = {}
resp_valid = bool(data.get("valid")) if isinstance(data, dict) else False
resp_valid_until = data.get("valid_until") if isinstance(data, dict) else None
payload = {"valid": resp_valid, "valid_until": resp_valid_until, "cached_at": time.time()}
_save_license_cache(payload)
now = int(time.time())
valid_until = resp_valid_until if isinstance(resp_valid_until, (int, float)) else None
if resp_valid and isinstance(resp_valid_until, (int, float)) and int(resp_valid_until) > now:
license_mode = "ACTIVE"
license_reason = "online"
valid_until = int(resp_valid_until)
elif isinstance(resp_valid_until, (int, float)) and int(resp_valid_until) <= now:
license_mode = "DEMO"
license_reason = "expired"
valid_until = int(resp_valid_until)
else:
license_mode = "DEMO"
license_reason = "not_valid"
valid_until = resp_valid_until if isinstance(resp_valid_until, (int, float)) else None
except Exception as e:
print(f"[LICENSE] exception={e}")
if cache and _cache_license_valid(cache):
license_mode = "ACTIVE"
license_reason = "offline_cache"
valid_until = cache.get("valid_until")
else:
license_mode = "DEMO"
license_reason = "no_backend"
valid_until = cache.get("valid_until") if isinstance(cache, dict) else None
self.license_mode = "active" if license_mode == "ACTIVE" else "demo"
print(f"[LICENSE] mode={license_mode} reason={license_reason} valid_until={_format_valid_until_ts(valid_until)}")
def _build_ui(self):
try:
def_font = tkfont.nametofont("TkDefaultFont")
font_size = max(10, def_font.actual()["size"]) # Mindestens Gr├Âe 10 f├╝r bessere Lesbarkeit
self._text_font = (def_font.actual()["family"], font_size)
except Exception:
self._text_font = ("Segoe UI", 10)
@@ -2282,369 +2293,385 @@ class KGDesktopApp(tk.Tk, TodoMixin, TextWindowsMixin, AzaDiktatMixin, AzaSettin
justify="left").pack(anchor="w", pady=(4, 12))
codes_text = tk.Text(form, font=("Consolas", 14), bg="#F0F8FF",
fg="#1a4d6d", relief="flat", bd=1,
height=len(codes), width=20)
codes_text.pack(pady=(0, 12))
for i, code in enumerate(codes, 1):
codes_text.insert("end", f" {i}. {code}\n")
codes_text.configure(state="disabled")
def do_copy():
dlg.clipboard_clear()
dlg.clipboard_append("\n".join(codes))
messagebox.showinfo("Kopiert",
"Backup-Codes in Zwischenablage kopiert.", parent=dlg)
btn_row = tk.Frame(form, bg="#E8F4FA")
btn_row.pack()
tk.Button(btn_row, text="Kopieren", font=("Segoe UI", 10),
bg="#C8DDE6", fg="#1a4d6d", relief="flat", padx=12,
pady=4, cursor="hand2", command=do_copy).pack(side="left", padx=4)
tk.Button(btn_row, text="Ich habe die Codes gesichert",
font=("Segoe UI", 10, "bold"),
bg="#27AE60", fg="white", relief="flat", padx=12,
pady=4, cursor="hand2",
command=dlg.destroy).pack(side="left", padx=4)
dlg.protocol("WM_DELETE_WINDOW", dlg.destroy)
self.wait_window(dlg)
def _show_registration_dialog(self):
"""Erstregistrierung: Profil + Passwort festlegen."""
dlg = tk.Toplevel(self)
dlg.title("Registrierung AzA Profil")
dlg.configure(bg="#E8F4FA")
dlg.resizable(True, True)
dlg.geometry("420x640")
dlg.minsize(380, 520)
add_resize_grip(dlg, 380, 520)
self._register_window(dlg)
dlg.attributes("-topmost", True)
dlg.grab_set()
center_window(dlg, 420, 640)
tk.Label(dlg, text="­ƒæñ Willkommen bei AzA", font=("Segoe UI", 16, "bold"),
bg="#B9ECFA", fg="#1a4d6d").pack(fill="x", ipady=12)
tk.Label(dlg, text="Bitte erfassen Sie Ihr Profil und legen Sie ein Passwort fest:",
font=("Segoe UI", 9), bg="#E8F4FA", fg="#4a8aaa").pack(fill="x", padx=16, pady=(8, 4))
form = tk.Frame(dlg, bg="#E8F4FA", padx=20, pady=8)
form.pack(fill="x")
tk.Label(form, text="Name / Titel:", font=("Segoe UI", 10, "bold"),
bg="#E8F4FA", fg="#1a4d6d").pack(anchor="w", pady=(4, 0))
name_entry = tk.Entry(form, font=("Segoe UI", 11), bg="white", fg="#1a4d6d",
relief="flat", bd=0)
name_entry.pack(fill="x", ipady=4, pady=(0, 6))
name_entry.insert(0, self._user_profile.get("name", ""))
tk.Label(form, text="Fachrichtung:", font=("Segoe UI", 10, "bold"),
bg="#E8F4FA", fg="#1a4d6d").pack(anchor="w", pady=(4, 0))
spec_entry = tk.Entry(form, font=("Segoe UI", 11), bg="white", fg="#1a4d6d",
relief="flat", bd=0)
spec_entry.pack(fill="x", ipady=4, pady=(0, 6))
spec_entry.insert(0, self._user_profile.get("specialty", ""))
tk.Label(form, text="Praxis / Klinik:", font=("Segoe UI", 10, "bold"),
bg="#E8F4FA", fg="#1a4d6d").pack(anchor="w", pady=(4, 0))
clinic_entry = tk.Entry(form, font=("Segoe UI", 11), bg="white", fg="#1a4d6d",
relief="flat", bd=0)
clinic_entry.pack(fill="x", ipady=4, pady=(0, 6))
clinic_entry.insert(0, self._user_profile.get("clinic", ""))
tk.Label(form, text="Code (ZSR/GLN, optional):", font=("Segoe UI", 10, "bold"),
bg="#E8F4FA", fg="#1a4d6d").pack(anchor="w", pady=(4, 0))
code_entry = tk.Entry(form, font=("Segoe UI", 11), bg="white", fg="#1a4d6d",
relief="flat", bd=0)
code_entry.pack(fill="x", ipady=4, pady=(0, 6))
code_entry.insert(0, self._user_profile.get("code", ""))
+ tk.Label(form, text="E-Mail (Kauf-Adresse):", font=("Segoe UI", 10, "bold"),
+ bg="#E8F4FA", fg="#1a4d6d").pack(anchor="w", pady=(4, 0))
+ email_entry = tk.Entry(form, font=("Segoe UI", 11), bg="white", fg="#1a4d6d",
+ relief="flat", bd=0)
+ email_entry.pack(fill="x", ipady=4, pady=(0, 6))
+ email_entry.insert(0, self._user_profile.get("email", ""))
+
sep = tk.Frame(form, bg="#B9ECFA", height=1)
sep.pack(fill="x", pady=(6, 6))
tk.Label(form, text="­ƒöæ Passwort festlegen:", font=("Segoe UI", 10, "bold"),
bg="#E8F4FA", fg="#1a4d6d").pack(anchor="w", pady=(4, 0))
pw_entry = tk.Entry(form, font=("Segoe UI", 11), bg="white", fg="#1a4d6d",
relief="flat", bd=0, show="")
pw_entry.pack(fill="x", ipady=4, pady=(0, 6))
tk.Label(form, text="Passwort bestätigen:", font=("Segoe UI", 10, "bold"),
bg="#E8F4FA", fg="#1a4d6d").pack(anchor="w", pady=(4, 0))
pw_confirm_entry = tk.Entry(form, font=("Segoe UI", 11), bg="white", fg="#1a4d6d",
relief="flat", bd=0, show="")
pw_confirm_entry.pack(fill="x", ipady=4, pady=(0, 6))
def do_save():
name = name_entry.get().strip()
if not name:
messagebox.showwarning("Pflichtfeld", "Bitte geben Sie Ihren Namen ein.", parent=dlg)
return
pw = pw_entry.get()
pw_confirm = pw_confirm_entry.get()
if not pw:
messagebox.showwarning("Pflichtfeld", "Bitte legen Sie ein Passwort fest.", parent=dlg)
return
if len(pw) < 4:
messagebox.showwarning("Passwort zu kurz", "Das Passwort muss mindestens 4 Zeichen lang sein.", parent=dlg)
return
if pw != pw_confirm:
messagebox.showerror("Fehler", "Die Passw├Ârter stimmen nicht ├╝berein.", parent=dlg)
pw_confirm_entry.delete(0, "end")
pw_confirm_entry.focus_set()
return
self._user_profile = {
"name": name,
"specialty": spec_entry.get().strip(),
"clinic": clinic_entry.get().strip(),
"code": code_entry.get().strip(),
+ "email": email_entry.get().strip(),
"password_hash": self._hash_password(pw),
}
+ save_user_profile(self._user_profile)
self._record_login()
dlg.destroy()
tk.Button(dlg, text="­ƒÆ¥ Registrieren & Starten", font=("Segoe UI", 11, "bold"),
bg="#5B8DB3", fg="white", activebackground="#4A7A9E",
relief="flat", bd=0, padx=20, pady=6, cursor="hand2",
command=do_save).pack(pady=12)
dlg.protocol("WM_DELETE_WINDOW", lambda: sys.exit(0))
name_entry.focus_set()
self.wait_window(dlg)
def _show_activation_dialog(self):
"""Dialog zum Eingeben/Aktualisieren des Aktivierungsschl├╝ssels."""
from aza_activation import load_activation_key
current_key = load_activation_key() or ""
allowed, status_msg = check_app_access()
dlg = tk.Toplevel(self)
dlg.title("AZA Aktivierung")
dlg.configure(bg="#E8F4FA")
dlg.resizable(False, False)
dlg.geometry("460x300")
dlg.attributes("-topmost", True)
self._register_window(dlg)
center_window(dlg, 460, 300)
tk.Label(dlg, text="AZA Aktivierung", font=("Segoe UI", 14, "bold"),
bg="#B9ECFA", fg="#1a4d6d").pack(fill="x", ipady=10)
tk.Label(dlg, text=f"Status: {status_msg}", font=("Segoe UI", 9),
bg="#E8F4FA", fg="#2a7a3a" if allowed else "#c04040",
wraplength=420, justify="left").pack(fill="x", padx=16, pady=(8, 4))
form = tk.Frame(dlg, bg="#E8F4FA", padx=16, pady=4)
form.pack(fill="x")
tk.Label(form, text="Aktivierungsschl├╝ssel:", font=("Segoe UI", 10, "bold"),
bg="#E8F4FA", fg="#1a4d6d").pack(anchor="w")
key_entry = tk.Entry(form, font=("Consolas", 12), bg="white", fg="#1a4d6d",
relief="flat", bd=0)
key_entry.pack(fill="x", ipady=4, pady=(0, 4))
if current_key:
key_entry.insert(0, current_key)
result_label = tk.Label(form, text="", font=("Segoe UI", 9),
bg="#E8F4FA", fg="#888888")
result_label.pack(fill="x")
def do_save():
k = key_entry.get().strip()
if not k:
result_label.configure(text="Bitte Schl├╝ssel eingeben.", fg="#E05050")
return
valid, expiry, reason = validate_key(k)
if valid:
save_activation_key(k)
result_label.configure(text=f"Gespeichert: {reason}", fg="#2a7a3a")
self.set_status(f"Aktivierung: {reason}")
else:
result_label.configure(text=reason, fg="#E05050")
btn_frame = tk.Frame(dlg, bg="#E8F4FA")
btn_frame.pack(pady=8)
tk.Button(btn_frame, text="Speichern", font=("Segoe UI", 10, "bold"),
bg="#5B8DB3", fg="white", activebackground="#4A7A9E",
relief="flat", bd=0, padx=16, pady=4, cursor="hand2",
command=do_save).pack(side="left", padx=4)
tk.Button(btn_frame, text="Schliessen", font=("Segoe UI", 10),
bg="#cccccc", fg="#333333", relief="flat", bd=0,
padx=16, pady=4, cursor="hand2",
command=dlg.destroy).pack(side="left", padx=4)
def _show_profile_editor(self):
"""Öffnet ein Fenster zum Bearbeiten des Benutzerprofils (inkl. Passwort ändern)."""
dlg = tk.Toplevel(self)
dlg.title("Profil bearbeiten")
dlg.configure(bg="#E8F4FA")
dlg.resizable(False, False)
dlg.geometry("380x440")
dlg.attributes("-topmost", True)
self._register_window(dlg)
center_window(dlg, 380, 440)
tk.Label(dlg, text="­ƒæñ Profil bearbeiten", font=("Segoe UI", 13, "bold"),
bg="#B9ECFA", fg="#1a4d6d").pack(fill="x", ipady=8)
form = tk.Frame(dlg, bg="#E8F4FA", padx=20, pady=8)
form.pack(fill="x")
tk.Label(form, text="Name / Titel:", font=("Segoe UI", 10, "bold"),
bg="#E8F4FA", fg="#1a4d6d").pack(anchor="w", pady=(8, 0))
name_e = tk.Entry(form, font=("Segoe UI", 11), bg="white", fg="#1a4d6d", relief="flat")
name_e.pack(fill="x", ipady=4, pady=(0, 6))
name_e.insert(0, self._user_profile.get("name", ""))
tk.Label(form, text="Fachrichtung:", font=("Segoe UI", 10, "bold"),
bg="#E8F4FA", fg="#1a4d6d").pack(anchor="w", pady=(4, 0))
spec_e = tk.Entry(form, font=("Segoe UI", 11), bg="white", fg="#1a4d6d", relief="flat")
spec_e.pack(fill="x", ipady=4, pady=(0, 6))
spec_e.insert(0, self._user_profile.get("specialty", ""))
tk.Label(form, text="Praxis / Klinik:", font=("Segoe UI", 10, "bold"),
bg="#E8F4FA", fg="#1a4d6d").pack(anchor="w", pady=(4, 0))
clinic_e = tk.Entry(form, font=("Segoe UI", 11), bg="white", fg="#1a4d6d", relief="flat")
clinic_e.pack(fill="x", ipady=4, pady=(0, 6))
clinic_e.insert(0, self._user_profile.get("clinic", ""))
tk.Label(form, text="Code (ZSR/GLN, optional):", font=("Segoe UI", 10, "bold"),
bg="#E8F4FA", fg="#1a4d6d").pack(anchor="w", pady=(4, 0))
code_e = tk.Entry(form, font=("Segoe UI", 11), bg="white", fg="#1a4d6d", relief="flat")
code_e.pack(fill="x", ipady=4, pady=(0, 6))
code_e.insert(0, self._user_profile.get("code", ""))
+ tk.Label(form, text="E-Mail (Kauf-Adresse):", font=("Segoe UI", 10, "bold"),
+ bg="#E8F4FA", fg="#1a4d6d").pack(anchor="w", pady=(4, 0))
+ email_e = tk.Entry(form, font=("Segoe UI", 11), bg="white", fg="#1a4d6d", relief="flat")
+ email_e.pack(fill="x", ipady=4, pady=(0, 6))
+ email_e.insert(0, self._user_profile.get("email", ""))
+
sep = tk.Frame(form, bg="#B9ECFA", height=1)
sep.pack(fill="x", pady=(8, 6))
tk.Label(form, text="­ƒöæ Passwort ├ñndern (optional):", font=("Segoe UI", 10, "bold"),
bg="#E8F4FA", fg="#1a4d6d").pack(anchor="w", pady=(2, 0))
tk.Label(form, text="Leer lassen, um das Passwort beizubehalten.",
font=("Segoe UI", 8), bg="#E8F4FA", fg="#888").pack(anchor="w")
pw_old_e = tk.Entry(form, font=("Segoe UI", 11), bg="white", fg="#1a4d6d",
relief="flat", bd=0, show="")
pw_new_e = tk.Entry(form, font=("Segoe UI", 11), bg="white", fg="#1a4d6d",
relief="flat", bd=0, show="")
pw_confirm_e = tk.Entry(form, font=("Segoe UI", 11), bg="white", fg="#1a4d6d",
relief="flat", bd=0, show="")
if self._user_profile.get("password_hash"):
tk.Label(form, text="Altes Passwort:", font=("Segoe UI", 9),
bg="#E8F4FA", fg="#1a4d6d").pack(anchor="w", pady=(4, 0))
pw_old_e.pack(fill="x", ipady=3, pady=(0, 4))
tk.Label(form, text="Neues Passwort:", font=("Segoe UI", 9),
bg="#E8F4FA", fg="#1a4d6d").pack(anchor="w", pady=(2, 0))
pw_new_e.pack(fill="x", ipady=3, pady=(0, 4))
tk.Label(form, text="Neues Passwort bestätigen:", font=("Segoe UI", 9),
bg="#E8F4FA", fg="#1a4d6d").pack(anchor="w", pady=(2, 0))
pw_confirm_e.pack(fill="x", ipady=3, pady=(0, 4))
def do_save():
name = name_e.get().strip()
if not name:
messagebox.showwarning("Pflichtfeld", "Name darf nicht leer sein.", parent=dlg)
return
new_pw = pw_new_e.get()
new_pw_confirm = pw_confirm_e.get()
old_hash = self._user_profile.get("password_hash", "")
if new_pw:
if old_hash and not self._verify_password(pw_old_e.get(), old_hash):
messagebox.showerror("Fehler", "Das alte Passwort ist nicht korrekt.", parent=dlg)
return
if len(new_pw) < 4:
messagebox.showwarning("Zu kurz", "Das neue Passwort muss mindestens 4 Zeichen lang sein.", parent=dlg)
return
if new_pw != new_pw_confirm:
messagebox.showerror("Fehler", "Die neuen Passw├Ârter stimmen nicht ├╝berein.", parent=dlg)
return
pw_hash = self._hash_password(new_pw)
else:
pw_hash = old_hash
updated = {
"name": name,
"specialty": spec_e.get().strip(),
"clinic": clinic_e.get().strip(),
"code": code_e.get().strip(),
+ "email": email_e.get().strip(),
"password_hash": pw_hash,
}
for k in ("totp_secret_enc", "totp_active", "backup_codes"):
if k in self._user_profile:
updated[k] = self._user_profile[k]
self._user_profile = updated
save_user_profile(self._user_profile)
self.set_status(f"Profil gespeichert: {name}")
dlg.destroy()
btn_row = tk.Frame(dlg, bg="#E8F4FA")
btn_row.pack(pady=10)
tk.Button(btn_row, text="­ƒÆ¥ Speichern", font=("Segoe UI", 10, "bold"),
bg="#5B8DB3", fg="white", activebackground="#4A7A9E",
relief="flat", padx=16, pady=4, cursor="hand2",
command=do_save).pack(side="left", padx=6)
tk.Button(btn_row, text="Abbrechen", font=("Segoe UI", 10),
bg="#C8DDE6", fg="#1a4d6d", activebackground="#B8CDD6",
relief="flat", padx=12, pady=4, cursor="hand2",
command=dlg.destroy).pack(side="left", padx=6)
if is_2fa_enabled():
sep2 = tk.Frame(dlg, bg="#B9ECFA", height=1)
sep2.pack(fill="x", padx=20, pady=(4, 4))
tfa_frame = tk.Frame(dlg, bg="#E8F4FA", padx=20)
tfa_frame.pack(fill="x")
is_active = self._user_profile.get("totp_active", False)
status_text = "2FA aktiv" if is_active else "2FA nicht aktiv"
status_color = "#27AE60" if is_active else "#E74C3C"
tk.Label(tfa_frame, text=f"­ƒöÉ {status_text}",
font=("Segoe UI", 10, "bold"), bg="#E8F4FA",
fg=status_color).pack(side="left")
def do_toggle_2fa():
if is_active:
if messagebox.askyesno("2FA deaktivieren",
"Zwei-Faktor-Authentifizierung wirklich deaktivieren?\n\n"
"Dies verringert die Sicherheit Ihres Kontos.",
parent=dlg):
self._user_profile.pop("totp_secret_enc", None)
self._user_profile.pop("backup_codes", None)
self._user_profile["totp_active"] = False
save_user_profile(self._user_profile)
dlg.destroy()
self._show_profile_editor()
else:
pw_check = simpledialog.askstring("Passwort",
"Bitte Passwort eingeben:", show="*", parent=dlg)
if pw_check and self._verify_password(pw_check,
self._user_profile.get("password_hash", "")):
dlg.destroy()
self._show_2fa_setup(pw_check)
elif pw_check:
messagebox.showerror("Fehler",
"Falsches Passwort.", parent=dlg)
btn_text = "Deaktivieren" if is_active else "2FA einrichten"
btn_color = "#E74C3C" if is_active else "#27AE60"
tk.Button(tfa_frame, text=btn_text, font=("Segoe UI", 9),
bg=btn_color, fg="white", relief="flat",
padx=8, pady=2, cursor="hand2",
command=do_toggle_2fa).pack(side="right")
def _reset_window_positions(self):
"""Setzt alle gespeicherten Fensterpositionen und KG-Einstellungen zur├╝ck."""
answer = messagebox.askyesno(
"Fensterpositionen zur├╝cksetzen",
"Alle Fensterpositionen und KG-Einstellungen zur├╝cksetzen?\n\n"
"Beim nächsten Start werden alle Fenster\n"
"in der Bildschirmmitte ge├Âffnet.\n"
"Die KG-Detailstufe (K├╝rzer/Ausf├╝hrlicher)\n"
"wird auf Standard zur├╝ckgesetzt.",
parent=self,
)
if not answer:
return
deleted = reset_all_window_positions()
self._update_kg_detail_display()
self._soap_section_levels = {k: 0 for k in _SOAP_SECTIONS}
self._update_soap_section_display()
@@ -8525,165 +8552,188 @@ def _show_openai_key_setup_dialog():
result["action"] = "skip"
dlg.destroy()
key_entry.bind("<Return>", lambda e: do_activate())
btn_area = tk.Frame(content, bg=_BG)
btn_area.pack(fill="x", pady=(0, 10))
btn_primary = tk.Button(
btn_area, text="\u2713 Schl├╝ssel aktivieren",
font=("Segoe UI", 11, "bold"),
bg=_ACCENT, fg="white", activebackground=_ACCENT_HOVER,
activeforeground="white",
relief="flat", bd=0, padx=24, pady=10, cursor="hand2",
command=do_activate,
)
btn_primary.pack(side="left")
def _on_enter_p(e):
btn_primary.configure(bg=_ACCENT_HOVER)
def _on_leave_p(e):
btn_primary.configure(bg=_ACCENT)
btn_primary.bind("<Enter>", _on_enter_p)
btn_primary.bind("<Leave>", _on_leave_p)
btn_skip = tk.Button(
btn_area, text="Später",
font=("Segoe UI", 10),
bg=_BG, fg=_SUBTLE, activebackground="#F0F0F0",
relief="solid", bd=1, padx=18, pady=8, cursor="hand2",
highlightbackground=_BORDER,
command=do_skip,
)
btn_skip.pack(side="left", padx=(12, 0))
links = tk.Frame(content, bg=_BG)
links.pack(fill="x")
if has_helper:
lnk_setup = tk.Label(links, text="\u2192 Einrichtungsassistent starten",
font=("Segoe UI", 9, "underline"), fg=_SUBTLE, bg=_BG,
cursor="hand2")
lnk_setup.pack(anchor="w", pady=(0, 2))
lnk_setup.bind("<Button-1>", lambda e: do_helper())
lnk_config = tk.Label(links, text="\u2192 Konfiguration manuell ├Âffnen",
font=("Segoe UI", 9, "underline"), fg=_SUBTLE, bg=_BG,
cursor="hand2")
lnk_config.pack(anchor="w")
lnk_config.bind("<Button-1>", lambda e: do_config())
dlg.protocol("WM_DELETE_WINDOW", do_skip)
dlg.grab_set()
dlg.wait_window(dlg)
if result["action"] == "stored":
return
elif result["action"] == "setup":
success = _run_setup_helper()
if success and has_openai_api_key():
return
if success:
messagebox.showinfo(
"Einrichtung",
"Die Einrichtung wurde abgeschlossen.\n\n"
"Bitte starten Sie AZA neu, damit die\n"
"KI-Funktionen aktiv werden.",
)
else:
messagebox.showinfo(
"Einrichtung",
"Die automatische Einrichtung ist auf diesem\n"
"System nicht verf├╝gbar.\n\n"
"Bitte nutzen Sie den Startmen├╝-Eintrag\n"
"\"AZA \u2013 OpenAI Schl├╝ssel einrichten\".",
)
elif result["action"] == "config":
open_runtime_config_in_editor()
+def _has_remote_backend() -> bool:
+ """True when a non-localhost backend URL is configured."""
+ url = os.getenv("MEDWORK_BACKEND_URL", "").strip()
+ if not url:
+ for base in (os.path.dirname(os.path.abspath(__file__)), os.getcwd()):
+ p = os.path.join(base, "backend_url.txt")
+ if os.path.isfile(p):
+ try:
+ with open(p, "r", encoding="utf-8-sig") as f:
+ url = f.read().replace("\ufeff", "").strip()
+ except Exception:
+ continue
+ if url:
+ break
+ if not url:
+ return False
+ return not any(h in url for h in ("127.0.0.1", "localhost", "0.0.0.0"))
+
+
def _show_activation_gate() -> bool:
"""Pr├╝ft Zugang und zeigt bei Bedarf Schl├╝ssel-Eingabe.
Returns True wenn die App starten darf, False wenn beendet werden soll.
"""
+ if _has_remote_backend():
+ print("[ACTIVATION] Remote-Backend konfiguriert ÔÇô lokales Aktivierungs-Gate uebersprungen. Backend-Lizenzstatus ist fuehrend.")
+ return True
+
allowed, msg = check_app_access()
if allowed:
print(f"[ACTIVATION] {msg}")
stored = load_activation_key()
is_trial = not stored or not validate_key(stored)[0]
if is_trial:
trial_msg = msg + "\n\nSie k\u00f6nnen jetzt einen Aktivierungsschl\u00fcssel eingeben\noder die Testversion weiter nutzen."
root = tk.Tk()
root.withdraw()
result = _activation_key_dialog(root, trial_msg, can_continue=True)
root.destroy()
if result:
valid, expiry, reason = validate_key(result)
if valid:
save_activation_key(result)
print(f"[ACTIVATION] Schl├╝ssel akzeptiert: {reason}")
return True
while True:
root = tk.Tk()
root.withdraw()
result = _activation_key_dialog(root, msg)
root.destroy()
if result is None:
return False
valid, expiry, reason = validate_key(result)
if valid:
save_activation_key(result)
print(f"[ACTIVATION] Schl├╝ssel akzeptiert: {reason}")
return True
msg = f"Schl├╝ssel ung├╝ltig: {reason}\nBitte erneut versuchen."
def _activation_key_dialog(parent, message: str, can_continue: bool = False) -> Optional[str]:
"""Premium-Dialog zur Eingabe eines Aktivierungsschl├╝ssels.
can_continue=True: Trial-Modus ÔÇô "Weiter ohne Schl├╝ssel" statt "Beenden".
"""
_BG = "#FFFFFF"
_ACCENT = "#0078D7"
_ACCENT_HOVER = "#005FA3"
_TEXT = "#2D3436"
_SUBTLE = "#636E72"
_BORDER = "#E2E8F0"
result = {"key": None}
dlg = tk.Toplevel(parent)
dlg.title("AZA \u2013 Aktivierung")
dlg.configure(bg=_BG)
dlg.resizable(True, True)
w, h = 540, 520
dlg.minsize(460, 420)
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=40, pady=28)
tk.Label(content, text="\U0001F511",
font=("Segoe UI", 28), fg=_ACCENT, bg=_BG).pack(anchor="w")
title_text = "AZA Aktivierung" if not can_continue else "AZA \u2013 Testversion aktiv"
tk.Label(content, text=title_text,
font=("Segoe UI", 18, "bold"), fg=_TEXT, bg=_BG
).pack(anchor="w", pady=(6, 4))
tk.Label(content, text=message, font=("Segoe UI", 10),
fg=_SUBTLE, bg=_BG, wraplength=420,
justify="left").pack(anchor="w", pady=(0, 16))