Files
bin/docs/superpowers/plans/2026-07-20-ausbaustufe-6.md
2026-07-20 12:54:19 +02:00

24 KiB
Raw Blame History

Ausbaustufe 6 Implementation Plan — Szenario-Einträge-Tabelle (v0.7.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: Pro Szenario eine immer sichtbare, editierbare Tabelle „Einträge" (Modifikatoren + szenario-eigene Einmalzahlungen gemeinsam) mit Inline-Bearbeiten und einem gemeinsamen Anlege-Formular; Release v0.7.0.

Architecture: Datenmodell unverändert — zwei neue Full-Body-PATCH-Endpunkte in routers/scenarios.py; Template-Umbau in planning.html ersetzt die zwei <details>-Abschnitte durch eine Tabelle; Feld-Umschaltung per disabled (JS onEntryArtChange ersetzt onModKindChange), Endpunkt-Wechsel des Anlege-Formulars per hx-post-Attribut + htmx.process.

Tech Stack: FastAPI + Pydantic v2, Jinja2, htmx + json-form-Extension, pytest.

Spec: docs/superpowers/specs/2026-07-20-szenario-eintraege-design.md

Global Constraints

  • Geldbeträge Decimal; JSON-API Punkt-Dezimal; HTML deutsch (|eur, |de_label, TT.MM.JJJJ).
  • API-/DB-Werte englisch; neuer Formular-Artwert einmal ist NUR clientseitig (landet nie in der DB — an /planned gesendet wird er von Pydantic ignoriert).
  • UX-Regel: Umschalten/Verstecken nur per disabled/hidden, nie DOM-Entfernen.
  • Fable-Testagent-Gate je Task VOR dem Commit; Ledger-Eintrag je Task in .superpowers/sdd/progress.md.
  • DATENSCHUTZ: keine echten Namen/Beträge in Commits/Tests/Doku; Live-Demo-Daten nur DB/Chat.
  • Testlauf: cd /home/wlfb/bin/finance && .venv/bin/python -m pytest -q — Basis 178 passed, muss grün bleiben.
  • Pfade unten relativ zu /home/wlfb/bin/finance (Ledger/Plan unter /home/wlfb/bin).

Task 1: PATCH-Endpunkte für Modifikatoren und Szenario-Einmalzahlungen

Files:

  • Modify: app/routers/scenarios.py
  • Modify: tests/test_crud_api.py

Interfaces:

  • Consumes: ModifierIn/ModifierOut, ScenarioPlannedIn/ScenarioPlannedOut, _get_scenario (alle vorhanden in scenarios.py).

  • Produces: PATCH /api/scenarios/{scenario_id}/modifiers/{mod_id} (Body ModifierIn, Antwort ModifierOut, 404 „Modifikator nicht gefunden") und PATCH /api/scenarios/{scenario_id}/planned/{item_id} (Body ScenarioPlannedIn, Antwort ScenarioPlannedOut, 404 „Einmalzahlung nicht gefunden"). Task 2 (GUI) sendet auf genau diese URLs.

  • Step 1: Failing Tests — in tests/test_crud_api.py ergänzen:

def test_modifier_patch(client, db):
    sc = client.post("/api/scenarios", headers=H, json={"name": "ModPatch"}).json()
    m = client.post(f"/api/scenarios/{sc['id']}/modifiers", headers=H, json={
        "target_type": "recurring", "target_id": 1, "kind": "percent",
        "value": "10.00"}).json()
    # Wert aendern
    r = client.patch(f"/api/scenarios/{sc['id']}/modifiers/{m['id']}", headers=H, json={
        "target_type": "recurring", "target_id": 1, "kind": "percent", "value": "25.00"})
    assert r.status_code == 200 and r.json()["value"] == "25.00"
    # Art auf ende wechseln (mit Datum); value wird vom Validator genullt
    r = client.patch(f"/api/scenarios/{sc['id']}/modifiers/{m['id']}", headers=H, json={
        "target_type": "recurring", "target_id": 1, "kind": "ende",
        "end_date": "2026-08-31"})
    assert r.status_code == 200
    assert r.json()["kind"] == "ende" and r.json()["end_date"] == "2026-08-31"
    # ende ohne Datum -> 422
    r = client.patch(f"/api/scenarios/{sc['id']}/modifiers/{m['id']}", headers=H, json={
        "target_type": "recurring", "target_id": 1, "kind": "ende"})
    assert r.status_code == 422
    # zurueck auf remove: end_date wird genullt
    r = client.patch(f"/api/scenarios/{sc['id']}/modifiers/{m['id']}", headers=H, json={
        "target_type": "recurring", "target_id": 1, "kind": "remove",
        "end_date": "2026-08-31"})
    assert r.status_code == 200 and r.json()["end_date"] is None
    # fremdes Szenario -> 404
    other = client.post("/api/scenarios", headers=H, json={"name": "ModPatch2"}).json()
    r = client.patch(f"/api/scenarios/{other['id']}/modifiers/{m['id']}", headers=H, json={
        "target_type": "recurring", "target_id": 1, "kind": "remove"})
    assert r.status_code == 404


def test_scenario_planned_patch(client, db):
    sc = client.post("/api/scenarios", headers=H, json={"name": "SpiPatch"}).json()
    p = client.post(f"/api/scenarios/{sc['id']}/planned", headers=H, json={
        "name": "Zufluss", "amount": "5000.00", "due": "2026-07-30"}).json()
    r = client.patch(f"/api/scenarios/{sc['id']}/planned/{p['id']}", headers=H, json={
        "name": "Zufluss geaendert", "amount": "6000.00", "due": "2026-08-15"})
    assert r.status_code == 200
    body = r.json()
    assert body["name"] == "Zufluss geaendert"
    assert body["amount"] == "6000.00" and body["due"] == "2026-08-15"
    # fremdes Szenario -> 404
    other = client.post("/api/scenarios", headers=H, json={"name": "SpiPatch2"}).json()
    r = client.patch(f"/api/scenarios/{other['id']}/planned/{p['id']}", headers=H, json={
        "name": "x", "amount": "1.00", "due": "2026-08-15"})
    assert r.status_code == 404

Run: .venv/bin/python -m pytest tests/test_crud_api.py -q → neue Tests FAIL (405 Method Not Allowed)

  • Step 2: Endpunkte implementierenapp/routers/scenarios.py, direkt nach delete_modifier bzw. nach delete_scenario_planned:
@router.patch("/{scenario_id}/modifiers/{mod_id}", response_model=ModifierOut)
def patch_modifier(scenario_id: int, mod_id: int, data: ModifierIn,
                   session: Session = Depends(get_session)):
    # Full-Body-Update: das Edit-Formular sendet immer alle Felder des Typs.
    _get_scenario(session, scenario_id)
    modifier = session.get(ScenarioModifier, mod_id)
    if modifier is None or modifier.scenario_id != scenario_id:
        raise HTTPException(404, "Modifikator nicht gefunden")
    for key, value in data.model_dump().items():
        setattr(modifier, key, value)
    session.commit()
    session.refresh(modifier)
    return ModifierOut.model_validate(modifier)
@router.patch("/{scenario_id}/planned/{item_id}", response_model=ScenarioPlannedOut)
def patch_scenario_planned(scenario_id: int, item_id: int, data: ScenarioPlannedIn,
                           session: Session = Depends(get_session)):
    # Full-Body-Update, Muster patch_modifier.
    _get_scenario(session, scenario_id)
    item = session.get(ScenarioPlannedItem, item_id)
    if item is None or item.scenario_id != scenario_id:
        raise HTTPException(404, "Einmalzahlung nicht gefunden")
    for key, value in data.model_dump().items():
        setattr(item, key, value)
    session.commit()
    session.refresh(item)
    return ScenarioPlannedOut.model_validate(item)
  • Step 3: Tests grün + Suite

Run: .venv/bin/python -m pytest tests/test_crud_api.py -q && .venv/bin/python -m pytest -q Expected: alles PASS (178 + 2)

  • Step 4: Fable-Testagent-Abnahme (Faktencheck: Validator-Wirkung beim PATCH [ende↔andere Arten], 404-Pfade, keine Nebenwirkungen auf Projektion). Erst nach VERIFIED weiter.

  • Step 5: Commit

cd /home/wlfb/bin && git add finance/app/routers/scenarios.py finance/tests/test_crud_api.py
git commit -m "feat: PATCH-Endpunkte fuer Szenario-Modifikatoren und -Einmalzahlungen"

Task 2: GUI-Umbau — eine Einträge-Tabelle je Szenario

Files:

  • Modify: app/templates/planning.html (Szenario-Abschnitt: die beiden <details> „Modifikatoren" [Zeilen ~300-362] und „Einmalzahlungen in diesem Szenario" [~364-395] ersetzen; JS-Block: onModKindChange ersetzen)
  • Modify: app/routers/gui.py (_scenario_rows: Modifikator-Query stabil sortieren)
  • Modify: app/templates/hilfe.html (Szenarien-Absatz an neue Tabelle anpassen)
  • Modify: tests/test_gui.py (neuer Test + Anpassung test_szenario_gui_ende_und_einmalzahlungen)

Interfaces:

  • Consumes: PATCH-Endpunkte aus Task 1; toggleEdit, onModTargetTypeChange, Filter |eur/|de_label, Kontext row.modifiers/row.planned_items/modifier_kinds/recurring_names/category_names (alle vorhanden).

  • Produces: Element-IDs mod-row-{id}/mod-edit-{id} und spi-row-{id}/spi-edit-{id}; JS onEntryArtChange(select) (ersetzt onModKindChange vollständig — auch in den Art-Selects); Anlege-Formular mit data-modifiers-url/data-planned-url und Eintragsart-Option value="einmal".

  • Step 1: Failing GUI-Test — in tests/test_gui.py ergänzen (Imports Scenario, ScenarioModifier, ScenarioPlannedItem existieren dort seit A5):

def test_szenario_eintraege_tabelle(client, db):
    client.post("/login", data={"username": "admin", "password": "geheim"})
    sc = Scenario(name="Eintraege-GUI", description="")
    db.add(sc)
    db.flush()
    db.add(ScenarioModifier(scenario_id=sc.id, target_type="recurring",
                            target_id=1, kind="ende", value=Decimal("0"),
                            end_date=date(2026, 8, 31)))
    db.add(ScenarioModifier(scenario_id=sc.id, target_type="category",
                            target_id=1, kind="percent", value=Decimal("25.00")))
    db.add(ScenarioPlannedItem(scenario_id=sc.id, name="Zufluss-Eintrag",
                               amount=Decimal("5000.00"), due=date(2026, 7, 30)))
    db.commit()
    r = client.get("/planung").text
    assert "Einträge" in r
    # alte details-Abschnitte sind ersetzt
    assert "<summary>Modifikatoren</summary>" not in r
    assert "Einmalzahlungen in diesem Szenario" not in r
    # gemischte Zeilen in EINER Tabelle
    assert "31.08.2026" in r and "25,00" in r and "Zufluss-Eintrag" in r
    assert ">Einmalzahlung<" in r
    # Inline-Edit je Typ
    assert f'hx-patch="/api/scenarios/{sc.id}/modifiers/' in r
    assert f'hx-patch="/api/scenarios/{sc.id}/planned/' in r
    # Anlege-Formular: Eintragsart inkl. Einmalzahlung + Endpunkt-Wechsel-Attribute
    assert 'value="einmal"' in r
    assert f'data-planned-url="/api/scenarios/{sc.id}/planned"' in r
    assert "onEntryArtChange" in r and "onModKindChange" not in r

Run: → FAIL

  • Step 2: _scenario_rows sortierenapp/routers/gui.py, Modifikator-Query um .order_by(ScenarioModifier.id) ergänzen:
        modifiers = session.execute(
            select(ScenarioModifier).where(ScenarioModifier.scenario_id == sc.id)
            .order_by(ScenarioModifier.id)
        ).scalars().all()
  • Step 3: Einträge-Tabelle — in planning.html die BEIDEN <details>-Blöcke „Modifikatoren" und „Einmalzahlungen in diesem Szenario" komplett ersetzen durch:
    <h3>Einträge</h3>
    <table>
      <thead>
        <tr><th>Was</th><th>Art</th><th>Wert/Betrag</th><th>Datum</th><th></th></tr>
      </thead>
      <tbody>
        {% for m in row.modifiers %}
        <tr id="mod-row-{{ m.id }}">
          <td>
            {% if m.target_type == "category" %}Kategorie: {{ category_names.get(m.target_id, m.target_id) }}
            {% else %}Posten: {{ recurring_names.get(m.target_id, m.target_id) }}{% endif %}
          </td>
          <td>{{ m.kind|de_label }}</td>
          <td>{% if m.kind in ('percent', 'absolute') %}{{ m.value|eur }}{% else %}{% endif %}</td>
          <td>{% if m.kind == 'ende' %}{{ m.end_date.strftime('%d.%m.%Y') if m.end_date else '' }}{% else %}{% endif %}</td>
          <td>
            <button type="button" onclick="toggleEdit('mod', {{ m.id }}, true)">Bearbeiten</button>
            <form class="inline-form" hx-delete="/api/scenarios/{{ sc.id }}/modifiers/{{ m.id }}" hx-swap="none"
                  hx-on::after-request="if(event.detail.successful){window.location.reload()}">
              <button type="submit">Löschen</button>
            </form>
          </td>
        </tr>
        <tr id="mod-edit-{{ m.id }}" hidden>
          <td colspan="5">
            <form hx-ext="json-form" hx-patch="/api/scenarios/{{ sc.id }}/modifiers/{{ m.id }}" hx-swap="none"
                  hx-on::after-request="if(event.detail.successful){window.location.reload()}">
              <label>Ziel-Typ
                <select name="target_type" data-type="str" onchange="onModTargetTypeChange(this)">
                  <option value="category" {% if m.target_type == 'category' %}selected{% endif %}>Kategorie</option>
                  <option value="recurring" {% if m.target_type == 'recurring' %}selected{% endif %}>Wiederkehrender Posten</option>
                </select>
              </label>
              <span class="mod-target-category" {% if m.target_type != 'category' %}style="display:none"{% endif %}>
                <label>Kategorie
                  <select name="target_id" data-type="int" {% if m.target_type != 'category' %}disabled{% endif %}>
                    {% for c in categories %}<option value="{{ c.id }}" {% if m.target_type == 'category' and c.id == m.target_id %}selected{% endif %}>{{ c.name }}</option>{% endfor %}
                  </select>
                </label>
              </span>
              <span class="mod-target-recurring" {% if m.target_type != 'recurring' %}style="display:none"{% endif %}>
                <label>Posten
                  <select name="target_id" data-type="int" {% if m.target_type != 'recurring' %}disabled{% endif %}>
                    {% for r in recurring %}<option value="{{ r.id }}" {% if m.target_type == 'recurring' and r.id == m.target_id %}selected{% endif %}>{{ r.name }}</option>{% endfor %}
                  </select>
                </label>
              </span>
              <label>Art
                <select name="kind" data-type="str" onchange="onEntryArtChange(this)">
                  {% for k in modifier_kinds %}<option value="{{ k }}" {% if k == m.kind %}selected{% endif %}>{{ k|de_label }}</option>{% endfor %}
                </select>
              </label>
              <label>Wert <input type="text" name="value" data-type="amount" value="{{ m.value|eur }}" {% if m.kind not in ('percent', 'absolute') %}disabled{% endif %}></label>
              <label>Endet am <input type="date" name="end_date" value="{{ m.end_date.isoformat() if m.end_date else '' }}" {% if m.kind != 'ende' %}disabled{% endif %}></label>
              <button type="submit">Speichern</button>
              <button type="button" onclick="toggleEdit('mod', {{ m.id }}, false)">Abbrechen</button>
            </form>
          </td>
        </tr>
        {% endfor %}
        {% for sp in row.planned_items %}
        <tr id="spi-row-{{ sp.id }}">
          <td>{{ sp.name }}</td>
          <td>Einmalzahlung</td>
          <td class="{{ 'neg' if sp.amount < 0 else '' }}">{{ sp.amount|eur }} €</td>
          <td>{{ sp.due.strftime('%d.%m.%Y') }}</td>
          <td>
            <button type="button" onclick="toggleEdit('spi', {{ sp.id }}, true)">Bearbeiten</button>
            <form class="inline-form" hx-delete="/api/scenarios/{{ sc.id }}/planned/{{ sp.id }}" hx-swap="none"
                  hx-confirm="„{{ sp.name }}“ wirklich löschen?"
                  hx-on::after-request="if(event.detail.successful){window.location.reload()}">
              <button type="submit">Löschen</button>
            </form>
          </td>
        </tr>
        <tr id="spi-edit-{{ sp.id }}" hidden>
          <td colspan="5">
            <form hx-ext="json-form" hx-patch="/api/scenarios/{{ sc.id }}/planned/{{ sp.id }}" hx-swap="none"
                  hx-on::after-request="if(event.detail.successful){window.location.reload()}">
              <label>Name <input type="text" name="name" value="{{ sp.name }}" required></label>
              <label>Betrag <input type="text" name="amount" data-type="amount" value="{{ sp.amount|eur }}" required></label>
              <label>Fällig am <input type="date" name="due" value="{{ sp.due.isoformat() }}" required></label>
              <button type="submit">Speichern</button>
              <button type="button" onclick="toggleEdit('spi', {{ sp.id }}, false)">Abbrechen</button>
            </form>
          </td>
        </tr>
        {% endfor %}
        {% if not row.modifiers and not row.planned_items %}
        <tr><td colspan="5">Keine Einträge.</td></tr>
        {% endif %}
      </tbody>
    </table>
  • Step 4: Anlege-Formular „Neuer Eintrag" — direkt nach der Tabelle (Default-Art ist die erste Option percent → Wert aktiv, Datumsfelder/Einmal-Felder disabled):
    <form hx-ext="json-form" hx-post="/api/scenarios/{{ sc.id }}/modifiers" hx-swap="none"
          data-modifiers-url="/api/scenarios/{{ sc.id }}/modifiers"
          data-planned-url="/api/scenarios/{{ sc.id }}/planned"
          hx-on::after-request="if(event.detail.successful){window.location.reload()}">
      <strong>Neuer Eintrag:</strong>
      <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="mod-target-category">
        <label>Kategorie
          <select name="target_id" data-type="int">
            {% for c in categories %}<option value="{{ c.id }}">{{ c.name }}</option>{% endfor %}
          </select>
        </label>
      </span>
      <span class="mod-target-recurring" style="display:none">
        <label>Posten
          <select name="target_id" data-type="int" disabled>
            {% for r in recurring %}<option value="{{ r.id }}">{{ r.name }}</option>{% endfor %}
          </select>
        </label>
      </span>
      <label>Ziel-Typ
        <select name="target_type" data-type="str" onchange="onModTargetTypeChange(this)">
          <option value="category">Kategorie</option>
          <option value="recurring">Wiederkehrender Posten</option>
        </select>
      </label>
      <label>Wert <input type="text" name="value" data-type="amount" value="0"></label>
      <label>Endet am <input type="date" name="end_date" disabled></label>
      <label>Name <input type="text" name="name" disabled></label>
      <label>Betrag <input type="text" name="amount" data-type="amount" placeholder="5000,00" disabled></label>
      <label>Fällig am <input type="date" name="due" disabled></label>
      <button type="submit">Hinzufügen</button>
    </form>

WICHTIG: Der Ziel-Typ-Select muss VOR den beiden Ziel-Spans stehen wie im bisherigen Formular? Nein — Reihenfolge im bisherigen Formular war Ziel-Typ, dann Spans. Diese Reihenfolge beibehalten (Ziel-Typ zuerst, dann die Spans), damit das Formular lesbar bleibt — den obigen Block entsprechend anordnen: Eintragsart, Ziel-Typ, beide Spans, Wert, Endet am, Name, Betrag, Fällig am, Button.

  • Step 5: JS — im <script>-Block von planning.html die Funktion onModKindChange ERSETZEN durch:
  function onEntryArtChange(select) {
    // Umschaltmatrix "Neuer Eintrag"/Modifikator-Edit: Felder nur per
    // disabled schalten (UX-Regel). Fehlt ein Feld (Edit-Formulare haben
    // kein name/amount/due), wird es uebersprungen.
    var form = select.closest('form');
    var art = select.value;
    var isMod = art !== 'einmal';
    function setDisabled(name, disabled) {
      var el = form.querySelector('[name="' + name + '"]');
      if (el) { el.disabled = disabled; }
    }
    setDisabled('value', !(art === 'percent' || art === 'absolute'));
    setDisabled('end_date', art !== 'ende');
    setDisabled('name', isMod);
    setDisabled('amount', isMod);
    setDisabled('due', isMod);
    var typeSelect = form.querySelector('[name="target_type"]');
    if (typeSelect) {
      typeSelect.disabled = !isMod;
      var isCategory = typeSelect.value === 'category';
      var catSelect = form.querySelector('.mod-target-category select');
      var recSelect = form.querySelector('.mod-target-recurring select');
      if (catSelect) { catSelect.disabled = !isMod || !isCategory; }
      if (recSelect) { recSelect.disabled = !isMod || isCategory; }
    }
    // POST-Ziel nur beim Anlege-Formular wechseln (Edit-Formulare patchen fix).
    if (form.hasAttribute('data-planned-url')) {
      form.setAttribute('hx-post', isMod ? form.getAttribute('data-modifiers-url')
                                         : form.getAttribute('data-planned-url'));
      htmx.process(form);
    }
  }

onModTargetTypeChange und toggleEdit bleiben unverändert.

  • Step 6: Bestehenden Test anpassentests/test_gui.py::test_szenario_gui_ende_und_einmalzahlungen (aus A5 Task 7): die Assertions "Einmalzahlungen in diesem Szenario" in r und f'hx-post="/api/scenarios/{sc.id}/planned"' in r ersetzen durch:
    assert "Einträge" in r
    assert f'data-planned-url="/api/scenarios/{sc.id}/planned"' in r

Die übrigen Assertions (value="ende", name="end_date", 31.08.2026, Zufluss-GUI, 5.000,00, 30.07.2026) bleiben gültig. Danach: grep -n "Einmalzahlungen in diesem Szenario" tests/ → 0 Treffer.

  • Step 7: Hilfe anpassenapp/templates/hilfe.html, im Szenarien-Absatz (aus A5) den Satzteil zu Modifikatoren/Einmalzahlungen ersetzen durch (generisch, keine echten Daten):
    Varianten wie „Best Case“ pflegst du in der Tabelle „Einträge“ des
    Szenarios: Art „Ende“ lässt einen wiederkehrenden Posten (z. B. eine
    auslaufende Miete) zu einem Datum enden, ohne den Posten selbst zu
    ändern; „Prozent“/„Absolut“ kürzen Beträge; „Entfällt“ blendet sie ganz
    aus; „Einmalzahlung“ ergänzt einen einmaligen Zu- oder Abfluss, der nur
    in diesem Szenario zählt. Jeder Eintrag ist über „Bearbeiten“ direkt
    änderbar.
  • Step 8: Tests + Suite grün

Run: .venv/bin/python -m pytest tests/test_gui.py -q && .venv/bin/python -m pytest -q Expected: alles PASS

  • Step 9: Fable-Testagent-Abnahme — Live-Anteil (TestClient-Approximation): gemischte Tabelle rendert beide Typen in korrekter Reihenfolge; Edit-Roundtrips für Modifikator (percent→ende inkl. Datumspflicht) und Einmalzahlung mit deutscher Betragseingabe; Umschaltmatrix-Handtrace für alle 5 Eintragsarten (inkl. disabled-Zustand von kind-Fremdfeld beim jeweils anderen Endpunkt); keine doppelten IDs. Erst nach VERIFIED weiter.

  • Step 10: Commit

cd /home/wlfb/bin && git add finance/app/templates/ finance/app/routers/gui.py finance/tests/test_gui.py
git commit -m "feat: Szenario-Eintraege als eine editierbare Tabelle"

Task 3: Release v0.7.0 — Redeploy, Live-Check, Ledger

Files:

  • Modify: finance/VERSION (0.6.00.7.0)
  • Modify: .superpowers/sdd/progress.md (Ledger), Plan-Häkchen in dieser Datei

Interfaces:

  • Consumes: Tasks 1-2 committet, Suite grün, bestehendes Live-Szenario „Best Case" (id 4) mit 3 Einträgen.

  • Step 1: Suite finalcd /home/wlfb/bin/finance && .venv/bin/python -m pytest -q → alle grün.

  • Step 2: Version + Commit

echo "0.7.0" > /home/wlfb/bin/finance/VERSION
cd /home/wlfb/bin && git add finance/VERSION && git commit -m "chore: Version 0.7.0"
  • Step 3: Redeploycd /home/wlfb/bin && ./create_pod_finance.sh (keine Migration nötig; Entrypoint-alembic upgrade head ist idempotent). Expected: Service aktiv, Readiness 200.

  • Step 4: Live-Smoke/api/version == 0.7.0 (Bearer-Key aus ~/.local/share/finance_pod/.env); GUI-Login; /planung zeigt im „Best Case"-Kasten die Einträge-Tabelle mit 3 Zeilen (2× Ende, 1× Einmalzahlung) OHNE aufklappen zu müssen; kein „Modifikatoren"-Summary mehr.

  • Step 5: Fable-Testagent-Abnahme (Release-Gate) — Live: zusätzlich zu Step 4 ein kompletter Roundtrip in einem WEGWERF-Szenario „SMOKE-A6" (anlegen → Eintrag percent anlegen → per PATCH auf ende mit Datum ändern → Einmalzahlung anlegen → Betrag per PATCH ändern → Szenario löschen [räumt alles ab]); Bestandsdaten unverändert (confirmed-Tx-Count, „Best Case"-Einträge unangetastet). Erst nach VERIFIED weiter.

  • Step 6: Ledger + Plan-Häkchen + Push

cd /home/wlfb/bin && git add docs/superpowers/plans/2026-07-20-ausbaustufe-6.md && git commit -m "docs: Ledger/Plan Ausbaustufe 6" && git push

Self-Review (beim Planschreiben)

  • Spec-Abdeckung: API-PATCH → Task 1; Tabelle/Anlege-Formular/Inline-Edit/JS/Sortierung/Hilfe → Task 2; Release/Live-Check → Task 3. Vollständig.
  • Platzhalter: keine.
  • Typ-Konsistenz: URLs/IDs (mod-/spi-), data-modifiers-url/data-planned-url, onEntryArtChange konsistent zwischen Tasks 1/2; Test-Anpassung des A5-Tests explizit benannt (Step 6), damit keine Suite-Regression entsteht.