V5 komplett: Auth, Admin, Federation, Channels, Devices, Cockpit, Profil, Autotext-Fix, Uebersetzer-Fix
Made-with: Cursor
This commit is contained in:
@@ -231,7 +231,8 @@ function boot(){
|
||||
f.onload=function(){
|
||||
if(!done&&f.src!=='about:blank'){done=true;view('frame')}
|
||||
};
|
||||
f.src=URL;
|
||||
var cacheBust = '?_t=' + Date.now();
|
||||
f.src=URL + (URL.indexOf('?')>=0 ? '&_t=' : '?_t=') + Date.now();
|
||||
setTimeout(function(){
|
||||
if(!done){diagnose()}
|
||||
},12000);
|
||||
@@ -258,7 +259,7 @@ async function doReload(){
|
||||
if(!done&&f.src!=='about:blank'){done=true;view('frame')}
|
||||
};
|
||||
f.src='about:blank';
|
||||
setTimeout(function(){f.src=URL},100);
|
||||
setTimeout(function(){f.src=URL + (URL.indexOf('?')>=0 ? '&_t=' : '?_t=') + Date.now()},100);
|
||||
setTimeout(function(){if(!done)diagnose()},12000);
|
||||
}
|
||||
|
||||
|
||||
@@ -69,10 +69,7 @@ def _draw_module_icon(c: tk.Canvas, key: str):
|
||||
fill=fg, outline="")
|
||||
|
||||
elif key == "kg":
|
||||
c.create_rectangle(10, 12, s - 10, s - 7, outline=fg, width=2)
|
||||
c.create_rectangle(14, 8, s - 14, 15, outline=fg, width=1.5)
|
||||
c.create_line(m, 18, m, s - 10, fill=fg, width=2.5)
|
||||
c.create_line(14, m + 2, s - 14, m + 2, fill=fg, width=2.5)
|
||||
c.create_text(m, m, text="AzA", font=("Segoe UI", 11, "bold"), fill=fg)
|
||||
|
||||
elif key == "notizen":
|
||||
c.create_oval(m - 5, 7, m + 5, 19, outline=fg, width=2)
|
||||
@@ -250,7 +247,7 @@ class AzaLauncher(tk.Tk):
|
||||
|
||||
title_block = tk.Frame(title_row, bg=BG)
|
||||
title_block.pack(side="left", anchor="w")
|
||||
aza_lbl = tk.Label(title_block, text="AZA",
|
||||
aza_lbl = tk.Label(title_block, text="AzA",
|
||||
font=(FONT_FAMILY, 24, "bold"), fg=ACCENT, bg=BG,
|
||||
cursor="hand2")
|
||||
aza_lbl.pack(anchor="w")
|
||||
@@ -531,10 +528,15 @@ class AzaLauncher(tk.Tk):
|
||||
top_row = tk.Frame(inner, bg=_cbg, cursor=_cursor)
|
||||
top_row.pack(fill="x", pady=(0, 10))
|
||||
|
||||
icon_cv = tk.Canvas(top_row, width=_ICON_SZ, height=_ICON_SZ,
|
||||
bg=icon_color, highlightthickness=0, cursor=_cursor)
|
||||
icon_cv.pack(side="left")
|
||||
_draw_module_icon(icon_cv, mod_key)
|
||||
if mod_key == "kg" and self._logo_img:
|
||||
icon_lbl = tk.Label(top_row, image=self._logo_img, bg=_cbg, cursor=_cursor)
|
||||
icon_lbl.image = self._logo_img
|
||||
icon_lbl.pack(side="left")
|
||||
else:
|
||||
icon_cv = tk.Canvas(top_row, width=_ICON_SZ, height=_ICON_SZ,
|
||||
bg=icon_color, highlightthickness=0, cursor=_cursor)
|
||||
icon_cv.pack(side="left")
|
||||
_draw_module_icon(icon_cv, mod_key)
|
||||
|
||||
lbl_title = tk.Label(inner, text=label, font=(FONT_FAMILY, 12, "bold"),
|
||||
fg=_text_fg, bg=_cbg, anchor="w", cursor=_cursor)
|
||||
|
||||
@@ -485,7 +485,7 @@ def save_paned_positions(positions):
|
||||
pass
|
||||
|
||||
|
||||
def load_text_font_size(key: str, default: int = 10) -> int:
|
||||
def load_text_font_size(key: str, default: int = 9) -> int:
|
||||
"""Lädt gespeicherte Schriftgröße für einen bestimmten Text-Widget."""
|
||||
try:
|
||||
path = _font_sizes_config_path()
|
||||
|
||||
@@ -503,7 +503,7 @@ class KGDesktopApp(tk.Tk, TodoMixin, TextWindowsMixin, AzaDiktatMixin, AzaSettin
|
||||
self._autotext_data.setdefault("eventsSort", "soonest")
|
||||
self._autotext_data.setdefault("eventsMonthsAhead", 13)
|
||||
self._autotext_data.setdefault("selectedLanguage", "system")
|
||||
self._autotext_data.setdefault("user_specialty_default", "dermatology")
|
||||
self._autotext_data.setdefault("user_specialty_default", "")
|
||||
self._autotext_data.setdefault("user_specialties_selected", [])
|
||||
self._autotext_data.setdefault("ui_font_delta", -1)
|
||||
self._autotext_data.setdefault("notizen_open_on_start", True)
|
||||
@@ -2881,61 +2881,104 @@ class KGDesktopApp(tk.Tk, TodoMixin, TextWindowsMixin, AzaDiktatMixin, AzaSettin
|
||||
dlg.title("Profil bearbeiten")
|
||||
dlg.configure(bg="#E8F4FA")
|
||||
dlg.resizable(True, True)
|
||||
dlg.geometry("400x560")
|
||||
dlg.minsize(380, 480)
|
||||
dlg.geometry("520x820")
|
||||
dlg.minsize(460, 680)
|
||||
dlg.attributes("-topmost", True)
|
||||
self._register_window(dlg)
|
||||
center_window(dlg, 400, 560)
|
||||
center_window(dlg, 520, 820)
|
||||
|
||||
tk.Label(dlg, text="👤 Profil bearbeiten", font=("Segoe UI", 13, "bold"),
|
||||
tk.Label(dlg, text="\U0001f464 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)
|
||||
scroll_canvas = tk.Canvas(dlg, bg="#E8F4FA", highlightthickness=0)
|
||||
scroll_vsb = ttk.Scrollbar(dlg, orient="vertical", command=scroll_canvas.yview)
|
||||
scroll_canvas.configure(yscrollcommand=scroll_vsb.set)
|
||||
scroll_vsb.pack(side="right", fill="y")
|
||||
scroll_canvas.pack(side="left", fill="both", expand=True)
|
||||
scroll_inner = tk.Frame(scroll_canvas, bg="#E8F4FA")
|
||||
scroll_win = scroll_canvas.create_window((0, 0), window=scroll_inner, anchor="nw")
|
||||
scroll_inner.bind("<Configure>", lambda e: scroll_canvas.configure(scrollregion=scroll_canvas.bbox("all")))
|
||||
scroll_canvas.bind("<Configure>", lambda e: scroll_canvas.itemconfigure(scroll_win, width=e.width))
|
||||
scroll_canvas.bind_all("<MouseWheel>", lambda e: scroll_canvas.yview_scroll(-1 * (e.delta // 120), "units"))
|
||||
dlg.bind("<Destroy>", lambda e: scroll_canvas.unbind_all("<MouseWheel>") if e.widget is dlg else None)
|
||||
|
||||
form = tk.Frame(scroll_inner, 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", ""))
|
||||
_FLD_FONT = ("Segoe UI", 9)
|
||||
_LBL_FONT = ("Segoe UI", 9, "bold")
|
||||
_PH_COLOR = "#aaa"
|
||||
_FG_COLOR = "#1a4d6d"
|
||||
|
||||
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", ""))
|
||||
def _make_placeholder_entry(parent, placeholder, initial=""):
|
||||
e = tk.Entry(parent, font=_FLD_FONT, bg="white", fg=_FG_COLOR, relief="flat")
|
||||
e.pack(fill="x", ipady=4, pady=(0, 6))
|
||||
|
||||
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", ""))
|
||||
def _set_ph():
|
||||
if not e.get().strip():
|
||||
e.delete(0, "end")
|
||||
e.insert(0, placeholder)
|
||||
e.configure(fg=_PH_COLOR)
|
||||
|
||||
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", ""))
|
||||
def _on_focus_in(ev):
|
||||
if e.get() == placeholder and e.cget("fg") == _PH_COLOR:
|
||||
e.delete(0, "end")
|
||||
e.configure(fg=_FG_COLOR)
|
||||
|
||||
tk.Label(form, text="E-Mail (optional):", font=("Segoe UI", 10),
|
||||
bg="#E8F4FA", fg="#888").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", ""))
|
||||
def _on_focus_out(ev):
|
||||
_set_ph()
|
||||
|
||||
def _get_val():
|
||||
v = e.get().strip()
|
||||
return "" if v == placeholder else v
|
||||
|
||||
e.bind("<FocusIn>", _on_focus_in)
|
||||
e.bind("<FocusOut>", _on_focus_out)
|
||||
e._get_real_value = _get_val
|
||||
|
||||
if initial and initial != placeholder:
|
||||
e.insert(0, initial)
|
||||
e.configure(fg=_FG_COLOR)
|
||||
else:
|
||||
_set_ph()
|
||||
return e
|
||||
|
||||
tk.Label(form, text="Name / Titel:", font=_LBL_FONT,
|
||||
bg="#E8F4FA", fg=_FG_COLOR).pack(anchor="w", pady=(8, 0))
|
||||
_name_initial = self._user_profile.get("name", "")
|
||||
if _name_initial == "Benutzer":
|
||||
_name_initial = ""
|
||||
name_e = _make_placeholder_entry(form, "Ihr Name / Titel eingeben...", _name_initial)
|
||||
|
||||
tk.Label(form, text="Fachrichtung:", font=_LBL_FONT,
|
||||
bg="#E8F4FA", fg=_FG_COLOR).pack(anchor="w", pady=(4, 0))
|
||||
spec_e = _make_placeholder_entry(form, "z.B. Dermatologie", self._user_profile.get("specialty", ""))
|
||||
|
||||
tk.Label(form, text="Praxis / Klinik:", font=_LBL_FONT,
|
||||
bg="#E8F4FA", fg=_FG_COLOR).pack(anchor="w", pady=(4, 0))
|
||||
clinic_e = _make_placeholder_entry(form, "z.B. Hautarztpraxis Winterthur", self._user_profile.get("clinic", ""))
|
||||
|
||||
tk.Label(form, text="Code (ZSR/GLN, optional):", font=_LBL_FONT,
|
||||
bg="#E8F4FA", fg=_FG_COLOR).pack(anchor="w", pady=(4, 0))
|
||||
code_e = _make_placeholder_entry(form, "ZSR / GLN", self._user_profile.get("code", ""))
|
||||
|
||||
tk.Label(form, text="E-Mail:", font=_LBL_FONT,
|
||||
bg="#E8F4FA", fg=_FG_COLOR).pack(anchor="w", pady=(4, 0))
|
||||
email_e = _make_placeholder_entry(form, "praxis@beispiel.ch", 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="\U0001f511 Passwort \u00e4ndern (optional):", font=_LBL_FONT,
|
||||
bg="#E8F4FA", fg=_FG_COLOR).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",
|
||||
pw_old_e = tk.Entry(form, font=_FLD_FONT, bg="white", fg=_FG_COLOR,
|
||||
relief="flat", bd=0, show="")
|
||||
pw_new_e = tk.Entry(form, font=("Segoe UI", 11), bg="white", fg="#1a4d6d",
|
||||
pw_new_e = tk.Entry(form, font=_FLD_FONT, bg="white", fg=_FG_COLOR,
|
||||
relief="flat", bd=0, show="")
|
||||
pw_confirm_e = tk.Entry(form, font=("Segoe UI", 11), bg="white", fg="#1a4d6d",
|
||||
pw_confirm_e = tk.Entry(form, font=_FLD_FONT, bg="white", fg=_FG_COLOR,
|
||||
relief="flat", bd=0, show="")
|
||||
|
||||
if self._user_profile.get("password_hash"):
|
||||
@@ -2951,7 +2994,7 @@ class KGDesktopApp(tk.Tk, TodoMixin, TextWindowsMixin, AzaDiktatMixin, AzaSettin
|
||||
pw_confirm_e.pack(fill="x", ipady=3, pady=(0, 4))
|
||||
|
||||
def do_save():
|
||||
name = name_e.get().strip()
|
||||
name = name_e._get_real_value() if hasattr(name_e, '_get_real_value') else name_e.get().strip()
|
||||
if not name:
|
||||
messagebox.showwarning("Pflichtfeld", "Name darf nicht leer sein.", parent=dlg)
|
||||
return
|
||||
@@ -2967,18 +3010,19 @@ class KGDesktopApp(tk.Tk, TodoMixin, TextWindowsMixin, AzaDiktatMixin, AzaSettin
|
||||
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)
|
||||
messagebox.showerror("Fehler", "Die neuen Passw\u00f6rter stimmen nicht \u00fcberein.", parent=dlg)
|
||||
return
|
||||
pw_hash = self._hash_password(new_pw)
|
||||
else:
|
||||
pw_hash = old_hash
|
||||
|
||||
_gv = lambda e: e._get_real_value() if hasattr(e, '_get_real_value') else e.get().strip()
|
||||
updated = {
|
||||
"name": name,
|
||||
"specialty": spec_e.get().strip(),
|
||||
"clinic": clinic_e.get().strip(),
|
||||
"code": code_e.get().strip(),
|
||||
"email": email_e.get().strip(),
|
||||
"specialty": _gv(spec_e),
|
||||
"clinic": _gv(clinic_e),
|
||||
"code": _gv(code_e),
|
||||
"email": _gv(email_e),
|
||||
"password_hash": pw_hash,
|
||||
}
|
||||
for k in ("totp_secret_enc", "totp_active", "backup_codes"):
|
||||
@@ -2989,9 +3033,90 @@ class KGDesktopApp(tk.Tk, TodoMixin, TextWindowsMixin, AzaDiktatMixin, AzaSettin
|
||||
self.set_status(f"Profil gespeichert: {name}")
|
||||
dlg.destroy()
|
||||
|
||||
btn_row = tk.Frame(dlg, bg="#E8F4FA")
|
||||
# --- Lizenz / Einladung ---
|
||||
lic_sep = tk.Frame(form, bg="#B9ECFA", height=1)
|
||||
lic_sep.pack(fill="x", pady=(10, 6))
|
||||
tk.Label(form, text="\U0001f4cb Lizenz & Einladung", font=("Segoe UI", 10, "bold"),
|
||||
bg="#E8F4FA", fg="#1a4d6d").pack(anchor="w", pady=(2, 4))
|
||||
|
||||
_lic_key = (self._user_profile.get("license_key") or "").strip()
|
||||
_lic_email = (self._user_profile.get("email") or "").strip()
|
||||
if _lic_key:
|
||||
tk.Label(form, text="Lizenznummer:", font=("Segoe UI", 9),
|
||||
bg="#E8F4FA", fg="#5B8DB3").pack(anchor="w")
|
||||
_lic_lbl = tk.Label(form, text=_lic_key, font=("Consolas", 9),
|
||||
bg="#f0f4f8", fg="#1a3a5a", relief="flat",
|
||||
padx=6, pady=2, cursor="hand2", anchor="w")
|
||||
_lic_lbl.pack(fill="x", pady=(0, 6))
|
||||
_lic_lbl.bind("<Button-1>", lambda e: (
|
||||
dlg.clipboard_clear(), dlg.clipboard_append(_lic_key),
|
||||
self.set_status("Lizenznummer kopiert.")))
|
||||
|
||||
_invite_frame = tk.Frame(form, bg="#E8F4FA")
|
||||
_invite_frame.pack(fill="x", pady=(0, 4))
|
||||
_invite_label = tk.Label(_invite_frame, text="Einladungscode:", font=("Segoe UI", 9),
|
||||
bg="#E8F4FA", fg="#5B8DB3")
|
||||
_invite_label.pack(anchor="w")
|
||||
_invite_val = tk.Label(_invite_frame, text="Wird geladen...", font=("Consolas", 9),
|
||||
bg="#f0f4f8", fg="#1a3a5a", relief="flat",
|
||||
padx=6, pady=2, anchor="w")
|
||||
_invite_val.pack(fill="x")
|
||||
|
||||
_link_frame = tk.Frame(form, bg="#E8F4FA")
|
||||
_link_frame.pack(fill="x", pady=(0, 6))
|
||||
_link_label = tk.Label(_link_frame, text="Einladungslink:", font=("Segoe UI", 9),
|
||||
bg="#E8F4FA", fg="#5B8DB3")
|
||||
_link_label.pack(anchor="w")
|
||||
_link_val = tk.Label(_link_frame, text="", font=("Segoe UI", 8),
|
||||
bg="#f0f4f8", fg="#1a3a5a", relief="flat",
|
||||
padx=6, pady=2, anchor="w", wraplength=440, justify="left")
|
||||
_link_val.pack(fill="x")
|
||||
|
||||
_link_btn_row = tk.Frame(form, bg="#E8F4FA")
|
||||
_link_btn_row.pack(fill="x", pady=(0, 8))
|
||||
|
||||
def _copy_invite_link():
|
||||
text = _link_val.cget("text")
|
||||
if text:
|
||||
dlg.clipboard_clear()
|
||||
dlg.clipboard_append(text)
|
||||
self.set_status("Einladungslink kopiert.")
|
||||
|
||||
tk.Button(_link_btn_row, text="Link kopieren", font=("Segoe UI", 9),
|
||||
bg="#e8f0f8", fg="#2a5a8a", relief="flat", padx=10, pady=3,
|
||||
cursor="hand2", command=_copy_invite_link).pack(side="left", padx=(0, 6))
|
||||
|
||||
def _load_invite_info():
|
||||
try:
|
||||
bu = self.get_backend_url()
|
||||
bt = self.get_backend_token()
|
||||
r = requests.get(f"{bu}/empfang/practice/info",
|
||||
headers={"X-API-Token": bt}, timeout=5)
|
||||
if r.status_code == 200:
|
||||
d = r.json()
|
||||
code = d.get("invite_code", "")
|
||||
pname = d.get("practice_name", "")
|
||||
if code:
|
||||
link = f"https://empfang.aza-medwork.ch/?invite={code}"
|
||||
self.after(0, lambda: _invite_val.configure(text=code))
|
||||
self.after(0, lambda: _link_val.configure(text=link))
|
||||
else:
|
||||
self.after(0, lambda: _invite_val.configure(text=""))
|
||||
self.after(0, lambda: _link_val.configure(text=""))
|
||||
self.after(0, lambda: _invite_frame.pack_forget())
|
||||
self.after(0, lambda: _link_frame.pack_forget())
|
||||
self.after(0, lambda: _link_btn_row.pack_forget())
|
||||
except Exception:
|
||||
self.after(0, lambda: _invite_frame.pack_forget())
|
||||
self.after(0, lambda: _link_frame.pack_forget())
|
||||
self.after(0, lambda: _link_btn_row.pack_forget())
|
||||
|
||||
threading.Thread(target=_load_invite_info, daemon=True).start()
|
||||
|
||||
# --- Buttons ---
|
||||
btn_row = tk.Frame(scroll_inner, bg="#E8F4FA")
|
||||
btn_row.pack(pady=(12, 10))
|
||||
tk.Button(btn_row, text="💾 Speichern", font=("Segoe UI", 11, "bold"),
|
||||
tk.Button(btn_row, text="\U0001f4be Speichern", font=("Segoe UI", 11, "bold"),
|
||||
bg="#5B8DB3", fg="white", activebackground="#4A7A9E",
|
||||
relief="flat", padx=20, pady=6, cursor="hand2",
|
||||
command=do_save).pack(side="left", padx=8)
|
||||
@@ -3001,9 +3126,9 @@ class KGDesktopApp(tk.Tk, TodoMixin, TextWindowsMixin, AzaDiktatMixin, AzaSettin
|
||||
command=dlg.destroy).pack(side="left", padx=8)
|
||||
|
||||
if is_2fa_enabled():
|
||||
sep2 = tk.Frame(dlg, bg="#B9ECFA", height=1)
|
||||
sep2 = tk.Frame(scroll_inner, bg="#B9ECFA", height=1)
|
||||
sep2.pack(fill="x", padx=20, pady=(4, 4))
|
||||
tfa_frame = tk.Frame(dlg, bg="#E8F4FA", padx=20)
|
||||
tfa_frame = tk.Frame(scroll_inner, 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"
|
||||
@@ -4001,22 +4126,24 @@ class KGDesktopApp(tk.Tk, TodoMixin, TextWindowsMixin, AzaDiktatMixin, AzaSettin
|
||||
dlg.transient(self)
|
||||
dlg.grab_set()
|
||||
dlg.configure(bg="#F2F8FC")
|
||||
dlg.geometry("520x620")
|
||||
dlg.minsize(420, 500)
|
||||
add_resize_grip(dlg, 420, 500)
|
||||
center_window(dlg, 520, 620)
|
||||
body = tk.Frame(dlg, bg="#F2F8FC", padx=12, pady=12)
|
||||
body.pack(fill="both", expand=True)
|
||||
tk.Label(body, text="Primäres Fachgebiet (Erststart)", bg="#F2F8FC", fg="#1a4d6d", font=("Segoe UI", 11, "bold")).pack(anchor="w")
|
||||
primary_var = tk.StringVar(value="dermatology")
|
||||
primary_var = tk.StringVar(value="")
|
||||
label_to_key = {label: key for key, label in catalog}
|
||||
key_to_label = {key: label for key, label in catalog}
|
||||
combo_val = tk.StringVar(value=key_to_label.get("dermatology", "Dermatologie"))
|
||||
combo = ttk.Combobox(body, values=[label for _, label in catalog], textvariable=combo_val, state="readonly", width=32)
|
||||
combo_val = tk.StringVar(value="")
|
||||
combo = ttk.Combobox(body, values=[""] + [label for _, label in catalog], textvariable=combo_val, state="readonly", width=32)
|
||||
combo.pack(anchor="w", pady=(6, 10))
|
||||
|
||||
tk.Label(body, text="Weitere Fachgebiete (optional)", bg="#F2F8FC", fg="#1a4d6d").pack(anchor="w")
|
||||
vars_map = {}
|
||||
for key, label in catalog:
|
||||
v = tk.BooleanVar(value=(key == "dermatology"))
|
||||
v = tk.BooleanVar(value=False)
|
||||
vars_map[key] = v
|
||||
tk.Checkbutton(body, text=label, variable=v, bg="#F2F8FC", activebackground="#F2F8FC", selectcolor="#E7F4FA").pack(anchor="w")
|
||||
|
||||
@@ -5185,7 +5312,7 @@ WICHTIG unbedingt einhalten:
|
||||
dlg.resizable(True, True)
|
||||
self._register_window(dlg)
|
||||
|
||||
_empfang_font_size = [load_text_font_size("empfang_dlg", 10)]
|
||||
_empfang_font_size = [load_text_font_size("empfang_dlg", 9)]
|
||||
_empfang_text_widgets: list = []
|
||||
|
||||
def _save_prefs():
|
||||
@@ -7386,11 +7513,12 @@ WICHTIG unbedingt einhalten:
|
||||
dlg.title("KI-Einwilligung erforderlich")
|
||||
dlg.transient(self)
|
||||
dlg.grab_set()
|
||||
dlg.geometry("680x520")
|
||||
dlg.minsize(500, 400)
|
||||
dlg.geometry("920x720")
|
||||
dlg.minsize(700, 550)
|
||||
dlg.attributes("-topmost", True)
|
||||
self._register_window(dlg)
|
||||
add_resize_grip(dlg, 500, 400)
|
||||
add_resize_grip(dlg, 700, 550)
|
||||
center_window(dlg, 920, 720)
|
||||
|
||||
frame = ttk.Frame(dlg, padding=12)
|
||||
frame.pack(fill="both", expand=True)
|
||||
@@ -8769,7 +8897,11 @@ WICHTIG:
|
||||
text_widget.bind("<FocusIn>", on_focus_in, add="+")
|
||||
text_widget.bind("<FocusOut>", on_focus_out, add="+")
|
||||
|
||||
_last_expansion = [0.0, ""]
|
||||
|
||||
def on_keyrelease(event):
|
||||
if getattr(self, "_autotext_injecting", [False])[0]:
|
||||
return
|
||||
if not getattr(self, "_autotext_data", {}).get("enabled", True):
|
||||
return
|
||||
entries = (self._autotext_data.get("entries") or {})
|
||||
@@ -8792,7 +8924,12 @@ WICHTIG:
|
||||
word = text_before[word_start:word_end]
|
||||
if not word or word not in entries:
|
||||
return
|
||||
now = time.time()
|
||||
if _last_expansion[1] == word and now - _last_expansion[0] < 1.0:
|
||||
return
|
||||
expansion = entries[word]
|
||||
_last_expansion[0] = now
|
||||
_last_expansion[1] = word
|
||||
start_idx = text_widget.index(f"{insert} - {len(word) + 1} chars")
|
||||
text_widget.delete(start_idx, insert)
|
||||
text_widget.insert(start_idx, expansion + last_char)
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
||||
# AZA – Master Handover / Operational Runbook
|
||||
|
||||
## Arbeitsmodus / Regeln
|
||||
## Arbeitsmodus / Regeln (VERBINDLICH)
|
||||
|
||||
User bastelt nicht; nur Composer-Patches (meist Opus) oder 1 exakter Command mit Pfad.
|
||||
|
||||
@@ -8,10 +8,76 @@ User bastelt nicht; nur Composer-Patches (meist Opus) oder 1 exakter Command mit
|
||||
- User fuehrt nur vorgegebene Commands aus, keine manuellen Edits.
|
||||
- Jede Aenderung in 1 Patch, kein schrittweises Anleiten.
|
||||
- Keine risky Refactors – immer minimal und sicher.
|
||||
- KEINE Bastelloesungen, KEINE Prototypisierung, KEINE Zwischenloesungen.
|
||||
- KEINE localStorage-Geschaeftsdaten als Zielbild.
|
||||
- KEINE vagen „spaeter besser"-Loesungen.
|
||||
- KEINE Diffs als Hauptlieferung – nur vollstaendige fertige Dateien.
|
||||
- Immer dazuschreiben WO etwas auszufuehren ist: Browser / Windows PowerShell / Hetzner SSH / Composer.
|
||||
- Root-cause-first bei jedem Problem.
|
||||
- Nur Schritte empfehlen, die zur Live-Architektur passen.
|
||||
|
||||
## AKTUELLE PROJEKTPHASE (Stand 2026-04-12)
|
||||
## Server-Deploy-Realitaet (VERBINDLICH)
|
||||
|
||||
**Phase:** Device-/Seat-Logik V1 implementiert. Backup-Konzept + Deinstallations-UX als naechste Hauptbloecke.
|
||||
Hetzner `/root/aza-app` ist KEIN Git-Repository.
|
||||
Server-Updates laufen real ueber:
|
||||
1. Lokal aendern (Composer/Max)
|
||||
2. Per `scp` auf Hetzner hochkopieren
|
||||
3. Auf Hetzner: `cd /root/aza-app/deploy && docker compose up --build -d`
|
||||
|
||||
NICHT `git pull` auf Hetzner annehmen. Das funktioniert dort nicht.
|
||||
|
||||
## AKTUELLE PROJEKTPHASE (Stand 2026-04-18)
|
||||
|
||||
**Phase:** Praxis-Chat V5 live (Auth + Sessions + Tasks + Admin + Devices + Channels + Federation).
|
||||
Admin-Panel im Browser eingebaut. Naechste Bloecke: Admin real pruefen, Empfangs-Huelle NaN-Bug, Uebersetzer-Bug.
|
||||
|
||||
## BEKANNTE REGRESSIONEN + KORREKTUREN
|
||||
|
||||
### Autotext-Regression April 2026 (GEFIXT + EINGEFROREN)
|
||||
|
||||
**Root Cause:** Race Condition zwischen zwei gleichzeitig aktiven Autotext-Systemen:
|
||||
1. In-App-Autotext (`on_keyrelease` auf Tkinter Text-Widgets, `_bind_autotext()`)
|
||||
2. Globaler Autotext-Listener (pynput `_run_global_autotext_listener()`)
|
||||
|
||||
Der Fokusstatus (`_autotext_focus_in_app`) wird per PID-Check alle 1000ms aktualisiert.
|
||||
In dem Zeitfenster konnten beide Systeme feuern → doppelte Einfuegung.
|
||||
|
||||
**Symptome:**
|
||||
- Doppelte Autotext-Einfuegung in AZA-Textfeldern
|
||||
- Teils unzuverlaessiges Verhalten in externen Programmen
|
||||
|
||||
**Funktionierende Korrektur (basis14.py, `_bind_autotext()`):**
|
||||
- `_autotext_injecting`-Check am Anfang von `on_keyrelease`: ueberspringt wenn der
|
||||
globale Listener gerade injiziert
|
||||
- Deduplizierung: wenn dasselbe Wort innerhalb von 1 Sekunde erneut expandiert werden
|
||||
soll, wird es uebersprungen (`_last_expansion` mit Zeitstempel + Wort)
|
||||
- Kein grosser Umbau, nur minimale Absicherung
|
||||
|
||||
**Do-Not-Break-Regeln (VERBINDLICH):**
|
||||
- `_run_global_autotext_listener()` NICHT aendern (Freeze seit Maerz 2026)
|
||||
- `_bind_autotext()` NICHT aendern ausser bei reproduzierbarem Bug
|
||||
- `_periodic_focus_check()` NICHT aendern
|
||||
- `_check_autotext_focus_out()` NICHT aendern
|
||||
- Die `_last_expansion`-Deduplizierung NICHT entfernen
|
||||
- Den `_autotext_injecting`-Check in `on_keyrelease` NICHT entfernen
|
||||
- KEINE Disk-I/O in on_press/on_release
|
||||
- KEINE Aenderung der Timing-Konstanten (REPLACE_DELAY, 0.04s/0.01s, etc.)
|
||||
- Bei kuenftigen Aenderungen an basis14.py: Autotext-Bereich (Zeilen ~8825-9082) explizit
|
||||
NICHT anfassen, ausser bei reproduzierbarem Bug mit klarer Root Cause
|
||||
|
||||
## OFFENE REALE BUGS (Stand 2026-04-18)
|
||||
|
||||
1. **Empfangs-Huelle: Schriftgroesse NaN** (GEFIXT, Deploy ausstehend)
|
||||
- Root Cause: Huelle laedt alte empfang.html. Neue Version hat robustes parseInt + kein uiScale.
|
||||
- Fix: neue empfang.html per scp deployen
|
||||
|
||||
2. **Uebersetzer: Zielsprache wird nicht eingehalten** (GEFIXT, Neustart noetig)
|
||||
- Root Cause: `get_lang_codes()` fiel bei fehlendem ` – ` im Combo-Wert auf `"en"` zurueck
|
||||
- Fix: 3 Stellen in translate.py gefixt, Fallback nutzt gespeicherte Sprache
|
||||
|
||||
3. **`An Empfang senden` (`basis14.py`): noch nicht professionell**
|
||||
- ~1100 Zeilen `_send_to_empfang()` – Hochrisikobereich
|
||||
- Offen: Auto-Integration medizinischer Inhalte, Bilder, Chatfluss, Aktionsleiste
|
||||
|
||||
## VERBINDLICHE PROJEKTKONTINUITAET (ab 2026-04-12)
|
||||
|
||||
@@ -218,40 +284,190 @@ kurze Antworten, kein voller Hauptclient).
|
||||
|
||||
Festlegung: Mobile spaeter lieber als echte App, nicht als dauerhafte Browser-Notloesung.
|
||||
|
||||
#### 6.11 Aktueller Stand vs. Zielarchitektur
|
||||
#### 6.11 Praxis-zu-Praxis-Kopplung (VERBINDLICH, 2026-04-18)
|
||||
|
||||
**Was schon da ist (V1):**
|
||||
- Benutzer-Sync via Backend (`empfang_users.json`)
|
||||
- Thread-basierter Chat (thread_id, reply_to)
|
||||
- Aufgaben-Panel (localStorage, user-scoped)
|
||||
- 3-Panel-Layout im Browser-Empfang
|
||||
- Ton-/Benachrichtigungssystem
|
||||
- Empfangs-Desktop-Huelle
|
||||
Interne Kurzbezeichnung: **AZA Praxis-Federation**
|
||||
|
||||
**Was noch fehlt fuer V2:**
|
||||
- practice_id in allen Entitaeten
|
||||
- Echte Authentifizierung (JWT/Session)
|
||||
- Serverseitige Kanalstruktur
|
||||
- Serverseitige Aufgaben (statt localStorage)
|
||||
- Geraeteverwaltung fuer Praxis-Admin
|
||||
- QR-Code-Kopplung fuer Mobile
|
||||
**Grundprinzip:** Praxen sind standardmaessig vollstaendig getrennt.
|
||||
Eine Verbindung entsteht NUR durch explizite beidseitige Zustimmung.
|
||||
|
||||
**Ablauf:**
|
||||
1. Admin Praxis A erzeugt Verbindungseinladung (Einmal-Code, 48h gueltig)
|
||||
2. Admin Praxis B gibt Code ein und bestaetigt
|
||||
3. Serverseitig: `PracticeConnection`-Objekt wird angelegt
|
||||
4. Definierte externe Kommunikationsraeume werden freigeschaltet
|
||||
5. Jeder Admin kann die Verbindung jederzeit trennen
|
||||
|
||||
**Datenmodell `PracticeConnection`:**
|
||||
|
||||
| Feld | Typ | Beschreibung |
|
||||
|---|---|---|
|
||||
| `connection_id` | UUID | Eindeutige Verbindung |
|
||||
| `practice_a_id` | UUID | Initiierende Praxis |
|
||||
| `practice_b_id` | UUID | Annehmende Praxis |
|
||||
| `status` | enum | pending / active / revoked |
|
||||
| `created_by` | user_id | Admin der die Einladung erstellt hat |
|
||||
| `accepted_by` | user_id | Admin der angenommen hat |
|
||||
| `created_at` | timestamp | Erstellzeitpunkt |
|
||||
| `accepted_at` | timestamp | Annahmezeitpunkt |
|
||||
| `revoked_at` | timestamp | Trennzeitpunkt (optional) |
|
||||
| `shared_channels` | list | Freigegebene Kanaltypen |
|
||||
|
||||
**Externe Kanalstruktur bei Kopplung:**
|
||||
- Allgemeiner externer Kanal (Praxis A ↔ Praxis B)
|
||||
- Optional: Aerzte ↔ Aerzte (nur Aerzte beider Praxen)
|
||||
- Optional: Sekretariat ↔ Sekretariat
|
||||
- Jeder externe Kanal wird vom jeweiligen Admin freigegeben
|
||||
|
||||
**Sicherheitsregeln:**
|
||||
- Externe Nachrichten sind IMMER als extern markiert
|
||||
- Externe Benutzer sehen NUR den freigegebenen Kanal, NICHT interne Daten
|
||||
- Trennung sofort wirksam (kein Nachlauf)
|
||||
- Verbindungshistorie im Audit-Log
|
||||
|
||||
#### 6.12 Admin-Benutzerverwaltung (VERBINDLICH, 2026-04-18)
|
||||
|
||||
**Benutzer-Objekt (Mindestfelder):**
|
||||
|
||||
| Feld | Typ | Beschreibung |
|
||||
|---|---|---|
|
||||
| `user_id` | UUID | Eindeutiger Benutzer |
|
||||
| `practice_id` | UUID | Praxis-Zugehoerigkeit |
|
||||
| `display_name` | string | Anzeigename |
|
||||
| `email` | string | E-Mail (optional, fuer Passwort-Reset) |
|
||||
| `role` | enum | admin / arzt / mpa / empfang |
|
||||
| `status` | enum | active / deactivated / deleted |
|
||||
| `pw_hash` | string | PBKDF2-SHA256 |
|
||||
| `pw_salt` | string | Zufaelliger Salt |
|
||||
| `created_by` | user_id | Wer hat den Account angelegt |
|
||||
| `created_at` | timestamp | Erstellzeitpunkt |
|
||||
| `last_login` | timestamp | Letzter Login |
|
||||
| `deactivated_at` | timestamp | Deaktivierungszeitpunkt (optional) |
|
||||
|
||||
**Admin-Aktionen:**
|
||||
- Benutzer anlegen (mit Name, Rolle, optionalem Passwort)
|
||||
- Benutzer deaktivieren (Login gesperrt, Daten bleiben)
|
||||
- Benutzer reaktivieren
|
||||
- Benutzer endgueltig loeschen (DSGVO)
|
||||
- Rolle aendern (arzt ↔ mpa ↔ empfang, Admin nur durch anderen Admin)
|
||||
- Passwort zuruecksetzen (erzeugt temporaeres Passwort)
|
||||
- Alle Sitzungen eines Benutzers beenden
|
||||
|
||||
**Nachvollziehbarkeit:**
|
||||
- `created_by` zeigt wer den Benutzer angelegt hat
|
||||
- Rollenaenderungen im Audit-Log
|
||||
- Deaktivierung/Loeschung im Audit-Log
|
||||
|
||||
#### 6.13 Admin-Geraeteverwaltung (VERBINDLICH, 2026-04-18)
|
||||
|
||||
**Device-Objekt (Mindestfelder):**
|
||||
|
||||
| Feld | Typ | Beschreibung |
|
||||
|---|---|---|
|
||||
| `device_id` | UUID | Eindeutiges Geraet |
|
||||
| `user_id` | UUID | Zugehoeriger Benutzer |
|
||||
| `practice_id` | UUID | Zugehoerige Praxis |
|
||||
| `device_name` | string | z.B. "Praxis-PC Empfang", "iPhone" |
|
||||
| `platform` | string | Windows / macOS / iOS / Android / Browser |
|
||||
| `device_type` | enum | desktop / browser / mobile / tablet |
|
||||
| `user_agent` | string | Browser-/App-Kennung |
|
||||
| `last_active` | timestamp | Letzter API-Zugriff |
|
||||
| `first_seen` | timestamp | Erstmalige Registrierung |
|
||||
| `trust_status` | enum | trusted / pending / blocked |
|
||||
| `ip_last` | string | Letzte IP-Adresse |
|
||||
|
||||
**Admin sieht pro Benutzer:**
|
||||
- Alle registrierten Geraete
|
||||
- Typ (Desktop-Icon, Browser-Icon, Handy-Icon)
|
||||
- Letzter Zugriff (relativ: "vor 2 Stunden", "vor 3 Tagen")
|
||||
- Status-Badge (aktiv / verdaechtig / gesperrt)
|
||||
|
||||
**Admin-Aktionen:**
|
||||
- Geraet als vertrauenswuerdig markieren
|
||||
- Geraet sperren (sofort, alle Sitzungen werden beendet)
|
||||
- Geraet loeschen (aus der Liste entfernen)
|
||||
- Alle Sitzungen auf einem Geraet beenden
|
||||
- Erneute Anmeldung erzwingen
|
||||
|
||||
**Automatische Erkennung:**
|
||||
- Bei jedem Login: device_id wird aus User-Agent + fingerprint abgeleitet
|
||||
(oder Cookie-basiert fuer Browser)
|
||||
- Neues Geraet: Admin erhaelt optional Benachrichtigung
|
||||
- Zu viele Geraete: Warnung an Admin
|
||||
|
||||
#### 6.14 UI-Sichtbarkeit nach Rolle
|
||||
|
||||
**Admin sieht:**
|
||||
- Praxis-Einstellungen (Name, Einladungscode, Admin-E-Mail)
|
||||
- Alle Benutzer mit Rollen + Status + letztem Login
|
||||
- Alle Geraete aller Benutzer
|
||||
- Gekoppelte Praxen + Verbindungsstatus
|
||||
- Alle internen + externen Kanaele
|
||||
- Audit-Log
|
||||
|
||||
#### 6.12 Umsetzungsphasen
|
||||
**Arzt / MPA / Empfang sieht:**
|
||||
- Eigene Praxis (Name)
|
||||
- Eigene Kanaele (interner Allgemein + Rollen-Kanal + Direktchats)
|
||||
- Eigene Aufgaben + an eigene Rolle zugewiesene Aufgaben
|
||||
- Freigegebene externe Kanaele (falls Praxis-Kopplung aktiv)
|
||||
- Eigene Geraete (nur eigene, nicht die anderer Benutzer)
|
||||
|
||||
**Phase 1 (kurzfristig – aktuell):**
|
||||
Frontend-Layout, Benutzer-Sync, Chat-Threads. Kein Backend-Umbau.
|
||||
Einzelpraxis-Betrieb reicht. practice_id wird als Konzept vorbereitet,
|
||||
aber noch nicht erzwungen.
|
||||
**Externer Praxis-Kontakt sieht:**
|
||||
- NUR den freigegebenen externen Kanal
|
||||
- Keine internen Daten, Benutzer, Aufgaben der anderen Praxis
|
||||
|
||||
**Phase 2 (mittelfristig):**
|
||||
Backend: practice_id + user_id + JWT-Auth einfuehren. Kanalstruktur serverseitig.
|
||||
Aufgaben serverseitig. Geraeteverwaltung. Admin-Panel fuer Practice Admin.
|
||||
Presence/Heartbeat. Invite-Links.
|
||||
#### 6.15 Aktueller Stand (nach V4-Deploy, 2026-04-18)
|
||||
|
||||
**Phase 3 (spaeter):**
|
||||
Multi-Tenant produktiv (mehrere Praxen). WebSocket statt Polling. Mobile-App.
|
||||
QR-Code-Kopplung. 2FA. Verschluesselte Speicherung. Externe Praxis-Verbindungen.
|
||||
**Was jetzt implementiert ist:**
|
||||
- Serverseitige Auth: PBKDF2 + Session-Cookie (empfang_routes.py V4)
|
||||
- practice_id in allen Nachrichten + Benutzern + Aufgaben
|
||||
- Login / Setup / Register mit Einladungscode
|
||||
- Rollen: admin, arzt, mpa, empfang
|
||||
- Serverseitige Aufgaben (GET/POST/DELETE /tasks)
|
||||
- Browser-Empfang mit Login-Overlay, Server-only-Daten
|
||||
- Praxisname + Admin-E-Mail beim Setup
|
||||
- Einladungscode kopieren + erneuern (Admin)
|
||||
- 3-Panel-Layout (Sidebar, Chat, Aufgaben)
|
||||
- Ton-/Benachrichtigungssystem
|
||||
- Empfangs-Desktop-Huelle
|
||||
- Caddy Rewrite (empfang.aza-medwork.ch ohne /empfang/)
|
||||
|
||||
**Was als naechstes fehlt:**
|
||||
- Serverseitige Kanalstruktur (Allgemein, Aerzte, MPA statt flacher Thread-Liste)
|
||||
- Device-Tracking (device_id bei jedem Login)
|
||||
- Admin-Panel im Browser (Benutzer verwalten, Geraete sehen)
|
||||
- Praxis-zu-Praxis-Kopplung (Federation)
|
||||
- Audit-Log
|
||||
- QR-Code-Kopplung fuer Mobile
|
||||
|
||||
#### 6.16 Umsetzungsphasen (aktualisiert 2026-04-18)
|
||||
|
||||
**Phase 1 (ERLEDIGT):**
|
||||
Backend V4 mit Auth + practice_id + Sessions + Tasks.
|
||||
Browser-Empfang mit Login, Server-only-Daten, 3-Panel-Layout.
|
||||
Einzelpraxis-Betrieb produktiv moeglich.
|
||||
|
||||
**Phase 2 (naechster Hauptblock):**
|
||||
- Serverseitige Kanalstruktur (Channel-Objekte im Backend)
|
||||
- Device-Tracking bei jedem Login
|
||||
- Admin-Panel im Browser: Benutzer verwalten, Geraete sehen
|
||||
- Presence/Heartbeat (Online-Status)
|
||||
- Hauptfenster `_send_to_empfang()` aufraumen (AO2/AO4/AO5/AO6)
|
||||
|
||||
**Phase 3 (mittelfristig):**
|
||||
- Praxis-zu-Praxis-Federation
|
||||
- WebSocket statt Polling
|
||||
- Mobile-App (iPhone/Android)
|
||||
- QR-Code-Kopplung
|
||||
- 2FA fuer Admin
|
||||
- Verschluesselte Nachrichtenspeicherung
|
||||
|
||||
**Phase 4 (spaeter):**
|
||||
- Multi-Praxis produktiv (mehrere zahlende Praxen)
|
||||
- Apple Watch / Wearable (nur Benachrichtigungen)
|
||||
- Externe Praxis-Kanaele
|
||||
- DSGVO-Loeschfunktionen
|
||||
- Audit-Export
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
{
|
||||
"project": "AZA Medical AI Assistant",
|
||||
"phase": "Device-/Seat-Logik V1 implementiert. Backup-Konzept + Deinstallations-UX als naechste Hauptbloecke.",
|
||||
"current_step": "Device-Logik V1 fertig (2026-04-12). Naechste Bloecke: (1) Backup-Konzept vollstaendig, (2) Deinstallations-UX, (3) WooCommerce Verkaufspfad, (4) Browser-AZA.",
|
||||
"last_completed": 19,
|
||||
"next_step": "Naechsten Hauptblock waehlen (Admin-Token-Rotation / Betreiber-Runbook / WooCommerce / Lizenz-Lifecycle).",
|
||||
"last_update": "STATUS-PATCH 2026-04-12: (1) Device-/Seat-Logik V1 implementiert: 1 Lizenz = 2 Geraete, Stacking ueber Email, Backend fuehrend. (2) aza_device_enforcement.py komplett neu, backend_main.py + admin_routes.py + basis14.py erweitert. (3) Admin-Endpoint GET /admin/devices fuer Geraete-Uebersicht. (4) Naechste Bloecke: Backup-Konzept + Deinstallations-UX.",
|
||||
"updated_at": "2026-04-12",
|
||||
"phase": "Praxis-Chat V5 live (Auth + Admin + Devices + Channels + Federation). Admin-Panel eingebaut. Naechste Bloecke: Admin real pruefen, Empfangs-Huelle NaN-Bug, Uebersetzer-Bug.",
|
||||
"current_step": "V5 deployed (2026-04-18). 40 Backend-Routen. Admin-Panel mit 5 Tabs (Praxis/Benutzer/Geraete/Kanaele/Verbindungen). Naechste Bloecke: (1) Admin-Panel real pruefen/fertigziehen, (2) Empfangs-Huelle NaN-Skalierungsbug fixen, (3) Uebersetzer-Zielsprache-Bug, (4) basis14.py _send_to_empfang Hauptblock.",
|
||||
"last_completed": 21,
|
||||
"next_step": "Block 1: Browser-Admin-Panel als Admin real pruefen. Block 2: Empfangs-Huelle Aa/UI NaN-Bug fixen. Block 3: Uebersetzer-Bug aufnehmen.",
|
||||
"last_update": "STATUS-PATCH 2026-04-18b: V5 live. (1) Admin-Endpoints: Benutzer verwalten (deaktivieren/loeschen/Rolle/PW-Reset), Geraete (sperren/loeschen). (2) Device-Tracking bei jedem Login. (3) Channel-Grundlage (Allgemein/Aerzte/MPA/Empfang + custom + extern). (4) Federation-Grundlage (Einladung/Annahme/Trennung + automatische externe Kanaele). (5) Admin-Panel im Browser mit 5 Tabs. (6) Offene Bugs: Empfangs-Huelle Aa/UI NaN, Uebersetzer-Zielsprache. (7) Server-Deploy via scp (kein Git auf Hetzner). (8) Arbeitsmodus: nur live-tauglich, keine Provisorien, keine localStorage-Geschaeftsdaten.",
|
||||
"updated_at": "2026-04-18",
|
||||
"workspace": {
|
||||
"project_root": "C:\\Users\\surov\\Documents\\AZA_GIT\\aza",
|
||||
"current_working_folder": "C:\\Users\\surov\\Documents\\AZA_GIT\\aza\\AzA march 2026",
|
||||
@@ -22,6 +22,7 @@
|
||||
"Do-not-break: Keine riskanten DNS-/Domain-Aenderungen an aza-medwork.ch ohne klare Pruefung. Website-Chaos durch vorschnelle Umschaltung war ein Fehler.",
|
||||
"Do-not-break: OpenAI-Key NIEMALS hardcoded in App einbauen. NIEMALS Shared-Key an Kunden ausliefern. Lokale Key-Eingabe wird bei Produktivauslieferung entfernt (Variante B). Secrets NIEMALS loggen.",
|
||||
"Do-not-break: Audioaufnahme IMMER als M4A (AAC via ffmpeg-Pipe). NIEMALS auf WAV zurueckaendern. WAV nur als Fallback wenn ffmpeg fehlt. Diese Entscheidung ist ENDGUELTIG – wurde bereits einmal faelschlich rueckgaengig gemacht.",
|
||||
"Do-not-break: AUTOTEXT FREEZE (April 2026). _run_global_autotext_listener(), _bind_autotext(), _periodic_focus_check(), _check_autotext_focus_out() sind eingefroren. NICHT aendern ausser bei reproduzierbarem Bug. Race-Condition-Fix (Deduplizierung + _autotext_injecting-Check in on_keyrelease) NICHT entfernen. Keine Disk-I/O in on_press/on_release. Keine Timing-Aenderungen. Autotext-Bereich in basis14.py (Zeilen ~8825-9082) explizit NICHT anfassen.",
|
||||
"VERBINDLICH – V1-Lizenzmodell (2026-04-12): 1 aktive Lizenz = 2 gleichzeitig aktive Computer. Dieselbe Kaeufer-Email darf mehrere Lizenzen kaufen. Erlaubte Geraete addieren sich: 1 Lizenz = 2, 2 Lizenzen = 4, 3 Lizenzen = 6. Backend entscheidet fuehrend. Desktop sendet email + device_id. Backend liefert: license_active, allowed_devices, used_devices, device_allowed, reason. aza_device_enforcement.py ist fuehrende Implementierung.",
|
||||
"VERBINDLICH – Deinstallation: Nach Deinstallation KEIN sofortiger Zwangs-Neustart. Benutzer muss Option haben: jetzt neu starten / spaeter neu starten.",
|
||||
"VERBINDLICH – Backup-Konzept HOHE PRIORITAET: Lokale App/Code/Builds, Installer-Artefakte, Hetzner-Backend/Server-Config/Daten, Stripe-Config/Referenzen, WordPress/WooCommerce/Website, Release-/Download-Dateien. Zusaetzlich Offsite-Sicherheitskopie in Luino.",
|
||||
|
||||
@@ -420,7 +420,7 @@ def extract_terms_from_exchange(user_msg: str, ai_msg: str) -> list[str]:
|
||||
return terms
|
||||
|
||||
|
||||
def load_text_font_size(key: str, default: int = 10) -> int:
|
||||
def load_text_font_size(key: str, default: int = 9) -> int:
|
||||
"""Lädt gespeicherte Schriftgröße aus translate_config.json."""
|
||||
try:
|
||||
if os.path.exists(CONFIG_PATH):
|
||||
@@ -677,12 +677,12 @@ def main(parent=None):
|
||||
save_main_geometry(root.geometry())
|
||||
in_val = (lang_in_var.get() or "").strip()
|
||||
out_val = (lang_out_var.get() or "").strip()
|
||||
lin = in_val.split(" \u2013 ")[0].strip() if " \u2013 " in in_val else "de"
|
||||
lou = out_val.split(" \u2013 ")[0].strip() if " \u2013 " in out_val else "en"
|
||||
lin = in_val.split(" \u2013 ")[0].strip() if " \u2013 " in in_val else in_val
|
||||
lou = out_val.split(" \u2013 ")[0].strip() if " \u2013 " in out_val else out_val
|
||||
if lin not in ALL_GOOGLE_LANGUAGES:
|
||||
lin = "de"
|
||||
if lou not in ALL_GOOGLE_LANGUAGES:
|
||||
lou = "en"
|
||||
lou = load_main_languages()[1]
|
||||
save_main_languages(lin, lou)
|
||||
except Exception:
|
||||
pass
|
||||
@@ -868,12 +868,12 @@ def main(parent=None):
|
||||
try:
|
||||
in_val = (lang_in_var.get() or "").strip()
|
||||
out_val = (lang_out_var.get() or "").strip()
|
||||
lin = in_val.split(" \u2013 ")[0].strip() if " \u2013 " in in_val else "de"
|
||||
lou = out_val.split(" \u2013 ")[0].strip() if " \u2013 " in out_val else "en"
|
||||
lin = in_val.split(" \u2013 ")[0].strip() if " \u2013 " in in_val else in_val
|
||||
lou = out_val.split(" \u2013 ")[0].strip() if " \u2013 " in out_val else out_val
|
||||
if lin not in ALL_GOOGLE_LANGUAGES:
|
||||
lin = "de"
|
||||
if lou not in ALL_GOOGLE_LANGUAGES:
|
||||
lou = "en"
|
||||
lou = load_main_languages()[1]
|
||||
save_main_languages(lin, lou)
|
||||
except Exception:
|
||||
pass
|
||||
@@ -899,10 +899,14 @@ def main(parent=None):
|
||||
combo_out.bind("<<ComboboxSelected>>", _on_lang_selected)
|
||||
|
||||
def get_lang_codes():
|
||||
in_val = lang_in_var.get()
|
||||
out_val = lang_out_var.get()
|
||||
lang_in = in_val.split(" – ")[0] if " – " in in_val else "de"
|
||||
lang_out = out_val.split(" – ")[0] if " – " in out_val else "en"
|
||||
in_val = (lang_in_var.get() or "").strip()
|
||||
out_val = (lang_out_var.get() or "").strip()
|
||||
lang_in = in_val.split(" \u2013 ")[0].strip() if " \u2013 " in in_val else in_val
|
||||
lang_out = out_val.split(" \u2013 ")[0].strip() if " \u2013 " in out_val else out_val
|
||||
if lang_in not in ALL_GOOGLE_LANGUAGES:
|
||||
lang_in = "de"
|
||||
if lang_out not in ALL_GOOGLE_LANGUAGES:
|
||||
lang_out = load_main_languages()[1]
|
||||
return lang_in, lang_out
|
||||
|
||||
def swap_languages():
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user