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("", 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("", _on_enter_p) btn_primary.bind("", _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("", 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("", 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))