This commit is contained in:
2026-06-13 22:47:31 +02:00
parent add3da5177
commit d1446fc452
8032 changed files with 2650751 additions and 1551 deletions

View File

@@ -423,6 +423,7 @@ def manual_check_for_updates(parent=None) -> None:
f"UPDATE_NOT_NEEDED mode=manual reason=already_current local_ver={APP_VERSION} "
f"local_build={_local_build_stamp()}"
)
sync_office_update_hint(None)
return
lvl = info.get("update_level")
@@ -446,9 +447,8 @@ def _startup_should_show_dialog(info: dict[str, Any]) -> bool:
return True
if info.get("update_level") == "required":
return True
if info.get("update_level") == "optional":
return False
return True
# optional, recommended und unbekannte Level: kein Startup-Popup (Badge stattdessen).
return False
# Verhindert zwei gleichzeitig offene Update-Dialoge (Startup-Poll + manueller Button).
@@ -552,10 +552,9 @@ def _show_update_notification(info: dict[str, Any], parent) -> None:
def maybe_show_startup_update_dialog(info: dict[str, Any] | None, parent=None) -> None:
"""Einmaliger Startup-Dialog aus AzA Office (Owner).
"""Startup-Dialog nur für Pflicht-/Mindestversion-Updates.
Verwendet das bereits vom Update-Button-Poll geladene ``info`` (kein zweiter
Manifest-Fetch), respektiert den Session-Guard und die optional/required-Regeln.
Optionale Updates erscheinen nur als Badge (kein störendes Popup beim Start).
"""
if not info:
return
@@ -564,17 +563,213 @@ def maybe_show_startup_update_dialog(info: dict[str, Any] | None, parent=None) -
_show_update_notification(info, parent=parent)
def show_update_dialog_from_info(info: dict[str, Any] | None, parent=None) -> None:
"""Öffnet den bestehenden Update-Dialog aus bereits geladenem Manifest-Info."""
if not info:
return
_show_update_notification(info, parent=parent)
def _close_app_after_update(parent) -> None:
"""Schliesst die App nach gestartetem Updater sauber."""
try:
if hasattr(parent, "shutdown_app_completely"):
parent.shutdown_app_completely(reason="update_on_exit")
elif hasattr(parent, "shutdown"):
parent.shutdown()
elif hasattr(parent, "_on_close"):
parent._on_close()
else:
parent.destroy()
except Exception:
pass
# --- Chat-only Update-Kanal (separates Manifest, IPC fuer Kontaktpanel) ---
CHAT_UPDATE_MANIFEST_URLS = (
"https://api.aza-medwork.ch/download/chat_version.json",
"https://api.aza-medwork.ch/release/chat_version.json",
)
CHAT_UPDATE_TEST_MANIFEST_URLS = (
os.path.join(os.path.dirname(os.path.abspath(__file__)), "test_data", "chat_version_test.json"),
)
_CHAT_UPDATE_HINT_FILE = "chat_update_hint.json"
_CHAT_UPDATE_REQUEST_FLAG = "chat_update_request.flag"
_chat_update_install_inflight = False
def _chat_update_ipc_path(name: str) -> str:
return os.path.join(get_writable_data_dir(), name)
def sync_chat_update_hint(info: dict[str, Any] | None) -> None:
hint_path = _chat_update_ipc_path(_CHAT_UPDATE_HINT_FILE)
try:
if not info or not info.get("update_available"):
if os.path.isfile(hint_path):
os.remove(hint_path)
return
payload = {
"available": True,
"latest_version": str(info.get("latest_version") or "").strip(),
"update_level": str(info.get("update_level") or "recommended").strip(),
}
with open(hint_path, "w", encoding="utf-8") as f:
json.dump(payload, f, ensure_ascii=False)
except Exception:
pass
def read_chat_update_hint() -> dict[str, Any] | None:
try:
path = _chat_update_ipc_path(_CHAT_UPDATE_HINT_FILE)
if not os.path.isfile(path):
return None
with open(path, "r", encoding="utf-8") as f:
data = json.load(f)
if isinstance(data, dict) and data.get("available"):
return data
except Exception:
pass
return None
def fetch_chat_remote_manifest() -> tuple[dict[str, Any] | None, str | None]:
test_urls = list(CHAT_UPDATE_TEST_MANIFEST_URLS) if os.getenv("AZA_CHAT_UPDATE_TEST", "").strip() == "1" else []
urls = test_urls + list(CHAT_UPDATE_MANIFEST_URLS) + list(UPDATE_MANIFEST_URLS)
last: str | None = None
for url in urls:
try:
if url.startswith("file:"):
from urllib.request import urlopen
with urlopen(url) as resp:
data = json.loads(resp.read().decode("utf-8"))
elif url.endswith(".json") and os.path.isfile(url):
with open(url, "r", encoding="utf-8") as f:
data = json.load(f)
else:
r = requests.get(url, timeout=6)
if r.status_code != 200:
last = f"http_status={r.status_code} url={url}"
continue
data = r.json()
if not isinstance(data, dict):
last = f"invalid_manifest_shape url={url}"
continue
data = dict(data)
data["_manifest_url"] = url
return data, None
except Exception as e:
last = f"exc={type(e).__name__} url={url}"
return None, last or "no_manifest"
def check_for_chat_updates() -> dict[str, Any] | None:
data, err = fetch_chat_remote_manifest()
if err or not data:
sync_chat_update_hint(None)
return None
if str(data.get("channel") or "stable").strip() != APP_CHANNEL:
sync_chat_update_hint(None)
return None
info = _build_update_info(data)
sync_chat_update_hint(info)
return info
def prompt_chat_update_if_required(parent=None) -> None:
data, err = fetch_chat_remote_manifest()
if err or not data:
return
if str(data.get("channel") or "stable").strip() != APP_CHANNEL:
return
info = _build_update_info(data)
if not info:
sync_chat_update_hint(None)
return
sync_chat_update_hint(info)
if _startup_should_show_dialog(info):
_show_chat_update_notification(info, parent=parent)
def request_chat_update_dialog_ipc() -> None:
try:
path = _chat_update_ipc_path(_CHAT_UPDATE_REQUEST_FLAG)
with open(path, "w", encoding="utf-8") as f:
f.write("1")
except Exception:
pass
def consume_chat_update_request_flag() -> bool:
try:
path = _chat_update_ipc_path(_CHAT_UPDATE_REQUEST_FLAG)
if not os.path.isfile(path):
return False
os.remove(path)
return True
except Exception:
return False
def manual_check_for_chat_updates(parent=None) -> None:
global _chat_update_install_inflight
if _chat_update_install_inflight:
return
info = check_for_chat_updates()
if not info:
sync_chat_update_hint(None)
return
_show_chat_update_notification(info, parent=parent)
def _show_chat_update_notification(info: dict[str, Any], parent=None) -> None:
global _update_dialog_open, _chat_update_install_inflight
if _update_dialog_open or _chat_update_install_inflight:
return
latest = str(info.get("latest_version", "") or "")
current = str(info.get("current_version", "") or "")
notes = info.get("notes") or []
mandatory = bool(info.get("below_min_required") or info.get("update_level") == "required")
result_for_dialog = {
"local": {"version": current},
"latest_version": latest,
"notes": notes,
"mandatory": mandatory,
"below_min_supported": bool(info.get("below_min_required")),
}
_update_dialog_open = True
try:
from aza_updater import _show_update_dialog_professional
choice = _show_update_dialog_professional(result_for_dialog, parent=parent)
except Exception:
choice = "later"
finally:
_update_dialog_open = False
if choice != "now":
return
_chat_update_install_inflight = True
try:
from aza_updater import launch_external_installer
manifest_url = str(info.get("_manifest_url") or CHAT_UPDATE_MANIFEST_URLS[0])
ok, _msg = launch_external_installer(
{"remote": {"_manifest_url": manifest_url}, "latest_version": latest},
restart_exe="AZA_Chat.exe",
)
if ok and parent is not None:
try:
parent.after(300, lambda: _close_app_after_update(parent))
except Exception:
pass
finally:
_chat_update_install_inflight = False
if __name__ == "__main__":
d, e = fetch_remote_manifest()
print("fetch_err", e, "keys", list(d.keys()) if d else None)