Files
aza/AzA march 2026 - Kopie (6)/dev_status_window.py

1295 lines
54 KiB
Python
Raw Normal View History

2026-04-16 13:32:32 +02:00
# -*- 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)