# -*- coding: utf-8 -*-
"""
AzA – Einheitliches Startpanel (WebView-Huelle mit MP4-Hintergrund).
Sichtbar: AzA Office | Praxis Chat
Technisch Praxischat = bisheriger MiniChat-Modus (minichat=1), unveraendert.
UI: pywebview + lokalem HTTP (127.0.0.1) fuer MP4/PNG/Logo.
WebView2 blockiert file:// in html= about:blank — daher kein reines as_uri() im Panel.
Fallback nur bei fehlendem pywebview: Tkinter + PNG (kein laufendes Video).
"""
from __future__ import annotations
import html
import mimetypes
import os
import subprocess
import sys
import threading
import tkinter as tk
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
from pathlib import Path
from tkinter import messagebox
from urllib.parse import parse_qs, unquote, urlencode, urlparse, urlunparse
try:
from PIL import Image, ImageEnhance, ImageFilter, ImageTk
_HAS_PIL = True
except Exception:
_HAS_PIL = False
_ROOT = Path(__file__).resolve().parent
_W, _H = 940, 625
_CARD_W = 270 # Feinschliff 235 px + 15 %
_MP4 = _ROOT / "assets" / "matternhorn-aza-2.mp4"
_PNG = _ROOT / "assets" / "matternhorn.png"
_LOGO = _ROOT / "logo.png"
_WIN_APP_ID = "ch.aza-medwork.startpanel.v1"
_FALLBACK_BG = "#d4e3ef"
_CARD_BG = "#f5f8fb"
_ACCENT = "#1a5f8a"
_ACCENT_HOVER = "#164f73"
_TEXT = "#1a2a3a"
_SUBTLE = "#5a6d7d"
_FONT_TITLE = ("Segoe UI", 18, "bold")
_FONT_SUB = ("Segoe UI", 10)
_FONT_BTN = ("Segoe UI", 10, "bold")
_FONT_FOOT = ("Segoe UI", 8)
def _file_uri(path: Path) -> str:
if path.is_file():
return path.resolve().as_uri()
return ""
def _short_uri(uri: str, max_len: int = 72) -> str:
if len(uri) <= max_len:
return uri
return uri[: max_len - 1] + "…"
def _log_asset_status(*, asset_mode: str = "", start_url: str = "") -> None:
video_exists = _MP4.is_file()
video_size = int(_MP4.stat().st_size) if video_exists else 0
print(f"[AzA Startpanel] VIDEO_EXISTS={video_exists}")
print(f"[AzA Startpanel] VIDEO_SIZE_BYTES={video_size}")
print(f"[AzA Startpanel] VIDEO_URI={_short_uri(_file_uri(_MP4))}")
print(f"[AzA Startpanel] PNG_EXISTS={_PNG.is_file()}")
print(f"[AzA Startpanel] LOGO_EXISTS={_LOGO.is_file()}")
if asset_mode:
print(f"[AzA Startpanel] ASSET_MODE={asset_mode}")
if start_url:
print(f"[AzA Startpanel] START_URL={_short_uri(start_url, 96)}")
def _make_start_panel_handler(root_dir: Path, panel_html_supplier):
"""Liefert nur /start (HTML) und statische Dateien unter Projektroot."""
class Handler(BaseHTTPRequestHandler):
def log_message(self, _format, *_args) -> None:
pass
def do_GET(self) -> None:
path = unquote(urlparse(self.path).path)
if path in ("/start", "/"):
panel_html = panel_html_supplier()
self.send_response(200)
self.send_header("Content-Type", "text/html; charset=utf-8")
self.send_header("Content-Length", str(len(panel_html)))
self.send_header("Cache-Control", "no-store")
self.end_headers()
self.wfile.write(panel_html)
return
rel = path.lstrip("/").replace("/", os.sep)
if not rel or ".." in rel.split(os.sep):
self.send_error(404)
return
fp = (root_dir / rel).resolve()
try:
fp.relative_to(root_dir.resolve())
except ValueError:
self.send_error(403)
return
if not fp.is_file():
self.send_error(404)
return
data = fp.read_bytes()
ctype = mimetypes.guess_type(str(fp))[0] or "application/octet-stream"
self.send_response(200)
self.send_header("Content-Type", ctype)
self.send_header("Content-Length", str(len(data)))
self.send_header("Cache-Control", "no-store")
self.end_headers()
self.wfile.write(data)
return Handler
class _LocalAssetServer:
"""localhost-only HTTP fuer WebView2-Assets (MP4/PNG/Logo)."""
def __init__(self, root_dir: Path) -> None:
self._root_dir = root_dir
self._panel_html: bytes = b""
self._httpd: ThreadingHTTPServer | None = None
self.base_url = ""
def _panel_html_bytes(self) -> bytes:
if not self._panel_html and self.base_url:
self._panel_html = _build_panel_html(self.base_url).encode("utf-8")
return self._panel_html
def start(self) -> str:
handler = _make_start_panel_handler(self._root_dir, self._panel_html_bytes)
self._httpd = ThreadingHTTPServer(("127.0.0.1", 0), handler)
host, port = self._httpd.server_address
self.base_url = f"http://{host}:{port}"
self._panel_html_bytes()
threading.Thread(target=self._httpd.serve_forever, daemon=True).start()
return f"{self.base_url}/start"
def stop(self) -> None:
if self._httpd is not None:
try:
self._httpd.shutdown()
except Exception:
pass
try:
self._httpd.server_close()
except Exception:
pass
self._httpd = None
def _center_window(win: tk.Tk, width: int, height: int) -> None:
win.update_idletasks()
sw = win.winfo_screenwidth()
sh = win.winfo_screenheight()
x = max(0, (sw - width) // 2)
y = max(0, (sh - height) // 2)
win.geometry(f"{width}x{height}+{x}+{y}")
def _minichat_url() -> str:
"""Technischer URL-Modus minichat=1 (Praxischat)."""
try:
from aza_empfang_webview import _build_default_empfang_chat_shell_url
base = _build_default_empfang_chat_shell_url()
except Exception:
base = (
os.environ.get("AZA_EMPFANG_CHAT_SHELL_URL") or ""
).strip() or (
"https://empfang.aza-medwork.ch/empfang/"
"?empfang_chat_shell=1&shell_source=empfang_chat_shell"
)
try:
p = urlparse(base)
qs = parse_qs(p.query, keep_blank_values=True)
qs["minichat"] = ["1"]
return urlunparse(p._replace(query=urlencode(qs, doseq=True)))
except Exception:
sep = "&" if "?" in base else "?"
return f"{base}{sep}minichat=1"
def _resolve_office_executable() -> Path | None:
if getattr(sys, "frozen", False):
exe_dir = Path(sys.executable).resolve().parent
for name in ("aza_desktop.exe", "AzA.exe", "AzA_Start.exe"):
p = exe_dir / name
if p.is_file():
return p
meipass = getattr(sys, "_MEIPASS", "")
if meipass:
p = Path(meipass) / "aza_desktop.exe"
if p.is_file():
return p
for p in (_ROOT / "aza_desktop.exe", _ROOT / "dist" / "aza_desktop" / "aza_desktop.exe"):
if p.is_file():
return p
return None
def _resolve_empfang_shell_executable() -> Path | None:
if getattr(sys, "frozen", False):
exe_dir = Path(sys.executable).resolve().parent
p = exe_dir / "AZA_EmpfangShell.exe"
if p.is_file():
return p
meipass = getattr(sys, "_MEIPASS", "")
if meipass:
p = Path(meipass) / "AZA_EmpfangShell.exe"
if p.is_file():
return p
for p in (_ROOT / "AZA_EmpfangShell.exe", _ROOT / "dist" / "AZA_EmpfangShell.exe"):
if p.is_file():
return p
return None
def start_office() -> tuple[bool, str]:
exe = _resolve_office_executable()
try:
if exe is not None:
subprocess.Popen(
[str(exe)],
cwd=str(exe.parent),
close_fds=(sys.platform != "win32"),
)
return True, f"Gestartet: {exe.name}"
script = _ROOT / "basis14.py"
if not script.is_file():
return False, "basis14.py und aza_desktop.exe wurden nicht gefunden."
subprocess.Popen(
[sys.executable, str(script)],
cwd=str(_ROOT),
close_fds=(sys.platform != "win32"),
)
return True, "AzA Office wird gestartet."
except Exception as exc:
return False, f"Office konnte nicht gestartet werden: {exc}"
def start_praxischat() -> tuple[bool, str]:
"""Praxischat = bestehender MiniChat-WebView-Starter (minichat=1)."""
url = _minichat_url()
mode_arg = "--shell-mode=empfang_chat_shell"
w, h = "350", "700"
exe = _resolve_empfang_shell_executable()
try:
if exe is not None:
subprocess.Popen(
[str(exe), mode_arg, url, w, h],
cwd=str(exe.parent),
close_fds=(sys.platform != "win32"),
)
return True, f"Praxischat wird gestartet ({exe.name})."
script = _ROOT / "aza_empfang_webview.py"
if not script.is_file():
return (
False,
"aza_empfang_webview.py und AZA_EmpfangShell.exe wurden nicht gefunden.",
)
subprocess.Popen(
[sys.executable, str(script), mode_arg, url, w, h],
cwd=str(_ROOT),
close_fds=(sys.platform != "win32"),
)
return True, "Praxischat wird gestartet."
except Exception as exc:
return False, f"Praxischat konnte nicht gestartet werden: {exc}"
# Alias fuer aeltere HTML-API-Namen
start_minichat = start_praxischat
class StartPanelApi:
"""pywebview JS-API (Chromium/WebView2)."""
def __init__(self) -> None:
self._window = None
def bind_window(self, window) -> None:
self._window = window
def _close_on_success(self, ok: bool) -> None:
if ok and self._window is not None:
try:
self._window.destroy()
except Exception:
pass
def open_office(self) -> dict:
ok, msg = start_office()
self._close_on_success(ok)
return {"ok": ok, "message": msg}
def open_praxischat(self) -> dict:
ok, msg = start_praxischat()
self._close_on_success(ok)
return {"ok": ok, "message": msg}
# Kompatibilitaet aelterer HTML-Handler
launch_office = open_office
launch_minichat = open_praxischat
def _build_panel_html(asset_base: str) -> str:
"""HTML mit http://127.0.0.1:PORT/... Asset-URLs (WebView2-kompatibel)."""
base = asset_base.rstrip("/")
png = html.escape(f"{base}/assets/matternhorn.png", quote=True)
mp4 = ""
if _MP4.is_file():
mp4 = html.escape(f"{base}/assets/matternhorn-aza-2.mp4", quote=True)
favicon = ""
if _LOGO.is_file():
favicon = (
f''
)
logo_block = ""
if _LOGO.is_file():
logo_u = html.escape(f"{base}/logo.png", quote=True)
logo_block = (
f''
)
video_block = ""
if mp4:
video_block = f"""
Was möchten Sie öffnen?
AzA Medwork