Files
bin/docs/superpowers/plans/2026-07-20-ausbaustufe-7.md
2026-07-20 14:21:11 +02:00

19 KiB
Raw Blame History

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-<section> 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.

  • Step 1: Failing GUI-Tests — in tests/test_gui.py:

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)

  • Step 2: Routeapp/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:
@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"],
    })
  • Step 3: Template-Umzug — Neues app/templates/szenarien.html:
{% extends "base.html" %}
{% block title %}Szenarien  Finanzberatung{% endblock %}
{% block content %}
<h1>Szenarien</h1>

<section class="planning-section">
  <!-- HIER: die komplette bisherige Szenarien-<section>-INNEREI aus
       planning.html unverändert einfügen (alles zwischen
       '<h2>Szenarien</h2>' … einschließlich des Fieldsets
       'Neues Szenario anlegen'), ohne das umschließende <section>-Tag
       doppelt zu setzen. -->
</section>

<script>
  // Szenario-spezifische Helfer (aus planning.html hierher umgezogen):
  // onModTargetTypeChange, onEntryArtChange, toggleScenarioLoan
  // unverändert einfügen.
</script>
{% endblock %}

Der <h2>Szenarien</h2> entfällt (die Seite hat die <h1>-Überschrift). In planning.html: die gesamte Szenarien-<section> 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.

  • Step 4: Nav + Hilfebase.html: <a href="/szenarien">Szenarien</a> 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).

  • Step 5: Bestehende Tests umziehentests/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.

  • Step 6: Suite grün

Run: .venv/bin/python -m pytest -q → PASS

  • 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.

  • Step 8: Commit

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.

  • Step 1: Failing GUI-Test

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 '<hr' in r                                    # Durchrechnen abgesetzt

Run: → FAIL

  • Step 2: CSSapp/static/style.css ergänzen:
/* Ausbaustufe 7: Formular-Paare als Einheit, gesperrte Felder erkennbar. */
.entry-form label,
.planning-section form label {
  display: inline-flex;
  align-items: center;
  gap: 0.4rem;
  margin: 0 1.25rem 0.6rem 0;
  white-space: nowrap;
}
.entry-form .entry-art {
  display: block;
  margin-bottom: 0.75rem;
}
.entry-form .entry-art > 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.)

  • Step 3: Formular-Markup — in szenarien.html das Neuer-Eintrag-Formular umbauen:
    • <form … class="entry-form" …> (Attribute inkl. data-modifiers-url/data-planned-url unverändert).
    • Statt <strong>Neuer Eintrag:</strong>: erste Zeile
      <div class="entry-art">
        <label>Eintragsart
          <select name="kind" data-type="str" onchange="onEntryArtChange(this)">
            {% for k in modifier_kinds %}<option value="{{ k }}">{{ k|de_label }}</option>{% endfor %}
            <option value="einmal">Einmalzahlung</option>
          </select>
        </label>
        <span class="muted">— die zur Art passenden Felder werden aktiv, gesperrte Felder sind ausgegraut.</span>
      </div>
  • Wert-Label dynamisch: <label><span class="value-label">Prozentsatz (%)</span> <input type="text" name="value" data-type="amount" value="0" title="Für diese Eintragsart nicht relevant"></label> (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: <h3>Einträge</h3>-Tabelle bleibt; über dem Formular <h4>Neuer Eintrag</h4> (ersetzt das bisherige Inline-<strong>).
  • Durchrechnen-Formular in einen abgesetzten Block:
    <hr>
    <div class="project-block">
      <form hx-post="/api/scenarios/{{ sc.id }}/project" hx-swap="none"
            hx-on::after-request="if(event.detail.successful){window.location.reload()}">
        <button type="submit">Durchrechnen</button>
      </form>
    </div>
  • Step 4: Dynamisches Wert-Label — in onEntryArtChange (szenarien.html) nach der setDisabled('value', …)-Zeile ergänzen:
    var valueLabel = form.querySelector('.value-label');
    if (valueLabel) {
      valueLabel.textContent = art === 'percent' ? 'Prozentsatz (%)'
        : art === 'absolute' ? 'Kürzung (€)' : 'Wert';
    }
  • Step 5: Suite grün.venv/bin/python -m pytest -q → PASS

  • 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, <hr>+project-block), Wert-Label-Wechsel für alle 5 Arten, Startzustand konsistent (percent: Wert aktiv mit Label „Prozentsatz (%)"). Erst nach VERIFIED weiter.

  • Step 7: Commit

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}.

  • Step 1: Failing API-Testtests/test_crud_api.py:

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)

  • Step 2: PATCH-Endpunktapp/routers/categories.py, nach create_category:
@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)
  • Step 3: Failing GUI-Testtests/test_admin.py (Muster für Login dort übernehmen; falls die Datei GUI-Zugriffe anders aufbaut, an bestehende Fixtures anlehnen):
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

  • Step 4: Admin-Kontext + Templateapp/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 <script>:
