Files
bin/docs/superpowers/plans/2026-07-19-ausbaustufe-2.md
2026-07-19 14:30:40 +02:00

20 KiB
Raw Blame History

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 <noreply@anthropic.com>.
  • 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 57) 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_<bank>.json (gitignored):

    {"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:

"""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):

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: Commitfix: 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, 1100 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:

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: Commitfeat: 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

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:

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 </body>: <footer class="version">Finanzberatungs-Tool v{{ app_version }}</footer>.

  • Step 4: PASS, Suite grün · Step 5: Commitfeat: 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: Implementierenhilfe.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: Commitfeat: 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:

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='<klartext>' (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: Commitfeat: gemeinsames Passwort fuer GUI und Grafana, Dashboard-Embedding


Task 7: Redeploy + Gesamt-Smoke + Disaster-Recovery-Probe

Files: keine neuen; führt Tasks 16 im Livesystem zusammen.

  • Step 1: Image + Redeploypodman 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-Probesystemctl --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