20 KiB
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 unterfinance/; Phase „.env-Umzug" ändert zusätzlich Repo/home/wlfb/fb(dort separat committen). - Geldbeträge
Decimal; Nutzertexte Deutsch; vor jedem Commitcd /home/wlfb/bin/finance && .venv/bin/python -m pytest -qgrün (Basis: 61 passed). - Commit-Messages enden mit
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>. - DATENSCHUTZ:
tests/fixtures/*.pdfsind 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 gitignortentests/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.pynur falls Audit strukturgleiche Fehler zeigt) - Modify:
finance/tests/test_bank_parsers.py(Noise-Checks + expected-Datei-Tests) - Modify:
finance/.gitignore(Zeiletests/fixtures/expected_*.jsonergänzen)
Interfaces:
-
Produces:
parser_audit.pyCLI:python scripts/parser_audit.py tests/fixtures/dkb_beispiel.pdf→ gibt pro Transaktion eine nummerierte Zeile#i | booking | value | amount | counterparty | purposeaus, 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.pdfaus, 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ürvr_beispiel.pdfundhvb_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) -> ParsedStatementund 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: 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}(nurname, 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-patchvia bestehender json-form-Extension auf/api/accounts/{id}, danach Reload/Refresh der Tabelle). Buchungsliste zeigt weiterhinaccount_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_validatorstrippt 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(liestVERSIONneben demapp-Paket, Fallback"0.0.0-dev");GET /api/version(mitrequire_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: 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(RouteGET /hilfemitgui_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 —
/hilfeintest_pages_require_loginundtest_pages_render_after_loginaufnehmen; zusätzlichassert "Gebrauchsanleitung" in r.text. -
Step 2: FAIL · Step 3: Implementieren —
hilfe.htmlerbt vonbase.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_DIRmitchmod 700; Migration: existiert$HOME/bin/finance/.envund$ENV_FILEnicht →mv+ Meldung. Disaster-Recovery-Garantie:$BIND_DIRaus 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:
.envführtFB_PASSWORD='<klartext>'(chmod 600; nötig, um Grafana synchron zu halten);FB_GUI_PASSWORD_HASHwird daraus abgeleitet;GRAFANA_ADMIN_PASSWORDentfällt. Grafana:GF_SECURITY_ALLOW_EMBEDDING=true, Admin-Passwort wird bei JEDEM Skript-Lauf pergrafana cli admin reset-admin-passwordauf$FB_PASSWORDgesetzt (Invocation im Container verifizieren;grafana-clials Fallback). Anonymer Zugriff bleibt AUS. -
Passwort-Wechsel-Prozedur (in Skript-Kommentar dokumentieren):
FB_PASSWORDin der.envändern, Skript ausführen — Hash und Grafana ziehen nach. -
Step 1: Bootstrap-Block umbauen — Neuerzeugung: ein
FB_PASSWORDgenerieren, Hash daraus ableiten, beide single-quoted schreiben; einmalige Terminal-Ausgabe des Passworts. Migration bestehender.env(hatFB_GUI_PASSWORD_HASH, aber keinFB_PASSWORD): neuesFB_PASSWORDgenerieren, 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;
/login200; GUI-Login mitFB_PASSWORD→ 303+Cookie;/,/buchungen,/planung,/import,/hilfe200; Footer zeigtv0.2.0;/api/version200; 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 KEINX-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/finanzenohne 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_DIRdas 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,
.envunter$BIND_DIR, DR-Probe bestanden - Keine echten Kontodaten/Secrets in
git log -pbeider Repos - Beide Repos committet; Plan-Häkchen gesetzt