Ein Wert, ein Ort: Startwerte und Raten read-only, Verteil-Dialoge als einziger Eingabeort
Deploy App / deploy (push) Successful in 1m13s

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-16 21:17:39 +02:00
parent 5ff9bc0709
commit ce987b9978
8 changed files with 602 additions and 201 deletions
+104 -21
View File
@@ -4,10 +4,10 @@
| | |
|---|---|
| **Dokument** | Funktionale und Technische Spezifikation FPT |
| **Version** | 0.39.1 |
| **Version** | 0.40 |
| **Datum** | 2026-07-25 |
| **Status** | Lebendes Dokument |
| **Codestand** | Arbeitsstand nach `f22b0a3` inkl. Übersicht der offenen Punkte (Branch `main`) |
| **Codestand** | Arbeitsstand nach `5ff9bc0` inkl. «ein Wert, ein Ort» (Branch `main`) |
| **Ersetzt** | `FDD_TDD_FPT.docx` (v1v5) im Ordner `Info Dateien` diese sind ab Version 0.1 dieses Dokuments obsolet |
| **Geltungsbereich** | Gesamter Code im Verzeichnis `FPT` |
@@ -17,6 +17,7 @@
| Version | Datum | Autor | Änderung |
|---|---|---|---|
| 0.40 | 2026-08-16 | Claude (Opus 5) | **Ein Wert, ein Ort** (neues Kap. 3.14.3, aus einer Testrunde des Nutzers). Der rote Faden aller acht Punkte: Für dieselbe Zahl gab es an mehreren Stellen ein Eingabefeld, und der Rechenkern entschied still, welches gewinnt. (1) **Startwerte sind in der ersten Lebensphase read-only.** Sie standen dort als Eingabefeld, das `phaseValues[phase1]` schrieb der Rechenkern legt die Phase über die Stammdaten (`{...baseData, ...phaseValues}`), also überschattete jede Eingabe still die Bestandsaufnahme. Neu wird der Stammdatenwert angezeigt, mit Absprung in die Spalte «Start». Ab Phase 2 bleibt der Betrag bei Einkommen und Ausgaben **änderbar** Teilzeit, Beförderung, Jobwechsel sind echte Entscheide dieser Phase; die Bestände sind dort ohnehin schon fortgeschrieben und read-only. (2) **Jährliche Raten entstehen nur noch im Verteil-Dialog**, einmalige Kapitalverwendungen nur noch im Kapital-Dialog. In der Zelle stehen sie weiterhin, aber read-only mit Absprung. Nur im Dialog sieht man, wie viel überhaupt zu verteilen ist und ob die Summe aufgeht. (3) **Der PK-Beitrag wandert in den Verteil-Dialog** in einen **eigenen Block** «Aus dem Bruttolohn (ausserhalb der Quote)». Er ist bewusst nicht Teil der Quote ([4.6.3](#463-pension_fund)), aber der Dialog ist neu der einzige Ort für jährliche Beträge; ohne ihn wäre er unerreichbar und fiele still auf 0. (4) **Deckel und Schalter «grosse Säule 3a» stehen im Verteil-Dialog**, direkt über der Einzahlung, deren Obergrenze sie bestimmen getrennt davon war der Schalter eine Einstellung ohne sichtbare Wirkung. (5) **Sonderamortisation und Sofort-Tilgung verlassen die Übergangszelle.** Sie zehren vom Kapital der FOLGEphase und werden dort entschieden; angezeigt werden sie in der Phasenzelle, wo auch die Zusatzeinlage steht. (6) **Der «Kapital verteilen»-Knopf ist immer sichtbar.** Er hing an `pot.total > 0` wer alles verteilt hätte, käme an seine eigene Zuteilung nie mehr heran, sobald die Elementfelder read-only sind. (7) **Cash in der Bestandsaufnahme.** Es fehlte ganz: Die Summe «Vermögen heute» rechnete es bereits mit, erfassen konnte man es dort nirgends. Kein Plus-Knopf, sondern ein festes Feld Cash ist kein Element, sondern `Scenario.initialCash`. (8) **Die Tour startet direkt nach dem Anlegen des Plans.** Sie hing an `phases.length > 0`, einem Überbleibsel der Spotlight-Tour, die echte DOM-Ziele brauchte; die Attrappe braucht nichts und die Tour gehört genau dorthin, wo man noch nicht weiss, wie der Plan aufgebaut ist. (9) **Das Abzeichen im Phasenkopf öffnet eine Maske mit allen offenen Annahmen** (`PhaseReviewDialog`), analog zum Übergangs-Review. Bis 0.39 fragte es nur «N Annahmen bestätigen?» eine Zustimmung zu etwas Ungesehenem, also genau die Bewegung, die der Mechanismus verhindern soll. (10) **Unbestätigte Zellen sind deutlicher markiert**: getönter Grund, kräftiger linker Balken, Warnzeichen. Der dünne Ring aus 0.39 ging in einer vollen Matrix unter. (11) **Cash-Zeile mit `ValuePair`**: der Realwert steht im Modus «Beide» darunter statt daneben, wie in jeder anderen Zeile. (12) Zwei Folgen daraus: Der Dialog **«Element anlegen» schreibt neu in die Stammdaten** statt in Phase 1 und **braucht keine Lebensphase mehr**; und `needsConfirmation` verlangt keine Bestätigung mehr für Zellen **ohne jede Annahme** eine «Sonstige Schuld» trägt seit (2) nichts mehr in der Phasenzelle. 1 Test ergänzt (329 → 330). |
| 0.39.1 | 2026-08-16 | Claude (Opus 5) | **Fehlerbehebung: Die Bestandsaufnahme schloss sich beim ersten Plus-Knopf.** `InventoryDialog` kannte nur einen Rückkanal nach oben (`onSaved`) und benutzte ihn für zwei verschiedene Ereignisse: «Element angelegt, bitte Plan neu laden» und «Dialog fertig». In `PlanView` hängt an `onSaved` aber das Schliessen -- ein Klick auf «Ausgaben» legte das Element korrekt an und beendete den Dialog sofort, womit sich genau der eine Bildschirm nicht bedienen liess, der alles erfassen soll. Neu trägt der Dialog beide Rückkanäle getrennt: `onChanged` lädt nur nach (der Entwurf im Dialog überlebt das, weil `loadDetail(id, true)` still nachlädt und die `PlanView` montiert bleibt), `onSaved` schliesst. Betraf auch das Löschen einer Position aus dem Dialog heraus. |
| 0.39 | 2026-08-16 | Claude (Opus 5) | **Aus dem Assistenten wird eine Übersicht der offenen Punkte** (Kap. 3.14 neu geschrieben). Der Unterschied ist grundsätzlich: Ein Assistent ist ein **Ablauf** («tu dies, dann das») und trägt nur beim ersten Aufsetzen -- wer einen bestehenden Plan öffnete, bekam Schritte angeboten, die längst erledigt waren, und die Kachel wusste nichts davon. Die Übersicht ist ein **Zustand** («das ist noch offen»); sie wird aus dem Plan abgeleitet und trägt bei jedem Plan, in jeder Reihenfolge, auch beim zwanzigsten Szenario. (1) Der Assistent samt Schritten, gespeichertem Fortschritt (`Scenario.assistantProgress`) und Endpunkt entfällt. Oben rechts steht neu die Übersicht: je Lebensphase und je Übergang, was dort fehlt, jeweils mit Sprung dorthin. Ist nichts offen, meldet sie das ausdrücklich -- **grün und in Worten**, nicht als «0». (2) **Die Bestätigung gilt neu auch für Phasenwerte** (`PhaseData.confirmed`). Eine neue Lebensphase übernimmt die Werte der Vorphase, aber sie gelten als unbestätigt, bis jemand hingeschaut hat: vorbelegen ja, stillschweigend übernehmen nein. Betroffen sind die phasenspezifischen Annahmen -- Renditen, Lohnentwicklung, Teuerung, Hypothekarzins, Wertsteigerung. Damit gilt in der ganzen Matrix derselbe Mechanismus, den die Übergänge seit 0.35 haben. **Bestätigen heisst «ich habe hingeschaut», nicht «festnageln»**: Der Haken steht NEBEN den Werten und kopiert nichts -- die Feld-Vererbung ([3.12.4](#3124-punkt-a-aus-vorphase-übernehmen)) bleibt unberührt, sonst wäre jede bestätigte Phase eingefroren und der ganze Punkt-A-Mechanismus hinfällig. Ein Test sichert genau das. (3) **Die Spar-/Verzehrquote zählt eigens** (`Phase.ratesConfirmed`). Man kann jede Zelle angeschaut und die Verteilung trotzdem nie getroffen haben -- dann bliebe der ganze Überschuss still auf dem Cash-Konto liegen, und die Übersicht meldete «alles erledigt». (4) **Sammel-Bestätigung je Phase:** Bei sechs Elementen und fünf Phasen wären es dreissig Klicks; der Phasenkopf trägt deshalb ein Abzeichen mit der Anzahl offener Annahmen, das alle auf einmal bestätigt. Unbestätigte Zellen tragen dieselbe Attention-Markierung wie offene Übergänge. (5) **Die Bestandsaufnahme bleibt als eigener Dialog** (`InventoryDialog`, vormals `AssistantSteps`) -- als grosser Knopf im leeren Plan und dauerhaft unter den Schnellaktionen. Sie ist der einzige Sammel-Dialog, der geblieben ist, weil sie als einzige etwas leistet, das die Matrix nicht kann: sieben Kategorien in einem Zug erfassen, bevor man weiss, wie das Tool aufgebaut ist. (6) Zwei **Startzustände** statt einer Zahl: Ohne Elemente und ohne Lebensphasen gibt es naturgemäss nichts Offenes, obwohl der Plan leer ist -- eine «0» wäre dort eine Lüge. Die Kachel fordert stattdessen zum nächsten Handgriff auf. Neues Modul `review.ts`, neue Komponenten `ReviewTile` und `InventoryDialog`; entfallen sind `assistant.ts`, `Assistant` und `AssistantStepDialog`. **Keine Datenmigration** (Pläne wurden vorgängig gelöscht). 7 Tests ergänzt (322 → 329). |
| 0.38 | 2026-08-16 | Claude (Opus 5) | **Der Assistent führt bis zur ersten Lebensphase und nicht weiter.** Sieben Schritte waren ein Versprechen, das die letzten fünf nicht einlösten: Sie zeigten Stationen, statt zu führen. Neu sind es **zwei**, die tragen; wie es danach weitergeht, wird eigens entworfen. (1) **Trennung von Tatsache und Annahme.** Schritt 1 erfasst nur noch, was man nachschlagen kann Kontostand, Guthaben, Kaufpreis, Restschuld. Renditen, Lohnentwicklung, Teuerung, Hypothekarzins und Wertsteigerung wandern in Schritt 2, wo sie hingehören: **Annahmen gelten immer nur für einen Zeitraum.** Bis 0.37 standen beide zusammen in der Bestandsaufnahme, wodurch eine Annahme wie eine Tatsache aussah. Neu trägt jede Kategorie `baseFields` und `phaseFields`. (2) **Neuer Schritt 2 «Erste Lebensphase»**: Name → Dauer → jährliche Annahmen je Element → **Sparquote verteilen**. Erst danach ist der Schritt fertig; endete er nach den Annahmen, bliebe der ganze Überschuss stumm auf dem Cash-Konto liegen. Die Dauer ist an der Pensionierung **gekappt** und wird im Feld begrenzt, statt hinterher als Fehler zu erscheinen. Der **PK-Beitrag** steht hier er stammt aus dem Bruttolohn und lässt sich aus der Sparquote gar nicht verteilen, wäre also sonst durch alle Maschen gefallen. (3) **Pensionsalter im Basisszenario fest auf 65**, nicht änderbar. Vorbezug, Aufschub und gestaffelte Kapitalbezüge bleiben im Rechenkern vollständig erhalten (samt Tests), werden hier aber nicht angeboten sie gehören zu einer eigenen Szenario-Art (Roadmap Nr. 48). Der Knopf «Pensionsplanung» und der zugehörige Bildschirm entfallen; die Grundeinstellungen nennen das Pensionsalter je Person und weisen auf die spätere Szenario-Art hin. (4) **Der Planungshorizont wird abgeleitet** er ist die Summe der Lebensphasen. Dritter und letzter Anlauf: 0.35 führte ihn als Endalter je Person, 0.36 als Jahreszahl am Szenario, beide Male eine zweite Wahrheit über dieselbe Sache. Wer die Phasen einzeln plant, hat den Horizont bereits bestimmt. `Scenario.planningHorizonYears`, der Endpunkt `/horizon` und `planHorizonChange` entfallen. (5) Entfallen: die Schritte 3 bis 7 samt ihren Werkzeugen und `RetirementPanel`. |
@@ -455,7 +456,7 @@ Seitenleiste.
**Das Pensionsalter wird bewusst NICHT gefragt.** Es ist kein Stammdatum, sondern der erste
Entscheid der Pensionsplanung -- und es erzeugt eine Phasengrenze
([3.14.4](#3144-fixpunkte-jeder-bezugsbeginn-erzwingt-eine-phasengrenze)). Im Basisszenario
([3.14.5](#3145-fixpunkte-jeder-bezugsbeginn-erzwingt-eine-phasengrenze)). Im Basisszenario
gilt durchgehend das Referenzalter 65.
**Der frühere Plan-Assistent (`PlanWizard`) und der Beispielplan sind entfallen.** Der Wizard
@@ -558,10 +559,16 @@ Referenz: `src/lib/elements.ts` (`PERSON_ONLY_CATEGORIES`), `src/app/api/scenari
### 3.4.2 Element anlegen
Der Dialog „Finanzielles Element" fragt Kategorie, Zuordnung, Bezeichnung **und direkt die
Werte der ersten Lebensphase** ab. Nach dem Anlegen (`POST .../elements`) werden die Werte per
`PUT .../phase/<firstPhaseId>` gespeichert, sofern mindestens ein Feld gesetzt wurde. Der
Bezeichnungs-Default ist das Kategorie-Label.
Der Dialog „Finanzielles Element" fragt Kategorie, Zuordnung, Bezeichnung **und den Bestand
von heute** ab dieselben Felder wie die Bestandsaufnahme (`BaseFields`). Nach dem Anlegen
(`POST .../elements`) landen sie per `PUT .../elements/<id>/base` in den **Stammdaten**, sofern
mindestens ein Feld gesetzt wurde. Der Bezeichnungs-Default ist das Kategorie-Label.
Bis 0.39 schrieb der Dialog stattdessen in `phase/<firstPhaseId>` und legte damit einen zweiten
Ort für denselben Betrag an ([3.14.3](#3143-ein-wert-ein-ort)). Aus derselben Änderung folgt:
Der Dialog **braucht keine Lebensphase mehr**. Stammdaten hängen am Element, ein Bestand lässt
sich damit vor jeder Zeitplanung aufnehmen. Die Annahmen (Renditen, Teuerung, Zins) folgen in
der Phasenzelle, wo sie hingehören.
`orderIndex` = bisheriges Maximum + 1; bestimmt die Reihenfolge innerhalb der Kategoriegruppe.
@@ -1171,6 +1178,11 @@ bestehende Felder** über die bestehenden Endpunkte an der Berechnung änder
| Immobilie | `extraAmortization` | dem **Übergang davor** |
| Sonstige Schulden | `immediateRepayment` | dem **Übergang davor** |
Die beiden letzten waren bis 0.39 **zusätzlich** in der Übergangszelle erfassbar. Das war die
falsche Stelle: Sie zehren vom Kapital der FOLGEphase, und der zuständige Dialog hängt an deren
Phasenkopf. Seit 0.40 stehen sie nur noch hier; angezeigt werden sie in der **Phasenzelle**,
zusammen mit der Zusatzeinlage aus demselben Topf.
Jede Zeile nennt neben dem Namen die **Zuordnung** (Person A / Person B / Gemeinsam) ohne sie
sind zwei gleichnamige Guthaben nicht unterscheidbar. **Bezogene** PK- und 3a-Guthaben erscheinen
gar nicht mehr (Kennzeichen `acceptsCapital`, siehe 3.12.5): Die Rechnung ignoriert dort jede
@@ -1186,6 +1198,11 @@ Was nicht verteilt wird, **bleibt automatisch auf dem Cash** dafür braucht
ist das Verhalten des Modells. Wird mehr verteilt als vorhanden, startet die Folgephase mit
negativem Cash; der Dialog weist das als Liquiditätslücke aus.
**Der Knopf ist immer sichtbar**, auch wenn der Topf leer ist. Bis 0.39 hing er an
`pot.total > 0` also am **Rest**. Wer alles verteilt hatte, sah ihn nicht mehr. Solange die
Beträge im Element noch änderbar waren, ging das; seit 0.40 sind sie read-only, und der Knopf
ist der einzige Weg dorthin. Ohne ihn wäre man in der eigenen Zuteilung eingesperrt.
**Bereits zugeteiltes Alterskapital steht read-only darüber.** Das Eingabefeld führt nur den
hier erfassten Teil; was aus der Prozent-Quote des Bezugs-Entscheids stammt
([3.12.5](#3125-punkt-c-verwendung-des-bezogenen-alterskapitals)), liegt daneben und ist nach
@@ -1196,9 +1213,19 @@ wirkungslos.
jährliche Raten: `annualContribution` (3a, Sonstiges Vermögen), `annualWithdrawal` (Sonstiges
Vermögen), `amortization` (Immobilie), `annualRepayment` (Schulden).
> **Die Pensionskasse fehlt hier bewusst.** Ihr Beitrag stammt aus dem Bruttolohn und belastet
> das Cash-Konto nicht ([4.6.3](#463-pension_fund)) er lässt sich also gar nicht aus der Quote
> verteilen.
Seit 0.40 sind diese Felder **ausschliesslich** hier erfassbar; in der Phasenzelle stehen sie
read-only mit Absprung hierher ([3.14.3](#3143-ein-wert-ein-ort)).
> **Die Pensionskasse ist dabei aber nicht in der Quote.** Ihr Beitrag stammt aus dem
> Bruttolohn und belastet das Cash-Konto nicht ([4.6.3](#463-pension_fund)); er lässt sich also
> gar nicht aus der Quote verteilen. Er steht seit 0.40 trotzdem in diesem Dialog, in einem
> **eigenen Block** «Aus dem Bruttolohn (ausserhalb der Quote)» und ausserhalb der Bilanz
> darunter. Der Grund ist der Umbau selbst: Dies ist der einzige Ort für jährliche Beträge
> ohne ihn wäre der PK-Beitrag unerreichbar und fiele still auf 0.
Bei der **Säule 3a** stehen hier auch die **Obergrenze** und der Schalter «grosse Säule 3a»
(Selbstständige ohne PK). Der Schalter steht **über** dem Betrag, dessen Grenze er bestimmt
in der Phasenzelle, getrennt vom Feld, war er eine Einstellung ohne sichtbare Wirkung.
Auch hier gilt der Filter über `acceptsCapital`: Eine **bezogene Säule 3a** nimmt keine Einzahlung
mehr auf und erscheint deshalb nicht. Bis 0.33 stand sie in der Liste mitsamt dem aus der
@@ -1401,7 +1428,7 @@ Funktions-Leiste startet sie jederzeit neu.
Sie liegt seit 0.28 in `AppShell` (nicht mehr in `PlanView`), weil dort die Plan-Erstellung
zusammenläuft, und hat **zwei Auslöser**: Nach **jeder** Plan-Erstellung startet sie
**einmal** unabhängig davon, ob sie schon beendet
**einmal**, und zwar **sofort** unabhängig davon, ob sie schon beendet
wurde (die Erfolgsmeldung «die Tour zeigt dir gleich …» hält damit ihr Versprechen). Zusätzlich
startet sie beim ersten Öffnen eines bestehenden Plans mit Phasen, solange sie noch nie beendet
wurde (localStorage `fpt-tour-done`). Ein aus dem DOM gelesenes Ziel setzt voraus, dass die
@@ -2201,13 +2228,69 @@ ganze Überschuss still auf dem Cash-Konto liegen -- und die Übersicht meldete
erledigt», während das Geld unverzinst herumliegt. Das Flag wird gesetzt, sobald der
Verteil-Dialog einmal gespeichert hat.
**Sammel-Bestätigung je Phase.** Bei sechs Elementen und fünf Phasen wären dreissig einzelne
Klicks nötig -- das erzieht zum Durchklicken, also genau zum Gegenteil dessen, was der
Mechanismus will. Der Phasenkopf trägt deshalb ein Abzeichen mit der Anzahl offener Annahmen;
ein Klick bestätigt alle auf einmal, nach Rückfrage. Wer eine einzelne Zelle öffnet und
**Das Abzeichen im Phasenkopf öffnet eine Maske mit allen offenen Annahmen** (seit 0.40,
`PhaseReviewDialog`) -- das Gegenstück zum geführten Übergang ([3.5.4](#354-geführter-übergang-review-dialog)).
Sie listet jede unbestätigte Zelle mit ihren Feldern, bearbeitbar, und schliesst mit «Alle
bestätigen».
Bis 0.39 fragte das Abzeichen nur «N Annahmen bestätigen?». Das war eine Zustimmung zu etwas,
das man gar nicht sah -- also genau die Bewegung, die der Mechanismus verhindern soll. Der
Sammel-Abschluss bleibt trotzdem wichtig: Bei sechs Elementen und fünf Phasen wären dreissig
einzelne Klicks nötig, und das erzieht zum Durchklicken. Wer eine einzelne Zelle öffnet und
speichert, bestätigt sie dabei ohnehin.
### 3.14.3 Element-Stammdaten: Bestand vor Zeitachse
Ist zusätzlich die Quote dieser Phase noch nicht verteilt, steht das oben in der Maske mit
einem Absprung in den Verteil-Dialog -- die beiden zählen getrennt, gehören aber zusammen.
Unbestätigte Zellen tragen in der Matrix getönten Grund, einen kräftigen linken Balken und ein
Warnzeichen. Der dünne Ring aus 0.39 ging in einer gefüllten Matrix schlicht unter.
**Eine Zelle ohne Annahmen verlangt keine Bestätigung.** Seit 0.40 entstehen Bestände in den
Stammdaten und Raten im Verteil-Dialog ([3.14.3](#3143-ein-wert-ein-ort)); in der Phasenzelle
bleiben nur die Annahmen. Eine «Sonstige Schuld» trägt damit gar keine mehr -- eine Frage ohne
Gegenstand zu stellen, wäre schlimmer als keine.
### 3.14.3 Ein Wert, ein Ort
Die Regel, die seit 0.40 durchgehend gilt: **Für jede Zahl gibt es genau EIN Eingabefeld.**
Überall sonst steht sie read-only, mit einem Absprung dorthin, wo sie entsteht.
| Was | Entsteht in | Angezeigt (read-only) in |
|---|---|---|
| Bestand bei Planbeginn | Stammdaten Spalte «Start» bzw. Bestandsaufnahme | Phasenzelle der **ersten** Phase |
| Jährliche Rate (Sparbeitrag, Bezugsrate, Amortisation, Tilgung, 3a- und PK-Einzahlung) | «Quote verteilen» | Phasenzelle jeder Phase |
| Einmalige Kapitalverwendung (Zusatzeinlage, Sonderamortisation, Sofort-Tilgung) | «Kapital verteilen» | Phasenzelle der Phase, in der das Kapital anfällt |
| Annahme (Rendite, Lohnentwicklung, Teuerung, Hypothekarzins, Wertsteigerung) | Phasenzelle | |
| Cash bei Planbeginn | Bestandsaufnahme bzw. Spalte «Start» der Cash-Zeile | |
**Warum das kein Kosmetikpunkt ist.** Der Rechenkern legt in der ersten Phase die Phasenwerte
über die Stammdaten (`{...baseData, ...phaseValues}`). Ein zweites Eingabefeld für den
Startwert bedeutete also: Man tippt in Phase 1 eine Zahl, sie **gewinnt still** gegen die
Bestandsaufnahme, und in der Spalte «Start» steht weiterhin die alte. Niemand sieht den
Konflikt man sieht nur, dass eine Korrektur wirkungslos bleibt.
Bei den Raten ist der Grund ein anderer, aber nicht kleiner: **Nur der Verteil-Dialog kennt
die Quote.** Er zeigt, wie viel überhaupt zu verteilen ist, ob die Summe aufgeht und ob das
Cash-Konto dabei ins Minus fällt. Ein Eingabefeld am Element liess sich beliebig darüber
hinaus füllen, ohne dass es irgendwo auffiel.
**Der PK-Beitrag ist die Ausnahme, die die Regel trägt.** Er ist kein Teil der Quote er
stammt aus dem Bruttolohn und belastet das Cash-Konto nicht ([4.6.3](#463-pension_fund)).
Trotzdem steht er im Verteil-Dialog, weil der seit 0.40 der einzige Ort für jährliche Beträge
ist; ohne ihn wäre er unerreichbar und fiele still auf 0, obwohl er das Vorsorgekapital um
sechsstellige Beträge bewegt. Er steht dort in einem **eigenen Block** «Aus dem Bruttolohn
(ausserhalb der Quote)», damit ihn niemand für einen Teil der Quote hält.
**Was die Vererbung angeht, bleibt es beim Verhalten von 0.32:** Der Verteil-Dialog schreibt
beim Speichern jede Zeile explizit weg, auch die, die man nicht angefasst hat er belegt sie
mit dem effektiven, also geerbten Wert vor. Damit ist eine verteilte Phase für diese Felder
nicht mehr an die Vorphase gekoppelt. Das ist bewusst so: **Verteilt wird je Phase**, und eine
Zahl, die man in der Verteilung gesehen und stehen gelassen hat, ist ein Entscheid. Die
Annahmen (Renditen, Teuerung) erben unverändert weiter ([3.12.4](#3124-punkt-a-aus-vorphase-übernehmen)).
Referenz: `LinkedField` in `src/components/ElementDetail.tsx`.
### 3.14.4 Element-Stammdaten: Bestand vor Zeitachse
Bis 0.35 lagen alle Elementwerte unter `phaseValues[phaseId]`. Ohne Phase gab es keinen
Schlüssel -- eine Bestandsaufnahme als erster Schritt war damit unmöglich.
@@ -2243,7 +2326,7 @@ Orten, genau das Muster, das dieser Umbau beseitigt hat.
> vollständig an den Phasen -- die Bestandsaufnahme verschwand nach dem Speichern im Nichts.
> Behoben in 0.37.
### 3.14.4 Fixpunkte: jeder Bezugsbeginn erzwingt eine Phasengrenze
### 3.14.5 Fixpunkte: jeder Bezugsbeginn erzwingt eine Phasengrenze
`phaseplan.ts` kannte bis 0.35 genau einen Fixpunkt je Person -- das Erwerbsende. Seit AHV,
Pensionskasse und jedes 3a-Konto ein eigenes Bezugsalter haben, sind es bis zu vier:
@@ -2264,7 +2347,7 @@ dort mehrfach beschriftet.
Die Folge ist ehrlich, aber spürbar: Ein Paar mit gestaffelten Bezügen kommt schnell auf acht
bis zehn Pflichtphasen.
### 3.14.5 Der Bildschirm
### 3.14.6 Der Bildschirm
Zwei farblich getrennte Hälften, damit sichtbar ist: **oben stellst du ein, unten siehst du
das Ergebnis.**
@@ -2294,7 +2377,7 @@ Unten die Matrix. Die frühere Aktionsleiste darüber ist verschwunden: **«+ El
sitzen in der Ecke oben links der Matrix.** Sie steuern die Matrix und lagen vorher lose
darüber wie Aktionen der ganzen Seite.
### 3.14.6 Die Tour
### 3.14.7 Die Tour
Ein grosses Popup mit einem **nachgebauten** Bildschirm und erfundenen Zahlen, in neun
Schritten erklärt. Der letzte führt zur Bestandsaufnahme.
@@ -3776,7 +3859,7 @@ PlanComputed ← an den Client geliefert
| `distribution.ts` | Kapitaltopf und Quoten-Zerlegung, Anwenden von Entwurfswerten für die Verteil-Werkzeuge (Kap. 3.6.9/3.6.10). Rein. |
| `retirement.ts` | Pensionsalter verschieben: Spielraum je Person, Verschiebung der Phasengrenze, Zusammenlegung zweier Übergänge (Kap. 4.16). Rein, ohne I/O. |
| `transitions.ts` | Reine Übergangs-Regeln (Vorbelegung, «beantwortet?», Cash-Zusammenfassung). Liegt hier und nicht in einer Komponente, weil auch der Server sie braucht -- ein Import aus `src/components` bricht erst in der Produktion. |
| `phaseplan.ts` | Ableitung der Lebensabschnitte (Erwerb/Misch/Pension) aus den fixen Pensionierungszeitpunkten, Fixpunkte und Dauer-Kappung (Kap. 3.14.4). Rein. |
| `phaseplan.ts` | Ableitung der Lebensabschnitte (Erwerb/Misch/Pension) aus den fixen Pensionierungszeitpunkten, Fixpunkte und Dauer-Kappung (Kap. 3.14.5). Rein. |
| `review.ts` | Leitet die offenen Punkte eines Plans ab: unbestätigte Phasenzellen, nicht verteilte Quoten, offene Übergänge (Kap. 3.14). Rein. |
| `constants.ts` (erweitert) | zusätzlich `SYSTEM_PARAMETERS`: dieselben Werte maschinenlesbar mit Bedeutung, Herleitung, Quelle und Stand Grundlage der Systemparameter-Ansicht |
| `diff.ts` | Abweichungs-Erkennung eines Szenarios gegen sein Eltern-Szenario (Kap. 3.2.6) |
@@ -4387,7 +4470,7 @@ Include `src/**/*.test.ts`). Es gibt **keine** Komponenten-, API- oder E2E-Tests
| `migrations.test.ts` | 3 | spielt alle Migrationen gegen echtes PostgreSQL (PGlite) ein; prüft zusätzlich die V7-**Datenübernahme** (Basisszenario gewinnt, Pensionsalter bleiben szenario-eigen) |
| `rate-limit.test.ts` | 6 | Fixed-Window: erlaubt bis Limit, blockt danach, startet nach Fensterablauf neu, trennt je Schlüssel; Client-IP aus X-Forwarded-For / X-Real-IP |
| `csv.test.ts` | 7 | BOM, alle vier Blöcke, jedes Element als Zeile, Beginn-/Ende-/Übergangsspalten, Entscheid im Klartext, ein Eintrag je Planjahr, Maskierung von `;` und `"` |
| **Total** | **329** | |
| **Total** | **330** | |
## 8.2 Testfälle