#!/usr/bin/env python3 # -*- coding: utf-8 -*- """ AZA Empfang - Schlanke Desktop-Huelle fuer die Empfangs-Weboberflaeche. Unabhaengig von AzA Office / Hauptfenster: Standard-URL ist die oeffentliche Empfang-Instanz (empfang.aza-medwork.ch). Kein lokales Backend noetig. URL-Reihenfolge: 1) Umgebung AZA_EMPFANG_URL oder EMPFANG_URL (volle Basis, siehe unten) 2) backend_url.txt neben der EXE (optional, erste nicht-leere Zeile) 3) Im EXE-Bundle nur mit AZA_EMPFANG_USE_BUNDLED_URL=1 4) Fallback: https://empfang.aza-medwork.ch/empfang/ Lokale URLs (localhost / 127.0.0.1 / 192.168...): bei gebundelter EXE nur mit AZA_EMPFANG_ALLOW_LOCAL=1, sonst Fallback auf (4) — damit die Huelle ohne Haupt-App und ohne laufenden lokalen Server funktioniert. Standard: Laedt die URL direkt im Fenster (kein iframe). Optional: AZA_EMPFANG_IFRAME=1 = alte iframe-Huelle mit HTML-Toolbar. """ import json import os import ssl import sys import threading import webbrowser _APP_TITLE = "AZA Empfang" _MIN_SIZE = (380, 500) _DEFAULT_W, _DEFAULT_H = 480, 820 _PUBLIC_EMPFANG_URL = "https://empfang.aza-medwork.ch" def _data_dir(): if getattr(sys, "frozen", False): return sys._MEIPASS return os.path.dirname(os.path.abspath(__file__)) def _user_dir(): if getattr(sys, "frozen", False): return os.path.dirname(sys.executable) return os.path.dirname(os.path.abspath(__file__)) def _normalize_to_empfang_url(base: str) -> str: """Aus Basis-URL (Host oder .../empfang) wird .../empfang/ mit Slash am Ende.""" b = (base or "").strip().rstrip("/") if not b: b = _PUBLIC_EMPFANG_URL.rstrip("/") if b.endswith("/empfang"): return b + "/" return b + "/empfang/" def _is_local_or_lan_url(url: str) -> bool: u = (url or "").lower() if "localhost" in u or "127.0.0.1" in u: return True if u.startswith("http://192.168.") or u.startswith("http://10."): return True if u.startswith("https://192.168.") or u.startswith("https://10."): return True return False def _read_backend_url_file_in_dir(dir_path: str) -> str | None: try: p = os.path.join(dir_path, "backend_url.txt") if not os.path.isfile(p): return None with open(p, "r", encoding="utf-8") as f: for ln in f: s = ln.strip() if s and not s.startswith("#"): return s.rstrip("/") except Exception: return None return None def _empfang_url() -> str: env = ( (os.environ.get("AZA_EMPFANG_URL") or os.environ.get("EMPFANG_URL") or "") .strip() ) if env: return _normalize_to_empfang_url(env) search_dirs: list[str] = [_user_dir()] if getattr(sys, "frozen", False): if os.environ.get("AZA_EMPFANG_USE_BUNDLED_URL", "").strip() == "1": search_dirs.append(_data_dir()) else: # Entwicklung: Skript-Verzeichnis (identisch zu _user_dir bei direktem Start) dd = _data_dir() if dd not in search_dirs: search_dirs.append(dd) raw: str | None = None for d in search_dirs: raw = _read_backend_url_file_in_dir(d) if raw: break if raw: if getattr(sys, "frozen", False) and _is_local_or_lan_url(raw): if os.environ.get("AZA_EMPFANG_ALLOW_LOCAL", "").strip() != "1": raw = None if raw: return _normalize_to_empfang_url(raw) return _normalize_to_empfang_url(_PUBLIC_EMPFANG_URL) _SETTINGS_FILE = os.path.join(_user_dir(), "empfang_app_settings.json") def _load_settings() -> dict: defaults = {"width": _DEFAULT_W, "height": _DEFAULT_H, "x": None, "y": None, "on_top": False} try: with open(_SETTINGS_FILE, "r", encoding="utf-8") as f: return {**defaults, **json.load(f)} except Exception: return defaults def _save_settings(s: dict): try: with open(_SETTINGS_FILE, "w", encoding="utf-8") as f: json.dump(s, f, indent=2) except Exception: pass class _Api: def __init__(self, window, on_top: bool, url: str): self._w = window self._on_top = on_top self._url = url self._pin_lock = threading.Lock() def check_url(self): """Diagnostic connectivity check (GET with SSL fallback).""" from urllib.request import urlopen, Request for verify in (True, False): try: ctx = ssl.create_default_context() if verify else ssl._create_unverified_context() req = Request(self._url) with urlopen(req, timeout=10, context=ctx) as r: r.read(512) return {"ok": True, "url": self._url, "error": ""} except ssl.SSLError: if verify: continue return {"ok": False, "url": self._url, "error": "SSL/TLS-Zertifikatsfehler. Bitte IT kontaktieren."} except Exception as e: if verify: continue return {"ok": False, "url": self._url, "error": str(e)} return {"ok": False, "url": self._url, "error": "Server nicht erreichbar. Bitte Netzwerk pruefen."} def open_in_browser(self): """Öffnet dieselbe Ziel-URL wie im Fenster (lokal oder Produktion).""" webbrowser.open(self._url) def toggle_on_top(self): if not self._pin_lock.acquire(blocking=False): return self._on_top try: self._on_top = not self._on_top new_val = self._on_top finally: self._pin_lock.release() def _apply(): try: self._w.on_top = new_val except Exception: pass threading.Thread(target=_apply, daemon=True).start() return new_val def get_on_top(self): return self._on_top def get_version(self): try: sys.path.insert(0, _data_dir()) from _build_info import BUILD_TIME, GIT_COMMIT return f"Build: {BUILD_TIME} ({GIT_COMMIT})" except Exception: return "Entwicklungsversion" def get_url(self): return self._url def get_public_url(self): return _PUBLIC_EMPFANG_URL _SHELL_HTML = r'''
AZA Empfang
Empfang wird geladen
Verbindung wird hergestellt…
⚠️
Empfang nicht erreichbar
Die Empfangsseite konnte nicht geladen werden.
''' def main(): try: import webview except ImportError: print("FEHLER: pywebview ist nicht installiert.") print("Bitte ausfuehren: pip install pywebview") sys.exit(1) try: from webview.menu import Menu, MenuAction, MenuSeparator except ImportError: Menu = None # type: ignore MenuAction = None # type: ignore MenuSeparator = None # type: ignore settings = _load_settings() url = _empfang_url() # iframe-Huelle (alt): nur setzen wenn noetig: AZA_EMPFANG_IFRAME=1 use_iframe_shell = os.environ.get("AZA_EMPFANG_IFRAME", "").strip() == "1" w = max(_MIN_SIZE[0], min(1920, settings.get("width") or _DEFAULT_W)) h = max(_MIN_SIZE[1], min(1200, settings.get("height") or _DEFAULT_H)) x = settings.get("x") y = settings.get("y") if isinstance(x, (int, float)) and isinstance(y, (int, float)): if x < -200 or y < -200 or x > 3800 or y > 2200: x, y = None, None else: x, y = None, None on_top = bool(settings.get("on_top", False)) if use_iframe_shell: window = webview.create_window( _APP_TITLE, html=_SHELL_HTML, width=w, height=h, x=x, y=y, min_size=_MIN_SIZE, on_top=on_top, text_select=True, background_color="#f0f4f8", ) api = _Api(window, on_top, url) window.expose( api.check_url, api.open_in_browser, api.toggle_on_top, api.get_on_top, api.get_version, api.get_url, api.get_public_url, ) else: window = webview.create_window( _APP_TITLE, url=url, width=w, height=h, x=x, y=y, min_size=_MIN_SIZE, on_top=on_top, text_select=True, background_color="#f0f4f8", ) api = _Api(window, on_top, url) window.expose( api.toggle_on_top, api.get_on_top, api.open_in_browser, api.get_version, api.get_url, api.get_public_url, api.check_url, ) def _reload(): try: window.evaluate_js("window.location.reload()") except Exception as exc: print(f"[AZA Empfang] Neu laden: {exc}") def _info_box(): try: msg = f"{api.get_version()}\n\n{api._url}" try: import ctypes ctypes.windll.user32.MessageBoxW(0, msg, _APP_TITLE, 0) except Exception: print(msg) except Exception: pass def _toggle_pin_menu(): api.toggle_on_top() menu = None if Menu is not None: menu = [ Menu( "Empfang", [ MenuAction("Neu laden", _reload), MenuAction("Im Browser \u00f6ffnen", api.open_in_browser), MenuAction("Immer im Vordergrund", _toggle_pin_menu), MenuSeparator(), MenuAction("Version / Info", _info_box), ], ), ] def _on_closing(): try: _save_settings({ "x": window.x, "y": window.y, "width": window.width, "height": window.height, "on_top": api._on_top, }) except Exception: pass return True try: window.events.closing += _on_closing except Exception: pass try: if use_iframe_shell: webview.start() elif menu: try: webview.start(menu=menu) except TypeError: webview.start() else: webview.start() except Exception as e: print(f"[AZA Empfang] Fehler: {e}") if __name__ == "__main__": try: import ctypes ctypes.windll.shell32.SetCurrentProcessExplicitAppUserModelID( "ch.aza-medwork.empfang.v2") except Exception: pass try: main() except Exception as e: print(f"[AZA Empfang] Kritischer Fehler: {e}") import traceback traceback.print_exc()