From acabc3d2a4a13698b169a7fa4d47700b5ec77188ece2b4b51fa59b9f05bdec30 Mon Sep 17 00:00:00 2001 From: wlfb Date: Tue, 29 Sep 2026 14:41:21 +0200 Subject: [PATCH] =?UTF-8?q?docs:=20A13=20=E2=80=93=20h=C3=B6chstens=208=20?= =?UTF-8?q?benannte=20Tortensegmente=20(Nutzerentscheidung)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 (1M context) --- .../plans/2026-09-29-ausbaustufe-13.md | 42 ++++++++++++------- .../specs/2026-09-29-ausbaustufe-13-design.md | 4 +- 2 files changed, 30 insertions(+), 16 deletions(-) diff --git a/docs/superpowers/plans/2026-09-29-ausbaustufe-13.md b/docs/superpowers/plans/2026-09-29-ausbaustufe-13.md index ae8badf..b5ace78 100644 --- a/docs/superpowers/plans/2026-09-29-ausbaustufe-13.md +++ b/docs/superpowers/plans/2026-09-29-ausbaustufe-13.md @@ -16,9 +16,9 @@ - **Geld = `decimal.Decimal`** überall. Einzige Ausnahme: Winkel/Koordinaten in `engine/pie.py` (reine Geometrie) dürfen `float` sein; `Slice.value`/`Slice.share` bleiben `Decimal`. - **Deutsch** für GUI-Texte, Meldungen, Kommentare, Commit-Messages; Monatsanzeige `MM.JJJJ`. - **Engine bleibt rein:** `engine/breakdown.py` und `engine/pie.py` importieren nur Standardbibliothek und `app.engine.*` — kein `app.models`, `app.services`, `app.reserved`. -- **Namen/Konstanten (exakt):** `PLAN_NAME = f"{SYSTEM_PREFIX}Plan"` (= „System-Plan", in `services/breakdown.py`); `FREI = "Frei verfügbar"` (`engine/breakdown.py`); `UEBRIGE = "Übrige"`, `MIN_SHARE = Decimal("0.03")`, `MAX_NAMED = 6` (`engine/pie.py`); Kategorie Kredite = `services.planning_chart.KREDITRATEN` („System-Kreditraten"), ohne Kategorie = `services.planning_chart.UNKATEGORISIERT` („unkategorisiert"). +- **Namen/Konstanten (exakt):** `PLAN_NAME = f"{SYSTEM_PREFIX}Plan"` (= „System-Plan", in `services/breakdown.py`); `FREI = "Frei verfügbar"` (`engine/breakdown.py`); `UEBRIGE = "Übrige"`, `MIN_SHARE = Decimal("0.03")`, `MAX_NAMED = 8` (`engine/pie.py`); Kategorie Kredite = `services.planning_chart.KREDITRATEN` („System-Kreditraten"), ohne Kategorie = `services.planning_chart.UNKATEGORISIERT` („unkategorisiert"). - **Rundung:** Monatsbeträge je Zeile `quantize(Decimal("0.01"), ROUND_HALF_UP)`; Prozente `quantize(Decimal("0.1"), ROUND_HALF_UP)`; Summen aus gerundeten Zeilen. -- **Tortenpalette** (dataviz-Referenzpalette, Slots 1–6, Validator auf `#ffffff`: adjacent CVD ΔE ≥ 9.1, normal ≥ 19.6 — PASS; Kontrast-WARN wird durch Legende + Tabelle entlastet): `--seg-1:#2a78d6; --seg-2:#eb6834; --seg-3:#1baf7a; --seg-4:#eda100; --seg-5:#e87ba4; --seg-6:#008300; --seg-uebrige:#8a8984; --seg-frei:#d6d5cf`. Höchstens **6 benannte farbige Segmente** (dataviz: Torte ≤ 6 Segmente), danach „Übrige" — Ergänzung zur Spec-3-%-Regel. +- **Tortenpalette** (dataviz-Referenzpalette, alle 8 Slots, Validator auf `#ffffff`: adjacent CVD ΔE ≥ 9.1, normal ≥ 19.6 — PASS; kreisförmige Nachbarschaften ebenfalls geprüft: s8↔s1 ΔE 21.6/32.3, s7/s8↔Übrige ≥ 8.8/18.9, s8↔Frei 25.1/31.3, Übrige↔Frei 24.3 — PASS; Kontrast-WARN für s3/s4/s5 wird durch Legende + Tabelle entlastet): `--seg-1:#2a78d6; --seg-2:#eb6834; --seg-3:#1baf7a; --seg-4:#eda100; --seg-5:#e87ba4; --seg-6:#008300; --seg-7:#4a3aa7; --seg-8:#e34948; --seg-uebrige:#8a8984; --seg-frei:#d6d5cf`. **Nutzerentscheidung (2026-09-29): höchstens 8 benannte farbige Segmente + „Übrige" + „Frei verfügbar" = max. 10 Segmente** — Ergänzung zur Spec-3-%-Regel. - **Kein Dark-Mode:** Die App hat keinen (`style.css` ohne `prefers-color-scheme`); Palette nur für den weißen Hintergrund. - **UX-Regel:** Formular immer sichtbar; alle Fehler inline, Status 200, dasselbe Template. - **Keine Schemaänderung**, keine Migration, keine View, Grafana unverändert. @@ -566,8 +566,8 @@ git commit -m "feat: Engine breakdown – Monatsbeträge je Posten, Summen, Kate **Interfaces:** - Produces (alle in `app.engine.pie`): - - `UEBRIGE = "Übrige"`, `MIN_SHARE = Decimal("0.03")`, `MAX_NAMED = 6` - - `Slice` (frozen dataclass: `label: str`, `value: Decimal`, `share: Decimal` (Prozent, 1 NK), `path: str | None` (SVG-`d` im Einheitskreis, Mittelpunkt 0,0, Start 12 Uhr, im Uhrzeigersinn; `None` = Vollkreis), `color: str` ∈ {`"s1"`…`"s6"`, `"uebrige"`, `"frei"`}, `merged: bool`, `members: tuple[str, ...]`, `start: float`, `end: float` (Anteile 0…1)) + - `UEBRIGE = "Übrige"`, `MIN_SHARE = Decimal("0.03")`, `MAX_NAMED = 8` + - `Slice` (frozen dataclass: `label: str`, `value: Decimal`, `share: Decimal` (Prozent, 1 NK), `path: str | None` (SVG-`d` im Einheitskreis, Mittelpunkt 0,0, Start 12 Uhr, im Uhrzeigersinn; `None` = Vollkreis), `color: str` ∈ {`"s1"`…`"s8"`, `"uebrige"`, `"frei"`}, `merged: bool`, `members: tuple[str, ...]`, `start: float`, `end: float` (Anteile 0…1)) - `pie_slices(values: list[tuple[str, Decimal]], rest: Decimal | None = None, rest_label: str = "", min_share: Decimal = MIN_SHARE, max_named: int = MAX_NAMED) -> list[Slice]` Regeln: Werte ≤ 0 entfallen; Basis = Summe der Werte + `rest` (falls > 0); sortiert absteigend nach Wert, bei Gleichstand nach Label; benannt werden höchstens `max_named` Werte mit Anteil ≥ `min_share` (Farben `s1`…); die übrigen: genau einer ⇒ bleibt mit eigenem Label, Farbe `"uebrige"`, `merged=False`; zwei oder mehr ⇒ ein Segment `UEBRIGE` (Farbe `"uebrige"`, `merged=True`, `members` in Größenreihenfolge); `rest` (> 0) wird nie zusammengefasst, kommt als letztes Segment mit Label `rest_label` und Farbe `"frei"`; genau ein Segment ⇒ `path=None`. @@ -623,10 +623,18 @@ def test_einzelner_kleiner_wert_behaelt_namen(): assert [(s.label, s.color, s.merged) for s in sl] == [("A", "s1", False), ("B", "uebrige", False)] -def test_hoechstens_sechs_benannte_segmente(): +def test_hoechstens_acht_benannte_segmente(): + sl = pie_slices([(f"K{i}", D("10")) for i in range(10)], rest=D("5"), + rest_label="Frei verfügbar") + assert [s.color for s in sl] == ["s1", "s2", "s3", "s4", "s5", "s6", "s7", "s8", + "uebrige", "frei"] + assert sl[-2].merged and sl[-2].members == ("K8", "K9") + + +def test_acht_kategorien_ohne_uebrige(): sl = pie_slices([(f"K{i}", D("10")) for i in range(8)]) - assert [s.color for s in sl] == ["s1", "s2", "s3", "s4", "s5", "s6", "uebrige"] - assert sl[-1].merged and sl[-1].members == ("K6", "K7") + assert [s.color for s in sl] == [f"s{i}" for i in range(1, 9)] + assert not any(s.merged for s in sl) def test_rest_wird_nie_zusammengefasst(): @@ -662,7 +670,7 @@ from decimal import ROUND_HALF_UP, Decimal UEBRIGE = "Übrige" MIN_SHARE = Decimal("0.03") -MAX_NAMED = 6 +MAX_NAMED = 8 PCT = Decimal("0.1") @@ -672,7 +680,7 @@ class Slice: value: Decimal share: Decimal # Prozent der Tortenbasis, eine Nachkommastelle path: str | None # SVG-d; None = Vollkreis (einziges Segment) - color: str # "s1".."s6", "uebrige", "frei" + color: str # "s1".."s8", "uebrige", "frei" merged: bool members: tuple[str, ...] start: float # Anteil 0..1 @@ -736,7 +744,7 @@ git add finance/app/engine/pie.py finance/tests/test_pie.py git commit -m "feat: Engine pie – Tortensegmente mit Übrige-Regel und SVG-Pfaden (A13-T3)" ``` -- [ ] **Step 6: Fable-Gate** — Geometrie-Faktencheck: für 3–4 eigene Wertelisten Endpunkte der Pfade nachrechnen (12 Uhr Start, Uhrzeigersinn, `large-arc` ab > 50 %), Anteilssumme ≈ 100 % (Rundung), Übrige-Regel (3 %, max. 6, Einzelner behält Namen), `rest` nie zusammengefasst; ein erzeugtes SVG (Wegwerfdatei im Scratchpad, `viewBox="-1.05 -1.05 2.1 2.1"`) rendern und ansehen (z. B. per `rsvg-convert` oder Browser-Container), keine Lücken/Überlappungen. +- [ ] **Step 6: Fable-Gate** — Geometrie-Faktencheck: für 3–4 eigene Wertelisten Endpunkte der Pfade nachrechnen (12 Uhr Start, Uhrzeigersinn, `large-arc` ab > 50 %), Anteilssumme ≈ 100 % (Rundung), Übrige-Regel (3 %, max. 8 benannte, Einzelner behält Namen, max. 10 Segmente), `rest` nie zusammengefasst; ein erzeugtes SVG (Wegwerfdatei im Scratchpad, `viewBox="-1.05 -1.05 2.1 2.1"`) rendern und ansehen (z. B. per `rsvg-convert` oder Browser-Container), keine Lücken/Überlappungen. --- @@ -1343,10 +1351,11 @@ def aufteilung_page(request: Request, monat: str | None = None, ```css /* Aufteilung (Ausbaustufe 13). Tortenfarben: dataviz-Referenzpalette, - Slots 1-6 (adjacent CVD-validiert auf weißem Grund); Übrige/Frei neutral. */ + Slots 1-8 (Nachbarpaare inkl. Kreisschluss und Grautöne CVD-validiert auf + weißem Grund); Übrige/Frei neutral. Max. 8 benannte + Übrige + Frei. */ .aufteilung { - --seg-1: #2a78d6; --seg-2: #eb6834; --seg-3: #1baf7a; - --seg-4: #eda100; --seg-5: #e87ba4; --seg-6: #008300; + --seg-1: #2a78d6; --seg-2: #eb6834; --seg-3: #1baf7a; --seg-4: #eda100; + --seg-5: #e87ba4; --seg-6: #008300; --seg-7: #4a3aa7; --seg-8: #e34948; --seg-uebrige: #8a8984; --seg-frei: #d6d5cf; } .kennzahlen { display: flex; flex-wrap: wrap; gap: 1.5rem; margin-bottom: 1rem; } @@ -1367,6 +1376,8 @@ def aufteilung_page(request: Request, monat: str | None = None, .seg.s4 { fill: var(--seg-4); } .swatch.s4 { background: var(--seg-4); } .seg.s5 { fill: var(--seg-5); } .swatch.s5 { background: var(--seg-5); } .seg.s6 { fill: var(--seg-6); } .swatch.s6 { background: var(--seg-6); } +.seg.s7 { fill: var(--seg-7); } .swatch.s7 { background: var(--seg-7); } +.seg.s8 { fill: var(--seg-8); } .swatch.s8 { background: var(--seg-8); } .seg.uebrige { fill: var(--seg-uebrige); } .swatch.uebrige { background: var(--seg-uebrige); } .seg.frei { fill: var(--seg-frei); } .swatch.frei { background: var(--seg-frei); box-shadow: inset 0 0 0 1px #aaa; } .legende { list-style: none; padding: 0; margin: 0.75rem 0 0; font-size: 0.9rem; } @@ -1738,7 +1749,8 @@ git commit -m "feat: Aufteilung – Drill-down in Kategorie per Klick auf Torten Einnahmen und Ausgaben. Das Tortendiagramm zeigt die Einkünfte (Kosten je Kategorie plus „Frei verfügbar"); übersteigen die Kosten die Einkünfte, zeigt es die Gesamtkosten. Kleine Kategorien (unter 3 %, bzw. ab der - siebten) sind zu „Übrige" zusammengefasst. Ein Klick auf ein Segment oder + neunten) sind zu „Übrige" zusammengefasst – höchstens 8 benannte + Kategorien plus „Übrige" und „Frei verfügbar". Ein Klick auf ein Segment oder einen Legendeneintrag zeigt darunter die Posten dieser Kategorie. ``` @@ -1746,7 +1758,7 @@ git commit -m "feat: Aufteilung – Drill-down in Kategorie per Klick auf Torten - [ ] **Step 2: `docs/ARCHITEKTUR.md`** — - Komponenten-Diagramm: `T_pages` um `aufteilung.html` ergänzen; `R_gui`-Label um `aufteilung_page, aufteilung_kategorie_fragment`; `S_projection`-Label um `load_scenario_inputs`; neue Knoten `S_breakdown["breakdown.py
compute_breakdown"]` (SERVICES), `E_breakdown["breakdown.py
breakdown_rows, summarize,
category_rows, share"]` und `E_pie["pie.py
pie_slices"]` (ENGINE); Kanten `R_gui --> S_breakdown`, `R_gui --> E_breakdown`, `R_gui --> E_pie`, `S_breakdown --> S_projection`, `S_breakdown --> E_breakdown`, `S_breakdown --> E_loans`, `S_breakdown --> S_doublecount`, `E_breakdown --> E_scenario`, `E_breakdown --> E_recurrence`. - Abschnitt 4 „Szenario-Engine ist rein": Satz „nur `scenario.py` importiert innerhalb der Engine …" korrigieren zu „`scenario.py` und `breakdown.py` importieren innerhalb der Engine (`loans.py`/`recurrence.py`/`scenario.py`)"; `services/projection_service.py::load_scenario_inputs` als gemeinsame Ladefunktion nennen (`None` = System-Plan). - - Neuer Konzeptabsatz **„Aufteilung (Ausbaustufe 13)."**: `/aufteilung` (GET-Formular, Auswahl in der URL), `services/breakdown.py::compute_breakdown` (`PLAN_NAME` „System-Plan" = `load_scenario_inputs(session, None)`), Rechenregeln aus `engine/breakdown.py` (Normalisierung per `recurrence.STEP`, Einmalposten/Kreditraten voll im Fälligkeitsmonat, Auszahlung nie), Torte serverseitig aus `engine/pie.py::pie_slices` (`MIN_SHARE` 3 %, `MAX_NAMED` 6, `UEBRIGE`), Drill-down `GET /aufteilung/kategorie` mit `hx-swap-oob` der Haupttorte und `sammel=1` für die Sammelgruppe; keine Tabelle/View, Grafana unberührt. + - Neuer Konzeptabsatz **„Aufteilung (Ausbaustufe 13)."**: `/aufteilung` (GET-Formular, Auswahl in der URL), `services/breakdown.py::compute_breakdown` (`PLAN_NAME` „System-Plan" = `load_scenario_inputs(session, None)`), Rechenregeln aus `engine/breakdown.py` (Normalisierung per `recurrence.STEP`, Einmalposten/Kreditraten voll im Fälligkeitsmonat, Auszahlung nie), Torte serverseitig aus `engine/pie.py::pie_slices` (`MIN_SHARE` 3 %, `MAX_NAMED` 8, `UEBRIGE`), Drill-down `GET /aufteilung/kategorie` mit `hx-swap-oob` der Haupttorte und `sammel=1` für die Sammelgruppe; keine Tabelle/View, Grafana unberührt. - Nur Namen verwenden, die im Code existieren. - [ ] **Step 3: Version** — `finance/VERSION` → `0.13.0`; `.venv/bin/python -m pytest -q` grün. diff --git a/docs/superpowers/specs/2026-09-29-ausbaustufe-13-design.md b/docs/superpowers/specs/2026-09-29-ausbaustufe-13-design.md index b14f82b..1945aa8 100644 --- a/docs/superpowers/specs/2026-09-29-ausbaustufe-13-design.md +++ b/docs/superpowers/specs/2026-09-29-ausbaustufe-13-design.md @@ -33,7 +33,9 @@ - **„Frei verfügbar" als Tabellenzeile** im `` (bei Unterdeckung „Unterdeckung", rot). Darunter „Gesamte Einkünfte". - **Kategorien < 3 %** der Tortenbasis werden nur in der Torte zu „Übrige" - zusammengefasst. + zusammengefasst. Zusätzlich (Nachtrag 2026-09-29): **höchstens 8 benannte + Kategorien** + „Übrige" + „Frei verfügbar" = max. 10 Segmente + (8-Farben-Referenzpalette, validiert). - **Endfällige Kredite**: Die Schlussrate enthält die volle Tilgung und erscheint im letzten Monat voll (konsequent zu „Einmalposten voll"). - **Drill-down**: Klick auf ein Kostensegment (oder dessen Legendeneintrag)