Files
aza/AzA march 2026 - Kopie (12)/dev_status_window.py
2026-04-16 13:32:32 +02:00

1295 lines
54 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# -*- 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 23 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("<Configure>", 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("<Enter>", lambda e, c=canvas: c.bind_all("<MouseWheel>", lambda ev: _mw(ev, c)))
canvas.bind("<Leave>", lambda e, c=canvas: c.unbind_all("<MouseWheel>"))
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)