# 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