# -*- coding: utf-8 -*- """ AZA Dev Status – Tkinter-Fenster im App-Stil mit sechs Reitern: 1) Projekt-Status (aktueller Stand + editierbar + History) 2) Erledigt (abgeschlossene Schritte aus project_plan.json) 3) To-Do (aus project_todos.json, editierbar, Area-Tags, Persistence) 4) Roadmap (Milestones aus project_roadmap.json, editierbare Dropdowns) 5) Projektnotizen (aus Projekt-Notizen-Ordner) 6) Handover (Operational Runbook, editierbar, project_handover.md) Aktualisiert sich alle 5 Minuten. """ from __future__ import annotations import json import os import subprocess import tkinter as tk from tkinter import ttk, filedialog from datetime import datetime, timezone from pathlib import Path # ---- DEV STATUS EXPORT TEMPLATE (Master Snapshot) ---- DEV_STATUS_EXPORT_PROMPT = r"""DEV STATUS EXPORT – AZA Bitte erstelle einen strukturierten Export (Markdown), basierend auf den aktuellen Projektdateien: - project_status.json - project_plan.json (Tab "Erledigt") - project_todos.json (Tab "To-Do") - project_roadmap.json (Tab "Roadmap") Ziel: Ein „Master Snapshot" der aktuellen Situation für Übergabe/Go-Live. FORMAT (genau diese Abschnitte): 1) Kontext - Datum/Time - Workspace: project_root + current_working_folder - Current step / last completed / phase - Do-not-break Regeln (kurz): /license/status Schema, Header X-API-Token, keine Secrets/Tokens loggen 2) Was ist fertig (Done) - Liste der abgeschlossenen Steps (mit 1 Satz Ergebnis) - Wichtigste Deliverables/Dateien je Step 3) Was ist aktiv/offen (In progress / Open) - Liste der offenen Steps in sinnvoller Reihenfolge bis Sell-Ready/Go-Live - Für jeden Step: Ziel, wichtigste Risiken, „Definition of Done" 4) To-Do Top 10 (priorisiert) - Nur die 10 wichtigsten offenen Todos (mit ID, Kurzbeschreibung, Bereich) - Falls vorhanden: Zuordnung zu Steps 5) Roadmap Snapshot - Phasen/Milestones kurz - Nächster Milestone und warum 6) Nächster Schritt (1 Step only) - Empfiehl genau EINEN nächsten Schritt als nächstes Ticket - Begründung in 2–3 Sätzen - Keine Refactors, keine Breaking Changes WICHTIG: - Keine Token/Secrets ausgeben. - Keine PHI. - Keine langen Texte: kompakt, aber vollständig. - Wenn Step 21/Browser/Web/Billing nicht als Step definiert ist, benenne explizit „fehlende Step-Definitionen" als Lücke. """ _BASE_DIR = Path(__file__).resolve().parent _STATUS_FILE = _BASE_DIR / "project_status.json" _PLAN_FILE = _BASE_DIR / "project_plan.json" _TODOS_FILE = _BASE_DIR / "project_todos.json" _ROADMAP_FILE = _BASE_DIR / "project_roadmap.json" _PROJECT_HANDOVER_FILE = _BASE_DIR / "project_handover.md" # Gleicher Pfad wie Projekt-Notizen-Fenster (aza_notizen_mixin: parent/notizen) _NOTIZEN_DIR = _BASE_DIR.parent / "notizen" _NOTIZEN_PROJEKT = _NOTIZEN_DIR / "projekt_status.md" _NOTIZEN_CHANGELOG = _NOTIZEN_DIR / "changelog.md" _HISTORY_FILE = _BASE_DIR / "project_status_history.jsonl" _TOKEN_FILE = _BASE_DIR / "backend_token.txt" _HANDOVER_FILE = _BASE_DIR / "handover.md" _API_BASE = "http://127.0.0.1:8000" _API_URL = _API_BASE + "/api/project/status" _HEALTH_URL = _API_BASE + "/health" _DEVICE_ID = "pc-local" _STARTUP_SCRIPT = _BASE_DIR / "deploy" / "local_reset_and_start.ps1" _HEALTH_POLL_MS = 3000 BG = "#B9ECFA" FG = "#1a4d6d" CARD_BG = "#E8F4FA" TEXT_BG = "#F5FCFF" ACCENT = "#5B8DB3" DONE_BG = "#e6f5ec" DONE_ACCENT = "#27AE60" TODO_BG = "#fef9e7" TODO_HIGH = "#E74C3C" TODO_MED = "#F39C12" TODO_LOW = "#7f8c8d" ROAD_BG = "#eef2f7" HAND_BG = "#f4f0eb" FONT = "Segoe UI" REFRESH_MS = 5 * 60 * 1000 _PRIO_ORDER = {"hoch": 0, "mittel": 1, "niedrig": 2} _STATUS_ORDER = {"in arbeit": 0, "offen": 1, "erledigt": 2} # project_todos.json schema mapping _TODO_PRIO_MAP = {"HIGH": "HOCH", "MEDIUM": "MITTEL", "LOW": "NIEDRIG"} _TODO_STATUS_MAP = {"open": "offen", "in_progress": "in Arbeit", "done": "erledigt"} _TODO_STATUS_REV = {"offen": "open", "in arbeit": "in_progress", "erledigt": "done"} _AREA_LABELS = { "backend": ("Backend", "#3498db"), "frontend": ("Frontend", "#9b59b6"), "web": ("Web", "#e67e22"), "billing": ("Billing", "#27ae60"), "release": ("Release", "#e74c3c"), "ops": ("Ops", "#1abc9c"), "desktop": ("Desktop", "#8e44ad"), "legal": ("Legal", "#2c3e50"), "product": ("Product", "#d35400"), "docs": ("Docs", "#16a085"), } _HANDOVER_DEFAULT = """\ # AZA - Master Handover / Operational Runbook ## Arbeitsmodus / Regeln User bastelt nicht; nur Composer-Patches (meist Opus) oder 1 exakter Command mit Pfad. ## Lokaler Start ``` cd "C:\\Users\\surov\\Documents\\AZA\\backup 24.2.26" powershell -ExecutionPolicy Bypass -File .\\deploy\\local_reset_and_start.ps1 ``` ## Step 14 - Docker/Compose Smoke-Test **Ziel:** Container bauen, starten, smoke_suite PASS gegen Docker. ``` cd "C:\\Users\\surov\\Documents\\AZA\\backup 24.2.26" powershell -ExecutionPolicy Bypass -File .\\deploy\\docker_smoke.ps1 ``` ## Do-Not-Break Regeln 1. Keine bestehenden API-Response-Formate aendern 2. Auth/Security nicht modifizieren 3. Keine Secrets loggen/printen """ # ── File helpers ─────────────────────────────────────────────────────────── def _read_json(path: Path) -> dict | None: try: with open(str(path), "r", encoding="utf-8") as f: return json.load(f) except Exception: return None def _save_status(data: dict) -> bool: """Atomic write: temp file then replace.""" tmp = _STATUS_FILE.with_suffix(".json.tmp") try: text = json.dumps(data, indent=2, ensure_ascii=False) + "\n" with open(str(tmp), "w", encoding="utf-8") as f: f.write(text) f.flush() os.fsync(f.fileno()) os.replace(str(tmp), str(_STATUS_FILE)) return True except Exception: try: tmp.unlink(missing_ok=True) except Exception: pass return False def _save_json_atomic(path: Path, data: dict) -> bool: tmp = path.with_suffix(path.suffix + ".tmp") try: text = json.dumps(data, indent=2, ensure_ascii=False) + "\n" with open(str(tmp), "w", encoding="utf-8") as f: f.write(text) f.flush() os.fsync(f.fileno()) os.replace(str(tmp), str(path)) return True except Exception: try: tmp.unlink(missing_ok=True) except Exception: pass return False def _append_history(entry: dict) -> None: try: line = json.dumps(entry, ensure_ascii=False) + "\n" with open(str(_HISTORY_FILE), "a", encoding="utf-8") as f: f.write(line) f.flush() os.fsync(f.fileno()) except Exception: pass def _read_token() -> str: tok = os.environ.get("MEDWORK_API_TOKEN", "").strip() if not tok: try: with open(str(_TOKEN_FILE), "r", encoding="utf-8") as f: tok = (f.readline() or "").strip() except Exception: tok = "" return tok or "" def _fetch_status_from_api() -> dict | None: try: from urllib.request import Request, urlopen tok = _read_token() if not tok: return None req = Request(_API_URL, headers={"X-API-Token": tok, "X-Device-Id": _DEVICE_ID}) with urlopen(req, timeout=3) as resp: return json.loads(resp.read().decode("utf-8", errors="replace")) except Exception: return None def _post_status_from_api(payload: dict) -> tuple[dict | None, str | None]: """POST /api/project/status. Returns (data, error_msg).""" try: from urllib.request import Request, urlopen tok = _read_token() if not tok: return None, "Kein Token (backend_token.txt)" body = json.dumps(payload, ensure_ascii=False).encode("utf-8") req = Request( _API_URL, data=body, method="POST", headers={ "X-API-Token": tok, "X-Device-Id": _DEVICE_ID, "Content-Type": "application/json", }, ) with urlopen(req, timeout=5) as resp: return json.loads(resp.read().decode("utf-8", errors="replace")), None except Exception as e: return None, str(e) or "Unbekannter Fehler" def _get_status_data() -> tuple[dict | None, str]: """Returns (data, source) where source is 'API' or 'lokal'.""" data = _fetch_status_from_api() if data is not None: return data, "API" return _read_json(_STATUS_FILE), "lokal" def _sort_todos(todos: list[dict]) -> list[dict]: def key(t: dict) -> tuple: p = _PRIO_ORDER.get(t.get("priority", "").lower(), 9) s = _STATUS_ORDER.get(t.get("status", "").lower(), 9) return (p, s, t.get("title", "")) return sorted(todos, key=key) class DevStatusWindow(tk.Toplevel): def __init__(self, parent): super().__init__(parent) self.title("AZA Dev Status") self.configure(bg=BG) self.geometry("680x640") self.minsize(540, 460) self.attributes("-topmost", False) try: parent._register_window(self) except Exception: pass self._build_ui() self._refresh() # ── UI ───────────────────────────────────────────────────────────────── def _build_ui(self): head = tk.Frame(self, bg=ACCENT, height=38) head.pack(fill="x") head.pack_propagate(False) tk.Label( head, text="AZA Dev Status", font=(FONT, 13, "bold"), bg=ACCENT, fg="white", ).pack(side="left", padx=12) self._source_lbl = tk.Label( head, text="", font=(FONT, 8), bg=ACCENT, fg="#c0dae8", ) self._source_lbl.pack(side="right", padx=12) self._build_backend_bar() style = ttk.Style(self) style.configure("Dev.TNotebook", background=BG) style.configure("Dev.TNotebook.Tab", font=(FONT, 10), padding=[8, 4]) self._nb = ttk.Notebook(self, style="Dev.TNotebook") self._nb.pack(fill="both", expand=True, padx=8, pady=(6, 4)) self._tab_status = tk.Frame(self._nb, bg=BG) self._tab_done = tk.Frame(self._nb, bg=BG) self._tab_todo = tk.Frame(self._nb, bg=BG) self._tab_road = tk.Frame(self._nb, bg=BG) self._tab_notizen = tk.Frame(self._nb, bg=BG) self._tab_hand = tk.Frame(self._nb, bg=BG) self._nb.add(self._tab_status, text=" Projekt-Status ") self._nb.add(self._tab_done, text=" Erledigt ") self._nb.add(self._tab_todo, text=" To-Do ") self._nb.add(self._tab_road, text=" Roadmap ") self._nb.add(self._tab_notizen, text=" Projektnotizen ") self._nb.add(self._tab_hand, text=" Handover ") self._build_status_tab() self._build_scrollable(self._tab_done, "_done_inner") self._build_todo_tab() self._build_roadmap_tab() self._build_notizen_tab() self._build_handover_tab() self._toast_var = tk.StringVar(value="") self._toast_lbl = tk.Label( self, textvariable=self._toast_var, font=(FONT, 8), bg=BG, fg=DONE_ACCENT, anchor="w", ) self._toast_lbl.pack(fill="x", padx=10, pady=(0, 2)) def _build_backend_bar(self): bar = tk.Frame(self, bg=CARD_BG, bd=1, relief="groove") bar.pack(fill="x", padx=8, pady=(4, 0)) tk.Label(bar, text="Backend:", font=(FONT, 9, "bold"), bg=CARD_BG, fg=FG).pack(side="left", padx=(8, 4)) self._be_start_btn = tk.Button( bar, text="Backend starten", font=(FONT, 9), width=14, relief="groove", bd=1, bg="#c8e6c9", fg="#1b5e20", command=self._start_backend, ) self._be_start_btn.pack(side="left", padx=(0, 4)) self._be_stop_btn = tk.Button( bar, text="Stoppen", font=(FONT, 9), width=8, relief="groove", bd=1, bg="#ffcdd2", fg="#b71c1c", command=self._stop_backend, ) self._be_stop_btn.pack(side="left", padx=(0, 6)) self._be_status_lbl = tk.Label( bar, text="Backend: ?", font=(FONT, 9), bg=CARD_BG, fg=FG, anchor="w", ) self._be_status_lbl.pack(side="left", padx=6, fill="x", expand=True) self._backend_proc = None self._health_polling = False self.after(500, self._health_poll) # ── Backend control ──────────────────────────────────────────────────── def _ping_health(self) -> bool: try: from urllib.request import Request, urlopen req = Request(_HEALTH_URL) with urlopen(req, timeout=2) as resp: return resp.status == 200 except Exception: return False def _health_poll(self): if not self.winfo_exists(): return alive = self._ping_health() if alive: self._be_status_lbl.config(text="Backend: RUNNING", fg=DONE_ACCENT) else: self._be_status_lbl.config(text="Backend: DOWN", fg=TODO_HIGH) self.after(_HEALTH_POLL_MS, self._health_poll) def _start_backend(self): script = _STARTUP_SCRIPT if not script.is_file(): self._toast("Startscript nicht gefunden: deploy/local_reset_and_start.ps1", TODO_HIGH) return try: CREATE_NEW_CONSOLE = 0x00000010 self._backend_proc = subprocess.Popen( [ "powershell.exe", "-ExecutionPolicy", "Bypass", "-File", str(script), ], cwd=str(_BASE_DIR), creationflags=CREATE_NEW_CONSOLE, ) self._be_status_lbl.config(text="Backend: start requested...", fg=TODO_MED) self._toast("Backend-Start angefordert") except Exception as exc: self._toast(f"Start fehlgeschlagen: {exc}", TODO_HIGH) def _stop_backend(self): try: subprocess.Popen( [ "powershell.exe", "-ExecutionPolicy", "Bypass", "-Command", "Get-NetTCPConnection -LocalPort 8000 -ErrorAction SilentlyContinue" " | Select-Object -ExpandProperty OwningProcess -Unique" " | ForEach-Object { Stop-Process -Id $_ -Force -ErrorAction SilentlyContinue }", ], creationflags=0x08000000, ) self._be_status_lbl.config(text="Backend: stop requested...", fg=TODO_MED) self._toast("Backend-Stop angefordert") except Exception as exc: self._toast(f"Stop fehlgeschlagen: {exc}", TODO_HIGH) def _build_status_tab(self): tab = self._tab_status # Status aktualisieren upd_frame = tk.Frame(tab, bg=CARD_BG, bd=1, relief="groove") upd_frame.pack(fill="x", padx=6, pady=8) tk.Label(upd_frame, text="Status aktualisieren", font=(FONT, 10, "bold"), bg=CARD_BG, fg=FG).pack(anchor="w", padx=10, pady=(8, 4)) self._status_entries: dict[str, tk.Entry] = {} for i, (display, key) in enumerate([ ("Phase", "phase"), ("Aktueller Step", "current_step"), ("Letzter abgeschl. Step", "last_completed_step"), ("Naechster Step", "next_step"), ("Letztes Update", "last_update"), ("Notizen", "notes"), ]): row = tk.Frame(upd_frame, bg=CARD_BG) row.pack(fill="x", padx=10, pady=2) tk.Label(row, text=display + ":", font=(FONT, 9), bg=CARD_BG, fg=FG, width=20, anchor="w").pack(side="left", padx=(0, 4)) ent = tk.Entry(row, font=(FONT, 9), bg=TEXT_BG, fg=FG, bd=1, relief="solid") ent.pack(side="left", fill="x", expand=True) self._status_entries[key] = ent btn_row = tk.Frame(upd_frame, bg=CARD_BG) btn_row.pack(fill="x", padx=10, pady=(6, 8)) tk.Button(btn_row, text="Reload", font=(FONT, 9), width=8, relief="groove", bd=1, command=self._reload_status).pack(side="left", padx=(0, 6)) tk.Button(btn_row, text="Save", font=(FONT, 9), width=8, relief="groove", bd=1, bg="#c8e6c9", fg="#1b5e20", command=self._save_status).pack(side="left", padx=(0, 6)) tk.Button(btn_row, text="Copy JSON", font=(FONT, 9), width=10, relief="groove", bd=1, command=self._copy_status_json).pack(side="left", padx=(0, 6)) self._status_feedback = tk.Label(btn_row, text="", font=(FONT, 8), bg=CARD_BG, fg=DONE_ACCENT, anchor="w") self._status_feedback.pack(side="left", padx=12) self._status_fields = {k: None for k in self._status_entries} hist_bar = tk.Frame(tab, bg=BG) hist_bar.pack(fill="x", padx=8, pady=(4, 2)) tk.Label(hist_bar, text="History (letzte 15 Einträge)", font=(FONT, 10, "bold"), bg=BG, fg=FG, anchor="w").pack(side="left") tk.Button(hist_bar, text="Reload History", font=(FONT, 8), width=12, relief="groove", bd=1, command=self._load_history).pack(side="right") hist_frame = tk.Frame(tab, bg=TEXT_BG, bd=1, relief="sunken") hist_frame.pack(fill="both", expand=True, padx=6, pady=(0, 6)) sb = tk.Scrollbar(hist_frame, orient="vertical") sb.pack(side="right", fill="y") self._hist_text = tk.Text( hist_frame, font=(FONT, 9), bg=TEXT_BG, fg=FG, wrap="word", state="disabled", bd=0, yscrollcommand=sb.set, ) self._hist_text.pack(fill="both", expand=True) sb.config(command=self._hist_text.yview) def _build_todo_tab(self): tab = self._tab_todo btn_bar = tk.Frame(tab, bg=BG) btn_bar.pack(fill="x", padx=6, pady=(6, 2)) self._todo_save_btn = tk.Button( btn_bar, text="Speichern", font=(FONT, 9), width=12, relief="groove", bd=1, bg="#c8e6c9", fg="#1b5e20", command=self._save_todos, ) self._todo_save_btn.pack(side="left", padx=(0, 6)) tk.Button( btn_bar, text="Kopieren", font=(FONT, 9), width=10, relief="groove", bd=1, bg="#e0e0e0", fg=FG, command=self._copy_todos_markdown, ).pack(side="left", padx=(0, 6)) tk.Button( btn_bar, text="Export .md", font=(FONT, 9), width=10, relief="groove", bd=1, bg="#e0e0e0", fg=FG, command=self._export_todos_markdown, ).pack(side="left", padx=(0, 6)) self._build_scrollable(tab, "_todo_inner") def _build_roadmap_tab(self): tab = self._tab_road btn_bar = tk.Frame(tab, bg=BG) btn_bar.pack(fill="x", padx=6, pady=(6, 2)) tk.Button( btn_bar, text="Neu laden", font=(FONT, 9), width=10, relief="groove", bd=1, command=self._load_roadmap, ).pack(side="left", padx=(0, 6)) tk.Button( btn_bar, text="Speichern", font=(FONT, 9), width=10, relief="groove", bd=1, bg="#c8e6c9", fg="#1b5e20", command=self._save_roadmap, ).pack(side="left", padx=(0, 6)) tk.Button( btn_bar, text="Kopieren", font=(FONT, 9), width=10, relief="groove", bd=1, bg="#e0e0e0", fg=FG, command=self._copy_roadmap_markdown, ).pack(side="left", padx=(0, 6)) tk.Button( btn_bar, text="Export .md", font=(FONT, 9), width=10, relief="groove", bd=1, bg="#e0e0e0", fg=FG, command=self._export_roadmap_markdown, ).pack(side="left", padx=(0, 6)) self._build_scrollable(tab, "_road_inner") def _build_scrollable(self, tab: tk.Frame, attr: str): container = tk.Frame(tab, bg=BG) container.pack(fill="both", expand=True, padx=6, pady=6) canvas = tk.Canvas(container, bg=BG, highlightthickness=0) sb = tk.Scrollbar(container, orient="vertical", command=canvas.yview) inner = tk.Frame(canvas, bg=BG) inner.bind("", lambda e, c=canvas: c.configure(scrollregion=c.bbox("all"))) canvas.create_window((0, 0), window=inner, anchor="nw") canvas.configure(yscrollcommand=sb.set) canvas.pack(side="left", fill="both", expand=True) sb.pack(side="right", fill="y") def _mw(event, c=canvas): c.yview_scroll(int(-1 * (event.delta / 120)), "units") canvas.bind("", lambda e, c=canvas: c.bind_all("", lambda ev: _mw(ev, c))) canvas.bind("", lambda e, c=canvas: c.unbind_all("")) setattr(self, attr, inner) def _build_handover_tab(self): tab = self._tab_hand btn_bar = tk.Frame(tab, bg=BG) btn_bar.pack(fill="x", padx=6, pady=(6, 2)) tk.Button( btn_bar, text="Neu laden", font=(FONT, 9), width=10, relief="groove", bd=1, command=self._load_handover, ).pack(side="left", padx=(0, 6)) tk.Button( btn_bar, text="Speichern", font=(FONT, 9), width=10, relief="groove", bd=1, bg="#c8e6c9", fg="#1b5e20", command=self._save_handover, ).pack(side="left", padx=(0, 6)) tk.Button( btn_bar, text="Alles kopieren", font=(FONT, 9), width=12, relief="groove", bd=1, bg="#e0e0e0", fg=FG, command=self._handover_copy, ).pack(side="left", padx=(0, 6)) self._hand_path_lbl = tk.Label( btn_bar, text="", font=(FONT, 8), bg=BG, fg="#999", anchor="w", ) self._hand_path_lbl.pack(side="left", fill="x", expand=True) frame = tk.Frame(tab, bg=HAND_BG, bd=1, relief="sunken") frame.pack(fill="both", expand=True, padx=6, pady=(0, 6)) sb = tk.Scrollbar(frame, orient="vertical") sb.pack(side="right", fill="y") self._hand_text = tk.Text( frame, font=(FONT, 10), bg=HAND_BG, fg="#2c2c2c", wrap="word", bd=0, yscrollcommand=sb.set, padx=12, pady=8, ) self._hand_text.pack(fill="both", expand=True) sb.config(command=self._hand_text.yview) self._hand_text.tag_configure("h1", font=(FONT, 14, "bold"), foreground=FG, spacing3=6) self._hand_text.tag_configure("h2", font=(FONT, 12, "bold"), foreground=ACCENT, spacing1=10, spacing3=4) self._hand_text.tag_configure("code", font=("Consolas", 9), background="#e8e4df", foreground="#1a1a1a") self._hand_text.tag_configure("bold", font=(FONT, 10, "bold")) self._hand_text.tag_configure("rule", foreground=TODO_HIGH, font=(FONT, 10)) def _build_notizen_tab(self): """Tab mit Inhalt aus Projekt-Notizen-Fenster (projekt_status.md, changelog.md).""" tab = self._tab_notizen btn_bar = tk.Frame(tab, bg=BG) btn_bar.pack(fill="x", padx=6, pady=(6, 2)) tk.Label( btn_bar, text="Aus Projekt-Notizen-Fenster (projekt_status.md, changelog.md)", font=(FONT, 8), bg=BG, fg="#666", anchor="w", ).pack(side="left") self._notizen_open_btn = tk.Button( btn_bar, text="Projekt-Notizen oeffnen", font=(FONT, 9), width=20, relief="groove", bd=1, bg="#e0e0e0", fg=FG, command=self._open_projekt_notizen, ) self._notizen_open_btn.pack(side="right", padx=(6, 0)) frame = tk.Frame(tab, bg=HAND_BG, bd=1, relief="sunken") frame.pack(fill="both", expand=True, padx=6, pady=(0, 6)) sb = tk.Scrollbar(frame, orient="vertical") sb.pack(side="right", fill="y") self._notizen_text = tk.Text( frame, font=(FONT, 10), bg=HAND_BG, fg="#2c2c2c", wrap="word", state="disabled", bd=0, yscrollcommand=sb.set, padx=12, pady=8, ) self._notizen_text.pack(fill="both", expand=True) sb.config(command=self._notizen_text.yview) self._notizen_text.tag_configure("h1", font=(FONT, 14, "bold"), foreground=FG, spacing3=6) self._notizen_text.tag_configure("h2", font=(FONT, 12, "bold"), foreground=ACCENT, spacing1=10, spacing3=4) self._notizen_text.tag_configure("code", font=("Consolas", 9), background="#e8e4df", foreground="#1a1a1a") def _open_projekt_notizen(self): """Projekt-Notizen-Fenster ueber Parent-App oeffnen.""" try: parent = self.master if hasattr(parent, "open_notizen_window"): parent.open_notizen_window() self._toast("Projekt-Notizen geoeffnet") else: self._toast("Projekt-Notizen nicht verfuegbar", TODO_HIGH) except Exception as exc: self._toast(f"Fehler: {exc}", TODO_HIGH) # ── Toast feedback ───────────────────────────────────────────────────── def _toast(self, msg: str, color: str = DONE_ACCENT, ms: int = 3000): self._toast_var.set(msg) self._toast_lbl.config(fg=color) if self.winfo_exists(): self.after(ms, lambda: self._toast_var.set("")) # ── Todo status action (project_todos.json) ───────────────────────────── def _set_todo_status(self, todo_id: str, new_status: str): data = _read_json(_TODOS_FILE) if data is None: self._toast("Fehler: project_todos.json nicht lesbar", TODO_HIGH) return items = data.get("items", []) target = None for t in items: if t.get("id") == todo_id: target = t break if target is None: self._toast(f"Fehler: {todo_id} nicht gefunden", TODO_HIGH) return old_status = target.get("status", "open") new_status_key = _TODO_STATUS_REV.get(new_status.lower(), new_status.lower().replace(" ", "_")) if old_status == new_status_key: return target["status"] = new_status_key data["updated_at"] = datetime.now(timezone.utc).isoformat() if not _save_json_atomic(_TODOS_FILE, data): self._toast("Fehler beim Speichern", TODO_HIGH) return self._toast(f"{todo_id}: {old_status} -> {new_status_key}") self._load_todos() def _save_todos(self): data = _read_json(_TODOS_FILE) if data is None: self._toast("Fehler: project_todos.json nicht lesbar", TODO_HIGH) return items = data.get("items", []) entries = getattr(self, "_todo_detail_entries", {}) for item in items: tid = item.get("id", "") ent = entries.get(tid) if ent is not None and ent.winfo_exists(): try: item["details"] = ent.get().strip() except Exception: pass data["updated_at"] = datetime.now(timezone.utc).isoformat() if not _save_json_atomic(_TODOS_FILE, data): self._toast("Fehler beim Speichern", TODO_HIGH) return self._toast("To-Dos gespeichert") def _refresh_data_only(self): """Re-read local file and refresh all tabs without API call.""" try: status_data = _read_json(_STATUS_FILE) self._load_status(status_data) self._load_history() self._load_todos() self._load_roadmap() self._load_notizen() except Exception: pass # ── Daten laden ──────────────────────────────────────────────────────── def _refresh(self): try: status_data, source = _get_status_data() self._source_lbl.config(text=f"Quelle: {source} · alle 5 Min.") self._load_status(status_data) self._load_history() self._load_completed() self._load_todos() self._load_roadmap() self._load_notizen() self._load_handover() except Exception: pass if self.winfo_exists(): self.after(REFRESH_MS, self._refresh) def _load_status(self, data: dict | None): if data is None: for ent in getattr(self, "_status_entries", {}).values(): if ent and ent.winfo_exists(): ent.delete(0, "end") return for key, ent in getattr(self, "_status_entries", {}).items(): if ent and ent.winfo_exists(): val = data.get(key, "") ent.delete(0, "end") ent.insert(0, str(val) if val is not None else "") def _reload_status(self): data = _fetch_status_from_api() if data is None: data = _read_json(_STATUS_FILE) if data: self._load_status(data) self._load_history() self._status_feedback.config(text="Geladen", fg=DONE_ACCENT) self.after(2000, lambda: self._status_feedback.config(text="")) else: self._status_feedback.config(text="Fehler: Backend nicht erreichbar", fg=TODO_HIGH) def _save_status(self): payload = {} for key, ent in getattr(self, "_status_entries", {}).items(): if ent and ent.winfo_exists(): val = ent.get().strip() if key in ("current_step", "last_completed_step", "next_step"): try: payload[key] = int(val) if val else 0 except ValueError: payload[key] = 0 else: payload[key] = val data, err = _post_status_from_api(payload) if err: self._status_feedback.config(text=f"Fehler: {err}", fg=TODO_HIGH) return self._load_status(data) self._load_history() self._status_feedback.config(text="Gespeichert", fg=DONE_ACCENT) self.after(2000, lambda: self._status_feedback.config(text="")) def _copy_status_json(self): payload = {} for key, ent in getattr(self, "_status_entries", {}).items(): if ent and ent.winfo_exists(): val = ent.get().strip() if key in ("current_step", "last_completed_step", "next_step"): try: payload[key] = int(val) if val else 0 except ValueError: payload[key] = 0 else: payload[key] = val try: self.clipboard_clear() self.clipboard_append(json.dumps(payload, indent=2, ensure_ascii=False)) self.update() self._status_feedback.config(text="Kopiert", fg=DONE_ACCENT) self.after(1500, lambda: self._status_feedback.config(text="")) except Exception: self._status_feedback.config(text="Kopieren fehlgeschlagen", fg=TODO_HIGH) # ── Export/Copy helpers (To-Do & Roadmap) ───────────────────────────── def _todos_to_markdown(self, data: dict) -> str: items = data.get("items", []) if isinstance(data, dict) else [] lines = [] lines.append("AZA – To-Do Export") lines.append("=" * 40) lines.append(f"Export: {datetime.now(timezone.utc).isoformat()}") updated = (data.get("updated_at") or "").strip() if isinstance(data, dict) else "" if updated: lines.append(f"updated_at: {updated}") lines.append("") if not items: lines.append("(keine To-Dos)") return "\n".join(lines) + "\n" for it in items: tid = it.get("id", "") title = it.get("title", "") status = it.get("status", "") prio = it.get("priority", "") area = it.get("area", []) if isinstance(area, str): area_txt = area elif isinstance(area, list): area_txt = ", ".join([str(a) for a in area]) else: area_txt = "" desc = (it.get("description") or "").strip() details = (it.get("details") or "").strip() lines.append(f"- **{tid}** [{prio}] ({status}) — {title}") if area_txt: lines.append(f" - Area: {area_txt}") if desc: lines.append(f" - Desc: {desc}") if details: lines.append(f" - Details: {details}") return "\n".join(lines) + "\n" def _roadmap_to_markdown(self, data: dict) -> str: phases = data.get("phases", []) if isinstance(data, dict) else [] lines = [] lines.append("AZA – Roadmap Export") lines.append("=" * 40) lines.append(f"Export: {datetime.now(timezone.utc).isoformat()}") updated = (data.get("updated_at") or "").strip() if isinstance(data, dict) else "" if updated: lines.append(f"updated_at: {updated}") lines.append("") if not phases: lines.append("(keine Roadmap-Phasen)") return "\n".join(lines) + "\n" for ph in phases: name = ph.get("name", "Phase") st = ph.get("status", "open") lines.append(f"## {name} ({st})") ms = ph.get("milestones", []) or [] if not ms: lines.append("- (keine Milestones)") lines.append("") continue for m in ms: mn = m.get("name", "") mst = m.get("status", "") lines.append(f"- [{mst}] {mn}") lines.append("") return "\n".join(lines) + "\n" def _copy_todos_markdown(self): data = _read_json(_TODOS_FILE) or {} md = self._todos_to_markdown(data) try: self.clipboard_clear() self.clipboard_append(md) self.update() self._toast("To-Dos kopiert (Markdown)") except Exception: self._toast("Kopieren fehlgeschlagen", TODO_HIGH) def _export_todos_markdown(self): data = _read_json(_TODOS_FILE) or {} md = self._todos_to_markdown(data) dest = filedialog.asksaveasfilename( title="To-Dos exportieren (Markdown)", defaultextension=".md", filetypes=[("Markdown", "*.md"), ("Text", "*.txt"), ("Alle Dateien", "*.*")], initialfile="aza_todos_export.md", ) if not dest: return try: Path(dest).write_text(md, encoding="utf-8") self._toast(f"Exportiert: {dest}") except Exception as exc: self._toast(f"Export fehlgeschlagen: {exc}", TODO_HIGH) def _copy_roadmap_markdown(self): data = _read_json(_ROADMAP_FILE) or {} md = self._roadmap_to_markdown(data) try: self.clipboard_clear() self.clipboard_append(md) self.update() self._toast("Roadmap kopiert (Markdown)") except Exception: self._toast("Kopieren fehlgeschlagen", TODO_HIGH) def _export_roadmap_markdown(self): data = _read_json(_ROADMAP_FILE) or {} md = self._roadmap_to_markdown(data) dest = filedialog.asksaveasfilename( title="Roadmap exportieren (Markdown)", defaultextension=".md", filetypes=[("Markdown", "*.md"), ("Text", "*.txt"), ("Alle Dateien", "*.*")], initialfile="aza_roadmap_export.md", ) if not dest: return try: Path(dest).write_text(md, encoding="utf-8") self._toast(f"Exportiert: {dest}") except Exception as exc: self._toast(f"Export fehlgeschlagen: {exc}", TODO_HIGH) def _load_history(self): lines: list[str] = [] if _HISTORY_FILE.is_file(): try: with open(str(_HISTORY_FILE), "r", encoding="utf-8") as f: all_lines = f.readlines() lines = [ln for ln in all_lines if ln.strip()][-15:] except Exception: pass self._hist_text.config(state="normal") self._hist_text.delete("1.0", "end") if not lines: self._hist_text.insert("end", "(noch keine History-Einträge)\n") else: for line in lines: try: e = json.loads(line.strip()) ts = (e.get("ts") or e.get("ts_utc") or "")[:19].replace("T", " ") action = e.get("action", "") if action == "todo_status_change": self._hist_text.insert( "end", f"{ts} | {e.get('todo_id','')} " f"{e.get('old_status','')} -> {e.get('new_status','')} " f"| Step {e.get('current_step','')}\n", ) elif action in ("update", "read"): st = e.get("status", {}) self._hist_text.insert( "end", f"{ts} | {action} | Step {st.get('current_step','')} | " f"{st.get('phase','')} | {st.get('last_update','')}\n", ) else: st = e.get("status", e) self._hist_text.insert( "end", f"{ts} | Step {st.get('current_step', e.get('current_step',''))} | " f"{st.get('phase', e.get('phase',''))} | {st.get('notes', e.get('notes',''))}\n", ) except Exception: pass self._hist_text.config(state="disabled") self._hist_text.see("end") # ── Erledigt ─────────────────────────────────────────────────────────── def _load_completed(self): plan = _read_json(_PLAN_FILE) for w in self._done_inner.winfo_children(): w.destroy() completed = (plan or {}).get("completed", []) if not completed: tk.Label(self._done_inner, text="(keine Einträge)", font=(FONT, 10), bg=BG, fg=FG).pack(pady=10) return tk.Label( self._done_inner, text=f"{len(completed)} Schritte abgeschlossen", font=(FONT, 11, "bold"), bg=BG, fg=DONE_ACCENT, anchor="w", ).pack(fill="x", pady=(0, 6)) for item in completed: self._render_done_card(item) def _render_done_card(self, item: dict): card = tk.Frame(self._done_inner, bg=DONE_BG, bd=1, relief="groove") card.pack(fill="x", pady=2) header = tk.Frame(card, bg=DONE_BG) header.pack(fill="x", padx=8, pady=(6, 0)) tk.Label(header, text=f"✓ Step {item.get('step','')}", font=(FONT, 9, "bold"), bg=DONE_BG, fg=DONE_ACCENT).pack(side="left") tk.Label(header, text=item.get("title", ""), font=(FONT, 10, "bold"), bg=DONE_BG, fg=FG).pack(side="left", padx=(8, 0)) desc = item.get("description", "") if desc: tk.Label(card, text=desc, font=(FONT, 9), bg=DONE_BG, fg="#3a6a5a", anchor="w", wraplength=560, justify="left").pack(fill="x", padx=(32, 8), pady=(0, 2)) files = ", ".join(item.get("files", [])) if files: tk.Label(card, text=files, font=(FONT, 8), bg=DONE_BG, fg="#7a9a8a", anchor="w").pack(fill="x", padx=(32, 8), pady=(0, 6)) # ── To-Do (project_todos.json) ────────────────────────────────────────── def _load_todos(self): for w in self._todo_inner.winfo_children(): w.destroy() self._todo_detail_entries = {} data = _read_json(_TODOS_FILE) if data is None: if not _TODOS_FILE.is_file(): _default = {"version": 1, "updated_at": None, "items": []} try: with open(str(_TODOS_FILE), "w", encoding="utf-8") as f: json.dump(_default, f, indent=2, ensure_ascii=False) data = _default except Exception: pass if data is None: tk.Label( self._todo_inner, text="project_todos.json nicht lesbar oder nicht vorhanden.", font=(FONT, 10), bg=BG, fg=TODO_HIGH, wraplength=500, ).pack(pady=20) return items = data.get("items", []) if not items: tk.Label( self._todo_inner, text="Keine Aufgaben in project_todos.json.", font=(FONT, 11), bg=BG, fg=FG, ).pack(pady=20) return def _norm(t): prio = _TODO_PRIO_MAP.get(t.get("priority", "").upper(), t.get("priority", "")) st = _TODO_STATUS_MAP.get(t.get("status", "").lower(), t.get("status", "")) return {"priority": prio, "status": st, "title": t.get("title", "")} sorted_items = sorted(items, key=lambda t: ( _PRIO_ORDER.get(_norm(t)["priority"].lower(), 9), _STATUS_ORDER.get(_norm(t)["status"].lower(), 9), t.get("title", ""), )) active = [t for t in sorted_items if t.get("status", "").lower() != "done"] done = [t for t in sorted_items if t.get("status", "").lower() == "done"] summary = tk.Frame(self._todo_inner, bg=CARD_BG, bd=1, relief="groove") summary.pack(fill="x", pady=(0, 8)) tk.Label( summary, text=f"Offen / In Arbeit: {len(active)}", font=(FONT, 11, "bold"), bg=CARD_BG, fg=TODO_HIGH, padx=10, pady=4, ).pack(side="left") tk.Label( summary, text=f"Erledigt: {len(done)}", font=(FONT, 11, "bold"), bg=CARD_BG, fg=DONE_ACCENT, padx=10, pady=4, ).pack(side="right") current_prio = None for item in sorted_items: prio = _TODO_PRIO_MAP.get(item.get("priority", "").upper(), item.get("priority", "").upper()) if prio != current_prio: current_prio = prio sep = tk.Frame(self._todo_inner, bg=BG) sep.pack(fill="x", pady=(8, 2)) prio_colors = {"HOCH": TODO_HIGH, "MITTEL": TODO_MED, "NIEDRIG": TODO_LOW} tk.Label(sep, text=f"── {prio} ──", font=(FONT, 9, "bold"), bg=BG, fg=prio_colors.get(prio, FG)).pack(side="left", padx=4) self._render_todo_card(item) def _render_todo_card(self, item: dict): st = item.get("status", "open").lower() cur_status = _TODO_STATUS_MAP.get(st, st) is_done = st == "done" bg = DONE_BG if is_done else TODO_BG card = tk.Frame(self._todo_inner, bg=bg, bd=1, relief="groove") card.pack(fill="x", pady=2) prio = _TODO_PRIO_MAP.get(item.get("priority", "").upper(), item.get("priority", "")) prio_color = TODO_LOW prio_sym = "○" if prio == "HOCH": prio_color = TODO_HIGH prio_sym = "●" elif prio == "MITTEL": prio_color = TODO_MED prio_sym = "◐" header = tk.Frame(card, bg=bg) header.pack(fill="x", padx=8, pady=(6, 0)) if is_done: tk.Label(header, text="✓", font=(FONT, 10, "bold"), bg=bg, fg=DONE_ACCENT).pack(side="left") else: tk.Label(header, text=prio_sym, font=(FONT, 10), bg=bg, fg=prio_color).pack(side="left") title_font = (FONT, 10, "bold") if not is_done else (FONT, 10, "overstrike") tk.Label(header, text=item.get("title", ""), font=title_font, bg=bg, fg=FG).pack(side="left", padx=(6, 0)) st_color = DONE_ACCENT if is_done else (TODO_HIGH if cur_status == "in Arbeit" else prio_color) tk.Label(header, text=f"[{cur_status}]", font=(FONT, 9), bg=bg, fg=st_color).pack(side="right") area = item.get("area", "") if area: area_key = area.lower() label_text, label_color = _AREA_LABELS.get(area_key, (area, "#95a5a6")) tag_fr = tk.Frame(card, bg=bg) tag_fr.pack(fill="x", padx=(30, 8), pady=(2, 0)) tk.Label(tag_fr, text=f" {label_text} ", font=(FONT, 8), bg=label_color, fg="white", padx=3).pack(side="left") detail = tk.Frame(card, bg=bg) detail.pack(fill="x", padx=(30, 8), pady=(0, 2)) ent = tk.Entry(detail, font=(FONT, 9), bg=TEXT_BG, fg="#5a5540", bd=1, relief="solid") ent.insert(0, item.get("details", "")) ent.pack(fill="x", pady=(2, 0)) tid = item.get("id", "") if tid: self._todo_detail_entries[tid] = ent btn_row = tk.Frame(card, bg=bg) btn_row.pack(fill="x", padx=(30, 8), pady=(2, 6)) if tid: tk.Label(btn_row, text=tid, font=(FONT, 8), bg=bg, fg="#aaa").pack(side="left") btn_offen = tk.Button( btn_row, text="Offen", font=(FONT, 8), width=8, relief="groove", bd=1, bg="#e0e0e0", fg=FG, state="disabled" if cur_status == "offen" else "normal", command=lambda t=tid: self._set_todo_status(t, "offen"), ) btn_offen.pack(side="right", padx=(3, 0)) btn_done = tk.Button( btn_row, text="Erledigt", font=(FONT, 8), width=8, relief="groove", bd=1, bg="#c8e6c9", fg="#1b5e20", state="disabled" if is_done else "normal", command=lambda t=tid: self._set_todo_status(t, "erledigt"), ) btn_done.pack(side="right", padx=(3, 0)) btn_active = tk.Button( btn_row, text="In Arbeit", font=(FONT, 8), width=8, relief="groove", bd=1, bg="#fff3e0", fg="#e65100", state="disabled" if cur_status == "in Arbeit" else "normal", command=lambda t=tid: self._set_todo_status(t, "in Arbeit"), ) btn_active.pack(side="right", padx=(3, 0)) # ── Roadmap (project_roadmap.json) ────────────────────────────────────── def _load_roadmap(self): for w in self._road_inner.winfo_children(): w.destroy() self._roadmap_combos = [] data = _read_json(_ROADMAP_FILE) if data is None: tk.Label( self._road_inner, text="project_roadmap.json nicht lesbar oder nicht vorhanden.", font=(FONT, 10), bg=BG, fg=TODO_HIGH, wraplength=500, ).pack(pady=20) return self._roadmap_data = data phases = data.get("phases", []) if not phases: tk.Label(self._road_inner, text="Keine Phasen in project_roadmap.json.", font=(FONT, 10), bg=BG, fg=FG).pack(pady=20) return status_options = ["open", "in_progress", "done"] for pi, ph in enumerate(phases): ph_status = ph.get("status", "open").lower() border_color = DONE_ACCENT if ph_status == "done" else (TODO_MED if ph_status == "in_progress" else ACCENT) frame = tk.Frame(self._road_inner, bg=ROAD_BG, bd=2, relief="groove") frame.pack(fill="x", pady=4) head = tk.Frame(frame, bg=border_color, height=28) head.pack(fill="x") head.pack_propagate(False) sym = "✓" if ph_status == "done" else ("▶" if ph_status == "in_progress" else "○") tk.Label(head, text=f" {sym} {ph.get('name', '')}", font=(FONT, 10, "bold"), bg=border_color, fg="white").pack(side="left") total = len(ph.get("milestones", [])) done_count = sum(1 for m in ph.get("milestones", []) if m.get("status", "").lower() == "done") if total > 0: pct = int(done_count / total * 100) tk.Label(head, text=f"{done_count}/{total} ({pct}%)", font=(FONT, 8), bg=border_color, fg="#e0e0e0").pack(side="right", padx=8) for mi, ms in enumerate(ph.get("milestones", [])): ms_status = ms.get("status", "open").lower() ms_color = DONE_ACCENT if ms_status == "done" else (TODO_MED if ms_status == "in_progress" else "#888") ms_sym = "✓" if ms_status == "done" else ("▶" if ms_status == "in_progress" else "○") row = tk.Frame(frame, bg=ROAD_BG) row.pack(fill="x", padx=12, pady=2) tk.Label(row, text=f" {ms_sym}", font=(FONT, 9), bg=ROAD_BG, fg=ms_color).pack(side="left") tk.Label(row, text=ms.get("name", ""), font=(FONT, 9), bg=ROAD_BG, fg=FG).pack(side="left", padx=(4, 0)) var = tk.StringVar(value=ms.get("status", "open")) combo = ttk.Combobox(row, textvariable=var, values=status_options, state="readonly", width=12, font=(FONT, 8)) combo.pack(side="right", padx=(4, 0)) self._roadmap_combos.append((pi, mi, var)) def _save_roadmap(self): data = getattr(self, "_roadmap_data", None) or _read_json(_ROADMAP_FILE) if data is None: self._toast("Fehler: project_roadmap.json nicht lesbar", TODO_HIGH) return for pi, mi, var in getattr(self, "_roadmap_combos", []): try: phases = data.get("phases", []) if pi < len(phases): milestones = phases[pi].get("milestones", []) if mi < len(milestones): milestones[mi]["status"] = var.get() except Exception: pass for ph in data.get("phases", []): statuses = [m.get("status", "open").lower() for m in ph.get("milestones", [])] if all(s == "done" for s in statuses) and statuses: ph["status"] = "done" elif any(s == "in_progress" for s in statuses): ph["status"] = "in_progress" elif any(s == "done" for s in statuses): ph["status"] = "in_progress" data["updated_at"] = datetime.now(timezone.utc).isoformat() if not _save_json_atomic(_ROADMAP_FILE, data): self._toast("Fehler beim Speichern", TODO_HIGH) return self._toast("Roadmap gespeichert") self._load_roadmap() # ── Projektnotizen ──────────────────────────────────────────────────── def _load_notizen(self): """Laedt projekt_status.md und changelog.md aus dem Projekt-Notizen-Ordner.""" parts = [] for label, full_path in [ ("Projektstatus", _NOTIZEN_PROJEKT), ("Changelog", _NOTIZEN_CHANGELOG), ]: content = "" if full_path.is_file(): try: with open(str(full_path), "r", encoding="utf-8") as f: content = f.read() except Exception: content = "(Fehler beim Lesen)" else: content = f"(Datei noch nicht vorhanden. Im Projekt-Notizen-Fenster den Tab \"{label}\" oeffnen und Inhalt anlegen.)" parts.append((label, content)) combined = "" for label, content in parts: combined += f"\n## {label}\n\n{content.strip()}\n\n" self._notizen_text.config(state="normal") self._notizen_text.delete("1.0", "end") if not combined.strip(): self._notizen_text.insert("end", "Keine Projektnotizen vorhanden.\n\nOeffnen Sie das Projekt-Notizen-Fenster und legen Sie projekt_status.md oder changelog.md an.") else: in_code = False for line in combined.split("\n"): if line.strip().startswith("```"): in_code = not in_code continue if in_code: self._notizen_text.insert("end", line + "\n", "code") elif line.startswith("# "): self._notizen_text.insert("end", line[2:] + "\n", "h1") elif line.startswith("## "): self._notizen_text.insert("end", line[3:] + "\n", "h2") elif line.startswith("- ") or line.startswith("* "): self._notizen_text.insert("end", " " + line + "\n") else: self._notizen_text.insert("end", line + "\n") self._notizen_text.config(state="disabled") # ── Handover actions ────────────────────────────────────────────────── def _save_handover(self): try: content = self._hand_text.get("1.0", "end-1c") with open(str(_PROJECT_HANDOVER_FILE), "w", encoding="utf-8") as f: f.write(content) self._hand_path_lbl.config(text=str(_PROJECT_HANDOVER_FILE)) self._toast("Handover gespeichert") except Exception as exc: self._toast(f"Fehler: {exc}", TODO_HIGH) def _handover_copy(self): try: content = self._hand_text.get("1.0", "end-1c") self.clipboard_clear() self.clipboard_append(content) self.update() self._toast("Handover in Zwischenablage kopiert") except Exception as exc: self._toast(f"Fehler: {exc}", TODO_HIGH) # ── Handover (project_handover.md) ────────────────────────────────────── def _load_handover(self): content = "" if _PROJECT_HANDOVER_FILE.is_file(): try: with open(str(_PROJECT_HANDOVER_FILE), "r", encoding="utf-8") as f: content = f.read() try: self._hand_path_lbl.config(text=str(_PROJECT_HANDOVER_FILE)) except Exception: pass except Exception: content = _HANDOVER_DEFAULT try: self._hand_path_lbl.config(text="(Fehler beim Lesen)") except Exception: pass else: content = _HANDOVER_DEFAULT try: self._hand_path_lbl.config(text="(Default - Speichern erstellt Datei)") except Exception: pass self._hand_text.config(state="normal") self._hand_text.delete("1.0", "end") self._hand_text.insert("1.0", content)