# Ausbaustufe 7 Implementation Plan — Szenarien-Seite, Formular-UX, Kategorien (v0.8.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 (`- [x]`) syntax for tracking. **Goal:** Szenarien auf eigene Seite `/szenarien`, verständliches Eintrags-Formular (Paar-Layout, Gesperrt-Kennzeichnung, dynamisches Wert-Label, abgesetzter Durchrechnen-Button), Kategorien-Verwaltung auf der Admin-Seite; Release v0.8.0. **Architecture:** Kein Datenmodell-/Migrationsbedarf. Template-Umzug der Szenarien-Sektion aus `planning.html` in neues `szenarien.html` + Route in `gui.py`; CSS-Regeln in `style.css`; ein neuer Endpunkt `PATCH /api/categories/{id}`. **Tech Stack:** FastAPI, Jinja2, htmx + json-form, CSS, pytest. **Spec:** `docs/superpowers/specs/2026-07-20-szenarien-seite-ux-design.md` ## Global Constraints - GUI deutsch, TT.MM.JJJJ, `|eur`; API-Werte englisch; Geldbeträge `Decimal`. - UX-Regel: gesperrte Felder sichtbar + `disabled` + optisch gekennzeichnet; Edit-Zeilen per `hidden`. - Fable-Testagent-Gate je Task VOR dem Commit; Ledger-Eintrag je Task in `.superpowers/sdd/progress.md`. - DATENSCHUTZ: keine echten Kontodaten in Commits/Tests/Doku. - Testlauf: `cd /home/wlfb/bin/finance && .venv/bin/python -m pytest -q` — Basis 181 passed, muss grün bleiben. - Pfade relativ zu `/home/wlfb/bin/finance` (Ledger/Plan unter `/home/wlfb/bin`). --- ### Task 1: Szenarien-Seite `/szenarien` **Files:** - Create: `app/templates/szenarien.html` - Modify: `app/templates/planning.html` (Szenarien-`
` Zeilen ~263-464 und szenario-spezifisches JS entfernen), `app/templates/base.html` (Nav + `toggleEdit`), `app/routers/gui.py` (neue Route, `planung_page` verschlanken), `app/templates/hilfe.html` (Ortsangaben) - Modify: `tests/test_gui.py` **Interfaces:** - Consumes: bestehenden Szenario-Kontext (`_scenario_rows`, `modifier_kinds`), `gui_session`, alle bestehenden Templates/JS-Helfer. - Produces: `GET /szenarien` (login-geschützt) mit Kontext `scenario_rows`, `loans`, `categories`, `category_names`, `recurring`, `recurring_names`, `modifier_kinds`; `toggleEdit` global in `base.html`. Task 2 baut auf `szenarien.html` auf. - [x] **Step 1: Failing GUI-Tests** — in `tests/test_gui.py`: ```python def test_szenarien_seite_und_nav(client, db): client.post("/login", data={"username": "admin", "password": "geheim"}) db.add(Scenario(name="Seiten-Test", description="d")) db.commit() r = client.get("/szenarien") assert r.status_code == 200 assert "Seiten-Test" in r.text and "Neues Szenario anlegen" in r.text assert 'href="/szenarien"' in r.text # Nav-Punkt # Planung enthaelt keine Szenarien-Sektion mehr: p = client.get("/planung").text assert "Seiten-Test" not in p and "Neues Szenario anlegen" not in p assert "Wiederkehrende Posten" in p and "Kredite" in p def test_szenarien_seite_braucht_login(client): r = client.get("/szenarien", follow_redirects=False) assert r.status_code in (302, 303) and r.headers["location"] == "/login" ``` Run: `.venv/bin/python -m pytest tests/test_gui.py -q` → neue Tests FAIL (404) - [x] **Step 2: Route** — `app/routers/gui.py`: in `planung_page` die Zeilen `scenarios = _list_scenarios(...)`, `"scenario_rows": ...` und `"modifier_kinds": ...` entfernen (die übrigen Kontexteinträge bleiben). Neue Route direkt darunter: ```python @router.get("/szenarien", dependencies=[Depends(gui_session)]) def szenarien_page(request: Request, session: Session = Depends(get_session)): categories = session.execute(select(Category)).scalars().all() recurring = _list_recurring(session=session) loans = _list_loans(session=session) scenarios = _list_scenarios(session=session) return templates.TemplateResponse(request, "szenarien.html", { "categories": categories, "category_names": {c.id: c.name for c in categories}, "recurring": recurring, "recurring_names": {r.id: r.name for r in recurring}, "loans": loans, "scenario_rows": _scenario_rows(session, scenarios, loans), "modifier_kinds": ["percent", "absolute", "remove", "ende"], }) ``` - [x] **Step 3: Template-Umzug** — Neues `app/templates/szenarien.html`: ```html {% extends "base.html" %} {% block title %}Szenarien – Finanzberatung{% endblock %} {% block content %}

Szenarien

{% endblock %} ``` Der `

Szenarien

` entfällt (die Seite hat die `

