diff --git a/docs/superpowers/plans/2026-07-20-ausbaustufe-5.md b/docs/superpowers/plans/2026-07-20-ausbaustufe-5.md
new file mode 100644
index 0000000..9d142da
--- /dev/null
+++ b/docs/superpowers/plans/2026-07-20-ausbaustufe-5.md
@@ -0,0 +1,1211 @@
+# Ausbaustufe 5 Implementation Plan — Deutsche Formate, Posten-Bearbeitung, Szenario-Ende (v0.6.0)
+
+> **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:** Deutsche Zahlenformate in der gesamten Web-GUI, Inline-Bearbeitung aller Planungs-Objekte inkl. Start/Ende bei Fixposten, Szenario-Modifikator-Art »Ende« plus szenario-eigene Einmalzahlungen; Release v0.6.0 mit Live-Demo-Szenario »Best Case«.
+
+**Architecture:** Anzeige-Formatierung als reine Funktionen in neuem Modul `app/formats.py` (Jinja-Filter `eur`/`de_label`, kein `locale`-Modul); Eingabe-Konvertierung clientseitig in der bestehenden json-form-htmx-Extension (`data-type="amount"`), API bleibt strikt Punkt-Dezimal. Szenario-Ende als neue Modifikator-Art `ende` (nullable `end_date`-Spalte) in der reinen Engine; szenario-eigene Einmalzahlungen als neue Tabelle `scenario_planned_items`, die `services/projection_service.py` unabhängig von `include_planned` einspeist. Eine Alembic-Migration.
+
+**Tech Stack:** FastAPI + Pydantic v2, SQLAlchemy 2 (Mapped), Alembic, Jinja2, htmx + eigene json-form-Extension, pytest (SQLite in-memory via `conftest.py`), Podman-Pod-Deployment via `create_pod_finance.sh`.
+
+**Spec:** `docs/superpowers/specs/2026-07-20-ausbaustufe-5-design.md`
+
+## Global Constraints
+
+- **Geldbeträge immer `decimal.Decimal`** (DB-Typ `MONEY = Numeric(12,2)`), niemals float-Arithmetik (CLAUDE.md).
+- **GUI-Texte, Fehlermeldungen, Commit-Messages auf Deutsch**; Datumsformat TT.MM.JJJJ.
+- **UX-Regel:** Bedienelemente sichtbar lassen (`hidden`/`disabled`, nie aus dem DOM entfernen); Fehler inline bzw. über den bestehenden `htmx:responseError`-Handler.
+- **API-Werte/DB-Werte bleiben englisch** (`monthly`, `annuity`, `percent`, `ende`, …) — nur Anzeige-Labels sind deutsch.
+- **Fable-Testagent-Gate:** Jeder Task braucht VOR dem Commit eine Abnahme durch den Fable-Testagenten (Faktencheck/Live-Test je nach Task). Kein Task ist ohne dieses Gate abgeschlossen.
+- **Ledger:** Nach jedem abgeschlossenen Task ein Eintrag in `.superpowers/sdd/progress.md` (Commit-Range + Fable-Befund).
+- **DATENSCHUTZ:** `tests/fixtures/*` = echte Kontodaten, nie committen/zitieren. Die konkreten Demo-Daten (Empfängernamen, Beträge, Termine des »Best Case«) stammen aus dem Nutzer-Chat und dürfen NUR in die Live-DB und den Chat — nicht in Commits, Ledger, Tests oder Doku.
+- **Testlauf immer:** `cd /home/wlfb/bin/finance && .venv/bin/python -m pytest -q` — Basis-Erwartung: alle grün (aktuell 154 passed).
+- **Arbeitsverzeichnis:** alle Pfade unten relativ zu `/home/wlfb/bin/finance` (Ausnahme: Ledger/Plan unter `/home/wlfb/bin`).
+
+---
+
+### Task 1: Anzeige-Formatierung — `app/formats.py` + Template-Umstellung
+
+**Files:**
+- Create: `app/formats.py`
+- Create: `tests/test_formats.py`
+- Modify: `app/routers/gui.py` (Filter-Registrierung, nach Zeile 30 `templates.env.globals[...]`)
+- Modify: `app/templates/index.html`, `app/templates/salden.html`, `app/templates/transactions.html`, `app/templates/planning.html`, `app/templates/_preview_table.html`
+- Modify: `tests/test_gui.py` (bestehende Punktformat-Assertions)
+
+**Interfaces:**
+- Produces: `app.formats.eur(value) -> str` (Decimal/str/int → `"−"`-lose deutsche Darstellung, z.B. `Decimal("-2474.5")` → `"-2.474,50"`, ohne €-Zeichen), `app.formats.de_label(value: str) -> str` (englischer API-Wert → deutsches Label, unbekannt → Rohwert), `app.formats.DE_LABELS: dict[str, str]`. Jinja-Filter `|eur` und `|de_label` in allen Templates verfügbar. Spätere Tasks (2, 4, 7) nutzen beide Filter.
+
+- [ ] **Step 1: Failing Tests schreiben** — `tests/test_formats.py`:
+
+```python
+from decimal import Decimal
+
+from app.formats import DE_LABELS, de_label, eur
+
+
+def test_eur_basisformat():
+ assert eur(Decimal("2474.00")) == "2.474,00"
+
+
+def test_eur_negativ_mit_mehreren_tausenderpunkten():
+ assert eur(Decimal("-1234567.5")) == "-1.234.567,50"
+
+
+def test_eur_null_und_kleinbetraege():
+ assert eur(Decimal("0")) == "0,00"
+ assert eur(Decimal("-0.5")) == "-0,50"
+ assert eur(Decimal("999.99")) == "999,99"
+
+
+def test_eur_rundung_half_up():
+ assert eur(Decimal("1.005")) == "1,01"
+ assert eur(Decimal("-1.005")) == "-1,01"
+
+
+def test_eur_akzeptiert_strings_und_int():
+ # Templates reichen teils DB-Werte als str/int durch (z.B. Suggestions).
+ assert eur("1234.5") == "1.234,50"
+ assert eur(7) == "7,00"
+
+
+def test_de_label_bekannte_und_unbekannte_werte():
+ assert de_label("monthly") == "monatlich"
+ assert de_label("quarterly") == "vierteljährlich"
+ assert de_label("yearly") == "jährlich"
+ assert de_label("annuity") == "Annuität"
+ assert de_label("bullet") == "endfällig"
+ assert de_label("percent") == "Prozent"
+ assert de_label("absolute") == "Absolut"
+ assert de_label("remove") == "Entfällt"
+ assert de_label("ende") == "Ende"
+ assert de_label("weirdvalue") == "weirdvalue"
+ assert set(DE_LABELS) == {"monthly", "quarterly", "yearly", "annuity",
+ "bullet", "percent", "absolute", "remove", "ende"}
+```
+
+- [ ] **Step 2: Fehlschlag verifizieren**
+
+Run: `.venv/bin/python -m pytest tests/test_formats.py -q`
+Expected: FAIL/ERROR mit `ModuleNotFoundError: No module named 'app.formats'`
+
+- [ ] **Step 3: `app/formats.py` implementieren**
+
+```python
+"""Deutsche Anzeige-Formate fuer die GUI (Ausbaustufe 5).
+
+Bewusst ohne locale-Modul: Container-Locales sind nicht garantiert
+installiert, und die Ausgabe hinge sonst vom Host ab. Reine
+Decimal-Formatierung; die JSON-API bleibt Punkt-Dezimal.
+"""
+from decimal import Decimal, ROUND_HALF_UP
+
+DE_LABELS = {
+ "monthly": "monatlich",
+ "quarterly": "vierteljährlich",
+ "yearly": "jährlich",
+ "annuity": "Annuität",
+ "bullet": "endfällig",
+ "percent": "Prozent",
+ "absolute": "Absolut",
+ "remove": "Entfällt",
+ "ende": "Ende",
+}
+
+
+def eur(value) -> str:
+ """Decimal("-2474.5") -> "-2.474,50" (ohne Euro-Zeichen, das setzt das Template)."""
+ d = Decimal(str(value)).quantize(Decimal("0.01"), rounding=ROUND_HALF_UP)
+ s = f"{d:,.2f}" # US-Trenner: "-2,474.50"
+ return s.replace(",", "\x00").replace(".", ",").replace("\x00", ".")
+
+
+def de_label(value: str) -> str:
+ return DE_LABELS.get(value, value)
+```
+
+- [ ] **Step 4: Tests grün verifizieren**
+
+Run: `.venv/bin/python -m pytest tests/test_formats.py -q`
+Expected: alle Tests PASS
+
+- [ ] **Step 5: Filter registrieren** — `app/routers/gui.py`, direkt nach `templates.env.globals["app_version"] = get_version()`:
+
+```python
+from app.formats import de_label, eur # (oben bei den Imports einsortieren)
+
+templates.env.filters["eur"] = eur
+templates.env.filters["de_label"] = de_label
+```
+
+- [ ] **Step 6: Failing GUI-Test für deutsche Anzeige** — in `tests/test_gui.py` ergänzen:
+
+```python
+def test_gui_zeigt_deutsche_betragsformate(client, db):
+ client.post("/login", data={"username": "admin", "password": "geheim"})
+ acc = Account(bank="dkb", iban="DE-FMT-1", name="Formatkonto", type="giro")
+ db.add(acc)
+ db.flush()
+ acc.anchor_date = date(2026, 6, 30)
+ acc.anchor_balance = Decimal("12345.60")
+ db.commit()
+ r = client.get("/")
+ assert "12.345,60" in r.text
+ assert "12345.60" not in r.text
+
+
+def test_planung_zeigt_deutsche_labels(client, db):
+ client.post("/login", data={"username": "admin", "password": "geheim"})
+ db.add(RecurringItem(name="Miete-Label-Test", amount=Decimal("-600.00"),
+ rhythm="monthly", due_day=1))
+ db.commit()
+ r = client.get("/planung")
+ assert "monatlich" in r.text
+ assert "-600,00" in r.text
+```
+
+`RecurringItem` zum Import in `tests/test_gui.py` ergänzen: `from app.models.tables import Account, RecurringItem`.
+
+Run: `.venv/bin/python -m pytest tests/test_gui.py -q` → die zwei neuen Tests FAIL (Punktformat/englische Labels im HTML).
+
+- [ ] **Step 7: Templates umstellen** — alle `{{ '%.2f'|format(X) }}`-Vorkommen ersetzen durch `{{ X|eur }}` (Fundstellen: `index.html` Zeilen 14/29/32/71, `salden.html` 20/27/51/53, `transactions.html` 56, `_preview_table.html` 11/28/29, `planning.html` 16/67/101/146/147/231/284). In `planning.html` zusätzlich Labels:
+ - Zeile 17 `{{ r.rhythm }}` → `{{ r.rhythm|de_label }}`; Zeile 68 (Vorschläge) `{{ s.rhythm }}` → `{{ s.rhythm|de_label }}`; Zeile 148 `{{ l.repayment_type }}` → `{{ l.repayment_type|de_label }}`; Zeile 230 `{{ m.kind }}` → `{{ m.kind|de_label }}`.
+ - Dropdown-Optionen: ``, ``, ``.
+ - Kredit-Zins Zeile 147: `{{ l.annual_rate_pct|eur }} %`.
+ - Tilgungsplan-JS (`loadLoanSchedule`): Hilfsfunktion ergänzen und alle vier `Number(r.X).toFixed(2)` ersetzen:
+
+```javascript
+function fmtEur(n) {
+ return Number(n).toLocaleString('de-DE', {minimumFractionDigits: 2, maximumFractionDigits: 2});
+}
+// in loadLoanSchedule:
++ '
' + fmtEur(r.payment) + ' €
'
++ '
' + fmtEur(r.interest) + ' €
'
++ '
' + fmtEur(r.principal) + ' €
'
++ '
' + fmtEur(r.remaining) + ' €
';
+```
+
+- [ ] **Step 8: Bestehende Punktformat-Assertions anpassen**
+
+Run: `grep -rn '\.00\|\.50\|\.99' tests/test_gui.py` und jede Assertion prüfen, die gerendertes HTML gegen Punktformat testet. Bekannt: `test_index_shows_account_anchor` erwartet `"Anker: 321.00"` → ändern in `"Anker: 321,00"`. Weitere Treffer analog auf Komma-Format umstellen (NUR Assertions gegen `r.text`/HTML — JSON-API-Assertions bleiben Punktformat!).
+
+- [ ] **Step 9: Gesamte Suite grün**
+
+Run: `.venv/bin/python -m pytest -q`
+Expected: alle Tests passed (154 alt + 8 neu)
+
+- [ ] **Step 10: Fable-Testagent-Abnahme einholen** (Faktencheck: Filter-Korrektheit inkl. Rundung/negativer Werte, Vollständigkeit der Template-Umstellung via grep `'%.2f'` → 0 Treffer in `app/templates/`, keine API-Format-Änderung). Erst nach VERIFIED weiter.
+
+- [ ] **Step 11: Commit**
+
+```bash
+cd /home/wlfb/bin && git add finance/app/formats.py finance/tests/test_formats.py finance/app/routers/gui.py finance/app/templates/ finance/tests/test_gui.py
+git commit -m "feat: deutsche Betragsanzeige (eur-Filter) und deutsche GUI-Labels"
+```
+
+---
+
+### Task 2: Deutsche Betrags-Eingabe — json-form `data-type="amount"`
+
+**Files:**
+- Modify: `app/templates/base.html` (json-form-Extension, `encodeParameters`)
+- Modify: `app/templates/planning.html` (alle Betrags-Inputs), `app/templates/index.html` (Anker-Saldo-Input)
+- Modify: `tests/test_gui.py`
+
+**Interfaces:**
+- Consumes: Filter `|eur` aus Task 1 (Vorbefüllung des Anker-Inputs).
+- Produces: json-form-Konvention `data-type="amount"`: Eingabewert wird vor dem JSON-Versand normalisiert (Leerzeichen/`€` entfernen; enthält der Wert ein Komma → Tausenderpunkte entfernen, Komma→Punkt; ohne Komma unverändert). Tasks 4 und 7 verwenden `data-type="amount"` in ihren Formularen.
+
+- [ ] **Step 1: Failing Test — Attribute im gerenderten HTML** (die JS-Konvertierung selbst ist serverseitig nicht testbar; sie wird im Fable-Live-Test geprüft). In `tests/test_gui.py`:
+
+```python
+def test_betragsfelder_haben_amount_typ_und_deutsche_platzhalter(client):
+ client.post("/login", data={"username": "admin", "password": "geheim"})
+ planung = client.get("/planung").text
+ # Fixposten-, Einmalposten-, Kredit-Formulare senden deutsche Eingaben:
+ assert planung.count('data-type="amount"') >= 4
+ assert '-49,99' in planung and '-2000,00' in planung
+ assert '-49.99' not in planung and '-2000.00' not in planung
+ index = client.get("/").text
+ assert 'placeholder="500,00"' in index
+```
+
+Run: `.venv/bin/python -m pytest tests/test_gui.py::test_betragsfelder_haben_amount_typ_und_deutsche_platzhalter -q` → FAIL
+
+- [ ] **Step 2: json-form-Extension erweitern** — `base.html`, in `encodeParameters` vor dem `else`-Zweig:
+
+```javascript
+ } else if (kind === 'int') {
+ out[k] = parseInt(v, 10);
+ } else if (kind === 'amount') {
+ // Deutsche Betragseingabe: "1.234,56" -> "1234.56". Ohne Komma
+ // bleibt der Wert unveraendert (Punkt-Eingaben funktionieren weiter).
+ var s = String(v).replace(/[\s€]/g, '');
+ if (s.indexOf(',') !== -1) {
+ s = s.replace(/\./g, '').replace(',', '.');
+ }
+ out[k] = s;
+ } else {
+```
+
+- [ ] **Step 3: Eingabefelder umstellen**
+ - `planning.html` Fixposten-Formular: ``
+ - `planning.html` Einmalposten-Formular: `placeholder="-2000,00"`, `data-type="amount"`
+ - `planning.html` Kredit-Formular: `principal` → `data-type="amount" placeholder="10000,00"`; `annual_rate_pct` → `data-type="amount" placeholder="4,5"`
+ - `planning.html` Modifikator-Formular: ``
+ - `index.html` Anker-Formular: ``
+ - Die versteckten Felder der Vorschlags-Übernahme (``) bekommen KEIN `data-type="amount"` — der Server liefert dort Punktformat, und die amount-Normalisierung ließe es unverändert; ohne Attribut bleibt die Semantik explizit.
+
+- [ ] **Step 4: Tests grün + Suite**
+
+Run: `.venv/bin/python -m pytest tests/test_gui.py -q && .venv/bin/python -m pytest -q`
+Expected: alles PASS
+
+- [ ] **Step 5: Fable-Testagent-Abnahme** — diesmal MIT Live-Anteil: gegen die laufende Instanz (oder lokal gestartete App) im Browser/per curl prüfen, dass ein Fixposten mit Eingabe `1.234,56` als `1234.56` gespeichert wird und `-49,99` korrekt ankommt. Erst nach VERIFIED weiter. (Hinweis: Wenn erst beim Release-Task deployt wird, den Live-Anteil per `uvicorn`-Teststart gegen SQLite erledigen: `cd finance && FB_DATABASE_URL=sqlite:///./demo.sqlite FB_GUI_PASSWORD_HASH=$(.venv/bin/python -c "from app.auth import hash_password; print(hash_password('demo'))") .venv/bin/uvicorn app.main:app --port 8123` — Wegwerf-DB danach löschen.)
+
+- [ ] **Step 6: Commit**
+
+```bash
+cd /home/wlfb/bin && git add finance/app/templates/ finance/tests/test_gui.py
+git commit -m "feat: Betragseingabe mit Komma (data-type=amount in json-form)"
+```
+
+---
+
+### Task 3: Fixposten — Start/Ende in der GUI + Datumsvalidierung
+
+**Files:**
+- Modify: `app/routers/planning.py` (`RecurringIn`, `patch_recurring`)
+- Modify: `app/templates/planning.html` (Tabelle + Anlege-Formular)
+- Modify: `tests/test_planning_api.py`, `tests/test_gui.py`
+
+**Interfaces:**
+- Consumes: bestehende Felder `RecurringItem.start_date/end_date`, `RecurringIn/RecurringPatch.start_date/end_date` (existieren schon), Filter `|eur`/`|de_label`.
+- Produces: Validierungsregel »`end_date >= start_date`, sonst 422 mit Meldung „Ende darf nicht vor Start liegen"« bei POST und PATCH `/api/recurring`. GUI-Spalten »Start«/»Ende«. Task 4 baut das Bearbeiten-Formular auf dieselben Felder.
+
+- [ ] **Step 1: Failing API-Tests** — in `tests/test_planning_api.py`:
+
+```python
+def test_recurring_ende_vor_start_wird_abgelehnt(client):
+ r = client.post("/api/recurring", headers=H, json={
+ "name": "Falschrum", "amount": "-10.00", "rhythm": "monthly", "due_day": 1,
+ "start_date": "2026-10-01", "end_date": "2026-09-01"})
+ assert r.status_code == 422
+
+
+def test_recurring_start_ende_roundtrip_und_patch_validierung(client):
+ r = client.post("/api/recurring", headers=H, json={
+ "name": "Befristet", "amount": "-10.00", "rhythm": "monthly", "due_day": 1,
+ "start_date": "2026-01-01", "end_date": "2026-12-31"})
+ assert r.status_code == 201
+ item = r.json()
+ assert item["start_date"] == "2026-01-01" and item["end_date"] == "2026-12-31"
+ # PATCH, der Ende vor den (bestehenden) Start schieben will -> 422:
+ bad = client.patch(f"/api/recurring/{item['id']}", headers=H,
+ json={"end_date": "2025-06-30"})
+ assert bad.status_code == 422
+ # Datum explizit loeschen (null) ist erlaubt:
+ ok = client.patch(f"/api/recurring/{item['id']}", headers=H,
+ json={"end_date": None})
+ assert ok.status_code == 200 and ok.json()["end_date"] is None
+```
+
+Run: `.venv/bin/python -m pytest tests/test_planning_api.py -q` → die neuen Tests FAIL (POST liefert 201 statt 422 bzw. PATCH 200 statt 422)
+
+- [ ] **Step 2: Validierung implementieren** — `app/routers/planning.py`:
+
+Import ergänzen: `from pydantic import BaseModel, ConfigDict, Field, model_validator`
+
+In `RecurringIn` (gilt via Vererbung NICHT für `RecurringPatch` — dort prüft der Router den Merge-Zustand):
+
+```python
+class RecurringIn(BaseModel):
+ name: str
+ amount: Decimal
+ rhythm: Literal["monthly", "quarterly", "yearly"]
+ due_day: int = Field(ge=1, le=31)
+ start_date: date | None = None
+ end_date: date | None = None
+ category_id: int | None = None
+
+ @model_validator(mode="after")
+ def _ende_nicht_vor_start(self):
+ if (self.start_date is not None and self.end_date is not None
+ and self.end_date < self.start_date):
+ raise ValueError("Ende darf nicht vor Start liegen")
+ return self
+```
+
+In `patch_recurring` nach der `setattr`-Schleife, vor `session.commit()`:
+
+```python
+ if (item.start_date is not None and item.end_date is not None
+ and item.end_date < item.start_date):
+ session.rollback()
+ raise HTTPException(422, "Ende darf nicht vor Start liegen")
+```
+
+- [ ] **Step 3: API-Tests grün**
+
+Run: `.venv/bin/python -m pytest tests/test_planning_api.py -q` → PASS
+
+- [ ] **Step 4: Failing GUI-Test** — in `tests/test_gui.py`:
+
+```python
+def test_planung_fixposten_zeigt_start_und_ende(client, db):
+ client.post("/login", data={"username": "admin", "password": "geheim"})
+ db.add(RecurringItem(name="Befristet-GUI", amount=Decimal("-5.00"),
+ rhythm="monthly", due_day=1,
+ start_date=date(2026, 1, 1), end_date=date(2026, 12, 31)))
+ db.commit()
+ r = client.get("/planung").text
+ assert "
Start
" in r and "
Ende
" in r
+ assert "01.01.2026" in r and "31.12.2026" in r
+ assert 'name="start_date"' in r and 'name="end_date"' in r
+```
+
+Run: → FAIL
+
+- [ ] **Step 5: Template** — `planning.html`, Fixposten-Tabelle:
+
+Kopfzeile: `
Name
Betrag
Rhythmus
Fälligkeitstag
Start
Ende
Kategorie
`
+
+Datenzeile (nach der Fälligkeitstag-Zelle):
+
+```html
+
{{ r.start_date.strftime('%d.%m.%Y') if r.start_date else '–' }}
+
{{ r.end_date.strftime('%d.%m.%Y') if r.end_date else '–' }}
+```
+
+`colspan` der Leerzeile von 6 auf 8 erhöhen. Anlege-Formular (nach Fälligkeitstag):
+
+```html
+
+
+```
+
+(Leere Datumsfelder sendet die json-form-Extension bereits als `null`.)
+
+- [ ] **Step 6: Suite grün**
+
+Run: `.venv/bin/python -m pytest -q` → PASS
+
+- [ ] **Step 7: Fable-Testagent-Abnahme** (Faktencheck: Validierung POST+PATCH inkl. Null-Löschen; GUI-Spalten; `occurrences` klammert Start/Ende korrekt — bestehende Engine-Tests referenzieren). Erst nach VERIFIED weiter.
+
+- [ ] **Step 8: Commit**
+
+```bash
+cd /home/wlfb/bin && git add finance/app/routers/planning.py finance/app/templates/planning.html finance/tests/
+git commit -m "feat: Start/Ende fuer Fixposten in der GUI + Datumsvalidierung"
+```
+
+---
+
+### Task 4: Inline-Bearbeiten — Fixposten, Einmalposten, Kredite, Szenario-Kopf
+
+**Files:**
+- Modify: `app/templates/planning.html`
+- Modify: `tests/test_gui.py`
+
+**Interfaces:**
+- Consumes: `PATCH /api/recurring/{id}`, `/api/planned/{id}`, `/api/loans/{id}`, `/api/scenarios/{id}` (existieren alle); json-form inkl. `data-type="amount"` (Task 2); Filter `|eur`/`|de_label` (Task 1); Start/Ende-Felder (Task 3).
+- Produces: JS-Helfer `toggleEdit(prefix, id, editing)` in `planning.html`; Formularzeilen-IDs `rec-edit-{id}`, `pln-edit-{id}`, `loan-edit-{id}`, `sc-edit-{id}` und Anzeige-IDs `rec-row-{id}`, `pln-row-{id}`, `loan-row-{id}`, `sc-head-{id}`. Task 7 folgt demselben Muster nicht (Szenario-Unterobjekte haben eigene Formulare), benötigt aber `toggleEdit` nicht.
+
+- [ ] **Step 1: Failing GUI-Test**
+
+```python
+def test_planung_hat_bearbeiten_formulare(client, db):
+ client.post("/login", data={"username": "admin", "password": "geheim"})
+ db.add(RecurringItem(name="EditR", amount=Decimal("-5.00"), rhythm="monthly", due_day=1))
+ db.add(PlannedItem(name="EditP", amount=Decimal("-7.00"), due=date(2026, 8, 1)))
+ db.add(Loan(name="EditL", principal=Decimal("1000.00"),
+ annual_rate_pct=Decimal("4.50"), term_months=12,
+ payout_date=date(2026, 8, 1), repayment_type="annuity"))
+ db.add(Scenario(name="EditS", description="d"))
+ db.commit()
+ r = client.get("/planung").text
+ assert r.count(">Bearbeiten") >= 4
+ for fragment in ('hx-patch="/api/recurring/', 'hx-patch="/api/planned/',
+ 'hx-patch="/api/loans/', 'hx-patch="/api/scenarios/'):
+ assert fragment in r, fragment
+```
+
+Imports in `tests/test_gui.py` ergänzen: `from app.models.tables import Account, Loan, PlannedItem, RecurringItem, Scenario`
+
+Run: `.venv/bin/python -m pytest tests/test_gui.py::test_planung_hat_bearbeiten_formulare -q` → FAIL
+
+- [ ] **Step 2: JS-Helfer in `planning.html`** (in den bestehenden `