Files
aza/AzA march 2026/aza_empfang_app.py
2026-04-19 20:41:37 +02:00

372 lines
11 KiB
Python

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
AZA Empfang - Schlanke Desktop-Huelle fuer die Empfangs-Weboberflaeche.
Laedt die Empfangs-Seite in einem WebView-Fenster.
"""
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 _empfang_url():
try:
p = os.path.join(_data_dir(), "backend_url.txt")
with open(p, "r", encoding="utf-8") as f:
return f.read().strip().rstrip("/") + "/empfang/"
except Exception:
return _PUBLIC_EMPFANG_URL + "/empfang/"
_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):
webbrowser.open(_PUBLIC_EMPFANG_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'''<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="utf-8">
<style>
*{margin:0;padding:0;box-sizing:border-box}
html,body{height:100%;overflow:hidden}
body{font-family:'Segoe UI',system-ui,sans-serif;background:#f0f4f8}
#toolbar{
height:34px;
background:linear-gradient(135deg,#5B8DB3,#3a6d93);
display:flex;align-items:center;justify-content:space-between;
padding:0 10px;box-shadow:0 1px 4px rgba(0,0,0,.15);
position:relative;z-index:20;
}
.tb-l,.tb-r{display:flex;align-items:center;gap:6px}
.tb-title{color:#fff;font-size:12px;font-weight:600;letter-spacing:.3px}
.tb-btn{
background:rgba(255,255,255,.08);border:1px solid rgba(255,255,255,.18);
color:#fff;border-radius:4px;padding:3px 9px;font-size:11px;
cursor:pointer;transition:all .15s;font-family:inherit;white-space:nowrap;
}
.tb-btn:hover{background:rgba(255,255,255,.2)}
.tb-btn.on{background:rgba(255,255,255,.35);border-color:rgba(255,255,255,.6);font-weight:600}
.tb-btn.busy{opacity:.5;pointer-events:none}
#content{position:absolute;top:34px;left:0;right:0;bottom:0}
#frame{width:100%;height:100%;border:none;display:none}
#loading{
position:absolute;top:0;left:0;right:0;bottom:0;
display:flex;flex-direction:column;align-items:center;justify-content:center;
background:#f0f4f8;z-index:10;
}
.spin{width:34px;height:34px;border:3px solid #dde8f0;border-top-color:#5B8DB3;
border-radius:50%;animation:sp .8s linear infinite;margin-bottom:16px}
@keyframes sp{to{transform:rotate(360deg)}}
.ld-t{color:#5B8DB3;font-size:13px;font-weight:500}
.ld-s{color:#8aa8bc;font-size:11px;margin-top:5px}
#error{
position:absolute;top:0;left:0;right:0;bottom:0;
display:none;flex-direction:column;align-items:center;justify-content:center;
background:#f0f4f8;padding:30px;text-align:center;z-index:10;
}
.er-i{font-size:44px;margin-bottom:14px;opacity:.65}
.er-h{font-size:15px;font-weight:600;color:#1a4d6d;margin-bottom:6px}
.er-m{font-size:12px;color:#6a8a9a;margin-bottom:20px;line-height:1.6}
.er-b{display:flex;gap:8px}
.er-btn{background:#5B8DB3;color:#fff;border:none;border-radius:6px;
padding:8px 18px;font-size:12px;cursor:pointer;font-weight:500;font-family:inherit}
.er-btn:hover{background:#4A7A9E}
.er-btn.s{background:#dde8f0;color:#1a4d6d}
.er-btn.s:hover{background:#ccd8e0}
</style>
</head>
<body>
<div id="toolbar">
<div class="tb-l"><span class="tb-title">AZA Empfang</span></div>
<div class="tb-r">
<button class="tb-btn" onclick="doReload()" title="Seite neu laden">&#8635; Laden</button>
<button class="tb-btn" onclick="doOpen()" title="Im Browser oeffnen">&#8599; Browser</button>
<button class="tb-btn" id="bp" onclick="doPin()" title="Immer im Vordergrund">&#128204;</button>
<button class="tb-btn" onclick="doInfo()" title="Version / Info">&#8505;</button>
</div>
</div>
<div id="content">
<div id="loading">
<div class="spin"></div>
<div class="ld-t">Empfang wird geladen</div>
<div class="ld-s">Verbindung wird hergestellt&hellip;</div>
</div>
<div id="error">
<div class="er-i">&#9888;&#65039;</div>
<div class="er-h">Empfang nicht erreichbar</div>
<div class="er-m" id="erMsg">Die Empfangsseite konnte nicht geladen werden.</div>
<div class="er-b">
<button class="er-btn" onclick="doReload()">&#8635; Neu laden</button>
<button class="er-btn s" onclick="doOpen()">&#8599; Im Browser oeffnen</button>
</div>
</div>
<iframe id="frame" src="about:blank"></iframe>
</div>
<script>
var RDY=false, URL='';
window.addEventListener('pywebviewready',function(){
RDY=true;
pywebview.api.get_url().then(function(u){URL=u;boot()});
pinInit();
});
function view(id){
['loading','error','frame'].forEach(function(k){
var e=document.getElementById(k);
e.style.display=(k===id)?(k==='frame'?'block':'flex'):'none';
});
}
function boot(){
view('loading');
var f=document.getElementById('frame');
var done=false;
f.onload=function(){
if(!done&&f.src!=='about:blank'){done=true;view('frame')}
};
f.src=URL;
setTimeout(function(){
if(!done){diagnose()}
},12000);
}
async function diagnose(){
try{
var r=await pywebview.api.check_url();
if(r&&r.ok){
view('frame');
}else{
document.getElementById('erMsg').innerHTML=
(r&&r.error?r.error:'Server nicht erreichbar.')+'<br><br><small>URL: '+(r&&r.url||'')+'</small>';
view('error');
}
}catch(e){view('error')}
}
async function doReload(){
view('loading');
var f=document.getElementById('frame');
var done=false;
f.onload=function(){
if(!done&&f.src!=='about:blank'){done=true;view('frame')}
};
f.src='about:blank';
setTimeout(function(){f.src=URL},100);
setTimeout(function(){if(!done)diagnose()},12000);
}
function doOpen(){if(RDY)pywebview.api.open_in_browser()}
var pinBusy=false;
async function doPin(){
if(!RDY||pinBusy)return;
var b=document.getElementById('bp');
pinBusy=true;
b.classList.add('busy');
try{
var v=await pywebview.api.toggle_on_top();
pinUI(v);
}catch(e){}
setTimeout(function(){pinBusy=false;b.classList.remove('busy')},300);
}
async function pinInit(){
try{var v=await pywebview.api.get_on_top();pinUI(v)}catch(e){}
}
function pinUI(on){
var b=document.getElementById('bp');
if(on){b.classList.add('on');b.textContent='\uD83D\uDCCC An';b.title='Immer im Vordergrund (aktiv)'}
else {b.classList.remove('on');b.textContent='\uD83D\uDCCC';b.title='Immer im Vordergrund'}
}
async function doInfo(){
if(!RDY)return;
var v=await pywebview.api.get_version();
alert('AZA Empfang\n\n'+v+'\n'+URL);
}
</script>
</body>
</html>'''
def main():
try:
import webview
except ImportError:
print("FEHLER: pywebview ist nicht installiert.")
print("Bitte ausfuehren: pip install pywebview")
sys.exit(1)
settings = _load_settings()
url = _empfang_url()
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
window = webview.create_window(
_APP_TITLE,
html=_SHELL_HTML,
width=w,
height=h,
x=x,
y=y,
min_size=_MIN_SIZE,
on_top=settings.get("on_top", False),
text_select=True,
background_color="#f0f4f8",
)
api = _Api(window, settings.get("on_top", False), 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)
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:
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()