`-Überschrift). In `planning.html`: die gesamte Szenarien-`
` löschen; aus dem Script-Block `onModTargetTypeChange`, `onEntryArtChange`, `toggleScenarioLoan` und `toggleEdit` entfernen — `toggleEdit` zieht in den zentralen Script-Block von `base.html` (dort nach der json-form-Extension einfügen, unverändert inkl. Kommentar); `loadLoanSchedule`/`fmtEur`/`formatIsoDate` und `loadedSchedules` bleiben in `planning.html`. - [x] **Step 4: Nav + Hilfe** — `base.html`: `Szenarien` zwischen Planung- und Admin-Link. `hilfe.html`: Formulierungen, die die Szenarien auf der Planung-Seite verorten, auf „Szenarien-Seite" anpassen (grep nach „Szenari" in `hilfe.html` und Ortsangaben prüfen). - [x] **Step 5: Bestehende Tests umziehen** — `tests/test_gui.py`: die Szenario-GUI-Tests (`test_szenario_gui_ende_und_einmalzahlungen`, `test_szenario_eintraege_tabelle`, Szenario-Anteile von `test_planung_hat_bearbeiten_formulare`) von `client.get("/planung")` auf `client.get("/szenarien")` umstellen. In `test_planung_hat_bearbeiten_formulare` den Scenario-Teil (Scenario-Seed + `hx-patch="/api/scenarios/`-Assertion) in einen neuen Test `test_szenarien_hat_bearbeiten_formular` auslagern, der `/szenarien` lädt; der Planung-Test behält rec/pln/loan mit `>= 3` Bearbeiten-Buttons. `test_pages_require_login`/`test_pages_render_after_login`: Pfad `/szenarien` in die jeweilige Pfadliste aufnehmen. - [x] **Step 6: Suite grün** Run: `.venv/bin/python -m pytest -q` → PASS - [x] **Step 7: Fable-Testagent-Abnahme** (Live-Approximation: beide Seiten rendern; keine Szenario-Reste auf /planung; `toggleEdit` genau EINMAL definiert [base.html], auf beiden Seiten funktionale Edit-Formulare; keine JS-Referenzen auf entfernte Funktionen in planning.html; Nav auf allen Seiten). Erst nach VERIFIED weiter. - [x] **Step 8: Commit** ```bash cd /home/wlfb/bin && git add finance/app/ finance/tests/test_gui.py git commit -m "feat: eigene Szenarien-Seite /szenarien" ``` --- ### Task 2: Formular-UX — Paar-Layout, Gesperrt-Kennzeichnung, klare Struktur **Files:** - Modify: `app/static/style.css`, `app/templates/szenarien.html`, `tests/test_gui.py` **Interfaces:** - Consumes: `szenarien.html` aus Task 1 (Neuer-Eintrag-Formular mit Feldern kind/target_type/target_id/value/end_date/name/amount/due; `onEntryArtChange`). - Produces: CSS-Klassen `.entry-form`, `.entry-art`, `.value-label`; dynamisches Wert-Label in `onEntryArtChange`; abgesetzter Durchrechnen-Block. - [x] **Step 1: Failing GUI-Test** ```python def test_neuer_eintrag_formular_struktur(client, db): client.post("/login", data={"username": "admin", "password": "geheim"}) db.add(Scenario(name="UX-Test", description="")) db.commit() r = client.get("/szenarien").text assert 'class="entry-form"' in r assert 'class="entry-art"' in r # Eintragsart-Zeile assert 'class="value-label"' in r # dynamisches Wert-Label assert 'Für diese Eintragsart nicht relevant' in r # Tooltip an Umschaltfeldern assert ' label { font-weight: bold; } .entry-form .entry-art .muted { margin-left: 0.5rem; } input:disabled, select:disabled { background: #e3e3e3; color: #8a8a8a; cursor: not-allowed; } label:has(input:disabled), label:has(select:disabled) { opacity: 0.55; } .project-block { margin-top: 1rem; } ``` (Falls `style.css` bereits eine kollidierende `label`-Regel für `.planning-section` hat: die neue Regel dahinter einsortieren, Spezifität prüfen.) - [x] **Step 3: Formular-Markup** — in `szenarien.html` das Neuer-Eintrag-Formular umbauen: - `
` (Attribute inkl. `data-modifiers-url`/`data-planned-url` unverändert). - Statt `Neuer Eintrag:`: erste Zeile ```html
— die zur Art passenden Felder werden aktiv, gesperrte Felder sind ausgegraut.
``` - Wert-Label dynamisch: `` (Startzustand passend zur Default-Art `percent`). - Die umschaltbaren Felder `end_date`, `name`, `amount`, `due` sowie die beiden Ziel-Selects bekommen `title="Für diese Eintragsart nicht relevant"`. - Fieldset-Titel davor: `

Einträge

`-Tabelle bleibt; über dem Formular `

Neuer Eintrag

