From 597f7ba552967fc5e002bc60c43008fc1515b7aab9b766070188705578d5994e Mon Sep 17 00:00:00 2001 From: wlfb Date: Sun, 19 Jul 2026 14:30:40 +0200 Subject: [PATCH] docs: Implementierungsplan Ausbaustufe 2 Co-Authored-By: Claude Fable 5 --- .../plans/2026-07-19-ausbaustufe-2.md | 310 ++++++++++++++++++ 1 file changed, 310 insertions(+) create mode 100644 docs/superpowers/plans/2026-07-19-ausbaustufe-2.md diff --git a/docs/superpowers/plans/2026-07-19-ausbaustufe-2.md b/docs/superpowers/plans/2026-07-19-ausbaustufe-2.md new file mode 100644 index 0000000..c6772c9 --- /dev/null +++ b/docs/superpowers/plans/2026-07-19-ausbaustufe-2.md @@ -0,0 +1,310 @@ +# Ausbaustufe 2 — Implementierungsplan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Parser-Qualität auf Fable-Abnahmeniveau bringen, Konto-Namen und Betrag-Spalte verbessern, Version + Hilfe-Seite in die GUI, alle Anwenderdaten (inkl. Secrets) unter `~/.local/share/finance_pod`, ein gemeinsames Passwort für GUI und Grafana mit funktionierendem Dashboard-Embedding. + +**Architecture:** Bestehendes System (FastAPI + Jinja2/HTMX, Postgres, Grafana, rootless Podman) wird erweitert, nicht umgebaut. Secrets wandern in das Bind-Verzeichnis des Pods, damit Backup+`create_pod_finance.sh` allein für Disaster-Recovery reichen. Grafana-Admin-Passwort wird bei jedem Skript-Lauf aus dem gemeinsamen `FB_PASSWORD` synchronisiert. + +**Tech Stack:** unverändert (Python 3.12 im Container, venv 3.11 lokal, pytest, pdfplumber, Grafana OSS 12.1.0). + +## Global Constraints + +- Arbeitsverzeichnis `/home/wlfb/bin`, Tool-Code unter `finance/`; Phase „.env-Umzug" ändert zusätzlich Repo `/home/wlfb/fb` (dort separat committen). +- Geldbeträge `Decimal`; Nutzertexte Deutsch; vor jedem Commit `cd /home/wlfb/bin/finance && .venv/bin/python -m pytest -q` grün (Basis: 61 passed). +- Commit-Messages enden mit `Co-Authored-By: Claude Fable 5 `. +- DATENSCHUTZ: `tests/fixtures/*.pdf` sind echte Kontoauszüge. Keine echten Daten (Beträge, IBANs, Namen, Verwendungszwecke, Datumsangaben aus Auszügen) in Commits, Berichten, Code-Kommentaren oder Test-Dateien. Erwartungswerte für Tests liegen NUR in gitignorten `tests/fixtures/expected_*.json`. +- Parser-Abnahme erfolgt durch den Controller (Fable) persönlich, nicht durch Subagenten-Selbsteinschätzung. +- Deployment-Änderungen (Tasks 5–7) erst nach den App-Tasks; Redeploy gesammelt in Task 7. + +--- + +### Task 1: Parser-Audit-Werkzeug + DKB-Parser-Korrektur (Fable-Abnahme) + +**Files:** +- Create: `finance/scripts/parser_audit.py` +- Modify: `finance/app/parsers/dkb.py` (nach Audit-Befund; `vr.py`/`hvb.py` nur falls Audit strukturgleiche Fehler zeigt) +- Modify: `finance/tests/test_bank_parsers.py` (Noise-Checks + expected-Datei-Tests) +- Modify: `finance/.gitignore` (Zeile `tests/fixtures/expected_*.json` ergänzen) + +**Interfaces:** +- Produces: `parser_audit.py` CLI: `python scripts/parser_audit.py tests/fixtures/dkb_beispiel.pdf` → gibt pro Transaktion eine nummerierte Zeile `#i | booking | value | amount | counterparty | purpose` aus, danach den Rohtext (via bestehender Extraktion). Nur lokale Nutzung; Ausgabe enthält echte Daten und darf nirgends persistiert werden außer im gitignorten `.superpowers/`-Workspace. +- Produces: Testschema `tests/fixtures/expected_.json` (gitignored): + ```json + {"transaction_count": 0, + "spot_checks": [{"index": 0, "booking_date": "YYYY-MM-DD", + "amount": "0.00", "counterparty_contains": "...", + "purpose_contains": "..."}]} + ``` + +- [ ] **Step 1: Audit-Skript schreiben** + +`finance/scripts/parser_audit.py`: +```python +"""Parser-Audit: geparste Transaktionen + Rohtext einer Fixture anzeigen. + +NUR lokal verwenden - Ausgabe enthaelt echte Kontodaten und darf nicht +in Commits, Reports oder Tickets uebernommen werden. +Aufruf: python scripts/parser_audit.py tests/fixtures/dkb_beispiel.pdf +""" +import sys +from pathlib import Path + +import pdfplumber + +from app.parsers.registry import parse_pdf +from app.parsers.validate import balance_difference + +path = Path(sys.argv[1]) +stmt = parse_pdf(path) +print(f"bank={stmt.bank} iban={stmt.iban} " + f"period={stmt.period_start}..{stmt.period_end}") +print(f"opening={stmt.opening_balance} closing={stmt.closing_balance} " + f"diff={balance_difference(stmt)} n_tx={len(stmt.transactions)}") +print("-" * 100) +for i, t in enumerate(stmt.transactions): + print(f"#{i:3d} | {t.booking_date} | {t.value_date} | {t.amount:>12} " + f"| {t.counterparty[:40]:40} | {t.purpose[:60]}") +print("=" * 100) +with pdfplumber.open(path) as pdf: + for n, page in enumerate(pdf.pages, 1): + print(f"--- Seite {n} ---") + print(page.extract_text() or "(kein Text)") +``` + +- [ ] **Step 2: Controller-Audit (Fable, kein Subagent)** — Der Controller führt das Skript für `dkb_beispiel.pdf` aus, vergleicht JEDE geparste Transaktion feldweise mit dem Rohtext (Buchungs-/Wertstellungsdatum, Betrag, Empfänger, Verwendungszweck, keine Seitenkopf-/Übertrag-/Saldo-Reste, keine verschluckten oder zusammengeklebten Buchungen) und schreibt eine Mängelliste nach `.superpowers/sdd/parser-audit-dkb.md` (gitignorter Workspace — Rohdaten-Zitate nur dort). Danach dasselbe kompakt für `vr_beispiel.pdf` und `hvb_beispiel.pdf` (Strukturprüfung; Detailkorrektur nur bei Befund). + +- [ ] **Step 3: Parser fixen (Subagent, Mängelliste als Brief)** — Nur die im Audit benannten Defekte beheben; Interface `parse(path) -> ParsedStatement` und Modulstruktur unverändert. Nach jedem Fix: `pytest tests/test_bank_parsers.py -v` (Saldo-Gate bleibt Pflicht). + +- [ ] **Step 4: Tests härten** + +In `finance/tests/test_bank_parsers.py` ergänzen (committebar, ohne echte Daten): +```python +import json +import re + +NOISE = re.compile( + r"(Kontostand|Übertrag|alter Saldo|neuer Saldo|Seite \d|Blatt \d)", re.I) + + +@pytest.mark.parametrize("filename,bank", CASES) +def test_no_noise_in_parsed_fields(filename, bank): + path = FIXTURES / filename + if not path.exists(): + pytest.skip(f"Fixture {filename} fehlt") + stmt = parse_pdf(path) + for t in stmt.transactions: + assert not NOISE.search(t.purpose), f"Noise im Verwendungszweck: #{stmt.transactions.index(t)}" + assert not NOISE.search(t.counterparty), f"Noise im Empfänger: #{stmt.transactions.index(t)}" + assert stmt.period_start <= t.booking_date <= stmt.period_end + + +@pytest.mark.parametrize("filename,bank", CASES) +def test_expected_values(filename, bank): + path = FIXTURES / filename + exp_path = FIXTURES / f"expected_{bank}.json" + if not path.exists() or not exp_path.exists(): + pytest.skip("Fixture oder expected-Datei fehlt") + stmt = parse_pdf(path) + exp = json.loads(exp_path.read_text()) + assert len(stmt.transactions) == exp["transaction_count"] + for c in exp["spot_checks"]: + t = stmt.transactions[c["index"]] + assert str(t.booking_date) == c["booking_date"] + assert str(t.amount) == c["amount"] + assert c["counterparty_contains"].lower() in t.counterparty.lower() + assert c["purpose_contains"].lower() in t.purpose.lower() +``` + +`expected_dkb.json` (und bei Befund `expected_vr.json`/`expected_hvb.json`) erstellt der CONTROLLER aus dem Audit (mind. 5 Spot-Checks über den Auszug verteilt, inkl. erster + letzter Buchung). Datei bleibt gitignored — `finance/.gitignore` um `tests/fixtures/expected_*.json` ergänzen. + +- [ ] **Step 5: Fable-Abnahme** — Controller wiederholt Step 2 auf dem gefixten Stand; Abnahme erst, wenn feldweise keine Mängel mehr bestehen und die volle Suite grün ist. + +- [ ] **Step 6: Commit** — `fix: DKB-Parser feldweise korrigiert, Audit-Werkzeug und gehaertete Parser-Tests` (Mängel im Commit-Body nur GENERISCH beschreiben, ohne echte Daten). + +--- + +### Task 2: Konto-Namen + Betrag-Spalte + +**Files:** +- Modify: `finance/app/routers/accounts.py` (PATCH-Route), `finance/app/routers/gui.py` (Konten in Übersicht-Kontext), `finance/app/templates/index.html` (Umbenennen-Formular), `finance/app/templates/transactions.html` (Spaltenkopf „Betrag (€)", Zellen ohne €, rechtsbündig), `finance/app/static/style.css` (`.amount { text-align: right }`, `td.account { word-break: break-all; max-width: 12rem }`) +- Test: `finance/tests/test_crud_api.py` (PATCH-Tests) + +**Interfaces:** +- Produces: `PATCH /api/accounts/{id}` mit Body `{"name": str}` (nur `name`, 1–100 Zeichen, führende/abschließende Leerzeichen gestrippt; leer → 422) → 200 mit AccountOut; 404 bei unbekanntem Konto. +- GUI: In der Übersicht je Konto-Zeile Inline-Formular (Textfeld vorbefüllt mit aktuellem Namen, Button „Umbenennen", `hx-patch` via bestehender json-form-Extension auf `/api/accounts/{id}`, danach Reload/Refresh der Tabelle). Buchungsliste zeigt weiterhin `account_names`-Mapping — profitiert automatisch. + +- [ ] **Step 1: Failing Tests** — in `finance/tests/test_crud_api.py`: +```python +def test_patch_account_name(client): + acc = client.post("/api/accounts", headers=H, json={ + "bank": "DKB", "iban": "DE71", "name": "DE71", "type": "giro"}).json() + r = client.patch(f"/api/accounts/{acc['id']}", headers=H, + json={"name": "DKB Giro"}) + assert r.status_code == 200 and r.json()["name"] == "DKB Giro" + assert client.patch("/api/accounts/9999", headers=H, + json={"name": "x"}).status_code == 404 + assert client.patch(f"/api/accounts/{acc['id']}", headers=H, + json={"name": " "}).status_code == 422 +``` +- [ ] **Step 2: Ausführen — FAIL** · **Step 3: Implementieren** (Route mit `AccountPatch(BaseModel)`; `field_validator` strippt und verwirft leere Namen) · **Step 4: Templates/CSS anpassen** · **Step 5: Suite grün** · **Step 6: Commit** — `feat: Konten umbenennbar, Betrag-Spalte mit Euro im Kopf` + +--- + +### Task 3: Versionsanzeige + +**Files:** +- Create: `finance/VERSION` (Inhalt: `0.2.0`), `finance/app/version.py` +- Modify: `finance/app/templates/base.html` (Footer), `finance/app/routers/gui.py` (Jinja-Global), `finance/app/main.py` (`GET /api/version`), `finance/Containerfile` (`COPY VERSION .`), `create_pod_finance.sh` (`API_IMAGE="localhost/finance-api:$(cat "$FINANCE_DIR/VERSION")"`) +- Test: `finance/tests/test_gui.py` + +**Interfaces:** +- Produces: `app.version.get_version() -> str` (liest `VERSION` neben dem `app`-Paket, Fallback `"0.0.0-dev"`); `GET /api/version` (mit `require_auth`) → `{"version": "0.2.0"}`; Footer jeder GUI-Seite: `Finanzberatungs-Tool v0.2.0`. + +- [ ] **Step 1: Failing Test** +```python +def test_version_visible(client): + r = client.get("/api/version", headers={"Authorization": "Bearer test-key"}) + assert r.status_code == 200 and r.json()["version"] == "0.2.0" + client.post("/login", data={"username": "admin", "password": "geheim"}) + assert "v0.2.0" in client.get("/").text +``` +- [ ] **Step 2: FAIL** · **Step 3: Implementieren** + +`finance/app/version.py`: +```python +from pathlib import Path + + +def get_version() -> str: + f = Path(__file__).resolve().parent.parent / "VERSION" + try: + return f.read_text().strip() + except OSError: + return "0.0.0-dev" +``` +In `gui.py`: `templates.env.globals["app_version"] = get_version()`; in `base.html` vor ``: `
Finanzberatungs-Tool v{{ app_version }}
`. +- [ ] **Step 4: PASS, Suite grün** · **Step 5: Commit** — `feat: Versionsanzeige (VERSION-Datei, Footer, /api/version, Image-Tag)` + +--- + +### Task 4: Hilfe-Seite + +**Files:** +- Create: `finance/app/templates/hilfe.html` +- Modify: `finance/app/routers/gui.py` (Route `GET /hilfe` mit `gui_session`), `finance/app/templates/base.html` (Nav-Link „Hilfe"), `finance/tests/test_gui.py` (Route in beide bestehende Seiten-Tests aufnehmen) + +**Interfaces:** +- Produces: `GET /hilfe` (Session-pflichtig, 302 → /login ohne Session). + +- [ ] **Step 1: Tests erweitern** — `/hilfe` in `test_pages_require_login` und `test_pages_render_after_login` aufnehmen; zusätzlich `assert "Gebrauchsanleitung" in r.text`. +- [ ] **Step 2: FAIL** · **Step 3: Implementieren** — `hilfe.html` erbt von `base.html`; Inhalt (vollständig, Deutsch — Feinschliff der Formulierungen erlaubt, Struktur bindend): + +``` +# Gebrauchsanleitung + +## Was dieses Tool ist +Liquiditätsplanung auf Basis echter Kontoauszüge: Ist-Stand aus importierten +Auszügen, Zukunft aus gepflegten Planposten und durchgerechneten Szenarien. + +## Konzepte +- Konten: entstehen automatisch beim ersten Import (per IBAN). Über die + Übersicht umbenennbar (z. B. "DKB Giro"). +- Kontoauszug-Import: PDF in die Drop-Zone auf der Import-Seite ziehen (oder + in die Inbox ~/.local/share/finance_pod/data/inbox/ legen und "Inbox + scannen"). Jeder Import wird geprüft (Anfangssaldo + Buchungen = Endsaldo) + und landet als Entwurf: erst nach Kontrolle der Vorschau "Übernehmen" + klicken. Duplikate werden erkannt und nicht doppelt übernommen. +- Kategorien & Regeln: Buchungen lassen sich kategorisieren; eine Regel + ("Muster im Text → Kategorie") kategorisiert künftige Importe automatisch. + Regel direkt aus einer Buchung erzeugen: Button in der Buchungsliste. +- Wiederkehrende Posten: Miete, Gehalt, Abos - mit Rhythmus (monatlich/ + quartalsweise/jährlich) und Fälligkeitstag. Der Vorschlags-Knopf erkennt + Kandidaten aus mind. 3 Monaten gleichartiger Buchungen. +- Einmalposten: einzelne künftige Zahlungen (z. B. Steuernachzahlung). +- Kredite: Annuität oder endfällig; Tilgungsplan aufklappbar. Ein Kredit + wirkt erst, wenn er einem Szenario zugeordnet ist. +- Szenarien: eine "Was-wäre-wenn"-Rechnung. Basis = alle wiederkehrenden + Posten + Einmalposten. Varianten entstehen durch Zuordnen von Krediten und + Modifikatoren (Kategorie oder Posten prozentual/absolut kürzen oder ganz + streichen). "Durchrechnen" liefert: tiefster Kontostand mit Datum, erstes + Datum unter 0 und unter der Warnschwelle. +- Grafana: Kontostand-Verläufe, Monatsausgaben nach Kategorie und der + Szenario-Vergleich als Kurven (einmal mit demselben Passwort wie hier + anmelden, dann erscheint das Dashboard auch in der Übersicht). + +## Empfohlener Arbeitsablauf +1. Monatlich: neue Auszüge importieren, Vorschau prüfen, übernehmen. +2. Unkategorisierte Buchungen durchsehen; für Wiederkehrendes Regeln anlegen. +3. Planung pflegen: Vorschläge prüfen, Einmalposten eintragen. +4. Fragestellung ("Können wir uns X leisten?") als Szenario-Varianten + abbilden und durchrechnen; Entscheidung anhand Tiefpunkt und + Unterschreitungsdaten treffen, Kurven in Grafana vergleichen. + +## Grundregeln +- Bestätigte Buchungen sind die Wahrheit - nie ändern, nur kategorisieren. +- Zukunft ausschließlich über Szenarien planen. +- Empfehlungen immer mit durchgerechneten Zahlen begründen. +``` +- [ ] **Step 4: PASS, Suite grün** · **Step 5: Commit** — `feat: Hilfe-Seite mit Gebrauchsanleitung` + +--- + +### Task 5: Secrets-Umzug nach `$BIND_DIR/.env` + +**Files:** +- Modify: `create_pod_finance.sh` +- Modify (Repo `/home/wlfb/fb`): `CLAUDE.md`, `.claude/skills/finanz-api/SKILL.md`, `.claude/skills/finanzberatung/SKILL.md`, `.claude/skills/auszug-import/SKILL.md` (überall Pfad `/home/wlfb/bin/finance/.env` → `$HOME/.local/share/finance_pod/.env`) + +**Interfaces:** +- Produces: `ENV_FILE="$BIND_DIR/.env"`; `$BIND_DIR` mit `chmod 700`; Migration: existiert `$HOME/bin/finance/.env` und `$ENV_FILE` nicht → `mv` + Meldung. Disaster-Recovery-Garantie: `$BIND_DIR` aus Backup + Skript-Lauf = lauffähig. + +- [ ] **Step 1: Skript ändern** — Reihenfolge: `mkdir -p "$BIND_DIR" && chmod 700 "$BIND_DIR"` VOR dem `.env`-Block; dann: +```bash +ENV_FILE="$BIND_DIR/.env" +LEGACY_ENV="$HOME/bin/finance/.env" +if [ ! -f "$ENV_FILE" ] && [ -f "$LEGACY_ENV" ]; then + mv "$LEGACY_ENV" "$ENV_FILE" && chmod 600 "$ENV_FILE" + echo "MIGRIERT: .env nach $ENV_FILE verschoben" +fi +``` +- [ ] **Step 2: fb-Repo anpassen** — alle Pfad-Referenzen, inkl. Key-Extraktions-Muster: `grep '^FB_API_KEY=' $HOME/.local/share/finance_pod/.env | cut -d= -f2 | tr -d "'"` (Werte sind single-quoted!). +- [ ] **Step 3: Verifizieren (ohne Redeploy, nur Trockenlauf)** — `bash -n create_pod_finance.sh`; Migration + Livegang erst in Task 7. +- [ ] **Step 4: Commits** — `~/bin`: `feat: Secrets unter ~/.local/share/finance_pod (Backup-vollstaendig)`; `~/fb`: `docs: .env-Pfad auf finance_pod-Datenverzeichnis umgestellt` + +--- + +### Task 6: Ein Passwort für GUI + Grafana, Embedding aktivieren + +**Files:** +- Modify: `create_pod_finance.sh`, `finance/app/templates/index.html` (Hinweis am iframe) + +**Interfaces:** +- Produces: `.env` führt `FB_PASSWORD=''` (chmod 600; nötig, um Grafana synchron zu halten); `FB_GUI_PASSWORD_HASH` wird daraus abgeleitet; `GRAFANA_ADMIN_PASSWORD` entfällt. Grafana: `GF_SECURITY_ALLOW_EMBEDDING=true`, Admin-Passwort wird bei JEDEM Skript-Lauf per `grafana cli admin reset-admin-password` auf `$FB_PASSWORD` gesetzt (Invocation im Container verifizieren; `grafana-cli` als Fallback). Anonymer Zugriff bleibt AUS. +- Passwort-Wechsel-Prozedur (in Skript-Kommentar dokumentieren): `FB_PASSWORD` in der `.env` ändern, Skript ausführen — Hash und Grafana ziehen nach. + +- [ ] **Step 1: Bootstrap-Block umbauen** — Neuerzeugung: ein `FB_PASSWORD` generieren, Hash daraus ableiten, beide single-quoted schreiben; einmalige Terminal-Ausgabe des Passworts. Migration bestehender `.env` (hat `FB_GUI_PASSWORD_HASH`, aber kein `FB_PASSWORD`): neues `FB_PASSWORD` generieren, Hash NEU ableiten, `GRAFANA_ADMIN_PASSWORD`-Zeile entfernen, Passwort einmalig ausgeben (Rotation unvermeidbar, da Klartext aus Hash nicht rekonstruierbar). +- [ ] **Step 2: Grafana-Container** — `-e GF_SECURITY_ALLOW_EMBEDDING=true`, `GF_SECURITY_ADMIN_PASSWORD="$FB_PASSWORD"`; nach Startwartezeit Sync-Kommando ausführen (idempotent, Fehler abfangen falls Grafana noch initialisiert — Retry-Schleife wie bei pg_isready). +- [ ] **Step 3: iframe-Hinweis** — unter dem iframe in `index.html`: kleiner Text „Kein Diagramm sichtbar? Einmal in Grafana anmelden (gleiches Passwort wie hier)." +- [ ] **Step 4: `bash -n`, Suite grün (App unverändert bis auf Template)** · **Step 5: Commit** — `feat: gemeinsames Passwort fuer GUI und Grafana, Dashboard-Embedding` + +--- + +### Task 7: Redeploy + Gesamt-Smoke + Disaster-Recovery-Probe + +**Files:** keine neuen; führt Tasks 1–6 im Livesystem zusammen. + +- [ ] **Step 1: Image + Redeploy** — `podman build -t "localhost/finance-api:$(cat /home/wlfb/bin/finance/VERSION)" /home/wlfb/bin/finance && bash /home/wlfb/bin/create_pod_finance.sh`. Migrationsmeldungen (Env-Umzug, Passwort-Rotation) protokollieren; neues Passwort NUR als letzte Zeile der Abschlussmeldung an den Nutzer. +- [ ] **Step 2: Smoke** — Service aktiv; `/login` 200; GUI-Login mit `FB_PASSWORD` → 303+Cookie; `/`, `/buchungen`, `/planung`, `/import`, `/hilfe` 200; Footer zeigt `v0.2.0`; `/api/version` 200; Grafana-Login mit demselben Passwort (`curl -u admin:$FB_PASSWORD .../api/dashboards/uid/finanzen` → 200); Embedding-Header: `curl -sI http://127.0.0.1:8097/` enthält KEIN `X-Frame-Options: deny`; Datenbestand unverändert (1 Konto, 26 bestätigte Buchungen); anonymer Zugriff verweigert (`curl -s http://127.0.0.1:8097/api/dashboards/uid/finanzen` ohne Auth → 401/403). +- [ ] **Step 3: Disaster-Recovery-Probe** — `systemctl --user stop pod-finance_pod.service`; alle finance-Units disablen und aus `~/.config/systemd/user/` entfernen; `podman pod rm -f finance_pod`; NUR mit vorhandenem `$BIND_DIR` das Skript erneut ausführen → alles wieder da (Login funktioniert, Datenbestand unverändert, keine Passwort-Neuerzeugung). +- [ ] **Step 4: Ledger + Abschluss** — Ergebnisse in `.superpowers/sdd/progress.md`; offene Folgeentscheidung notieren: Neuimport des DKB-Auszugs (bestehende 26 Buchungen tragen ggf. alte Parser-Textfehler) — Nutzerentscheidung, nicht automatisch ausführen. + +--- + +## Abschluss-Checkliste + +- [ ] Suite grün (>= 65 Tests inkl. neuer Parser-/PATCH-/Version-Tests; expected-Tests laufen lokal, skippen ohne expected-Dateien) +- [ ] Fable-Abnahme der Parser dokumentiert (`.superpowers/sdd/parser-audit-dkb.md`, ohne Commit) +- [ ] Live: v0.2.0 im Footer, ein Passwort für GUI+Grafana, Embedding aktiv ohne anonymen Zugriff, `.env` unter `$BIND_DIR`, DR-Probe bestanden +- [ ] Keine echten Kontodaten/Secrets in `git log -p` beider Repos +- [ ] Beide Repos committet; Plan-Häkchen gesetzt