update
This commit is contained in:
702
AzA march 2026 - Kopie (28)/aza_devstatus_handover_shell.py
Normal file
702
AzA march 2026 - Kopie (28)/aza_devstatus_handover_shell.py
Normal file
@@ -0,0 +1,702 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
AZA DevStatus- / Handover-Hülle — lokale read-only Status-App (kein Server, kein SSH).
|
||||
|
||||
Start (Cursor / Windows PowerShell, im Projektroot):
|
||||
py -3 .\\aza_devstatus_handover_shell.py
|
||||
|
||||
Umgebungsvariablen (optional):
|
||||
AZA_DEVSTATUS_XLSX — voller Pfad zur Excel-Datenquelle
|
||||
AZA_DEVSTATUS_JSON — voller Pfad zu Override-JSON (shallow merge in die eingebettete Basis)
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
import webbrowser
|
||||
import zipfile
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, List, Mapping, Optional, Sequence, Tuple
|
||||
import xml.etree.ElementTree as ET
|
||||
|
||||
import tkinter as tk
|
||||
from tkinter import messagebox, scrolledtext, ttk
|
||||
|
||||
# --- Pfade (fest dokumentiert; Override per ENV möglich) ---
|
||||
AZA_DRIVE = Path(r"C:\Users\surov\Documents\AzA Drive")
|
||||
PROJECT_ROOT = Path(r"C:\Users\surov\Documents\AZA_GIT\aza\AzA march 2026")
|
||||
EXCEL_BASENAME = "AZA_DevStatus_Handover_2026-05-14.xlsx"
|
||||
|
||||
_NS_MAIN = {"m": "http://schemas.openxmlformats.org/spreadsheetml/2006/main"}
|
||||
_NS_REL = {"r": "http://schemas.openxmlformats.org/package/2006/relationships"}
|
||||
_NS_REL_PKG = "http://schemas.openxmlformats.org/officeDocument/2006/relationships"
|
||||
_RID = "{%s}id" % _NS_REL_PKG
|
||||
|
||||
|
||||
def _app_base_dir() -> Path:
|
||||
if getattr(sys, "frozen", False) and hasattr(sys, "executable"):
|
||||
return Path(sys.executable).resolve().parent
|
||||
return Path(__file__).resolve().parent
|
||||
|
||||
|
||||
def _deep_merge(base: Dict[str, Any], ext: Mapping[str, Any]) -> None:
|
||||
for k, v in ext.items():
|
||||
if isinstance(v, dict) and isinstance(base.get(k), dict):
|
||||
_deep_merge(base[k], v)
|
||||
else:
|
||||
base[k] = v
|
||||
|
||||
|
||||
def _load_json_path(p: Path) -> Optional[Dict[str, Any]]:
|
||||
try:
|
||||
raw = p.read_text(encoding="utf-8")
|
||||
except OSError:
|
||||
return None
|
||||
try:
|
||||
data = json.loads(raw)
|
||||
except json.JSONDecodeError:
|
||||
return None
|
||||
return data if isinstance(data, dict) else None
|
||||
|
||||
|
||||
def _si_text(si: ET.Element) -> str:
|
||||
parts: List[str] = []
|
||||
for el in si.iter():
|
||||
if el.tag.endswith("}t") and el.text:
|
||||
parts.append(el.text)
|
||||
if el.tail:
|
||||
parts.append(el.tail)
|
||||
return "".join(parts).strip()
|
||||
|
||||
|
||||
def _read_xlsx_first_sheet(path: Path, max_rows: int = 80) -> Tuple[List[List[str]], str]:
|
||||
"""Erste Mappe, nur Text (shared strings / inline). Kein Excel nötig."""
|
||||
diag: List[str] = []
|
||||
rows: List[List[str]] = []
|
||||
try:
|
||||
with zipfile.ZipFile(path, "r") as zf:
|
||||
names = zf.namelist()
|
||||
diag.append(f"ZIP-Einträge: {len(names)}")
|
||||
|
||||
shared: List[str] = []
|
||||
if "xl/sharedStrings.xml" in names:
|
||||
with zf.open("xl/sharedStrings.xml") as fp:
|
||||
root = ET.parse(fp).getroot()
|
||||
for si in root.findall("m:si", _NS_MAIN):
|
||||
shared.append(_si_text(si))
|
||||
|
||||
if "xl/workbook.xml" not in names or "xl/_rels/workbook.xml.rels" not in names:
|
||||
return [], "workbook/rels fehlt in xlsx"
|
||||
|
||||
with zf.open("xl/workbook.xml") as fp:
|
||||
wb = ET.parse(fp).getroot()
|
||||
sheets = wb.findall("m:sheets/m:sheet", _NS_MAIN)
|
||||
if not sheets:
|
||||
return [], "Keine Sheets in workbook.xml"
|
||||
rid = sheets[0].attrib.get(_RID) or sheets[0].attrib.get("r:id")
|
||||
if not rid:
|
||||
return [], "sheet ohne r:id"
|
||||
|
||||
target: Optional[str] = None
|
||||
with zf.open("xl/_rels/workbook.xml.rels") as fp:
|
||||
rels = ET.parse(fp).getroot()
|
||||
for rel in rels.findall("r:Relationship", _NS_REL):
|
||||
if rel.attrib.get("Id") == rid:
|
||||
target = rel.attrib.get("Target")
|
||||
break
|
||||
if not target:
|
||||
return [], f"Kein Target für {rid}"
|
||||
sheet_path = ("xl/" + target.replace("xl/", "").lstrip("/")).replace("//", "/")
|
||||
if sheet_path not in names:
|
||||
sheet_path = target.lstrip("/")
|
||||
with zf.open(sheet_path) as fp:
|
||||
sroot = ET.parse(fp).getroot()
|
||||
sdata = sroot.find("m:sheetData", _NS_MAIN)
|
||||
if sdata is None:
|
||||
return [], "sheetData fehlt"
|
||||
|
||||
for row in sdata.findall("m:row", _NS_MAIN):
|
||||
vals: List[str] = []
|
||||
for c in row.findall("m:c", _NS_MAIN):
|
||||
typ = c.attrib.get("t")
|
||||
v_el = c.find("m:v", _NS_MAIN)
|
||||
is_el = c.find("m:is", _NS_MAIN)
|
||||
if typ == "inlineStr" and is_el is not None:
|
||||
t_el = is_el.find("m:t", _NS_MAIN)
|
||||
vals.append((t_el.text or "").strip() if t_el is not None else "")
|
||||
continue
|
||||
if v_el is None or v_el.text is None:
|
||||
vals.append("")
|
||||
continue
|
||||
if typ == "s":
|
||||
try:
|
||||
idx = int(v_el.text)
|
||||
vals.append(shared[idx] if 0 <= idx < len(shared) else "")
|
||||
except ValueError:
|
||||
vals.append("")
|
||||
else:
|
||||
vals.append(v_el.text)
|
||||
rows.append(vals)
|
||||
if len(rows) >= max_rows:
|
||||
break
|
||||
diag.append(f"Sheet gelesen: {sheet_path!r}, Zeilen: {len(rows)}")
|
||||
except zipfile.BadZipFile as e:
|
||||
return [], f"BadZipFile: {e}"
|
||||
except OSError as e:
|
||||
return [], f"OSError: {e}"
|
||||
return rows, " | ".join(diag)
|
||||
|
||||
|
||||
def _resolve_excel_path() -> Optional[Path]:
|
||||
env = (os.environ.get("AZA_DEVSTATUS_XLSX") or "").strip()
|
||||
if env:
|
||||
p = Path(env)
|
||||
if p.is_file():
|
||||
return p.resolve()
|
||||
candidates = [
|
||||
AZA_DRIVE / EXCEL_BASENAME,
|
||||
Path.home() / "Downloads" / EXCEL_BASENAME,
|
||||
Path(r"c:\Users\surov\Downloads") / EXCEL_BASENAME,
|
||||
_app_base_dir() / EXCEL_BASENAME,
|
||||
PROJECT_ROOT / EXCEL_BASENAME,
|
||||
]
|
||||
for c in candidates:
|
||||
if c.is_file():
|
||||
return c.resolve()
|
||||
return None
|
||||
|
||||
|
||||
def _resolve_override_json() -> Optional[Path]:
|
||||
env = (os.environ.get("AZA_DEVSTATUS_JSON") or "").strip()
|
||||
if env:
|
||||
p = Path(env)
|
||||
if p.is_file():
|
||||
return p.resolve()
|
||||
p = AZA_DRIVE / "AZA_DevStatus_Handover.override.json"
|
||||
if p.is_file():
|
||||
return p.resolve()
|
||||
return None
|
||||
|
||||
|
||||
# Eingebettete Basis (ohne Patientendaten, ohne Lizenzkeys, ohne Chatinhalte)
|
||||
EMBEDDED: Dict[str, Any] = {
|
||||
"dashboard": {
|
||||
"project": "AZA / AzA Desktop / AzA Empfang",
|
||||
"zustand": (
|
||||
"Fokus: Stabilität, getrennte Praxis-Slots (Lindengut / Obergasse), saubere Deploy-"
|
||||
"Nachweise. Externe Praxis-/Personen-Kontakte: führende practice_id darf nicht "
|
||||
"still überschrieben werden. Diese Hülle ist nur Dokumentation — keine Produktivänderung."
|
||||
),
|
||||
"naechste_schritte": [
|
||||
"Aktuelle Web-/Serverdateien vollständig deployen; Hash/Größe lokal vs. Hetzner vor Behauptung „live“.",
|
||||
"Desktop-Installer bauen, ServerdownloadPfad aktualisieren, auf zweitem PC installieren/testen.",
|
||||
"Externe Praxis + externe Person (Kontaktanfrage) gegen Prod testen; Kontroll-Hülle read-only zur Verifikation.",
|
||||
],
|
||||
"letzter_stabiler_stand": (
|
||||
"Bekannter guter Stand: nach Praxis-/Rollen-Korrekturen und read-only Kontroll-Hülle; "
|
||||
"Details siehe Tabs „Erledigt“ / „Änderungsverlauf“. Exakter Git-Commit/Build-Datum: nicht geprüft."
|
||||
),
|
||||
"hinweis_ro": "Keine Produktivänderung aus dieser App. Keine Serververbindung. Keine Datenbank.",
|
||||
},
|
||||
"aktueller_stand": {
|
||||
"Hauptprodukt AZA Desktop": "Aktiv im Projekt; Installer/Deploy siehe Tab Deploy/Build.",
|
||||
"Browser-/Empfangs-Chat": "empfang.html + empfang_routes.py; Stand/Hetzner per Hash prüfen.",
|
||||
"Empfang-Chat-Hülle": "Separates Setup; Download-Link im Empfang-Kontext dokumentiert.",
|
||||
"Kontroll-Hülle": "read-only; AZA_Kontroll_Huelle.exe + Snapshot-Pfad unter AzA Drive.",
|
||||
"Praxis Lindengut": "prac_883ddc21fb6a — siehe Tab Praxen.",
|
||||
"Praxis Obergasse": "prac_e864d294474e — siehe Tab Praxen.",
|
||||
"Lizenz-/Admin-Zustand": "Nur über Kontroll-Hülle/Prod prüfen; hier keine Schlüssel/Vollwerte.",
|
||||
"Notizen-/Aufgaben-/Chat-Zustand": "Operational; Stolperstein: Shared-Container siehe Tab Stolpersteine.",
|
||||
},
|
||||
"handover_chat": {
|
||||
"kommunikation": [
|
||||
"Immer präzise Schritt-für-Schritt; immer UO angeben: Windows PowerShell lokal / Hetzner SSH·Bash / Browser / Cursor Composer·Agent.",
|
||||
"Eine beste Empfehlung, keine Variantenflut; längere Antworten mit wiederholtem Copy-Paste-Block am Ende.",
|
||||
"Keine Blindänderungen; Root-cause-first; Hash·Dateistand·Pfad prüfen, bevor „fertig deployt“ behauptet wird.",
|
||||
"Keine Serverdaten blind löschen oder migrieren; Backup README_RESTORE vor strukturellen Arbeiten.",
|
||||
"Keine Produktivdaten vermischen; Praxis-Chats verbinden, aber nicht zusammenführen.",
|
||||
],
|
||||
"hauptaufgaben": [
|
||||
"Dateien deployen (web/empfang.html, empfang_routes.py, ggf. basis14.py) + docker compose rebuild.",
|
||||
"Installer bauen + Serverdownload ersetzen + Fremd-PC-Test.",
|
||||
"Externe Praxis·Personenverbindung testen; führende practice_id extern darf eigene nicht überschreiben.",
|
||||
"Kontroll-Hülle für Admin·Geräte·Sessions; Tuple·List-Regression nach EXE-Update erneut testen.",
|
||||
],
|
||||
"copy_repeat": (
|
||||
"UO Windows PowerShell (lokal): Befehle aus Tab „Copy-Paste-Befehle“.\n"
|
||||
"UO Hetzner SSH·Bash: docker compose unter /root/aza-app/deploy.\n"
|
||||
"UO Browser: Empfang·Chat·/ Admin nur lesend prüfen.\n"
|
||||
"UO Cursor Composer·Agent: Patches nur im abgesprochenen Scope."
|
||||
),
|
||||
},
|
||||
"deploy": {
|
||||
"relevant_files": [
|
||||
"web/empfang.html",
|
||||
"empfang_routes.py",
|
||||
"basis14.py",
|
||||
"aza_admin_control_shell.py",
|
||||
r"dist\AZA_Kontroll_Huelle.exe",
|
||||
],
|
||||
"serverpfade": [
|
||||
"/root/aza-app",
|
||||
"/root/aza-app/web/empfang.html",
|
||||
"/root/aza-app/empfang_routes.py",
|
||||
"/root/aza-app/deploy",
|
||||
"/root/aza-app/release/downloads/aza_desktop_setup.exe",
|
||||
],
|
||||
"lokal": [
|
||||
r"C:\Users\surov\Documents\AZA_GIT\aza\AzA march 2026",
|
||||
r"C:\Users\surov\Documents\AzA Drive",
|
||||
r"dist\installer\aza_desktop_setup.exe",
|
||||
r"dist\installer\aza_empfang_chat_setup.exe",
|
||||
],
|
||||
"hash_hinweis": (
|
||||
"Vor „live“: Get-FileHash lokal und sha256sum/entsprechend auf dem Server "
|
||||
"für dieselben Dateien; Größe + LastWriteTime dokumentieren."
|
||||
),
|
||||
},
|
||||
"praxen": {
|
||||
"Lindengut": {
|
||||
"practice_id": "prac_883ddc21fb6a",
|
||||
"Admin": "André M. Surovy",
|
||||
"E-Mail": "andre.surovy@haut-winterthur.ch",
|
||||
},
|
||||
"Obergasse": {
|
||||
"practice_id": "prac_e864d294474e",
|
||||
"Admin": "Birgit",
|
||||
"Lizenz-/Kunden-E-Mail": "dermapraxis.meier@hin.ch",
|
||||
"Susanne Rolle": "empfang",
|
||||
"André": "arzt, nicht Admin",
|
||||
},
|
||||
"hinweis": (
|
||||
"Praxis-Chats über externe Kontakte verbinden; keine automatische Zusammenführung. "
|
||||
"Fremder CHAT-Code darf keine führende practice_id still überschreiben."
|
||||
),
|
||||
},
|
||||
"todos_offen": [
|
||||
"aktuelle Dateien vollständig deployen",
|
||||
"aktuellen Desktop-Installer bauen",
|
||||
"Serverdownload ersetzen",
|
||||
"auf anderem Computer Produkt herunterladen und testen",
|
||||
"externe Praxis hinzufügen testen",
|
||||
"externe Person anfragen testen",
|
||||
"Kontroll-Hülle nutzen zur Prüfung",
|
||||
"Kontroll-Hülle-EXE nach Fix des Tuple/List-Fehlers erneut testen",
|
||||
"DevStatus-Hülle in AzA Drive legen",
|
||||
"später Admin von Praxis B weiter prüfen",
|
||||
"später externe Kontaktkommunikation UX verbessern",
|
||||
],
|
||||
"erledigt": [
|
||||
"Praxisnamen korrigiert: Praxis Lindengut / Praxis Obergasse",
|
||||
"Birgit als Admin der Praxis Obergasse gesetzt",
|
||||
"Birgit-E-Mail aus Stripe/License-Kontext: dermapraxis.meier@hin.ch",
|
||||
"André in Praxis Obergasse auf arzt herabgestuft",
|
||||
"Notizen von Aufgaben getrennt",
|
||||
"Notizen-Tab stabil gemacht",
|
||||
"Kontroll-Hülle read-only erstellt",
|
||||
"SSH-Key für Kontroll-Hülle eingerichtet (BatchMode)",
|
||||
"WebSocket-Beschleunigung eingeführt",
|
||||
"Empfang-Chat-Hülle aktualisiert",
|
||||
"Downloadlink für Empfang-Chat-Hülle eingebaut",
|
||||
"gefährlicher alter Praxis-Join-Pfad entschärft (UI-Texte professionell halten)",
|
||||
],
|
||||
"stolpersteine": [
|
||||
"Alte empfang.html auf Hetzner trotz lokaler Änderung — immer Hash/Größe vergleichen.",
|
||||
"Hash/Dateistand nicht geprüft — keine Live-Behauptung.",
|
||||
"Notizen im selben Container wie Briefe/Aufgaben — Mischrisiken beim UI.",
|
||||
"Fremder CHAT-Code überschreibt führende practice_id — Praxisdaten nie vermischen.",
|
||||
"Zwei Praxis-Slots mit falschem Namen — Namen/Lindengut/Obergasse strikt trennen.",
|
||||
"Adminrolle falsch übertragen — immer Lizenz·/Kontext prüfen.",
|
||||
"Session-Rollen alt nach Account-Änderung — ggf. Neuanmeldung / Gerät.",
|
||||
"PyInstaller-Dateisperre WinError 5 — EXE nicht von laufender Instanz gesperrt bauen.",
|
||||
"EmpfangShell im Desktop-Bundle älter als finale Shell — Bundle·Installer·Pfad prüfen.",
|
||||
"Kontroll-Hülle EXE Tuple/List-Fehler — nach Fix Regressionstest.",
|
||||
"fehlende optionale empfang_practice_links.json darf kein kritischer Fehler sein",
|
||||
"SSH-Key nötig, weil Kontroll-Hülle BatchMode nutzt",
|
||||
"Keine exakte Adresse aus IP ableiten — nur grobe Geo/ISP, datenschutzfreundlich",
|
||||
],
|
||||
"pfade_tabelle": [
|
||||
("lokal", "Repo", str(PROJECT_ROOT)),
|
||||
("Hetzner", "App-Root", "/root/aza-app"),
|
||||
("AzA Drive", "Artefakte", str(AZA_DRIVE)),
|
||||
("Installer", "Desktop", r"dist\installer\aza_desktop_setup.exe"),
|
||||
("Installer", "Empfang-Chat", r"dist\installer\aza_empfang_chat_setup.exe"),
|
||||
("Backups", "Timestamp README", str(PROJECT_ROOT) + r"\backup_*"),
|
||||
("Snapshots", "Kontroll-Hülle", str(AZA_DRIVE / "AZA_CONTROL_SNAPSHOTS")),
|
||||
("Kontroll-Hülle", "EXE", str(PROJECT_ROOT / r"dist\AZA_Kontroll_Huelle.exe")),
|
||||
("DevStatus-Hülle", "EXE", str(AZA_DRIVE / "AZA_DevStatus_Handover.exe")),
|
||||
],
|
||||
"befehle": {
|
||||
"web_empfang_upload": (
|
||||
r'scp "C:\Users\surov\Documents\AZA_GIT\aza\AzA march 2026\web\empfang.html" '
|
||||
r'root@178.104.51.177:/root/aza-app/web/empfang.html'
|
||||
),
|
||||
"empfang_routes_upload": (
|
||||
r'scp "C:\Users\surov\Documents\AZA_GIT\aza\AzA march 2026\empfang_routes.py" '
|
||||
r'root@178.104.51.177:/root/aza-app/empfang_routes.py'
|
||||
),
|
||||
"docker_rebuild": (
|
||||
"cd /root/aza-app/deploy && docker compose pull 2>/dev/null; docker compose up --build -d"
|
||||
),
|
||||
"release_ps1": (
|
||||
r'cd "C:\Users\surov\Documents\AZA_GIT\aza\AzA march 2026"; '
|
||||
r'if (Test-Path .\release.ps1) { .\release.ps1 } else { Write-Host "release.ps1 nicht gefunden" }'
|
||||
),
|
||||
"hash_installer": (
|
||||
r'Get-FileHash "C:\Users\surov\Documents\AZA_GIT\aza\AzA march 2026\dist\installer\aza_desktop_setup.exe" -Algorithm SHA256'
|
||||
),
|
||||
"upload_installer": (
|
||||
r'scp "C:\Users\surov\Documents\AZA_GIT\aza\AzA march 2026\dist\installer\aza_desktop_setup.exe" '
|
||||
r"root@178.104.51.177:/root/aza-app/release/downloads/aza_desktop_setup.exe"
|
||||
),
|
||||
"kontroll_start": (
|
||||
r'start "" "C:\Users\surov\Documents\AZA_GIT\aza\AzA march 2026\dist\AZA_Kontroll_Huelle.exe"'
|
||||
),
|
||||
"devstatus_start": (
|
||||
r'start "" "C:\Users\surov\Documents\AzA Drive\AZA_DevStatus_Handover.exe"'
|
||||
),
|
||||
"backup_zip": (
|
||||
r'$ts = Get-Date -Format "yyyyMMdd_HHmmss"; '
|
||||
r'Compress-Archive -Path "...\gewünschter_Ordner\*" -DestinationPath ("C:\Users\surov\Documents\AzA Drive\backup_"+$ts+".zip")'
|
||||
),
|
||||
},
|
||||
"prompts": {
|
||||
"Nächster Chat Startprompt": (
|
||||
"Arbeite root-cause-first für AZA/AzA Desktop/Empfang. "
|
||||
"Windows PowerShell / Hetzner SSH / Browser / Cursor angeben. "
|
||||
"Keine Blindänderungen, eine Empfehlung, Copy-Paste unten wiederholen. "
|
||||
"Praxis Lindengut und Obergasse getrennt halten; practice_id extern nicht überschreiben. "
|
||||
"Kein Deploy ohne Hash-Vergleich."
|
||||
),
|
||||
"Deploy/Installer Prompt": (
|
||||
"Scope nur Deploy/Installer. Liste konkrete Pfade (web/empfang.html, empfang_routes.py, deploy). "
|
||||
"Nach scp/docker: Hash lokal vs. Server. release.ps1 nur wenn vorhanden. Keine Lizenz-/Stripe-Logik ändern."
|
||||
),
|
||||
"Externe Kontakte Test Prompt": (
|
||||
"Nur externe Praxis/Person: Happy-Path und Fehlerfälle; nachweisen, dass chat_code die führende practice_id "
|
||||
"nicht überschreibt. Logs ohne Patientendaten. Rollen- und Session-Effekte erwähnen."
|
||||
),
|
||||
"Praxisdaten Diagnose Prompt": (
|
||||
"Diagnose read-only: SOLL für Lindengut/Obergasse, practice_id, Admin vs. arzt. "
|
||||
"Keine Datenmigration ohne Auftrag. Output für Kontroll-Hülle/Snapshot-Abgleich strukturieren."
|
||||
),
|
||||
"Kontroll-Hülle Erweiterung Prompt": (
|
||||
"Nur aza_admin_control_shell.py read-only-UI; keine Produktiv-APIs schreibend. "
|
||||
"SSH/BatchMode-Constraints beachten; keine vollen Lizenzkeys in UI-Exports."
|
||||
),
|
||||
},
|
||||
"changelog": [
|
||||
"2026-05-14 — DevStatus-/Handover-Hülle als eigenständige lokale Tkinter-App angelegt (diese EXE).",
|
||||
"2026-05 — Kontroll-Hülle Tuple/List-Fix laut Aufgabenliste erneut testen (manuell).",
|
||||
"2026-05 — Excel AZA_DevStatus_Handover_2026-05-14.xlsx als optionale Datenquelle (Vorschau + OS-Öffnen).",
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
class DevStatusHandoverApp(tk.Tk):
|
||||
def __init__(self, data: Dict[str, Any], excel_path: Optional[Path], xlsx_rows: List[List[str]], xlsx_diag: str):
|
||||
super().__init__()
|
||||
self._data = data
|
||||
self._excel_path = excel_path
|
||||
self._xlsx_rows = xlsx_rows
|
||||
self._xlsx_diag = xlsx_diag
|
||||
self.title("AZA DevStatus / Handover")
|
||||
self.configure(bg="#eaf1f8")
|
||||
self.minsize(1020, 720)
|
||||
|
||||
hdr = tk.Frame(self, bg="#eaf1f8")
|
||||
hdr.pack(fill="x", padx=12, pady=(10, 4))
|
||||
tk.Label(hdr, text="AZA DevStatus / Handover", font=("Segoe UI", 16, "bold"), fg="#356488", bg="#eaf1f8").pack(
|
||||
side="left"
|
||||
)
|
||||
bf = tk.Frame(hdr, bg="#eaf1f8")
|
||||
bf.pack(side="right")
|
||||
|
||||
self._excel_lbl = tk.StringVar(value=self._excel_summary())
|
||||
tk.Label(bf, textvariable=self._excel_lbl, fg="#334e66", bg="#eaf1f8", font=("Segoe UI", 9)).pack(
|
||||
side="left", padx=8
|
||||
)
|
||||
tk.Button(
|
||||
bf,
|
||||
text="Excel öffnen",
|
||||
bg="#5B8DB3",
|
||||
fg="white",
|
||||
relief="flat",
|
||||
padx=10,
|
||||
pady=5,
|
||||
cursor="hand2",
|
||||
command=self._open_excel,
|
||||
).pack(side="left", padx=4)
|
||||
tk.Button(
|
||||
bf,
|
||||
text="READ-ONLY",
|
||||
bg="#dceaf4",
|
||||
fg="#2E7D32",
|
||||
relief="flat",
|
||||
padx=10,
|
||||
pady=5,
|
||||
font=("Segoe UI", 9, "bold"),
|
||||
state="disabled",
|
||||
).pack(side="left", padx=4)
|
||||
|
||||
st = tk.Frame(self, bg="#dceaf4", highlightthickness=1, highlightbackground="#c8dae8")
|
||||
st.pack(fill="x", padx=12, pady=(0, 6))
|
||||
tk.Label(
|
||||
st,
|
||||
text="Lokal · keine Serververbindung · keine Produktivänderung · nur Status",
|
||||
fg="#334e66",
|
||||
bg="#dceaf4",
|
||||
font=("Segoe UI", 9),
|
||||
).pack(anchor="w", padx=8, pady=4)
|
||||
if xlsx_diag:
|
||||
tk.Label(st, text=("XLSX: " + xlsx_diag)[:300], fg="#5a4a15", bg="#dceaf4", font=("Segoe UI", 8)).pack(
|
||||
anchor="w", padx=8, pady=(0, 4)
|
||||
)
|
||||
|
||||
nb = ttk.Notebook(self)
|
||||
nb.pack(fill="both", expand=True, padx=10, pady=(0, 10))
|
||||
|
||||
self._tab_dashboard(nb)
|
||||
self._tab_dictsections(nb, "Aktueller Stand", data.get("aktueller_stand", {}))
|
||||
self._tab_handover(nb)
|
||||
self._tab_deploy(nb)
|
||||
self._tab_praxen(nb)
|
||||
self._tab_list(nb, "Offene ToDos", data.get("todos_offen", []))
|
||||
self._tab_list(nb, "Erledigt", data.get("erledigt", []))
|
||||
self._tab_list(nb, "Stolpersteine / Fehlerquellen", data.get("stolpersteine", []))
|
||||
self._tab_paths(nb)
|
||||
self._tab_commands(nb)
|
||||
self._tab_prompts(nb)
|
||||
self._tab_changelog(nb)
|
||||
|
||||
def _excel_summary(self) -> str:
|
||||
if self._excel_path is None:
|
||||
return "Excel: nicht gefunden (ENV AZA_DEVSTATUS_XLSX oder AzA Drive/Downloads)"
|
||||
return "Excel: " + str(self._excel_path)
|
||||
|
||||
def _open_excel(self) -> None:
|
||||
if self._excel_path is None or not self._excel_path.is_file():
|
||||
messagebox.showinfo("Excel", "Keine Excel-Datei am erwarteten Ort.")
|
||||
return
|
||||
try:
|
||||
os.startfile(str(self._excel_path)) # type: ignore[attr-defined]
|
||||
except AttributeError:
|
||||
webbrowser.open(self._excel_path.as_uri())
|
||||
|
||||
def _tab_dashboard(self, nb: ttk.Notebook) -> None:
|
||||
fr = tk.Frame(nb, bg="#eaf1f8")
|
||||
nb.add(fr, text="Dashboard")
|
||||
d = self._data.get("dashboard", {})
|
||||
lines = [
|
||||
"Projekt: " + str(d.get("project", "")),
|
||||
"",
|
||||
"Aktueller Zustand:",
|
||||
str(d.get("zustand", "")),
|
||||
"",
|
||||
"Nächste 3 Schritte:",
|
||||
]
|
||||
for i, s in enumerate(d.get("naechste_schritte", []), 1):
|
||||
lines.append(f" {i}. {s}")
|
||||
lines.extend(
|
||||
[
|
||||
"",
|
||||
"Letzter bekannter stabiler Stand:",
|
||||
str(d.get("letzter_stabiler_stand", "")),
|
||||
"",
|
||||
"Hinweis:",
|
||||
str(d.get("hinweis_ro", "")),
|
||||
]
|
||||
)
|
||||
if self._xlsx_rows:
|
||||
lines.extend(["", "— Excel erste Zeilen (Vorschau, Lesen via ZIP/XML) —"])
|
||||
for r in self._xlsx_rows[:15]:
|
||||
lines.append(" | ".join(c.replace("\n", " ") for c in r))
|
||||
|
||||
self._add_scrolled_text(fr, "\n".join(lines))
|
||||
|
||||
def _tab_dictsections(self, nb: ttk.Notebook, title: str, d: Mapping[str, Any]) -> None:
|
||||
fr = tk.Frame(nb, bg="#eaf1f8")
|
||||
nb.add(fr, text=title)
|
||||
parts: List[str] = []
|
||||
for k, v in d.items():
|
||||
parts.append(f"=== {k} ===\n{v}\n")
|
||||
self._add_scrolled_text(fr, "\n".join(parts))
|
||||
|
||||
def _tab_handover(self, nb: ttk.Notebook) -> None:
|
||||
fr = tk.Frame(nb, bg="#eaf1f8")
|
||||
nb.add(fr, text="Handover nächster Chat")
|
||||
h = self._data.get("handover_chat", {})
|
||||
buf: List[str] = []
|
||||
buf.append("Kommunikationsregeln:\n")
|
||||
for x in h.get("kommunikation", []):
|
||||
buf.append("• " + x)
|
||||
buf.append("\nAktuelle Hauptaufgaben:\n")
|
||||
for x in h.get("hauptaufgaben", []):
|
||||
buf.append("• " + x)
|
||||
buf.append("\nWiederholung Copy/Paste-Disziplin:\n" + str(h.get("copy_repeat", "")))
|
||||
self._add_scrolled_text(fr, "\n".join(buf))
|
||||
|
||||
def _tab_deploy(self, nb: ttk.Notebook) -> None:
|
||||
fr = tk.Frame(nb, bg="#eaf1f8")
|
||||
nb.add(fr, text="Deploy / Build")
|
||||
dv = self._data.get("deploy", {})
|
||||
lines: List[str] = ["Relevante Dateien:"]
|
||||
for x in dv.get("relevant_files", []):
|
||||
lines.append(" • " + x)
|
||||
lines.append("\nServerpfade:")
|
||||
for x in dv.get("serverpfade", []):
|
||||
lines.append(" • " + x)
|
||||
lines.append("\nLokal:")
|
||||
for x in dv.get("lokal", []):
|
||||
lines.append(" • " + x)
|
||||
lines.append("\nBuild: pyinstaller AZA_DevStatus_Handover.spec (siehe build_devstatus_handover.ps1)")
|
||||
lines.append("Deploy: scp + docker compose (siehe Copy-Paste-Tab)")
|
||||
lines.append("\n" + str(dv.get("hash_hinweis", "")))
|
||||
self._add_scrolled_text(fr, "\n".join(lines))
|
||||
|
||||
def _tab_praxen(self, nb: ttk.Notebook) -> None:
|
||||
fr = tk.Frame(nb, bg="#eaf1f8")
|
||||
nb.add(fr, text="Praxen / Benutzer / Admins")
|
||||
p = self._data.get("praxen", {})
|
||||
lines: List[str] = []
|
||||
for key in ("Lindengut", "Obergasse"):
|
||||
block = p.get(key)
|
||||
if isinstance(block, dict):
|
||||
lines.append(f"=== {key} ===")
|
||||
for kk, vv in block.items():
|
||||
lines.append(f" {kk}: {vv}")
|
||||
lines.append("")
|
||||
lines.append("Hinweis:\n" + str(p.get("hinweis", "")))
|
||||
self._add_scrolled_text(fr, "\n".join(lines))
|
||||
|
||||
def _tab_list(self, nb: ttk.Notebook, title: str, items: Sequence[Any]) -> None:
|
||||
fr = tk.Frame(nb, bg="#eaf1f8")
|
||||
nb.add(fr, text=title)
|
||||
lines = ["• " + str(x) for x in items]
|
||||
self._add_scrolled_text(fr, "\n".join(lines))
|
||||
|
||||
def _tab_paths(self, nb: ttk.Notebook) -> None:
|
||||
fr = tk.Frame(nb, bg="#eaf1f8")
|
||||
nb.add(fr, text="Wichtige Dateien & Pfade")
|
||||
rows = self._data.get("pfade_tabelle", [])
|
||||
tv = ttk.Treeview(fr, columns=("kat", "ort", "pfad"), show="headings", height=18)
|
||||
tv.heading("kat", text="Kategorie")
|
||||
tv.heading("ort", text="Ort/Type")
|
||||
tv.heading("pfad", text="Pfad")
|
||||
tv.column("kat", width=140)
|
||||
tv.column("ort", width=160)
|
||||
tv.column("pfad", width=620)
|
||||
sy = ttk.Scrollbar(fr, orient="vertical", command=tv.yview)
|
||||
tv.configure(yscrollcommand=sy.set)
|
||||
tv.pack(side="left", fill="both", expand=True, padx=(8, 0), pady=8)
|
||||
sy.pack(side="right", fill="y", pady=8)
|
||||
for tup in rows:
|
||||
if len(tup) == 3:
|
||||
tv.insert("", "end", values=tup)
|
||||
|
||||
def _tab_commands(self, nb: ttk.Notebook) -> None:
|
||||
fr = tk.Frame(nb, bg="#eaf1f8")
|
||||
nb.add(fr, text="Copy-Paste-Befehle")
|
||||
outer = tk.Frame(fr, bg="#eaf1f8")
|
||||
outer.pack(fill="both", expand=True, padx=6, pady=6)
|
||||
canvas = tk.Canvas(outer, bg="#eaf1f8", highlightthickness=0)
|
||||
sy = ttk.Scrollbar(outer, orient="vertical", command=canvas.yview)
|
||||
body = tk.Frame(canvas, bg="#eaf1f8")
|
||||
body_id = canvas.create_window((0, 0), window=body, anchor="nw")
|
||||
|
||||
def _on_cfg(_evt=None) -> None:
|
||||
canvas.configure(scrollregion=canvas.bbox("all"))
|
||||
canvas.itemconfig(body_id, width=canvas.winfo_width())
|
||||
|
||||
body.bind("<Configure>", _on_cfg)
|
||||
canvas.bind("<Configure>", lambda e: canvas.itemconfig(body_id, width=e.width))
|
||||
canvas.pack(side="left", fill="both", expand=True)
|
||||
sy.pack(side="right", fill="y")
|
||||
canvas.configure(yscrollcommand=sy.set)
|
||||
|
||||
befehle = self._data.get("befehle", {})
|
||||
order = [
|
||||
("web/empfang.html upload", "web_empfang_upload"),
|
||||
("empfang_routes.py upload", "empfang_routes_upload"),
|
||||
("docker compose rebuild/restart", "docker_rebuild"),
|
||||
("release.ps1", "release_ps1"),
|
||||
("Hash Installer", "hash_installer"),
|
||||
("Upload Installer", "upload_installer"),
|
||||
("Kontroll-Hülle starten", "kontroll_start"),
|
||||
("DevStatus-Hülle starten", "devstatus_start"),
|
||||
("Backup ZIP erstellen", "backup_zip"),
|
||||
]
|
||||
for label, key in order:
|
||||
self._command_block(body, label, str(befehle.get(key, "")))
|
||||
|
||||
def _command_block(self, parent: tk.Frame, title: str, text: str) -> None:
|
||||
wrap = tk.Frame(parent, bg="#eaf1f8")
|
||||
wrap.pack(fill="x", pady=6)
|
||||
row = tk.Frame(wrap, bg="#eaf1f8")
|
||||
row.pack(fill="x")
|
||||
tk.Label(row, text=title, font=("Segoe UI", 10, "bold"), fg="#356488", bg="#eaf1f8").pack(side="left")
|
||||
tk.Button(
|
||||
row,
|
||||
text="In Zwischenablage",
|
||||
command=lambda t=text: self._clip(t),
|
||||
bg="#dceaf4",
|
||||
relief="flat",
|
||||
cursor="hand2",
|
||||
).pack(side="right")
|
||||
st = scrolledtext.ScrolledText(wrap, height=5, wrap="word", font=("Consolas", 10), bg="#fafcff")
|
||||
st.pack(fill="x", pady=4)
|
||||
st.insert("1.0", text)
|
||||
st.configure(state="disabled")
|
||||
|
||||
def _clip(self, s: str) -> None:
|
||||
self.clipboard_clear()
|
||||
self.clipboard_append(s)
|
||||
self.update()
|
||||
|
||||
def _tab_prompts(self, nb: ttk.Notebook) -> None:
|
||||
fr = tk.Frame(nb, bg="#eaf1f8")
|
||||
nb.add(fr, text="Cursor-/Composer-Prompts")
|
||||
outer = tk.Frame(fr, bg="#eaf1f8")
|
||||
outer.pack(fill="both", expand=True, padx=6, pady=6)
|
||||
prompts = self._data.get("prompts", {})
|
||||
for title, body in prompts.items():
|
||||
self._command_block(outer, title, body)
|
||||
|
||||
def _tab_changelog(self, nb: ttk.Notebook) -> None:
|
||||
fr = tk.Frame(nb, bg="#eaf1f8")
|
||||
nb.add(fr, text="Änderungsverlauf")
|
||||
items = self._data.get("changelog", [])
|
||||
self._add_scrolled_text(fr, "\n".join("• " + str(x) for x in items))
|
||||
|
||||
def _add_scrolled_text(self, parent: tk.Frame, content: str) -> None:
|
||||
st = scrolledtext.ScrolledText(parent, wrap="word", font=("Segoe UI", 10), bg="#fafcff")
|
||||
st.pack(fill="both", expand=True, padx=8, pady=8)
|
||||
st.insert("1.0", content)
|
||||
st.configure(state="disabled")
|
||||
|
||||
|
||||
def _load_merged_data() -> Dict[str, Any]:
|
||||
base: Dict[str, Any] = json.loads(json.dumps(EMBEDDED))
|
||||
jp = _resolve_override_json()
|
||||
if jp is not None:
|
||||
ext = _load_json_path(jp)
|
||||
if ext:
|
||||
_deep_merge(base, ext)
|
||||
return base
|
||||
|
||||
|
||||
def main() -> None:
|
||||
data = _load_merged_data()
|
||||
xls = _resolve_excel_path()
|
||||
rows: List[List[str]] = []
|
||||
diag = ""
|
||||
if xls is not None:
|
||||
rows, diag = _read_xlsx_first_sheet(xls)
|
||||
DevStatusHandoverApp(data, xls, rows, diag).mainloop()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user