#!/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'''