This commit is contained in:
2026-05-16 20:33:36 +02:00
parent 96c1029d91
commit 968bf7d102
212 changed files with 954195 additions and 658 deletions

View File

@@ -0,0 +1,89 @@
# AZA Reverse Proxy + Static Files
#
# Production: DOMAIN env must point to this machine (DNS A-Record).
# Caddy handles automatic HTTPS via Let's Encrypt.
#
# Local testing: http://localhost:8080
{
email {$ACME_EMAIL:admin@aza-medwork.ch}
}
{$AZA_DOMAIN:localhost} {
encode gzip zstd
# Static web assets (landing, download, styles)
handle /web/* {
root * /app
file_server
}
# Release artifacts (version.json, installer)
handle /release/* {
root * /app
file_server
}
# Download shortcut: /download/filename -> /release/filename
handle /download/* {
root * /app/release
uri strip_prefix /download
file_server
}
handle /downloads/* {
root * /app/release
uri strip_prefix /downloads
file_server
}
# All other requests -> backend API
handle {
reverse_proxy {$BACKEND_UPSTREAM:backend:8000}
}
}
# Empfang-Subdomain: empfang.aza-medwork.ch
# Root "/" wird transparent auf /empfang/ umgeschrieben (kein sichtbarer Redirect)
{$AZA_EMPFANG_DOMAIN:empfang.aza-medwork.ch} {
encode gzip zstd
handle / {
rewrite * /empfang/
reverse_proxy {$BACKEND_UPSTREAM:backend:8000}
}
handle {
reverse_proxy {$BACKEND_UPSTREAM:backend:8000}
}
}
:8080 {
encode gzip zstd
handle /web/* {
root * /app
file_server
}
handle /release/* {
root * /app
file_server
}
handle /download/* {
root * /app/release
uri strip_prefix /download
file_server
}
handle /downloads/* {
root * /app/release
uri strip_prefix /downloads
file_server
}
handle {
reverse_proxy {$BACKEND_UPSTREAM_LOCAL:localhost:8000}
}
}
}

View File

@@ -0,0 +1,17 @@
Backup vor AZA Update-System Phase 1 (v2 — Logging, required, Build-Tie-Break).
Wiederherstellen (PowerShell, Arbeitsverzeichnis: AzA march 2026):
$b = 'backup_aza_update_phase1_v2_20260515_233059'
Copy-Item -LiteralPath "$b\desktop_update_check.py" -Destination .\desktop_update_check.py -Force
Copy-Item -LiteralPath "$b\basis14.py" -Destination .\basis14.py -Force
Copy-Item -LiteralPath "$b\build_release_manifest.ps1" -Destination .\build_release_manifest.ps1 -Force
Copy-Item -LiteralPath "$b\version.json" -Destination .\release\version.json -Force
Copy-Item -LiteralPath "$b\Caddyfile" -Destination .\deploy\Caddyfile -Force
Copy-Item -LiteralPath "$b\Caddyfile" -Destination .\deploy\aza-deploy\Caddyfile -Force
Copy-Item -LiteralPath "$b\publish_update.ps1" -Destination .\publish_update.ps1 -Force
Copy-Item -LiteralPath "$b\release.ps1" -Destination .\release.ps1 -Force
Hinweis: Im Backup liegen zwei Kopien unter demselben Dateinamen Caddyfile;
bei Restore den Inhalt von deploy\aza-deploy\Caddyfile manuell aus der zweiten
Server-Kopie verifizieren (falls abweichend).

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,97 @@
$projectRoot = $PSScriptRoot
$azaVersionPy = Join-Path $projectRoot "aza_version.py"
$manifestPath = Join-Path $projectRoot "release\version.json"
$releaseBaseUrl = $env:AZA_RELEASE_BASE_URL
$installerPath = Join-Path $projectRoot "dist\installer\aza_desktop_setup.exe"
if (-not (Test-Path $azaVersionPy)) {
Write-Error "aza_version.py nicht gefunden: $azaVersionPy"
exit 1
}
Write-Host "Erzeuge release/version.json aus aza_version.py..."
$versionContent = Get-Content $azaVersionPy -Raw
if ($versionContent -match 'APP_VERSION\s*=\s*"([^"]+)"') {
$appVersion = $matches[1].Trim()
} else {
Write-Error "APP_VERSION nicht in aza_version.py gefunden."
exit 1
}
if ($versionContent -match 'APP_CHANNEL\s*=\s*"([^"]+)"') {
$appChannel = $matches[1].Trim()
} else {
Write-Error "APP_CHANNEL nicht in aza_version.py gefunden."
exit 1
}
$manifest = @{}
if (Test-Path $manifestPath) {
$manifest = Get-Content $manifestPath -Raw | ConvertFrom-Json
}
$releaseDate = if ($manifest.release_date) { $manifest.release_date } else { (Get-Date).ToString("yyyy-MM-dd") }
$minVersion = if ($manifest.minimum_supported_version) { $manifest.minimum_supported_version } else { $appVersion }
$minRequired = if ($manifest.min_required_version) { $manifest.min_required_version } else { $minVersion }
$downloadUrl = if ($releaseBaseUrl) {
"$releaseBaseUrl/aza_desktop_setup.exe"
} elseif ($manifest.download_url) {
$manifest.download_url
} else {
"https://api.aza-medwork.ch/downloads/aza_desktop_setup.exe"
}
$installerType = if ($manifest.installer_type) { $manifest.installer_type } else { "inno-setup" }
$notesExisting = @()
if ($manifest.notes -and $manifest.notes.Length -gt 0) {
$notesExisting = @($manifest.notes)
} elseif ($manifest.release_notes -and $manifest.release_notes.Length -gt 0) {
$notesExisting = @($manifest.release_notes)
}
$notesList = if ($notesExisting.Count -gt 0) { $notesExisting } else { @("Desktop-Build aktualisiert") }
$updateLevel = if ($manifest.update_level) { $manifest.update_level.ToString().ToLowerInvariant() } else { "recommended" }
if ($updateLevel -ne "optional" -and $updateLevel -ne "recommended") {
$updateLevel = "recommended"
}
$buildStamp = (Get-Date).ToString("yyyyMMdd_HHmmss")
$sha256Hex = $null
if (Test-Path $installerPath) {
$buildStamp = (Get-Item $installerPath).LastWriteTime.ToString("yyyyMMdd_HHmmss")
$sha256Hex = (Get-FileHash -Path $installerPath -Algorithm SHA256).Hash
} elseif ($manifest.build) {
$buildStamp = $manifest.build.ToString()
}
if (-not $sha256Hex -and $manifest.sha256) {
$sha256Hex = $manifest.sha256.ToString()
}
$newManifest = [ordered]@{
version = $appVersion
build = $buildStamp
channel = $appChannel
release_date = $releaseDate
minimum_supported_version = $minVersion
min_required_version = $minRequired
download_url = $downloadUrl
sha256 = $sha256Hex
update_level = $updateLevel
installer_type = $installerType
notes = $notesList
release_notes = $notesList
}
$newManifest | ConvertTo-Json -Depth 6 | Set-Content $manifestPath -Encoding UTF8
if (-not $appVersion) {
Write-Error "release/version.json konnte nicht erzeugt werden."
exit 1
}
Write-Host "release/version.json aktualisiert."
Write-Host "Version: $appVersion Build: $buildStamp"
if ($sha256Hex) {
Write-Host "SHA256: $sha256Hex"
}
if ($releaseBaseUrl) {
Write-Host "Download-URL Base: $releaseBaseUrl"
}

View File

@@ -0,0 +1,286 @@
import webbrowser
import requests
from aza_version import APP_CHANNEL, APP_VERSION
# Mehrere Pfade: /download/ (bestehende Instanzen) und /release/ (direkt unter Dateibaum).
UPDATE_MANIFEST_URLS = (
"https://api.aza-medwork.ch/download/version.json",
"https://api.aza-medwork.ch/release/version.json",
)
DEFAULT_DOWNLOAD_URL = "https://api.aza-medwork.ch/downloads/aza_desktop_setup.exe"
def _parse_version_tuple(v: str) -> tuple:
parts = []
for p in str(v).strip().split("."):
try:
parts.append(int(p))
except Exception:
parts.append(0)
while len(parts) < 3:
parts.append(0)
return tuple(parts[:3])
def _is_newer_version(latest: str, current: str) -> bool:
return _parse_version_tuple(latest) > _parse_version_tuple(current)
def _below_min_required(current: str, min_req: str) -> bool:
mr = (min_req or "").strip()
if not mr:
return False
return _parse_version_tuple(current) < _parse_version_tuple(mr)
def _normalize_notes(data: dict) -> list[str]:
raw = data.get("notes")
if raw is None:
raw = data.get("release_notes")
if raw is None:
return []
if isinstance(raw, str):
return [raw.strip()] if raw.strip() else []
if isinstance(raw, list):
return [str(x).strip() for x in raw if str(x).strip()]
return []
def _min_required_from_manifest(data: dict) -> str:
for key in ("min_required_version", "minimum_supported_version"):
v = data.get(key)
if v is not None and str(v).strip():
return str(v).strip()
return ""
def fetch_remote_manifest() -> tuple[dict | None, str | None]:
"""Laedt das Release-Manifest. Fehler -> (None, fehlertext)."""
last_err = "Keine Antwort vom Update-Server"
for url in UPDATE_MANIFEST_URLS:
try:
r = requests.get(url, timeout=6)
if r.status_code != 200:
last_err = f"HTTP {r.status_code} von {url}"
continue
data = r.json()
if not isinstance(data, dict):
last_err = "Ungueltiges Manifest"
continue
return data, None
except Exception as e:
last_err = str(e) or repr(e)
return None, last_err
def check_for_updates():
"""Prueft, ob eine neuere Version angekuendigt ist. Keine Netzwerkmeldung bei Fehler."""
data, err = fetch_remote_manifest()
if err or not data:
return None
channel = str(data.get("channel") or "stable").strip()
if channel != APP_CHANNEL:
return None
latest = str(data.get("version") or "").strip()
if not latest:
return None
min_req = _min_required_from_manifest(data)
download_url = str(data.get("download_url") or "").strip() or DEFAULT_DOWNLOAD_URL
update_level = str(data.get("update_level") or "recommended").strip().lower()
if update_level not in ("optional", "recommended"):
update_level = "recommended"
notes = _normalize_notes(data)
remote_build = str(data.get("build") or "").strip()
sha256 = str(data.get("sha256") or "").strip()
need_version_bump = _is_newer_version(latest, APP_VERSION)
need_min = _below_min_required(APP_VERSION, min_req)
if not need_version_bump and not need_min:
return None
return {
"update_available": True,
"latest_version": latest,
"current_version": APP_VERSION,
"download_url": download_url,
"remote_build": remote_build,
"sha256": sha256,
"update_level": update_level,
"min_required_version": min_req,
"notes": notes,
"below_min_required": bool(min_req) and need_min,
}
def _startup_should_show_dialog(info: dict) -> bool:
"""Optionale Updates nur bei manueller Pruefung stoeren nicht den Start."""
if info.get("below_min_required"):
return True
if info.get("update_level") == "optional":
return False
return True
def _show_update_notification(info: dict, parent):
"""In-App-Dialog: Hinweis und Button oeffnet den offiziellen Download im Browser."""
import tkinter as tk
from tkinter import ttk, scrolledtext
download_url = info.get("download_url") or DEFAULT_DOWNLOAD_URL
latest = info.get("latest_version", "")
level = info.get("update_level", "recommended")
notes = info.get("notes") or []
root = parent
owns_root = False
if root is None:
root = tk.Tk()
root.withdraw()
owns_root = True
dlg = tk.Toplevel(root)
dlg.title("AZA — Aktualisierung")
if not owns_root:
dlg.transient(root)
dlg.resizable(True, True)
dlg.minsize(420, 280)
if level == "optional":
head = (
f"Optionales Update: AZA {latest} ist verfuegbar.\n"
f"Ihre Version: {APP_VERSION}\n\n"
"Ueber die Schaltflaeche oeffnen Sie den offiziellen Installer-Download im Browser. "
"AZA schliesst sich dafuer nicht automatisch."
)
elif info.get("below_min_required"):
head = (
f"Update erforderlich: Ihre Angabe bzw. Server-Vorgabe liegt unter der Mindestversion.\n"
f"Neueste angebotene Version: {latest}\n"
f"Ihre Version: {APP_VERSION}\n\n"
"Bitte installieren Sie die aktuelle AZA-Version ueber den offiziellen Installer."
)
else:
head = (
f"Empfohlenes Update: AZA {latest} ist verfuegbar.\n"
f"Ihre Version: {APP_VERSION}\n\n"
"Ueber die Schaltflaeche oeffnen Sie den offiziellen Installer-Download im Browser."
)
frm = ttk.Frame(dlg, padding=12)
frm.pack(fill="both", expand=True)
ttk.Label(frm, text=head, wraplength=520, justify="left").pack(anchor="w", pady=(0, 8))
if notes:
ttk.Label(frm, text="Aenderungen:", font=("Segoe UI", 9, "bold")).pack(anchor="w")
box = scrolledtext.ScrolledText(frm, height=8, wrap="word", font=("Segoe UI", 9))
box.pack(fill="both", expand=True, pady=(4, 8))
box.insert("1.0", "\n".join(f"{n}" for n in notes))
box.configure(state="disabled")
btn_row = ttk.Frame(frm)
btn_row.pack(fill="x", pady=(8, 0))
def on_download():
try:
webbrowser.open(download_url)
except Exception:
pass
def on_close():
dlg.destroy()
if owns_root:
try:
root.destroy()
except Exception:
pass
ttk.Button(btn_row, text="Installer herunterladen (Browser)", command=on_download).pack(
side="left", padx=(0, 8)
)
ttk.Button(btn_row, text="Schliessen", command=on_close).pack(side="left")
dlg.protocol("WM_DELETE_WINDOW", on_close)
dlg.update_idletasks()
try:
_px = root.winfo_x() if not owns_root else None
_py = root.winfo_y() if not owns_root else None
if _px is not None and _py is not None:
dlg.geometry(
f"+{_px + max(20, (root.winfo_width() - dlg.winfo_reqwidth()) // 2)}"
f"+{_py + max(20, (root.winfo_height() - dlg.winfo_reqheight()) // 2)}"
)
else:
sw = dlg.winfo_screenwidth()
sh = dlg.winfo_screenheight()
dlg.geometry(
f"+{(sw - dlg.winfo_reqwidth()) // 2}+{(sh - dlg.winfo_reqheight()) // 2}"
)
except Exception:
pass
if owns_root:
dlg.grab_set()
dlg.wait_window()
else:
dlg.grab_set()
parent.wait_window(dlg)
def prompt_update_if_available():
"""Beim Start: still pruefen; bei empfohlenem / Pflicht-Update In-App-Dialog (kein Auto-Download)."""
info = check_for_updates()
if not info or not info.get("update_available"):
return
if not _startup_should_show_dialog(info):
return
try:
_show_update_notification(info, parent=None)
except Exception:
pass
def manual_check_for_updates(parent=None):
"""Explizite Pruefung in der App (z. B. Ueber-Dialog)."""
from tkinter import messagebox
data, err = fetch_remote_manifest()
if err:
messagebox.showerror(
"Nach Updates suchen",
f"Das Aktualisierungs-Manifest konnte nicht geladen werden.\n\n{err}",
parent=parent,
)
return
channel = str(data.get("channel") or "stable").strip()
if channel != APP_CHANNEL:
messagebox.showinfo(
"Nach Updates suchen",
f"Kein passendes Release fuer diesen Kanal ({APP_CHANNEL!r}).",
parent=parent,
)
return
info = check_for_updates()
if not info:
messagebox.showinfo(
"Nach Updates suchen",
f"AZA {APP_VERSION} ist aktuell.\n\nEs ist kein neueres Release eingetragen.",
parent=parent,
)
return
_show_update_notification(info, parent=parent)
if __name__ == "__main__":
m, e = fetch_remote_manifest()
print({"manifest_error": e, "keys": list(m.keys()) if m else None})
print(check_for_updates())

View File

@@ -0,0 +1,48 @@
# publish_update.ps1 Upload Installer + Manifest nach Hetzner
$ErrorActionPreference = "Stop"
$projectRoot = $PSScriptRoot
$remoteHost = "root@178.104.51.177"
$remotePath = "/root/aza-app/release"
$installerPath = Join-Path $projectRoot "dist\installer\aza_desktop_setup.exe"
$manifestPath = Join-Path $projectRoot "release\version.json"
Write-Host ""
# --- [1/4] ---
Write-Host "[1/4] Lokale Dateien pruefen..."
if (-not (Test-Path $installerPath)) {
Write-Error "ABBRUCH: $installerPath nicht gefunden."
exit 1
}
$sizeMB = [math]::Round((Get-Item $installerPath).Length / 1MB, 2)
Write-Host " Installer OK ($sizeMB MB)"
if (-not (Test-Path $manifestPath)) {
Write-Error "ABBRUCH: $manifestPath nicht gefunden."
exit 1
}
Write-Host " version.json OK"
# --- [2/4] ---
Write-Host "[2/4] Installer hochladen..."
scp "$installerPath" "${remoteHost}:${remotePath}/aza_desktop_setup.exe"
if ($LASTEXITCODE -ne 0) {
Write-Error "ABBRUCH: Installer-Upload fehlgeschlagen."
exit 1
}
# --- [3/4] ---
Write-Host "[3/4] Manifest hochladen..."
scp "$manifestPath" "${remoteHost}:${remotePath}/version.json"
if ($LASTEXITCODE -ne 0) {
Write-Error "ABBRUCH: Manifest-Upload fehlgeschlagen."
exit 1
}
# --- [4/4] ---
Write-Host "[4/4] Remote-Verifizierung..."
ssh $remoteHost "ls -lh ${remotePath}/aza_desktop_setup.exe; ls -lh ${remotePath}/version.json; echo '---'; curl -sI https://api.aza-medwork.ch/downloads/aza_desktop_setup.exe | head -3; echo '---'; curl -sI https://api.aza-medwork.ch/download/version.json | head -3"
Write-Host ""
Write-Host "UPLOAD FERTIG"
Write-Host ""

View File

@@ -0,0 +1,215 @@
# release.ps1 Verbindlicher Single-Entry-Point fuer den lokalen AZA Release-Build
#
# Reihenfolge:
# 1. build_exe.ps1 (PyInstaller + Pre-Build-Validierung)
# 2. build_installer.ps1 (Inno Setup Installer)
# 3. build_release_manifest.ps1 (version.json aus aza_version.py)
# 4. Post-Build-Verifizierung
# 5. Abschlussmeldung mit Upload-Befehlen
#
# Verbindliche Release-Artefakte:
# dist\installer\aza_desktop_setup.exe
# release\version.json
$ErrorActionPreference = "Stop"
$projectRoot = $PSScriptRoot
Write-Host ""
Write-Host "=============================================="
Write-Host " AZA Release-Build"
Write-Host "=============================================="
Write-Host ""
# --- Schritt 1: Desktop EXE bauen ---
Write-Host "[1/4] Desktop EXE bauen (build_exe.ps1)..."
Write-Host ""
& (Join-Path $projectRoot "build_exe.ps1")
if ($LASTEXITCODE -ne 0) {
Write-Error "ABBRUCH: build_exe.ps1 fehlgeschlagen."
exit 1
}
Write-Host ""
# --- Schritt 2: Installer bauen ---
Write-Host "[2/4] Installer bauen (build_installer.ps1)..."
Write-Host ""
& (Join-Path $projectRoot "build_installer.ps1")
if ($LASTEXITCODE -ne 0) {
Write-Error "ABBRUCH: build_installer.ps1 fehlgeschlagen."
exit 1
}
Write-Host ""
# --- Schritt 3: Release-Manifest aktualisieren ---
Write-Host "[3/4] Release-Manifest aktualisieren (build_release_manifest.ps1)..."
Write-Host ""
& (Join-Path $projectRoot "build_release_manifest.ps1")
if ($LASTEXITCODE -ne 0) {
Write-Error "ABBRUCH: build_release_manifest.ps1 fehlgeschlagen."
exit 1
}
Write-Host ""
# --- Schritt 4: Post-Build-Verifizierung ---
Write-Host "[4/4] Post-Build-Verifizierung..."
Write-Host ""
$internalDir = Join-Path $projectRoot "dist\aza_desktop\_internal"
$installerPath = Join-Path $projectRoot "dist\installer\aza_desktop_setup.exe"
$manifestPath = Join-Path $projectRoot "release\version.json"
$errors = @()
# 4a) backend_url.txt pruefen
$urlFile = Join-Path $internalDir "backend_url.txt"
if (-not (Test-Path $urlFile)) {
$errors += "backend_url.txt fehlt in _internal"
} else {
$urlContent = (Get-Content $urlFile -Raw).Trim()
if (-not $urlContent) {
$errors += "backend_url.txt in _internal ist leer"
} elseif ($urlContent -match "127\.0\.0\.1|localhost|0\.0\.0\.0") {
$errors += "backend_url.txt in _internal enthaelt lokale Adresse: $urlContent"
} else {
Write-Host " backend_url.txt OK: $urlContent"
}
}
# 4b) backend_token.txt pruefen
$tokenFile = Join-Path $internalDir "backend_token.txt"
if (-not (Test-Path $tokenFile)) {
$errors += "backend_token.txt fehlt in _internal"
} else {
$tokenContent = (Get-Content $tokenFile -Raw).Trim()
if (-not $tokenContent) {
$errors += "backend_token.txt in _internal ist leer"
} elseif ($tokenContent -match "CHANGE_ME") {
$errors += "backend_token.txt in _internal enthaelt Platzhalter"
} else {
Write-Host " backend_token.txt OK (Token vorhanden)"
}
}
# 4c) Verbotene Dateien duerfen NICHT in _internal liegen
$forbidden = @(".env", "license_url.txt")
foreach ($f in $forbidden) {
$fp = Join-Path $internalDir $f
if (Test-Path $fp) {
$errors += "VERBOTEN: $f liegt in _internal (darf nicht im Installer sein)"
}
}
$forbiddenDb = Join-Path $internalDir "data\stripe_webhook.sqlite"
if (Test-Path $forbiddenDb) {
$errors += "VERBOTEN: data\stripe_webhook.sqlite liegt in _internal (darf nicht im Installer sein)"
}
if ($errors.Count -eq 0) {
Write-Host " Keine verbotenen Dateien in _internal"
}
# 4f) Empfang-Web-Huelle: PyInstaller-Ausgabe und Kopie neben aza_desktop muessen identisch sein
$shellExeRoot = Join-Path $projectRoot "dist\AZA_EmpfangShell.exe"
$shellExeDist = Join-Path $projectRoot "dist\aza_desktop\AZA_EmpfangShell.exe"
if (-not (Test-Path $shellExeRoot)) {
$errors += "AZA_EmpfangShell.exe fehlt in dist\ (PyInstaller-Ausgabe) - build_exe.ps1 pruefen"
} elseif (-not (Test-Path $shellExeDist)) {
$errors += "AZA_EmpfangShell.exe fehlt in dist\aza_desktop\ - build_exe.ps1 muss die Shell kopieren"
} else {
$tRoot = (Get-Item $shellExeRoot).LastWriteTime
$tDesk = (Get-Item $shellExeDist).LastWriteTime
$hRoot = (Get-FileHash -Path $shellExeRoot -Algorithm SHA256).Hash
$hDesk = (Get-FileHash -Path $shellExeDist -Algorithm SHA256).Hash
Write-Host (" AZA_EmpfangShell.exe dist\: Zeit {0:yyyy-MM-dd HH:mm:ss}, SHA256={1}" -f $tRoot, $hRoot)
Write-Host (" AZA_EmpfangShell.exe desktop\: Zeit {0:yyyy-MM-dd HH:mm:ss}, SHA256={1}" -f $tDesk, $hDesk)
if ($hRoot -ne $hDesk) {
$errors += "AZA_EmpfangShell.exe: SHA256-Unterschied zwischen dist\ und dist\aza_desktop\ - Build inkonsistent"
} else {
Write-Host " AZA_EmpfangShell.exe: dist\ und dist\aza_desktop\ identisch OK"
}
}
# 4d) Installer existiert und hat Groesse > 0
if (-not (Test-Path $installerPath)) {
$errors += "Installer nicht gefunden: $installerPath"
} else {
$fileInfo = Get-Item $installerPath
if ($fileInfo.Length -eq 0) {
$errors += "Installer hat Groesse 0: $installerPath"
}
}
# 4e) version.json existiert und Version stimmt mit aza_version.py ueberein
if (-not (Test-Path $manifestPath)) {
$errors += "release\version.json nicht gefunden"
} else {
try {
$manifest = Get-Content $manifestPath -Raw | ConvertFrom-Json
$manifestVersion = $manifest.version
$azaContent = Get-Content (Join-Path $projectRoot "aza_version.py") -Raw
if ($azaContent -match 'APP_VERSION\s*=\s*"([^"]+)"') {
$codeVersion = $matches[1].Trim()
if ($manifestVersion -ne $codeVersion) {
$errors += "version.json Version ($manifestVersion) stimmt nicht mit aza_version.py ($codeVersion) ueberein"
} else {
Write-Host " version.json OK: Version $manifestVersion"
}
}
$dlUrl = $manifest.download_url
if ($dlUrl -and ($dlUrl -match "127\.0\.0\.1|localhost")) {
$errors += "version.json download_url enthaelt localhost: $dlUrl"
}
$mfSha = $manifest.sha256
if ($mfSha -and (Test-Path $installerPath)) {
$instSha = (Get-FileHash -Path $installerPath -Algorithm SHA256).Hash
$mfShaNorm = $mfSha.ToString().Trim()
if ($mfShaNorm -and ($mfShaNorm -ne $instSha)) {
$errors += "version.json sha256 stimmt nicht mit Installer ueberein (Manifest=$mfShaNorm Installer=$instSha)"
} elseif ($mfShaNorm) {
Write-Host " sha256 OK: Manifest und Installer identisch"
}
}
} catch {
$errors += "version.json konnte nicht gelesen werden: $_"
}
}
# Fehler auswerten
if ($errors.Count -gt 0) {
Write-Host ""
Write-Host "=============================================="
Write-Host " RELEASE-VERIFIZIERUNG FEHLGESCHLAGEN"
Write-Host "=============================================="
foreach ($e in $errors) {
Write-Host " FEHLER: $e" -ForegroundColor Red
}
Write-Host ""
Write-Error "Release-Build nicht sauber. Bitte Fehler beheben und erneut ausfuehren."
exit 1
}
# --- Abschlussmeldung ---
$instInfo = Get-Item $installerPath
$sizeMB = [math]::Round($instInfo.Length / 1MB, 2)
$sha256 = (Get-FileHash -Path $installerPath -Algorithm SHA256).Hash
$stamp = Get-Date -Format "yyyyMMdd_HHmmss"
$stampedInstaller = Join-Path $projectRoot "dist\installer\aza_desktop_setup_$stamp.exe"
try {
Copy-Item -LiteralPath $installerPath -Destination $stampedInstaller -Force
Write-Host " Zeitstempel-Kopie des Installers: $stampedInstaller"
} catch {
Write-Warning "Zeitstempel-Kopie konnte nicht erstellt werden: $_"
}
Write-Host ""
Write-Host "=============================================="
Write-Host " RELEASE BEREIT FUER UPLOAD"
Write-Host "=============================================="
Write-Host ""
Write-Host " Installer: $installerPath"
Write-Host " Groesse: $sizeMB MB"
Write-Host " SHA256: $sha256"
Write-Host " Manifest: $manifestPath"
Write-Host ""
Write-Host " Upload-Befehle (Produktion laut Vorgabe; ggf. andere IP in Staging-Umgebungen):"
Write-Host " scp `"$installerPath`" root@178.104.51.177:/root/aza-app/release/aza_desktop_setup.exe"
Write-Host " scp `"$manifestPath`" root@178.104.51.177:/root/aza-app/release/version.json"
Write-Host ""

View File

@@ -0,0 +1,31 @@
{
"version": "1.2.0",
"build": "20260314_120000",
"channel": "stable",
"release_date": "2026-03-14",
"minimum_supported_version": "1.0.0",
"min_required_version": "1.0.0",
"download_url": "https://api.aza-medwork.ch/downloads/aza_desktop_setup.exe",
"update_level": "recommended",
"installer_type": "inno-setup",
"notes": [
"Erste stabile Verkaufsversion",
"6 Module: KI-Assistent, Krankengeschichte, Audio-Notizen, Uebersetzer, Aerzte-Netzwerk, Praxis-Intern",
"Automatischer lokaler Backend-Start",
"Integrierter Systemstatus mit 12 Pruefpunkten",
"Professioneller Launcher mit Modulauswahl",
"Projekt-Notizen mit Bild-Einfuegen und Diktat",
"OpenAI-Key Setup-Helfer",
"Windows-Firewall wird automatisch konfiguriert"
],
"release_notes": [
"Erste stabile Verkaufsversion",
"6 Module: KI-Assistent, Krankengeschichte, Audio-Notizen, Uebersetzer, Aerzte-Netzwerk, Praxis-Intern",
"Automatischer lokaler Backend-Start",
"Integrierter Systemstatus mit 12 Pruefpunkten",
"Professioneller Launcher mit Modulauswahl",
"Projekt-Notizen mit Bild-Einfuegen und Diktat",
"OpenAI-Key Setup-Helfer",
"Windows-Firewall wird automatisch konfiguriert"
]
}