docs: A11 Plan-Haken gesetzt

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-09-28 19:44:35 +02:00
co-authored by Claude Opus 5.5
parent b28b0e69c0
commit ffd7647c5f
@@ -56,7 +56,7 @@ geschrieben aus RecurringItems, PlannedItems und Tilgungsplänen aller Kredite.
**Interfaces:**
- Produces: `app.models.views.RESERVED_SCENARIO_NAME: str = "System-Ist"` (Task 2 verwendet sie in der View-SQL).
- [ ] **Step 1: Failing Tests schreiben**
- [x] **Step 1: Failing Tests schreiben**
An `finance/tests/test_crud_api.py` anhängen (dort existiert bereits `H = {"Authorization": "Bearer test-key"}`):
@@ -112,12 +112,12 @@ def test_szenario_loeschen_roundtrip_gui(client, db):
assert "Wegwerf-Roundtrip" not in client.get("/szenarien").text
```
- [ ] **Step 2: Tests laufen lassen — müssen fehlschlagen**
- [x] **Step 2: Tests laufen lassen — müssen fehlschlagen**
Run: `cd finance && .venv/bin/python -m pytest -q tests/test_crud_api.py tests/test_gui.py -k "reserviert or loeschen or normale_namen"`
Expected: FAIL (POST liefert 201 statt 422; Button fehlt). `test_szenario_loeschen_roundtrip_gui` und `test_szenario_normale_namen_weiter_erlaubt` dürfen schon grün sein.
- [ ] **Step 3: Konstante in `views.py`**
- [x] **Step 3: Konstante in `views.py`**
In `finance/app/models/views.py` direkt nach den Imports:
@@ -128,7 +128,7 @@ In `finance/app/models/views.py` direkt nach den Imports:
RESERVED_SCENARIO_NAME = "System-Ist"
```
- [ ] **Step 4: Namensprüfung in `routers/scenarios.py`**
- [x] **Step 4: Namensprüfung in `routers/scenarios.py`**
Import ergänzen: `from app.models.views import RESERVED_SCENARIO_NAME`
@@ -147,7 +147,7 @@ In `create_scenario` als erste Zeile: `_check_reserved_name(data.name)`.
In `patch_scenario` direkt nach `fields = data.model_dump(exclude_unset=True)`:
`_check_reserved_name(fields.get("name"))`.
- [ ] **Step 5: Löschen-Button in `szenarien.html`**
- [x] **Step 5: Löschen-Button in `szenarien.html`**
Im `<p id="sc-row-{{ sc.id }}">` direkt nach dem „Bearbeiten"-Button einfügen. Bewusst ein `<button>` mit htmx-Attributen, **kein** `<form>` — ein `<form>` innerhalb von `<p>` ist ungültiges HTML (der Parser schließt das `<p>` davor, der Button bliebe dann beim Bearbeiten sichtbar):
@@ -157,14 +157,14 @@ Im `<p id="sc-row-{{ sc.id }}">` direkt nach dem „Bearbeiten"-Button einfügen
hx-on::after-request="if(event.detail.successful){window.location.reload()}">Löschen</button>
```
- [ ] **Step 6: Tests grün + Gesamtsuite**
- [x] **Step 6: Tests grün + Gesamtsuite**
Run: `cd finance && .venv/bin/python -m pytest -q`
Expected: alle grün, 216 + 8 = 224 passed.
- [ ] **Step 7: Fable-Gate** — Faktencheck: Button im gerenderten HTML korrekt verschachtelt (nicht in `<form>`/außerhalb `<p>`), API-422 für alle Schreibvarianten, bestehende Löschlogik unverändert (Abräumung abhängiger Zeilen), Name-Konstante nur an einer Stelle definiert.
- [x] **Step 7: Fable-Gate** — Faktencheck: Button im gerenderten HTML korrekt verschachtelt (nicht in `<form>`/außerhalb `<p>`), API-422 für alle Schreibvarianten, bestehende Löschlogik unverändert (Abräumung abhängiger Zeilen), Name-Konstante nur an einer Stelle definiert.
- [ ] **Step 8: Commit**
- [x] **Step 8: Commit**
```bash
git add finance/app/models/views.py finance/app/routers/scenarios.py \
@@ -192,7 +192,7 @@ git commit -m "feat: Szenario per GUI löschen, Name System-Ist reserviert (A11-
- `app.services.projection_service.refresh_stale_projections(session: Session, today: date) -> int`.
- `app.routers.gui._safe_refresh(session: Session, refresh: Callable[[Session, date], object], today: date) -> None` (Task 3 nutzt es).
- [ ] **Step 1: Failing Service-Tests** — `finance/tests/test_projection_refresh.py`:
- [x] **Step 1: Failing Service-Tests** — `finance/tests/test_projection_refresh.py`:
```python
from datetime import date, timedelta
@@ -263,7 +263,7 @@ def test_refresh_ist_idempotent(db):
assert _count(db, sc.id) == get_settings().horizon_days
```
- [ ] **Step 2: Failing GUI-Tests + View-Strukturtest** — an `finance/tests/test_gui.py` anhängen:
- [x] **Step 2: Failing GUI-Tests + View-Strukturtest** — an `finance/tests/test_gui.py` anhängen:
```python
def test_szenarien_seite_rechnet_veraltete_projektion_neu(client, db):
@@ -300,7 +300,7 @@ def test_v_projection_zeigt_ist_serie_und_keine_alten_punkte():
assert names.index("v_balance_total") < names.index("v_projection")
```
- [ ] **Step 3: Postgres-View-Test anlegen** — `finance/tests/test_views_pg.py` (läuft nur, wenn `FB_TEST_PG_URL` auf eine Wegwerf-DB mit Namen `*_test` zeigt; sonst skip):
- [x] **Step 3: Postgres-View-Test anlegen** — `finance/tests/test_views_pg.py` (läuft nur, wenn `FB_TEST_PG_URL` auf eine Wegwerf-DB mit Namen `*_test` zeigt; sonst skip):
```python
"""Postgres-only-Prüfung der Grafana-Views (Ausbaustufe 11).
@@ -387,12 +387,12 @@ def test_v_projection_ist_serie_und_szenario_ab_heute(pg):
assert not any(r.scenario == "Ohne Punkte" for r in rows)
```
- [ ] **Step 4: Tests laufen lassen — müssen fehlschlagen**
- [x] **Step 4: Tests laufen lassen — müssen fehlschlagen**
Run: `cd finance && .venv/bin/python -m pytest -q tests/test_projection_refresh.py tests/test_gui.py tests/test_views_pg.py`
Expected: FAIL (ImportError `refresh_stale_projections`; View-Test: `System-Ist` fehlt); `test_views_pg.py` skipped.
- [ ] **Step 5: `advisory_xact_lock` in `finance/app/db.py`**
- [x] **Step 5: `advisory_xact_lock` in `finance/app/db.py`**
Import `from sqlalchemy import create_engine, text` und anhängen:
@@ -405,7 +405,7 @@ def advisory_xact_lock(session: Session, key: int) -> None:
session.execute(text("SELECT pg_advisory_xact_lock(:k)"), {"k": key})
```
- [ ] **Step 6: `refresh_stale_projections` in `projection_service.py`**
- [x] **Step 6: `refresh_stale_projections` in `projection_service.py`**
Imports ergänzen: `from sqlalchemy import delete, func, select` und `from app.db import advisory_xact_lock`. Anhängen:
@@ -439,7 +439,7 @@ def refresh_stale_projections(session: Session, today: date) -> int:
return refreshed
```
- [ ] **Step 7: `v_projection` in `views.py` ersetzen**
- [x] **Step 7: `v_projection` in `views.py` ersetzen**
```python
# Szenario-Vergleich (Ausbaustufe 11): Ist-Gesamtsaldo bis heute als eigene
@@ -465,7 +465,7 @@ def refresh_stale_projections(session: Session, today: date) -> int:
Hinweis: die Spalte `scenario` wechselt von `varchar(200)` auf `text` → `CREATE OR REPLACE` schlägt fehl, `create_views` fällt automatisch auf DROP+CREATE zurück (vorhandenes Verhalten, kein Code nötig).
- [ ] **Step 8: GUI-Hooks in `routers/gui.py`**
- [x] **Step 8: GUI-Hooks in `routers/gui.py`**
Imports: `import logging`, `from typing import Callable`, `from app.services.projection_service import refresh_stale_projections`. Nach `router = APIRouter()`:
@@ -494,14 +494,14 @@ In `index` als erste Zeile und in `szenarien_page` als erste Zeile:
(Der Name `refresh_stale_projections` wird zur Laufzeit im Modul nachgeschlagen — deshalb greift `monkeypatch.setattr("app.routers.gui.refresh_stale_projections", ...)`.)
- [ ] **Step 9: Tests grün + Gesamtsuite**
- [x] **Step 9: Tests grün + Gesamtsuite**
Run: `cd finance && .venv/bin/python -m pytest -q`
Expected: 224 + 8 = 232 passed, 1 skipped (`test_views_pg.py`).
- [ ] **Step 10: Fable-Gate** — (a) Wegwerf-Postgres starten (Kommando im Docstring von `test_views_pg.py`), `FB_TEST_PG_URL=... pytest tests/test_views_pg.py` → passed; Container danach entfernen. (b) Race-Probe auf derselben Wegwerf-DB: zwei Threads rufen `refresh_stale_projections` gleichzeitig für ein veraltetes Szenario → danach genau `horizon_days` Punkte (keine Duplikate). (c) Code-Review Lock-Semantik (Lock endet mit commit in `run_projection`).
- [x] **Step 10: Fable-Gate** — (a) Wegwerf-Postgres starten (Kommando im Docstring von `test_views_pg.py`), `FB_TEST_PG_URL=... pytest tests/test_views_pg.py` → passed; Container danach entfernen. (b) Race-Probe auf derselben Wegwerf-DB: zwei Threads rufen `refresh_stale_projections` gleichzeitig für ein veraltetes Szenario → danach genau `horizon_days` Punkte (keine Duplikate). (c) Code-Review Lock-Semantik (Lock endet mit commit in `run_projection`).
- [ ] **Step 11: Commit**
- [x] **Step 11: Commit**
```bash
git add finance/app/db.py finance/app/models/views.py finance/app/services/projection_service.py \
@@ -530,7 +530,7 @@ git commit -m "fix: Szenario-Vergleich zeigt vor heute Ist-Saldo, veraltete Szen
- `app.services.planning_chart.planning_monthly_totals(session, today: date, horizon_days: int) -> dict[tuple[date, str], Decimal]`
- `app.services.planning_chart.refresh_planning_chart(session, today: date) -> int` (Anzahl geschriebener Zeilen).
- [ ] **Step 1: Failing Tests** — `finance/tests/test_planning_chart.py` (Handrechnung; `today=28.09.2026`, `horizon_days=100` → Fenster 01.09.2026 … 06.01.2027):
- [x] **Step 1: Failing Tests** — `finance/tests/test_planning_chart.py` (Handrechnung; `today=28.09.2026`, `horizon_days=100` → Fenster 01.09.2026 … 06.01.2027):
```python
from datetime import date
@@ -687,12 +687,12 @@ def test_planning_chart_refresh_auf_postgres(pg):
assert pg.query(PlanningMonthly).count() == 1
```
- [ ] **Step 2: Tests laufen lassen — müssen fehlschlagen**
- [x] **Step 2: Tests laufen lassen — müssen fehlschlagen**
Run: `cd finance && .venv/bin/python -m pytest -q tests/test_planning_chart.py tests/test_gui.py tests/test_grafana_dashboard.py`
Expected: FAIL (ImportError `PlanningMonthly`/`planning_chart`, Panel fehlt).
- [ ] **Step 3: Modell** — an `finance/app/models/tables.py` anhängen:
- [x] **Step 3: Modell** — an `finance/app/models/tables.py` anhängen:
```python
class PlanningMonthly(Base):
@@ -706,7 +706,7 @@ class PlanningMonthly(Base):
total: Mapped[Decimal] = mapped_column(MONEY)
```
- [ ] **Step 4: Migration** — `finance/alembic/versions/d5e8b3c7a410_planning_monthly.py`:
- [x] **Step 4: Migration** — `finance/alembic/versions/d5e8b3c7a410_planning_monthly.py`:
```python
"""planning_monthly (Grafana-Panel Planung nach Kategorie)
@@ -741,7 +741,7 @@ def downgrade() -> None:
op.drop_table('planning_monthly')
```
- [ ] **Step 5: Service** — `finance/app/services/planning_chart.py`:
- [x] **Step 5: Service** — `finance/app/services/planning_chart.py`:
```python
"""Planungs-Diagramm (Grafana-Panel „Planung nach Kategorie", Ausbaustufe 11).
@@ -812,13 +812,13 @@ def refresh_planning_chart(session: Session, today: date) -> int:
return len(totals)
```
- [ ] **Step 6: GUI-Hooks** — in `routers/gui.py` Import `from app.services.planning_chart import refresh_planning_chart`. In `index` (nach dem Projektions-Refresh aus Task 2) und als erste Zeile in `planung_page`:
- [x] **Step 6: GUI-Hooks** — in `routers/gui.py` Import `from app.services.planning_chart import refresh_planning_chart`. In `index` (nach dem Projektions-Refresh aus Task 2) und als erste Zeile in `planung_page`:
```python
_safe_refresh(session, refresh_planning_chart, date.today())
```
- [ ] **Step 7: Grafana-Panel** — in `finance/grafana/dashboards/finanzen.json` nach Panel 4 als fünftes Element von `panels` einfügen (2 Leerzeichen Einrückung wie bestehend):
- [x] **Step 7: Grafana-Panel** — in `finance/grafana/dashboards/finanzen.json` nach Panel 4 als fünftes Element von `panels` einfügen (2 Leerzeichen Einrückung wie bestehend):
```json
{
@@ -847,14 +847,14 @@ def refresh_planning_chart(session: Session, today: date) -> int:
Danach `python3 -m json.tool finance/grafana/dashboards/finanzen.json >/dev/null` (Syntax-Check).
- [ ] **Step 8: Tests grün + Gesamtsuite**
- [x] **Step 8: Tests grün + Gesamtsuite**
Run: `cd finance && .venv/bin/python -m pytest -q`
Expected: 232 + 8 = 240 passed, 2 skipped (`test_views_pg.py`).
- [ ] **Step 9: Fable-Gate** — (a) Wegwerf-Postgres: `alembic upgrade head` → `alembic downgrade -1` → `alembic upgrade head` gegen `FB_DATABASE_URL` der Wegwerf-DB; Tabelle/PK per `\d planning_monthly` prüfen. (b) `FB_TEST_PG_URL=... pytest tests/test_views_pg.py` → 2 passed. (c) Unabhängige Handrechnung eines zweiten Szenarios (z.B. endfälliger Kredit `bullet` mit Zins) gegen `planning_monthly_totals`. (d) Datenschutz-Grep: keine Echtdaten in neuen Tests.
- [x] **Step 9: Fable-Gate** — (a) Wegwerf-Postgres: `alembic upgrade head` → `alembic downgrade -1` → `alembic upgrade head` gegen `FB_DATABASE_URL` der Wegwerf-DB; Tabelle/PK per `\d planning_monthly` prüfen. (b) `FB_TEST_PG_URL=... pytest tests/test_views_pg.py` → 2 passed. (c) Unabhängige Handrechnung eines zweiten Szenarios (z.B. endfälliger Kredit `bullet` mit Zins) gegen `planning_monthly_totals`. (d) Datenschutz-Grep: keine Echtdaten in neuen Tests.
- [ ] **Step 10: Commit**
- [x] **Step 10: Commit**
```bash
git add finance/app/models/tables.py finance/alembic/versions/d5e8b3c7a410_planning_monthly.py \
@@ -882,7 +882,7 @@ git commit -m "feat: Grafana-Panel Planung nach Kategorie inkl. Kreditraten (A11
- Produces (JS, `window.FBSort` bzw. `module.exports`): `sortKey(raw: string, fromDataSort: bool) -> {kind:'date'|'number'|'text', value, text} | null`, `columnKind(keys) -> 'date'|'number'|'text'`, `sortItems(items: [{key, index, ...}], dir: 'asc'|'desc') -> items`, `groupRows(rows, isFollow) -> rows[][]`, `sortTable(table, colIndex, dir)`, `initTable(table)`, `init(root)`.
- Template-Konventionen (Task 5 nutzt `data-sort-mode="server"`): `tr[data-sort-follow]` hängt an der Vorzeile; `td[data-sort]` überschreibt den Sortierwert (Zahlen Punkt-dezimal, Datum ISO); `th[data-nosort]` bekommt keinen Button; `table[data-sort-mode="server"]` wird übersprungen.
- [ ] **Step 1: Failing JS-Unit-Tests** — `finance/tests/js/sort.test.js`:
- [x] **Step 1: Failing JS-Unit-Tests** — `finance/tests/js/sort.test.js`:
```js
'use strict';
@@ -959,7 +959,7 @@ test('groupRows: Folgezeilen hängen an der Vorzeile', () => {
});
```
- [ ] **Step 2: DOM-Test (jsdom, optional)** — `finance/tests/js/sort.dom.test.js`:
- [x] **Step 2: DOM-Test (jsdom, optional)** — `finance/tests/js/sort.dom.test.js`:
```js
'use strict';
@@ -1024,7 +1024,7 @@ test('init zweimal erzeugt keine doppelten Buttons', { skip: !JSDOM }, () => {
});
```
- [ ] **Step 3: pytest-Wrapper** — `finance/tests/test_sort_js.py`:
- [x] **Step 3: pytest-Wrapper** — `finance/tests/test_sort_js.py`:
```python
"""Führt die JS-Tests von static/sort.js aus, falls `node` installiert ist.
@@ -1047,7 +1047,7 @@ def test_sort_js():
assert r.returncode == 0, r.stdout + r.stderr
```
- [ ] **Step 4: Failing Template-Tests** — `finance/tests/test_sort_templates.py`:
- [x] **Step 4: Failing Template-Tests** — `finance/tests/test_sort_templates.py`:
```python
import re
@@ -1124,12 +1124,12 @@ def test_buchungen_tabelle_ist_serverseitig(client):
assert 'data-sort-mode="server"' in client.get("/buchungen").text
```
- [ ] **Step 5: Tests laufen lassen — müssen fehlschlagen**
- [x] **Step 5: Tests laufen lassen — müssen fehlschlagen**
Run: `cd finance && .venv/bin/python -m pytest -q tests/test_sort_templates.py tests/test_sort_js.py`
Expected: FAIL in `test_sort_templates.py`; `test_sort_js.py` skipped (kein node).
- [ ] **Step 6: `finance/app/static/sort.js` implementieren**
- [x] **Step 6: `finance/app/static/sort.js` implementieren**
```js
/* Tabellensortierung per Spaltenkopf (Ausbaustufe 11).
@@ -1268,7 +1268,7 @@ Expected: FAIL in `test_sort_templates.py`; `test_sort_js.py` skipped (kein node
})();
```
- [ ] **Step 7: Einbinden + CSS**
- [x] **Step 7: Einbinden + CSS**
`base.html`: direkt nach `<script src="/static/htmx.min.js"></script>`:
```html
@@ -1295,7 +1295,7 @@ Expected: FAIL in `test_sort_templates.py`; `test_sort_js.py` skipped (kein node
}
```
- [ ] **Step 8: Template-Attribute setzen** (jede Stelle einzeln):
- [x] **Step 8: Template-Attribute setzen** (jede Stelle einzeln):
`planning.html`:
- `<tr id="rec-edit-{{ r.id }}" hidden>` → `<tr id="rec-edit-{{ r.id }}" hidden data-sort-follow>`
@@ -1319,19 +1319,19 @@ Expected: FAIL in `test_sort_templates.py`; `test_sort_js.py` skipped (kein node
Tilgungsplan (per JS erzeugt) bleibt bewusst unsortiert (kein htmx-Swap → kein `onLoad`).
- [ ] **Step 9: Tests grün + Gesamtsuite**
- [x] **Step 9: Tests grün + Gesamtsuite**
Run: `cd finance && .venv/bin/python -m pytest -q`
Expected: 240 + 6 = 246 passed, 3 skipped.
- [ ] **Step 10: Fable-Gate** — JS-Tests per Podman (Host hat kein node):
- [x] **Step 10: Fable-Gate** — JS-Tests per Podman (Host hat kein node):
```bash
cd finance && podman run --rm -v "$PWD":/w:ro -w /w docker.io/library/node:22-alpine \
sh -c 'npm i --silent --no-save --prefix /tmp/j jsdom >/dev/null 2>&1; NODE_PATH=/tmp/j/node_modules node --test tests/js/'
```
Erwartet: alle Unit- und DOM-Tests grün (DOM-Tests NICHT geskippt). Zusätzlich jede Tabelle der GUI (lokaler uvicorn gegen Test-/SQLite-DB oder gerenderter HTML-Dump) auf korrekte `data-sort`/`data-nosort`/`data-sort-follow` durchsehen; Salden-Gesamtzeile im `tfoot`.
- [ ] **Step 11: Commit**
- [x] **Step 11: Commit**
```bash
git add finance/app/static/sort.js finance/app/static/style.css finance/app/templates/ \
@@ -1353,7 +1353,7 @@ git commit -m "feat: Tabellen per Spaltenkopf sortierbar (A11-T4)"
- Consumes: `data-sort-mode="server"` an der Buchungstabelle (Task 4); CSS `.sort-btn` (Task 4).
- Produces: `app.routers.transactions.normalize_sort(sort: str | None, dir: str | None) -> tuple[str, str]`; `list_transactions(..., sort: str = "datum", dir: str = "asc", ...)`. Erlaubte Schlüssel: `datum, konto, zweck, empfaenger, betrag, kategorie`.
- [ ] **Step 1: Failing Tests** — `finance/tests/test_transactions_sort.py`:
- [x] **Step 1: Failing Tests** — `finance/tests/test_transactions_sort.py`:
```python
from datetime import date, timedelta
@@ -1449,12 +1449,12 @@ def test_links_erhalten_filter_und_sortierung(client, many):
Hinweis zur Link-Reihenfolge: die Parameter-Reihenfolge ergibt sich aus dem `filter_params`-Dict (account_id, date_from, date_to, category_id, q, status); leere Werte entfallen — wie beim bestehenden Pager.
- [ ] **Step 2: Tests laufen lassen — müssen fehlschlagen**
- [x] **Step 2: Tests laufen lassen — müssen fehlschlagen**
Run: `cd finance && .venv/bin/python -m pytest -q tests/test_transactions_sort.py`
Expected: FAIL (Sortierung wird ignoriert, Links fehlen). `test_standard_bleibt_datum_aufsteigend` darf grün sein.
- [ ] **Step 3: Sortierlogik in `routers/transactions.py`** (vor `list_transactions` einfügen):
- [x] **Step 3: Sortierlogik in `routers/transactions.py`** (vor `list_transactions` einfügen):
```python
# Serverseitige Sortierung der Buchungsliste (Ausbaustufe 11): Whitelist
@@ -1497,9 +1497,9 @@ In `list_transactions` Parameter `sort: str = "datum", dir: str = "asc",` vor `l
(Standard `datum/asc` ergibt `booking_date ASC NULLS LAST, id ASC` = bisherige Reihenfolge.)
- [ ] **Step 4: `gui.buchungen`** — Import `normalize_sort` ergänzen (`from app.routers.transactions import count_transactions, list_transactions, normalize_sort`). Parameter `sort: str = "datum", dir: str = "asc",` nach `page` ergänzen; nach `page = max(1, page)`: `sort, dir = normalize_sort(sort, dir)`; an `list_transactions(...)` zusätzlich `sort=sort, dir=dir,` übergeben; im Template-Kontext `"sort": sort, "dir": dir,` ergänzen.
- [x] **Step 4: `gui.buchungen`** — Import `normalize_sort` ergänzen (`from app.routers.transactions import count_transactions, list_transactions, normalize_sort`). Parameter `sort: str = "datum", dir: str = "asc",` nach `page` ergänzen; nach `page = max(1, page)`: `sort, dir = normalize_sort(sort, dir)`; an `list_transactions(...)` zusätzlich `sort=sort, dir=dir,` übergeben; im Template-Kontext `"sort": sort, "dir": dir,` ergänzen.
- [ ] **Step 5: Template `transactions.html`**
- [x] **Step 5: Template `transactions.html`**
Vor `<table data-sort-mode="server">` einfügen (ersetzt das bisherige `pager_params` im Pager):
@@ -1537,14 +1537,14 @@ Pager: den `{% set pager_params = … %}`-Block entfernen; beide Pager-Links auf
(`Weiter` analog mit `page + 1`.)
- [ ] **Step 6: Tests grün + Gesamtsuite**
- [x] **Step 6: Tests grün + Gesamtsuite**
Run: `cd finance && .venv/bin/python -m pytest -q`
Expected: 246 + 12 = 258 passed, 3 skipped. Bestehende Pager-Tests (`test_gui.py` „Weiter", `?page=2`) weiterhin grün.
- [ ] **Step 7: Fable-Gate** — Live-Approximation gegen Wegwerf-Postgres (Collation `en_US.utf8` wie Prod): `FB_DATABASE_URL` auf Wegwerf-DB, `alembic upgrade head`, künstliche Buchungen, `uvicorn` lokal, jede Spalte auf/ab per curl inkl. Seitengrenze und Filter; manipulierte Parameter (`sort=%27;--`, `dir=`) → 200.
- [x] **Step 7: Fable-Gate** — Live-Approximation gegen Wegwerf-Postgres (Collation `en_US.utf8` wie Prod): `FB_DATABASE_URL` auf Wegwerf-DB, `alembic upgrade head`, künstliche Buchungen, `uvicorn` lokal, jede Spalte auf/ab per curl inkl. Seitengrenze und Filter; manipulierte Parameter (`sort=%27;--`, `dir=`) → 200.
- [ ] **Step 8: Commit**
- [x] **Step 8: Commit**
```bash
git add finance/app/routers/transactions.py finance/app/routers/gui.py \
@@ -1566,22 +1566,22 @@ git commit -m "feat: Buchungen serverseitig nach Spalte sortierbar (A11-T5)"
**Interfaces:**
- Consumes: alles aus Task 1–5.
- [ ] **Step 1: `docs/ARCHITEKTUR.md`** — Komponenten-Diagramm: `S_projection` um `refresh_stale_projections` ergänzen; neuen Knoten `S_planning_chart["planning_chart.py<br/>refresh_planning_chart"]` mit Kanten von `gui.py` und zur DB; `M_views` unverändert benannt, im Abschnitt 4 (Zentrale Konzepte) kurz: `v_projection` = System-Ist + Szenarien ab heute, reservierter Name; Tabelle `planning_monthly` (abgeleitet, bei GUI-Aufruf neu geschrieben, Advisory-Locks 110001/110002); `static/sort.js` + serverseitige Buchungs-Sortierung. Jeder im Diagramm genannte Name muss real im Code existieren (CLAUDE.md-Regel).
- [x] **Step 1: `docs/ARCHITEKTUR.md`** — Komponenten-Diagramm: `S_projection` um `refresh_stale_projections` ergänzen; neuen Knoten `S_planning_chart["planning_chart.py<br/>refresh_planning_chart"]` mit Kanten von `gui.py` und zur DB; `M_views` unverändert benannt, im Abschnitt 4 (Zentrale Konzepte) kurz: `v_projection` = System-Ist + Szenarien ab heute, reservierter Name; Tabelle `planning_monthly` (abgeleitet, bei GUI-Aufruf neu geschrieben, Advisory-Locks 110001/110002); `static/sort.js` + serverseitige Buchungs-Sortierung. Jeder im Diagramm genannte Name muss real im Code existieren (CLAUDE.md-Regel).
- [ ] **Step 2: `hilfe.html`** — im Konzepte-Abschnitt: Szenarien-Absatz um „Löschen" und automatisches Neurechnen veralteter Szenarien ergänzen; Grafana-Absatz um „System-Ist"-Kurve und Panel „Planung nach Kategorie"; ein Satz zur Sortierung per Spaltenkopf (⇅/▲/▼; Buchungen über alle Seiten).
- [x] **Step 2: `hilfe.html`** — im Konzepte-Abschnitt: Szenarien-Absatz um „Löschen" und automatisches Neurechnen veralteter Szenarien ergänzen; Grafana-Absatz um „System-Ist"-Kurve und Panel „Planung nach Kategorie"; ein Satz zur Sortierung per Spaltenkopf (⇅/▲/▼; Buchungen über alle Seiten).
- [ ] **Step 3: Version** — `finance/VERSION`: `0.10.0` → `0.11.0`. Suite: `cd finance && .venv/bin/python -m pytest -q` → 258 passed, 3 skipped (bzw. aktueller Stand, alle grün).
- [x] **Step 3: Version** — `finance/VERSION`: `0.10.0` → `0.11.0`. Suite: `cd finance && .venv/bin/python -m pytest -q` → 258 passed, 3 skipped (bzw. aktueller Stand, alle grün).
- [ ] **Step 4: Commit Release**
- [x] **Step 4: Commit Release**
```bash
git add docs/ARCHITEKTUR.md finance/app/templates/hilfe.html finance/VERSION
git commit -m "chore: Release v0.11.0 (Szenario-Löschen, Grafana-Korrekturen, Tabellensortierung)"
```
- [ ] **Step 5: Backup + Redeploy** — `./backup_finance_pod.sh` (vor Migration), dann `./create_pod_finance.sh`. Prüfen: `curl -s 127.0.0.1:8096/api/version` → `0.11.0`; `alembic current` im API-Container → `d5e8b3c7a410`.
- [x] **Step 5: Backup + Redeploy** — `./backup_finance_pod.sh` (vor Migration), dann `./create_pod_finance.sh`. Prüfen: `curl -s 127.0.0.1:8096/api/version` → `0.11.0`; `alembic current` im API-Container → `d5e8b3c7a410`.
- [ ] **Step 6: Live-Smoke** (Details nur Chat/Scratchpad, DATENSCHUTZ):
- [x] **Step 6: Live-Smoke** (Details nur Chat/Scratchpad, DATENSCHUTZ):
- Seiten `/`, `/buchungen`, `/salden`, `/import`, `/planung`, `/szenarien`, `/admin`, `/hilfe` → 200.
- Nach Aufruf von `/`: alle bereits gerechneten Live-Szenarien haben ersten Projektionspunkt = morgen (SQL `min(day)` je Szenario, ohne Beträge ausgeben).
- `v_projection`: Serie `System-Ist` endet heute; keine Szenario-Zeile < heute.
@@ -1592,4 +1592,4 @@ git commit -m "chore: Release v0.11.0 (Szenario-Löschen, Grafana-Korrekturen, T
- `/buchungen?sort=betrag&dir=desc` → 200, erste Zeile = betragsgrößte Buchung (nur prüfen, nicht zitieren).
- Bestandsdaten unverändert: Anzahl confirmed Buchungen, Konten, Kategorien, Szenarien wie vor dem Deploy.
- [ ] **Step 7: Fable-Release-Gate** (Whole-Branch `dd6110b..HEAD`) + Ledger-Eintrag „AUSBAUSTUFE 11 ABGESCHLOSSEN" in `.superpowers/sdd/progress.md`; Plan-Haken setzen; commit `docs: A11 Ledger + Plan-Haken`.
- [x] **Step 7: Fable-Release-Gate** (Whole-Branch `dd6110b..HEAD`) + Ledger-Eintrag „AUSBAUSTUFE 11 ABGESCHLOSSEN" in `.superpowers/sdd/progress.md`; Plan-Haken setzen; commit `docs: A11 Ledger + Plan-Haken`.