703 lines
30 KiB
Python
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()
|