Files
aza/AzA march 2026/aza_devstatus_handover_shell.py
2026-05-16 20:33:36 +02:00

703 lines
30 KiB
Python

# -*- 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()