<section class="admin-section">
  <h2>Kategorien</h2>
  <p class="muted">Kategorien für Buchungen, Fixposten und Szenario-Modifikatoren.
     Löschen ist bewusst nicht vorgesehen (Kategorien hängen an Buchungen und Regeln).</p>
  <table>
    <thead><tr><th>Name</th><th></th></tr></thead>
    <tbody>
      {% for c in categories %}
      <tr id="cat-row-{{ c.id }}">
        <td>{{ c.name }}</td>
        <td><button type="button" onclick="toggleEdit('cat', {{ c.id }}, true)">Bearbeiten</button></td>
      </tr>
      <tr id="cat-edit-{{ c.id }}" hidden>
        <td colspan="2">
          <form hx-ext="json-form" hx-patch="/api/categories/{{ c.id }}" hx-swap="none"
                hx-on::after-request="if(event.detail.successful){window.location.reload()}">
            <label>Name <input type="text" name="name" value="{{ c.name }}" required maxlength="100"></label>
            <button type="submit">Speichern</button>
            <button type="button" onclick="toggleEdit('cat', {{ c.id }}, false)">Abbrechen</button>
          </form>
        </td>
      </tr>
      {% else %}
      <tr><td colspan="2">Noch keine Kategorien.</td></tr>
      {% endfor %}
    </tbody>
  </table>
  <fieldset>
    <legend>Neue Kategorie anlegen</legend>
    <form hx-ext="json-form" hx-post="/api/categories" hx-swap="none"
          hx-on::after-request="if(event.detail.successful){window.location.reload()}">
      <label>Name <input type="text" name="name" required maxlength="100"></label>
      <button type="submit">Anlegen</button>
    </form>
  </fieldset>
</section>
  • Step 5: Suite grün.venv/bin/python -m pytest -q → PASS

  • Step 6: Fable-Testagent-Abnahme (Live-Approximation: Anlegen → erscheint in Liste UND in den Kategorie-Dropdowns von Planung/Szenarien; Umbenennen-Roundtrip; 409 im Alert-Pfad; Admin-Fehler-Re-Render zeigt Abschnitt weiterhin). Erst nach VERIFIED weiter.

  • Step 7: Commit

cd /home/wlfb/bin && git add finance/app/routers/categories.py finance/app/routers/admin.py finance/app/templates/admin.html finance/tests/
git commit -m "feat: Kategorien-Verwaltung auf der Admin-Seite"

Task 4: Release v0.8.0 — Redeploy, Live-Check, Ledger

Files: finance/VERSION (0.7.00.8.0), .superpowers/sdd/progress.md, Plan-Häkchen.

  • Step 1: Suite final.venv/bin/python -m pytest -q → alle grün.
  • Step 2: Version + Commitecho "0.8.0" > finance/VERSION, Commit chore: Version 0.8.0.
  • Step 3: Redeploy./create_pod_finance.sh (keine Migration). Service aktiv, Readiness 200.
  • Step 4: Live-Smoke/api/version == 0.8.0; Nav zeigt „Szenarien"; /szenarien zeigt Best Case mit Einträgen; /planung ohne Szenarien; /admin mit Kategorien-Abschnitt.
  • Step 5: Fable-Testagent-Abnahme (Release-Gate) — Live: Wegwerf-Kategorie „SMOKE-A7" anlegen → umbenennen → in Dropdowns sichtbar (bleibt stehen, Hinweis im Bericht — Kategorien sind nicht löschbar; Namenswahl „zz-Smoke" damit sie unten einsortiert? Nein: Kategorie „SMOKE-A7" wird nach dem Test per direktem psql-DELETE entfernt, NUR wenn keine FK-Referenzen existieren — vorher COUNT-Checks auf transactions/category_rules/recurring_items/scenario_modifiers); Best-Case-Einträge unverändert; Formular-Struktur auf /szenarien (entry-art, Tooltips, hr). Erst nach VERIFIED weiter.
  • Step 6: Ledger + Plan-Häkchen + Push.

Self-Review (beim Planschreiben)

  • Spec-Abdeckung: Seite → Task 1; UX/CSS/Label/Durchrechnen → Task 2; Kategorien (PATCH + Admin-GUI) → Task 3; Release → Task 4. Vollständig.
  • Platzhalter: Der Template-Umzug in Task 1 Step 3 ist bewusst als präziser Move beschrieben (Quelle: bestehende Szenarien-Sektion) statt als 200-Zeilen-Duplikat — die Quelle ist eindeutig benannt und unverändert zu übernehmen.
  • Typ-Konsistenz: toggleEdit global (Task 1) wird von Task 3 (cat--Präfix) vorausgesetzt; entry-form/entry-art/value-label-Klassen konsistent zwischen CSS (Task 2 Step 2) und Markup (Step 3) und Test (Step 1).