update
This commit is contained in:
439
AzA march 2026 - Kopie (28)/tools/aza_clean_uninstall.ps1
Normal file
439
AzA march 2026 - Kopie (28)/tools/aza_clean_uninstall.ps1
Normal file
@@ -0,0 +1,439 @@
|
||||
<#
|
||||
.SYNOPSIS
|
||||
AZA Desktop - Sauberes Deinstallations- und Reset-Tool
|
||||
.DESCRIPTION
|
||||
Entfernt AZA Desktop sauber vom System, ohne Neustart.
|
||||
Zwei Modi:
|
||||
1 = Nur App entfernen, Benutzerdaten behalten
|
||||
2 = Vollstaendig zuruecksetzen (App + Benutzerdaten)
|
||||
.NOTES
|
||||
Fuer Entwicklungs- und Abnahmetests.
|
||||
Keine gefaehrlichen Loeschaktionen ausserhalb des AZA-Kontexts.
|
||||
#>
|
||||
|
||||
param(
|
||||
[ValidateSet("1","2")]
|
||||
[string]$Modus
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Continue"
|
||||
$Host.UI.RawUI.WindowTitle = "AZA Desktop - Clean Uninstall / Reset"
|
||||
|
||||
$AZA_APP_ID = "{B7E4C0D2-6B5D-4D39-9D7C-5B0D5E8C2A11}"
|
||||
$AZA_PROCESS_NAME = "aza_desktop"
|
||||
$AZA_DISPLAY_NAME = "AZA Desktop"
|
||||
$AZA_FIREWALL_RULE = "AZA Desktop - Lokale Kommunikation"
|
||||
|
||||
$AZA_APPDATA_DIR = Join-Path $env:APPDATA "AZA Desktop"
|
||||
$AZA_DOCS_DIR = Join-Path ([Environment]::GetFolderPath("MyDocuments")) "KG_Diktat_Ablage"
|
||||
|
||||
$INSTALL_DIR_DEFAULT = Join-Path $env:ProgramFiles "AZA Desktop"
|
||||
|
||||
# --- Hilfsfunktionen ------------------------------------------------
|
||||
|
||||
function Write-Step {
|
||||
param([string]$Icon, [string]$Text)
|
||||
Write-Host ""
|
||||
Write-Host " $Icon $Text" -ForegroundColor Cyan
|
||||
}
|
||||
|
||||
function Write-Ok {
|
||||
param([string]$Text)
|
||||
Write-Host " [OK] $Text" -ForegroundColor Green
|
||||
}
|
||||
|
||||
function Write-Skip {
|
||||
param([string]$Text)
|
||||
Write-Host " [--] $Text" -ForegroundColor DarkGray
|
||||
}
|
||||
|
||||
function Write-Warn {
|
||||
param([string]$Text)
|
||||
Write-Host " [!!] $Text" -ForegroundColor Yellow
|
||||
}
|
||||
|
||||
function Write-Fail {
|
||||
param([string]$Text)
|
||||
Write-Host " [FEHLER] $Text" -ForegroundColor Red
|
||||
}
|
||||
|
||||
function Write-Banner {
|
||||
Write-Host ""
|
||||
Write-Host " =============================================" -ForegroundColor White
|
||||
Write-Host " AZA Desktop - Clean Uninstall / Reset Tool" -ForegroundColor White
|
||||
Write-Host " =============================================" -ForegroundColor White
|
||||
Write-Host ""
|
||||
}
|
||||
|
||||
# --- Schritt 1: Modus waehlen ---------------------------------------
|
||||
|
||||
function Select-Mode {
|
||||
if ($Modus) { return $Modus }
|
||||
|
||||
Write-Host " Bitte waehlen Sie den Modus:" -ForegroundColor White
|
||||
Write-Host ""
|
||||
Write-Host " [1] Nur App entfernen, Benutzerdaten BEHALTEN" -ForegroundColor Green
|
||||
Write-Host ' (Profil, Transkripte, Briefe, Einstellungen bleiben erhalten)' -ForegroundColor DarkGray
|
||||
Write-Host ""
|
||||
Write-Host " [2] Vollstaendig zuruecksetzen (App + ALLE Benutzerdaten)" -ForegroundColor Yellow
|
||||
Write-Host ' (Alles wird geloescht: Profil, Transkripte, Briefe, Rezepte,' -ForegroundColor DarkGray
|
||||
Write-Host ' Kostenvoranschlaege, Autotext, Signatur, Einstellungen)' -ForegroundColor DarkGray
|
||||
Write-Host ""
|
||||
|
||||
do {
|
||||
$choice = Read-Host ' Ihre Wahl [1 oder 2]'
|
||||
} while ($choice -ne "1" -and $choice -ne "2")
|
||||
|
||||
return $choice
|
||||
}
|
||||
|
||||
# --- Schritt 2: AZA-Prozesse beenden --------------------------------
|
||||
|
||||
function Stop-AzaProcesses {
|
||||
Write-Step "1" "AZA-Prozesse pruefen und beenden"
|
||||
|
||||
$procs = Get-Process -Name $AZA_PROCESS_NAME -ErrorAction SilentlyContinue
|
||||
if (-not $procs) {
|
||||
Write-Ok "Keine laufenden AZA-Prozesse gefunden."
|
||||
return
|
||||
}
|
||||
|
||||
$count = @($procs).Count
|
||||
Write-Host " $count AZA-Prozess(e) gefunden. Beende..." -ForegroundColor White
|
||||
|
||||
foreach ($p in $procs) {
|
||||
try {
|
||||
$p.CloseMainWindow() | Out-Null
|
||||
} catch {}
|
||||
}
|
||||
Start-Sleep -Seconds 2
|
||||
|
||||
$remaining = Get-Process -Name $AZA_PROCESS_NAME -ErrorAction SilentlyContinue
|
||||
if ($remaining) {
|
||||
Write-Warn "Prozesse reagieren nicht - erzwinge Beenden..."
|
||||
$remaining | Stop-Process -Force -ErrorAction SilentlyContinue
|
||||
Start-Sleep -Seconds 1
|
||||
}
|
||||
|
||||
$still = Get-Process -Name $AZA_PROCESS_NAME -ErrorAction SilentlyContinue
|
||||
if ($still) {
|
||||
Write-Fail "Konnte nicht alle AZA-Prozesse beenden. Bitte manuell schliessen."
|
||||
} else {
|
||||
Write-Ok "Alle AZA-Prozesse beendet ($count Stueck)."
|
||||
}
|
||||
}
|
||||
|
||||
# --- Schritt 3: Inno Setup Uninstaller aufrufen ---------------------
|
||||
|
||||
function Find-UninstallString {
|
||||
$regPaths = @(
|
||||
"HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\$($AZA_APP_ID)_is1",
|
||||
"HKLM:\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\$($AZA_APP_ID)_is1",
|
||||
"HKCU:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\$($AZA_APP_ID)_is1"
|
||||
)
|
||||
foreach ($rp in $regPaths) {
|
||||
if (Test-Path $rp) {
|
||||
$val = (Get-ItemProperty -Path $rp -ErrorAction SilentlyContinue).UninstallString
|
||||
if ($val) { return $val }
|
||||
}
|
||||
}
|
||||
return $null
|
||||
}
|
||||
|
||||
function Find-InstallLocation {
|
||||
$regPaths = @(
|
||||
"HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\$($AZA_APP_ID)_is1",
|
||||
"HKLM:\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\$($AZA_APP_ID)_is1",
|
||||
"HKCU:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\$($AZA_APP_ID)_is1"
|
||||
)
|
||||
foreach ($rp in $regPaths) {
|
||||
if (Test-Path $rp) {
|
||||
$val = (Get-ItemProperty -Path $rp -ErrorAction SilentlyContinue).InstallLocation
|
||||
if ($val) { return $val.TrimEnd('\') }
|
||||
}
|
||||
}
|
||||
if (Test-Path $INSTALL_DIR_DEFAULT) { return $INSTALL_DIR_DEFAULT }
|
||||
return $null
|
||||
}
|
||||
|
||||
function Invoke-InnoUninstall {
|
||||
Write-Step "2" "AZA Desktop deinstallieren (Inno Setup)"
|
||||
|
||||
$uninstStr = Find-UninstallString
|
||||
if (-not $uninstStr) {
|
||||
Write-Skip "Kein Inno-Setup-Uninstaller in der Registry gefunden."
|
||||
Write-Host " Versuche manuelle Bereinigung..." -ForegroundColor DarkGray
|
||||
return $false
|
||||
}
|
||||
|
||||
$uninstExe = $uninstStr -replace '"', ''
|
||||
if (-not (Test-Path $uninstExe)) {
|
||||
Write-Warn "Uninstaller-Datei nicht gefunden: $uninstExe"
|
||||
return $false
|
||||
}
|
||||
|
||||
Write-Host " Starte Inno-Setup-Deinstallation (silent)..." -ForegroundColor White
|
||||
try {
|
||||
$proc = Start-Process -FilePath $uninstExe -ArgumentList "/VERYSILENT /NORESTART /SUPPRESSMSGBOXES" `
|
||||
-Wait -PassThru -ErrorAction Stop
|
||||
if ($proc.ExitCode -eq 0) {
|
||||
Write-Ok "Inno-Setup-Deinstallation erfolgreich abgeschlossen."
|
||||
Start-Sleep -Seconds 2
|
||||
return $true
|
||||
} else {
|
||||
Write-Warn "Uninstaller beendet mit Code $($proc.ExitCode)."
|
||||
return $false
|
||||
}
|
||||
} catch {
|
||||
Write-Fail "Fehler beim Ausfuehren des Uninstallers: $_"
|
||||
return $false
|
||||
}
|
||||
}
|
||||
|
||||
# --- Schritt 4: Restdateien bereinigen ------------------------------
|
||||
|
||||
function Remove-InstallRemnants {
|
||||
Write-Step "3" "Installationsreste bereinigen"
|
||||
|
||||
$installDir = Find-InstallLocation
|
||||
if (-not $installDir) {
|
||||
$installDir = $INSTALL_DIR_DEFAULT
|
||||
}
|
||||
|
||||
if (Test-Path $installDir) {
|
||||
Write-Host " Entferne Installationsverzeichnis: $installDir" -ForegroundColor White
|
||||
try {
|
||||
Remove-Item -Path $installDir -Recurse -Force -ErrorAction Stop
|
||||
Write-Ok "Installationsverzeichnis entfernt."
|
||||
} catch {
|
||||
Write-Warn "Einige Dateien konnten nicht entfernt werden (evtl. gesperrt)."
|
||||
Write-Host " $($_.Exception.Message)" -ForegroundColor DarkGray
|
||||
}
|
||||
} else {
|
||||
Write-Ok "Kein Installationsverzeichnis vorhanden."
|
||||
}
|
||||
|
||||
$startMenuDir = Join-Path $env:ProgramData 'Microsoft\Windows\Start Menu\Programs\AZA Desktop'
|
||||
if (Test-Path $startMenuDir) {
|
||||
Remove-Item -Path $startMenuDir -Recurse -Force -ErrorAction SilentlyContinue
|
||||
Write-Ok "Startmenue-Eintraege entfernt."
|
||||
}
|
||||
|
||||
$desktopShortcut = Join-Path ([Environment]::GetFolderPath("Desktop")) "AZA Desktop.lnk"
|
||||
if (Test-Path $desktopShortcut) {
|
||||
Remove-Item -Path $desktopShortcut -Force -ErrorAction SilentlyContinue
|
||||
Write-Ok "Desktop-Verknuepfung entfernt."
|
||||
}
|
||||
|
||||
$publicDesktop = Join-Path $env:PUBLIC 'Desktop\AZA Desktop.lnk'
|
||||
if (Test-Path $publicDesktop) {
|
||||
Remove-Item -Path $publicDesktop -Force -ErrorAction SilentlyContinue
|
||||
Write-Ok "Oeffentliche Desktop-Verknuepfung entfernt."
|
||||
}
|
||||
}
|
||||
|
||||
# --- Schritt 5: Firewall-Regel entfernen ----------------------------
|
||||
|
||||
function Remove-FirewallRule {
|
||||
Write-Step "4" "Firewall-Regel bereinigen"
|
||||
|
||||
try {
|
||||
$existing = netsh advfirewall firewall show rule name="$AZA_FIREWALL_RULE" 2>&1
|
||||
if ($existing -match "Regelname|Rule Name") {
|
||||
netsh advfirewall firewall delete rule name="$AZA_FIREWALL_RULE" 2>&1 | Out-Null
|
||||
Write-Ok "Firewall-Regel entfernt."
|
||||
} else {
|
||||
Write-Ok "Keine AZA-Firewall-Regel vorhanden."
|
||||
}
|
||||
} catch {
|
||||
Write-Skip "Firewall-Regel konnte nicht geprueft werden."
|
||||
}
|
||||
}
|
||||
|
||||
# --- Schritt 6: Benutzerdaten (optional) ----------------------------
|
||||
|
||||
function Remove-UserData {
|
||||
param([bool]$DeleteUserData)
|
||||
|
||||
Write-Step "5" "Benutzerdaten"
|
||||
|
||||
if (-not $DeleteUserData) {
|
||||
Write-Ok "Benutzerdaten werden BEHALTEN."
|
||||
if (Test-Path $AZA_APPDATA_DIR) {
|
||||
Write-Host " AppData: $AZA_APPDATA_DIR" -ForegroundColor DarkGray
|
||||
}
|
||||
if (Test-Path $AZA_DOCS_DIR) {
|
||||
Write-Host " Dokumente: $AZA_DOCS_DIR" -ForegroundColor DarkGray
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
Write-Warn "Benutzerdaten werden GELOESCHT."
|
||||
|
||||
if (Test-Path $AZA_APPDATA_DIR) {
|
||||
Write-Host " Entferne AppData: $AZA_APPDATA_DIR" -ForegroundColor White
|
||||
try {
|
||||
Remove-Item -Path $AZA_APPDATA_DIR -Recurse -Force -ErrorAction Stop
|
||||
Write-Ok "AppData entfernt (Profil, Einstellungen, Lizenz, Consent, Autotext, Stilprofile)."
|
||||
} catch {
|
||||
Write-Warn "Einige AppData-Dateien konnten nicht entfernt werden."
|
||||
Write-Host " $($_.Exception.Message)" -ForegroundColor DarkGray
|
||||
}
|
||||
} else {
|
||||
Write-Ok "Kein AppData-Verzeichnis vorhanden."
|
||||
}
|
||||
|
||||
if (Test-Path $AZA_DOCS_DIR) {
|
||||
Write-Host " Entferne Dokumente: $AZA_DOCS_DIR" -ForegroundColor White
|
||||
try {
|
||||
Remove-Item -Path $AZA_DOCS_DIR -Recurse -Force -ErrorAction Stop
|
||||
Write-Ok "Dokumente entfernt (Transkripte, Briefe, Rezepte, Kostengutsprachen)."
|
||||
} catch {
|
||||
Write-Warn "Einige Dokument-Dateien konnten nicht entfernt werden."
|
||||
Write-Host " $($_.Exception.Message)" -ForegroundColor DarkGray
|
||||
}
|
||||
} else {
|
||||
Write-Ok "Kein Dokumente-Verzeichnis vorhanden."
|
||||
}
|
||||
}
|
||||
|
||||
# --- Schritt 7: Registry-Reste pruefen ------------------------------
|
||||
|
||||
function Clean-RegistryRemnants {
|
||||
Write-Step "6" "Registry-Eintraege pruefen"
|
||||
|
||||
$regPaths = @(
|
||||
"HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\$($AZA_APP_ID)_is1",
|
||||
"HKLM:\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\$($AZA_APP_ID)_is1",
|
||||
"HKCU:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\$($AZA_APP_ID)_is1"
|
||||
)
|
||||
|
||||
$found = $false
|
||||
foreach ($rp in $regPaths) {
|
||||
if (Test-Path $rp) {
|
||||
try {
|
||||
Remove-Item -Path $rp -Recurse -Force -ErrorAction Stop
|
||||
Write-Ok "Registry-Eintrag entfernt: $rp"
|
||||
$found = $true
|
||||
} catch {
|
||||
Write-Warn "Registry-Eintrag konnte nicht entfernt werden: $rp"
|
||||
}
|
||||
}
|
||||
}
|
||||
if (-not $found) {
|
||||
Write-Ok "Keine AZA-Registry-Eintraege vorhanden."
|
||||
}
|
||||
}
|
||||
|
||||
# --- Schritt 8: Abschlussbericht ------------------------------------
|
||||
|
||||
function Show-Summary {
|
||||
param([string]$Mode, [bool]$DeletedData)
|
||||
|
||||
Write-Host ""
|
||||
Write-Host " =============================================" -ForegroundColor White
|
||||
Write-Host " ERGEBNIS" -ForegroundColor White
|
||||
Write-Host " =============================================" -ForegroundColor White
|
||||
Write-Host ""
|
||||
|
||||
if ($Mode -eq "1") {
|
||||
Write-Host " Modus: Nur App entfernt" -ForegroundColor Green
|
||||
Write-Host " Benutzerdaten: BEHALTEN" -ForegroundColor Green
|
||||
} else {
|
||||
Write-Host " Modus: Vollstaendig zurueckgesetzt" -ForegroundColor Yellow
|
||||
Write-Host " Benutzerdaten: GELOESCHT" -ForegroundColor Yellow
|
||||
}
|
||||
|
||||
Write-Host ""
|
||||
|
||||
$needsRestart = $false
|
||||
$installDir = Find-InstallLocation
|
||||
if (-not $installDir) { $installDir = $INSTALL_DIR_DEFAULT }
|
||||
if (Test-Path $installDir) {
|
||||
Write-Warn "Installationsverzeichnis existiert noch (evtl. gesperrte Dateien)."
|
||||
Write-Host " Ein Neustart koennte noetig sein." -ForegroundColor DarkGray
|
||||
$needsRestart = $true
|
||||
}
|
||||
|
||||
$azaStillRunning = Get-Process -Name $AZA_PROCESS_NAME -ErrorAction SilentlyContinue
|
||||
if ($azaStillRunning) {
|
||||
Write-Warn "AZA-Prozess laeuft noch! Bitte manuell schliessen oder PC neustarten."
|
||||
$needsRestart = $true
|
||||
}
|
||||
|
||||
if ($needsRestart) {
|
||||
Write-Host ""
|
||||
Write-Host " NEUSTART: Moeglicherweise erforderlich (s. Hinweise oben)" -ForegroundColor Yellow
|
||||
Write-Host " NEUINSTALLATION: Nach Neustart moeglich" -ForegroundColor Yellow
|
||||
} else {
|
||||
Write-Host ""
|
||||
Write-Host " NEUSTART: NICHT erforderlich" -ForegroundColor Green
|
||||
Write-Host " NEUINSTALLATION: Sofort moeglich" -ForegroundColor Green
|
||||
}
|
||||
|
||||
Write-Host ""
|
||||
|
||||
$setupExe = $null
|
||||
$searchPaths = @(
|
||||
(Join-Path $PSScriptRoot '..\dist\installer\aza_desktop_setup.exe'),
|
||||
(Join-Path $PSScriptRoot '..\aza_desktop_setup.exe')
|
||||
)
|
||||
foreach ($sp in $searchPaths) {
|
||||
if (Test-Path $sp) {
|
||||
$setupExe = (Resolve-Path $sp).Path
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if ($setupExe) {
|
||||
Write-Host " Installer gefunden:" -ForegroundColor Cyan
|
||||
Write-Host " $setupExe" -ForegroundColor White
|
||||
Write-Host ""
|
||||
Write-Host " Zum Neuinstallieren einfach den Installer starten." -ForegroundColor White
|
||||
} else {
|
||||
Write-Host " Kein Installer im Projekt gefunden." -ForegroundColor DarkGray
|
||||
Write-Host " Bitte den neuen Installer manuell starten." -ForegroundColor DarkGray
|
||||
}
|
||||
|
||||
Write-Host ""
|
||||
Write-Host " =============================================" -ForegroundColor White
|
||||
Write-Host ""
|
||||
}
|
||||
|
||||
# --- Hauptablauf ----------------------------------------------------
|
||||
|
||||
function Main {
|
||||
Write-Banner
|
||||
|
||||
$mode = Select-Mode
|
||||
$deleteData = ($mode -eq "2")
|
||||
|
||||
if ($deleteData) {
|
||||
Write-Host ""
|
||||
Write-Host " ACHTUNG: Modus 2 loescht ALLE Benutzerdaten unwiderruflich!" -ForegroundColor Red
|
||||
Write-Host " (Profil, Transkripte, Briefe, Rezepte, Einstellungen, Signatur, Autotext)" -ForegroundColor Red
|
||||
Write-Host ""
|
||||
$confirm = Read-Host ' Wirklich fortfahren? [j/n]'
|
||||
if ($confirm -ne "j" -and $confirm -ne "J" -and $confirm -ne "ja" -and $confirm -ne "Ja") {
|
||||
Write-Host ""
|
||||
Write-Host " Abgebrochen." -ForegroundColor Yellow
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
Write-Host ""
|
||||
Write-Host " Starte Deinstallation..." -ForegroundColor White
|
||||
|
||||
Stop-AzaProcesses
|
||||
$innoOk = Invoke-InnoUninstall
|
||||
Remove-InstallRemnants
|
||||
Remove-FirewallRule
|
||||
Remove-UserData -DeleteUserData $deleteData
|
||||
if (-not $innoOk) {
|
||||
Clean-RegistryRemnants
|
||||
}
|
||||
|
||||
Show-Summary -Mode $mode -DeletedData $deleteData
|
||||
}
|
||||
|
||||
Main
|
||||
134
AzA march 2026 - Kopie (28)/tools/dev_status_window.py
Normal file
134
AzA march 2026 - Kopie (28)/tools/dev_status_window.py
Normal file
@@ -0,0 +1,134 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
AZA Dev Status – lightweight console viewer.
|
||||
|
||||
Polls /api/project/status every 5 seconds and displays
|
||||
the current project state plus the last 15 history entries.
|
||||
Exit cleanly with Ctrl+C.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
from pathlib import Path
|
||||
from urllib.request import Request, urlopen
|
||||
from urllib.error import URLError
|
||||
|
||||
_BASE_DIR = Path(__file__).resolve().parent.parent
|
||||
_TOKEN_FILE = _BASE_DIR / "backend_token.txt"
|
||||
_HISTORY_FILE = _BASE_DIR / "project_status_history.jsonl"
|
||||
_API_URL = "http://127.0.0.1:8000/api/project/status"
|
||||
_REFRESH = 5
|
||||
|
||||
|
||||
def _read_token() -> str:
|
||||
tok = os.environ.get("MEDWORK_API_TOKEN", "").strip()
|
||||
if tok:
|
||||
return tok
|
||||
try:
|
||||
with open(str(_TOKEN_FILE), "r", encoding="utf-8") as f:
|
||||
return (f.readline() or "").strip()
|
||||
except Exception:
|
||||
return ""
|
||||
|
||||
|
||||
def _fetch_status(token: str) -> dict | None:
|
||||
try:
|
||||
req = Request(_API_URL, headers={"X-API-Token": token})
|
||||
with urlopen(req, timeout=4) as resp:
|
||||
return json.loads(resp.read().decode("utf-8", errors="replace"))
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
|
||||
def _read_history(n: int = 15) -> list[str]:
|
||||
try:
|
||||
with open(str(_HISTORY_FILE), "r", encoding="utf-8") as f:
|
||||
lines = f.readlines()
|
||||
return lines[-n:]
|
||||
except Exception:
|
||||
return []
|
||||
|
||||
|
||||
def _clear():
|
||||
os.system("cls" if os.name == "nt" else "clear")
|
||||
|
||||
|
||||
def _render(data: dict | None, history: list[str]):
|
||||
_clear()
|
||||
print("=" * 60)
|
||||
print(" AZA Dev Status")
|
||||
print("=" * 60)
|
||||
|
||||
if data is None:
|
||||
print("\n Waiting for backend ...\n")
|
||||
else:
|
||||
fields = [
|
||||
("Project", data.get("project", "")),
|
||||
("Phase", data.get("phase", "")),
|
||||
("Current Step", data.get("current_step", "")),
|
||||
("Next Step", data.get("next_step", "")),
|
||||
("Last Update", data.get("last_update", "")),
|
||||
("Updated At", data.get("updated_at", "")),
|
||||
]
|
||||
print()
|
||||
for label, val in fields:
|
||||
print(f" {label:14s}: {val}")
|
||||
|
||||
notes = data.get("notes", "")
|
||||
if notes:
|
||||
print(f"\n Notes: {notes}")
|
||||
|
||||
print()
|
||||
print("-" * 60)
|
||||
print(" History (last 15)")
|
||||
print("-" * 60)
|
||||
|
||||
if not history:
|
||||
print(" (no history yet)")
|
||||
else:
|
||||
for line in history:
|
||||
line = line.strip()
|
||||
if not line:
|
||||
continue
|
||||
try:
|
||||
entry = json.loads(line)
|
||||
ts = entry.get("ts_utc", "")[:19].replace("T", " ")
|
||||
phase = entry.get("phase", "")
|
||||
step = entry.get("current_step", "")
|
||||
print(f" {ts} {phase:12s} {step}")
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
print()
|
||||
print(f" Refresh every {_REFRESH}s | Ctrl+C to exit")
|
||||
print("=" * 60)
|
||||
|
||||
|
||||
def main():
|
||||
if os.name == "nt":
|
||||
try:
|
||||
os.system("title AZA Dev Status")
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
token = _read_token()
|
||||
if not token:
|
||||
print("WARNING: No API token found. Requests will fail (401).")
|
||||
|
||||
try:
|
||||
while True:
|
||||
data = _fetch_status(token)
|
||||
history = _read_history()
|
||||
_render(data, history)
|
||||
time.sleep(_REFRESH)
|
||||
except KeyboardInterrupt:
|
||||
print("\n Bye.")
|
||||
sys.exit(0)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,189 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Offline-Diagnose: Empfang-Praxen, Konten, Sessions, Geräte, externe Links.
|
||||
|
||||
Nur lesend. Keine Datenänderung. Für Betrieb: auf dem Server im Projektroot
|
||||
oder mit --data-dir ausführen.
|
||||
|
||||
Beispiel:
|
||||
python tools/diagnose_empfang_practice_inventory.py
|
||||
python tools/diagnose_empfang_practice_inventory.py --data-dir /pfad/zum/data
|
||||
|
||||
Ausgabe: stdout (UTF-8). Keine vollen Lizenzschlüssel; Konten ohne pw_hash-Inhalt.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import hashlib
|
||||
import json
|
||||
from collections import defaultdict
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
|
||||
def _load(path: Path, default):
|
||||
if not path.is_file():
|
||||
return default
|
||||
try:
|
||||
return json.loads(path.read_text(encoding="utf-8"))
|
||||
except Exception as exc:
|
||||
print(f"# FEHLER beim Lesen {path}: {exc}")
|
||||
return default
|
||||
|
||||
|
||||
def _short_uid(uid: str, keep: int = 10) -> str:
|
||||
s = (uid or "").strip()
|
||||
if len(s) <= keep:
|
||||
return s
|
||||
return s[:keep] + "…"
|
||||
|
||||
|
||||
def _license_tail(acc: dict) -> str:
|
||||
for key in ("license_key", "activation_key", "stored_license"):
|
||||
raw = (acc.get(key) or "").strip()
|
||||
if raw:
|
||||
return "***" + raw[-4:] if len(raw) >= 4 else "****"
|
||||
return ""
|
||||
|
||||
|
||||
def main() -> int:
|
||||
ap = argparse.ArgumentParser()
|
||||
ap.add_argument(
|
||||
"--data-dir",
|
||||
type=Path,
|
||||
help="Ordner mit empfang_*.json (Default: ../data relativ zu Repo)",
|
||||
)
|
||||
args = ap.parse_args()
|
||||
here = Path(__file__).resolve().parent
|
||||
repo = here.parent
|
||||
data_dir = args.data_dir or (repo / "data")
|
||||
|
||||
paths = {
|
||||
"practices": data_dir / "empfang_practices.json",
|
||||
"accounts": data_dir / "empfang_accounts.json",
|
||||
"sessions": data_dir / "empfang_sessions.json",
|
||||
"devices": data_dir / "empfang_devices.json",
|
||||
"links": data_dir / "empfang_practice_links.json",
|
||||
"connections": data_dir / "empfang_connections.json",
|
||||
}
|
||||
|
||||
print("=== Empfang Practice Inventory (read-only) ===")
|
||||
print(f"data_dir={data_dir.resolve()}")
|
||||
for name, p in paths.items():
|
||||
print(f" {name}: {'OK' if p.is_file() else '— fehlt —'} {p.name}")
|
||||
|
||||
practices: dict[str, Any] = _load(paths["practices"], {})
|
||||
accounts_raw = _load(paths["accounts"], {})
|
||||
sessions_raw = _load(paths["sessions"], {})
|
||||
devices_raw = _load(paths["devices"], {})
|
||||
links_raw = _load(paths["links"], {"links": []})
|
||||
connections_raw = _load(paths["connections"], [])
|
||||
|
||||
accounts: dict[str, dict] = accounts_raw if isinstance(accounts_raw, dict) else {}
|
||||
sessions_dict: dict = sessions_raw if isinstance(sessions_raw, dict) else {}
|
||||
devices_dict: dict = devices_raw if isinstance(devices_raw, dict) else {}
|
||||
links: list = links_raw.get("links") or [] if isinstance(links_raw, dict) else []
|
||||
|
||||
# Accounts by practice
|
||||
by_practice: dict[str, list[dict]] = defaultdict(list)
|
||||
for uid, a in accounts.items():
|
||||
if not isinstance(a, dict):
|
||||
continue
|
||||
pid = (a.get("practice_id") or "").strip()
|
||||
if pid:
|
||||
by_practice[pid].append(a)
|
||||
|
||||
# Session counts by practice_id (token -> session record)
|
||||
sess_by_practice: dict[str, int] = defaultdict(int)
|
||||
for _tok, s in sessions_dict.items():
|
||||
if isinstance(s, dict):
|
||||
pid = (s.get("practice_id") or "").strip()
|
||||
if pid:
|
||||
sess_by_practice[pid] += 1
|
||||
|
||||
dev_by_practice: dict[str, int] = defaultdict(int)
|
||||
for _dk, d in devices_dict.items():
|
||||
if not isinstance(d, dict):
|
||||
continue
|
||||
pid = (d.get("practice_id") or "").strip()
|
||||
if pid:
|
||||
dev_by_practice[pid] += 1
|
||||
|
||||
ext_count: dict[str, int] = defaultdict(int)
|
||||
for L in links:
|
||||
if not isinstance(L, dict):
|
||||
continue
|
||||
for k in ("source_practice_id", "target_practice_id"):
|
||||
pid = (L.get(k) or "").strip()
|
||||
if pid:
|
||||
ext_count[pid] += 1
|
||||
|
||||
print("\n--- Praxis-Übersicht ---")
|
||||
all_pids = sorted(set(practices.keys()) | set(by_practice.keys()))
|
||||
dup_name_hints: dict[str, list[str]] = defaultdict(list)
|
||||
for pid, pdata in practices.items():
|
||||
if isinstance(pdata, dict):
|
||||
nm = (pdata.get("name") or "").strip().lower()
|
||||
if nm:
|
||||
dup_name_hints[nm].append(pid)
|
||||
|
||||
for pid in all_pids:
|
||||
pdata = practices.get(pid, {}) if isinstance(practices.get(pid), dict) else {}
|
||||
name = (pdata.get("name") or "").strip() or "(ohne Eintrag in empfang_practices.json)"
|
||||
invite = (pdata.get("invite_code") or "").strip()
|
||||
invite_h = hashlib.sha256(invite.encode()).hexdigest()[:10] if invite else ""
|
||||
members = by_practice.get(pid, [])
|
||||
admins = [m for m in members if str(m.get("role") or "").lower() == "admin"]
|
||||
print(f"\n* practice_id: {pid}")
|
||||
print(f" practice_name: {name}")
|
||||
print(f" invite_code: {invite or '—'} (sha256-prefix: {invite_h or '—'})")
|
||||
print(f" user_count: {len(members)} admin_count: {len(admins)}")
|
||||
if admins:
|
||||
print(" admins: " + ", ".join((a.get("display_name") or "?") for a in admins[:20]))
|
||||
print(f" sessions_count: {sess_by_practice.get(pid, 0)}")
|
||||
print(f" devices_count: {dev_by_practice.get(pid, 0)}")
|
||||
print(f" external_link_rows_touching_practice: {ext_count.get(pid, 0)}")
|
||||
print(" users (gekürzt):")
|
||||
for m in sorted(members, key=lambda x: str(x.get("display_name") or "").lower())[:40]:
|
||||
dn = (m.get("display_name") or "").strip()
|
||||
ln = (m.get("login_name") or "").strip()
|
||||
role = (m.get("role") or "").strip()
|
||||
uid = _short_uid(str(m.get("user_id") or ""))
|
||||
lt = _license_tail(m)
|
||||
print(f" - {dn!r} login={ln!r} role={role} uid={uid} lic_hint={lt or '—'}")
|
||||
if len(members) > 40:
|
||||
print(f" … ({len(members) - 40} weitere)")
|
||||
|
||||
print("\n--- Hinweise Dubletten (gleicher Praxisname, verschiedene IDs) ---")
|
||||
found = False
|
||||
for nm, pids in dup_name_hints.items():
|
||||
if len(pids) > 1:
|
||||
found = True
|
||||
print(f" name_low={nm!r} -> {pids}")
|
||||
if not found:
|
||||
print(" keine Namens-Kollisionen in empfang_practices.json")
|
||||
|
||||
print("\n--- Konten ohne Praxis-Eintrag in practices.json ---")
|
||||
orphan_accounts = []
|
||||
for uid, a in accounts.items():
|
||||
if not isinstance(a, dict):
|
||||
continue
|
||||
pid = (a.get("practice_id") or "").strip()
|
||||
if pid and pid not in practices:
|
||||
orphan_accounts.append((pid, a))
|
||||
if not orphan_accounts:
|
||||
print(" keine")
|
||||
else:
|
||||
for pid, a in orphan_accounts[:50]:
|
||||
print(f" practice_id={pid} user={a.get('display_name')} (Praxis-Stammsatz fehlt)")
|
||||
|
||||
print("\n--- Ende (keine Änderungen vorgenommen) ---")
|
||||
print(
|
||||
"Reparatur: nicht automatisch. STRATEGIE: siehe Nutzer-Anforderung BLOCK 5 / Arzt-Admin entscheidet nach Backup."
|
||||
)
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
264
AzA march 2026 - Kopie (28)/tools/stack_probe.py
Normal file
264
AzA march 2026 - Kopie (28)/tools/stack_probe.py
Normal file
@@ -0,0 +1,264 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Project stack probe (backend focus).
|
||||
|
||||
Usage:
|
||||
python tools/stack_probe.py
|
||||
|
||||
What it does:
|
||||
- Scans common manifest files (pyproject.toml, requirements*.txt, Pipfile,
|
||||
package.json, etc.)
|
||||
- Greps for framework imports/usages in source files (FastAPI/Flask/Django,
|
||||
Express/Nest, etc.)
|
||||
- Prints a short, actionable summary.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from typing import Dict, List, Optional, Tuple
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
|
||||
|
||||
def _read_text(p: Path, max_bytes: int = 512_000) -> str:
|
||||
try:
|
||||
data = p.read_bytes()
|
||||
if len(data) > max_bytes:
|
||||
data = data[:max_bytes]
|
||||
return data.decode("utf-8", errors="ignore")
|
||||
except Exception:
|
||||
return ""
|
||||
|
||||
|
||||
def _exists(*parts: str) -> Optional[Path]:
|
||||
p = ROOT.joinpath(*parts)
|
||||
return p if p.exists() else None
|
||||
|
||||
|
||||
def _collect_manifests() -> List[Path]:
|
||||
candidates: List[Path] = []
|
||||
for name in [
|
||||
"pyproject.toml",
|
||||
"requirements.txt",
|
||||
"requirements-dev.txt",
|
||||
"requirements.in",
|
||||
"Pipfile",
|
||||
"poetry.lock",
|
||||
"setup.cfg",
|
||||
"package.json",
|
||||
"pnpm-lock.yaml",
|
||||
"yarn.lock",
|
||||
"package-lock.json",
|
||||
"composer.json",
|
||||
"go.mod",
|
||||
"Cargo.toml",
|
||||
]:
|
||||
p = _exists(name)
|
||||
if p:
|
||||
candidates.append(p)
|
||||
return candidates
|
||||
|
||||
|
||||
def _load_package_json(p: Path) -> Dict:
|
||||
try:
|
||||
return json.loads(_read_text(p))
|
||||
except Exception:
|
||||
return {}
|
||||
|
||||
|
||||
def _scan_deps_from_text(text: str) -> List[str]:
|
||||
"""Extracts 'likely dependency tokens' from common python/lock formats."""
|
||||
tokens = set()
|
||||
# requirements style
|
||||
for line in text.splitlines():
|
||||
line = line.strip()
|
||||
if not line or line.startswith("#") or line.startswith("-"):
|
||||
continue
|
||||
# package==1.2.3, package>=, package[extra]
|
||||
m = re.match(r"^([A-Za-z0-9_.\-]+)", line)
|
||||
if m:
|
||||
tokens.add(m.group(1).lower())
|
||||
return sorted(tokens)
|
||||
|
||||
|
||||
def _scan_source_signatures() -> Dict[str, List[Path]]:
|
||||
"""
|
||||
Grep a small number of files for telltale imports/usages.
|
||||
We keep it cheap: limit file count and size.
|
||||
"""
|
||||
patterns: Dict[str, re.Pattern] = {
|
||||
# Python
|
||||
"fastapi": re.compile(r"\bfrom\s+fastapi\b|\bFastAPI\s*\(", re.I),
|
||||
"flask": re.compile(r"\bfrom\s+flask\b|\bFlask\s*\(", re.I),
|
||||
"django": re.compile(r"\bdjango\b|\bmanage\.py\b", re.I),
|
||||
"starlette": re.compile(r"\bstarlette\b", re.I),
|
||||
"uvicorn": re.compile(r"\buvicorn\b", re.I),
|
||||
"gunicorn": re.compile(r"\bgunicorn\b", re.I),
|
||||
# Node
|
||||
"express": re.compile(r"\brequire\(['\"]express['\"]\)|\bfrom\s+['\"]express['\"]|\bexpress\(\)", re.I),
|
||||
"nestjs": re.compile(r"@nestjs/", re.I),
|
||||
"koa": re.compile(r"\bkoa\b", re.I),
|
||||
"hapi": re.compile(r"@hapi/|\bhapi\b", re.I),
|
||||
}
|
||||
hits: Dict[str, List[Path]] = {k: [] for k in patterns.keys()}
|
||||
|
||||
# Pick likely source roots
|
||||
roots = []
|
||||
for d in ["backend", "server", "api", "src", "app"]:
|
||||
p = _exists(d)
|
||||
if p and p.is_dir():
|
||||
roots.append(p)
|
||||
if not roots:
|
||||
roots = [ROOT]
|
||||
|
||||
exts = {".py", ".js", ".ts", ".mjs", ".cjs"}
|
||||
max_files = 1200
|
||||
max_file_bytes = 256_000
|
||||
scanned = 0
|
||||
|
||||
for base in roots:
|
||||
for p in base.rglob("*"):
|
||||
if scanned >= max_files:
|
||||
break
|
||||
if not p.is_file():
|
||||
continue
|
||||
if p.suffix.lower() not in exts:
|
||||
continue
|
||||
# skip huge files / node_modules / venv
|
||||
if any(part in {"node_modules", ".venv", "venv", "__pycache__", "dist", "build"} for part in p.parts):
|
||||
continue
|
||||
try:
|
||||
if p.stat().st_size > max_file_bytes:
|
||||
continue
|
||||
except Exception:
|
||||
continue
|
||||
|
||||
text = _read_text(p, max_bytes=max_file_bytes)
|
||||
scanned += 1
|
||||
for key, pat in patterns.items():
|
||||
if pat.search(text):
|
||||
hits[key].append(p.relative_to(ROOT))
|
||||
|
||||
# prune empties
|
||||
hits = {k: v for k, v in hits.items() if v}
|
||||
return hits
|
||||
|
||||
|
||||
def _infer_stack(manifests: List[Path], dep_tokens: List[str], pkg: Dict, sig_hits: Dict[str, List[Path]]) -> Tuple[str, List[str]]:
|
||||
notes: List[str] = []
|
||||
|
||||
# Node inference
|
||||
node_deps = set()
|
||||
if pkg:
|
||||
for section in ("dependencies", "devDependencies", "peerDependencies"):
|
||||
node_deps.update((pkg.get(section) or {}).keys())
|
||||
node_deps = {d.lower() for d in node_deps}
|
||||
if node_deps:
|
||||
notes.append(f"Node deps found: {', '.join(sorted(list(node_deps))[:12])}" + (" …" if len(node_deps) > 12 else ""))
|
||||
|
||||
# Python inference
|
||||
py_deps = set(dep_tokens)
|
||||
if py_deps:
|
||||
notes.append(f"Python deps found: {', '.join(sorted(list(py_deps))[:12])}" + (" …" if len(py_deps) > 12 else ""))
|
||||
|
||||
# Priority: signatures > deps
|
||||
def has_sig(k: str) -> bool:
|
||||
return k in sig_hits
|
||||
|
||||
# Decide primary backend framework
|
||||
if has_sig("fastapi") or "fastapi" in py_deps:
|
||||
stack = "Python / FastAPI"
|
||||
if "uvicorn" in py_deps or has_sig("uvicorn"):
|
||||
notes.append("Likely ASGI server: uvicorn")
|
||||
return stack, notes
|
||||
|
||||
if has_sig("flask") or "flask" in py_deps:
|
||||
stack = "Python / Flask"
|
||||
return stack, notes
|
||||
|
||||
if has_sig("django") or "django" in py_deps:
|
||||
stack = "Python / Django"
|
||||
return stack, notes
|
||||
|
||||
if has_sig("nestjs") or any(d.startswith("@nestjs/") for d in node_deps):
|
||||
return "Node / NestJS", notes
|
||||
|
||||
if has_sig("express") or "express" in node_deps:
|
||||
return "Node / Express", notes
|
||||
|
||||
if has_sig("koa") or "koa" in node_deps:
|
||||
return "Node / Koa", notes
|
||||
|
||||
if has_sig("hapi") or any(d.startswith("@hapi/") for d in node_deps):
|
||||
return "Node / hapi", notes
|
||||
|
||||
# Fallback
|
||||
if pkg and not py_deps:
|
||||
return "Node (framework unknown)", notes
|
||||
if py_deps and not pkg:
|
||||
return "Python (framework unknown)", notes
|
||||
if pkg and py_deps:
|
||||
return "Mixed (Node + Python) – backend unclear", notes
|
||||
return "Unknown", notes
|
||||
|
||||
|
||||
def main() -> int:
|
||||
print("=== STACK PROBE ===")
|
||||
print(f"Root: {ROOT}")
|
||||
|
||||
manifests = _collect_manifests()
|
||||
if manifests:
|
||||
print("\nManifests found:")
|
||||
for p in manifests:
|
||||
print(f" - {p.relative_to(ROOT)}")
|
||||
else:
|
||||
print("\nManifests found: (none)")
|
||||
|
||||
# Parse deps
|
||||
dep_tokens: List[str] = []
|
||||
pkg: Dict = {}
|
||||
|
||||
py_sources: List[Path] = []
|
||||
for p in manifests:
|
||||
if p.name == "package.json":
|
||||
pkg = _load_package_json(p)
|
||||
elif p.suffix in {".toml", ".cfg"} or p.name.startswith("requirements") or p.name in {"Pipfile"}:
|
||||
py_sources.append(p)
|
||||
|
||||
for p in py_sources:
|
||||
dep_tokens.extend(_scan_deps_from_text(_read_text(p)))
|
||||
dep_tokens = sorted(set(dep_tokens))
|
||||
|
||||
sig_hits = _scan_source_signatures()
|
||||
stack, notes = _infer_stack(manifests, dep_tokens, pkg, sig_hits)
|
||||
|
||||
print("\n--- Result ---")
|
||||
print(f"Likely backend stack: {stack}")
|
||||
|
||||
if sig_hits:
|
||||
print("\nFramework signatures (files):")
|
||||
for k, files in sorted(sig_hits.items()):
|
||||
sample = ", ".join(str(f) for f in files[:5])
|
||||
more = " …" if len(files) > 5 else ""
|
||||
print(f" - {k}: {sample}{more}")
|
||||
|
||||
if notes:
|
||||
print("\nNotes:")
|
||||
for n in notes:
|
||||
print(f" - {n}")
|
||||
|
||||
print("\nNext:")
|
||||
print(" - Paste the 'Likely backend stack' line back into chat.")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
|
||||
33
AzA march 2026 - Kopie (28)/tools/wait_http_ready.py
Normal file
33
AzA march 2026 - Kopie (28)/tools/wait_http_ready.py
Normal file
@@ -0,0 +1,33 @@
|
||||
import sys
|
||||
import time
|
||||
import urllib.request
|
||||
|
||||
|
||||
def main() -> int:
|
||||
if len(sys.argv) < 3:
|
||||
return 2
|
||||
|
||||
url = (sys.argv[1] or "").strip()
|
||||
try:
|
||||
timeout_sec = int(sys.argv[2])
|
||||
except Exception:
|
||||
timeout_sec = 30
|
||||
|
||||
if not url:
|
||||
return 2
|
||||
|
||||
deadline = time.time() + max(1, timeout_sec)
|
||||
while time.time() < deadline:
|
||||
try:
|
||||
with urllib.request.urlopen(url, timeout=2) as resp:
|
||||
code = getattr(resp, "status", 200)
|
||||
if 200 <= code < 500:
|
||||
return 0
|
||||
except Exception:
|
||||
pass
|
||||
time.sleep(1.0)
|
||||
return 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
Reference in New Issue
Block a user