` (ersetzt das bisherige Inline-``). - Durchrechnen-Formular in einen abgesetzten Block: ```html
``` - [x] **Step 4: Dynamisches Wert-Label** — in `onEntryArtChange` (szenarien.html) nach der `setDisabled('value', …)`-Zeile ergänzen: ```javascript var valueLabel = form.querySelector('.value-label'); if (valueLabel) { valueLabel.textContent = art === 'percent' ? 'Prozentsatz (%)' : art === 'absolute' ? 'Kürzung (€)' : 'Wert'; } ``` - [x] **Step 5: Suite grün** — `.venv/bin/python -m pytest -q` → PASS - [x] **Step 6: Fable-Testagent-Abnahme** — Live-Approximation + Handtrace: gerenderte Struktur (entry-art zuerst, Paare als `inline-flex`-Einheiten via CSS-Regeln vorhanden, Tooltips an allen Umschaltfeldern, `
`+project-block), Wert-Label-Wechsel für alle 5 Arten, Startzustand konsistent (percent: Wert aktiv mit Label „Prozentsatz (%)"). Erst nach VERIFIED weiter. - [x] **Step 7: Commit** ```bash cd /home/wlfb/bin && git add finance/app/static/style.css finance/app/templates/szenarien.html finance/tests/test_gui.py git commit -m "feat: Eintrags-Formular mit Paar-Layout und Gesperrt-Kennzeichnung" ``` --- ### Task 3: Kategorien-Verwaltung auf der Admin-Seite **Files:** - Modify: `app/routers/categories.py` (PATCH), `app/routers/admin.py` (Kontext), `app/templates/admin.html` - Modify: `tests/test_crud_api.py`, `tests/test_admin.py` **Interfaces:** - Consumes: `CategoryIn`/`CategoryOut`, `POST /api/categories` (vorhanden); `toggleEdit` (seit Task 1 global in base.html); json-form. - Produces: `PATCH /api/categories/{category_id}` (404 „Kategorie nicht gefunden", 409 „Kategorie existiert bereits"); Admin-Abschnitt „Kategorien" mit IDs `cat-row-{id}`/`cat-edit-{id}`. - [x] **Step 1: Failing API-Test** — `tests/test_crud_api.py`: ```python def test_category_patch(client): a = client.post("/api/categories", headers=H, json={"name": "Kat-A"}).json() b = client.post("/api/categories", headers=H, json={"name": "Kat-B"}).json() # Umbenennen r = client.patch(f"/api/categories/{a['id']}", headers=H, json={"name": "Kat-A-neu"}) assert r.status_code == 200 and r.json()["name"] == "Kat-A-neu" # Umbenennen auf den EIGENEN Namen ist erlaubt (kein 409) r = client.patch(f"/api/categories/{a['id']}", headers=H, json={"name": "Kat-A-neu"}) assert r.status_code == 200 # Kollision mit anderer Kategorie -> 409 r = client.patch(f"/api/categories/{a['id']}", headers=H, json={"name": "Kat-B"}) assert r.status_code == 409 # unbekannte id -> 404 assert client.patch("/api/categories/99999", headers=H, json={"name": "x"}).status_code == 404 ``` Run: `.venv/bin/python -m pytest tests/test_crud_api.py -q` → FAIL (405) - [x] **Step 2: PATCH-Endpunkt** — `app/routers/categories.py`, nach `create_category`: ```python @router.patch("/categories/{category_id}", response_model=CategoryOut) def patch_category(category_id: int, data: CategoryIn, session: Session = Depends(get_session)): cat = session.get(Category, category_id) if cat is None: raise HTTPException(404, "Kategorie nicht gefunden") clash = session.execute( select(Category).where(Category.name == data.name)).scalar() if clash is not None and clash.id != category_id: raise HTTPException(409, "Kategorie existiert bereits") cat.name = data.name session.commit() session.refresh(cat) return CategoryOut.model_validate(cat) ``` - [x] **Step 3: Failing GUI-Test** — `tests/test_admin.py` (Muster für Login dort übernehmen; falls die Datei GUI-Zugriffe anders aufbaut, an bestehende Fixtures anlehnen): ```python def test_admin_zeigt_kategorien_verwaltung(client, db): from app.models.tables import Category client.post("/login", data={"username": "admin", "password": "geheim"}) db.add(Category(name="Admin-Kat")) db.commit() r = client.get("/admin").text assert "Kategorien" in r and "Admin-Kat" in r assert "Neue Kategorie anlegen" in r assert 'hx-patch="/api/categories/' in r assert 'hx-post="/api/categories"' in r ``` Run: → FAIL - [x] **Step 4: Admin-Kontext + Template** — `app/routers/admin.py`: in `admin_page` (und im Fehler-/Erfolgs-Re-Render von `admin_change_password`, damit der Abschnitt nie verschwindet — UX-Regel) `"categories": session.execute(select(Category)).scalars().all()` in den Template-Kontext aufnehmen (Imports `select`, `Category` ergänzen; `session`-Dependency, falls die Route noch keine hat). `admin.html`, neuer Abschnitt vor dem `