Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
382 KiB
FPT – Financial Planning Tool
Funktionale und Technische Spezifikation
| Dokument | Funktionale und Technische Spezifikation FPT |
| Version | 0.42.3 |
| Datum | 2026-07-25 |
| Status | Lebendes Dokument |
| Codestand | Arbeitsstand nach 5842a84 inkl. Zeitstrahl (Branch main) |
| Ersetzt | FDD_TDD_FPT.docx (v1–v5) im Ordner Info Dateien – diese sind ab Version 0.1 dieses Dokuments obsolet |
| Geltungsbereich | Gesamter Code im Verzeichnis FPT |
Änderungshistorie
| Version | Datum | Autor | Änderung |
|---|---|---|---|
| 0.42.3 | 2026-08-17 | Claude (Opus 5) | Die Zeitachse als klassischer Zeitstrahl. Die Beschriftungen stehen neu abwechselnd ober- und unterhalb des Balkens, verbunden über ein Dreieck und eine dünne Linie. Das ist nicht Zierde, sondern löst ein Platzproblem: Nebeneinander gesetzte Beschriftungen kollidieren, sobald eine Phase kurz ist – abwechselnd liegen Nachbarn auf derselben Seite zwei Phasen auseinander und haben den doppelten Raum. Jede Beschriftung nennt Jahresspanne, Phasenname und Dauer. Der Balken ist durchgehend und dunkel, seine Segmente teilen sich die Breite streng nach Dauer; die aktive Phase ist im Akzent gefüllt, samt Dreieck, Verbindungslinie und hervorgehobener Beschriftung. Das Startjahr steht als grosse Zahl am linken Ende – es ist der Nullpunkt, auf den sich die reale Kaufkraft und die ganze Achse beziehen –, das Planende kleiner rechts. Die Marker (Pensionierung je Person, Ruinalter, erfasste Ist-Jahre) liegen neu auf dem Balken statt darüber zu schweben; oberhalb kollidierten sie mit den Beschriftungen. |
| 0.42.2 | 2026-08-17 | Claude (Opus 5) | Memo 2026081703 – Feinschliff der neuen Oberfläche, sechs Punkte. (1) Die Menüleiste schiebt, statt zu überlappen. Beim Ausfahren legte sie sich über den Inhalt; ein Streifen verschwand jedes Mal, und das las sich wie ein Fehler. Neu ist der ausgefahrene Zustand optisch identisch mit dem angepinnten – das Anpinnen entscheidet nur noch, ob es so BLEIBT. (2) Suchen und Anpinnen stehen direkt unter den Systemparametern statt am Fuss der Leiste. Die Leiste ist so hoch wie die Seite, und mit einer aufgeklappten Karte wird die sehr lang – am Fuss waren die beiden ausserhalb des Blickfelds. Zusätzlich klebt der Inhalt beider Leisten neu am oberen Bildschirmrand (sticky), sonst scrollt die Steuerung beim Arbeiten weg. (3) Die Karte nutzt nicht mehr die volle Breite, sondern höchstens 46 rem. Eine Zeile aus 1300 px liest sich schlecht: Das Auge verliert beim Zeilenwechsel den Anschluss, und Beschriftung und Wert driften so weit auseinander, dass man sie nicht mehr zusammenbringt. Die frei werdende Luft links und rechts trägt zwei grosse Pfeile zur Nachbarphase – ein Bedienelement, das keinen zusätzlichen Platz kostet. (4) Die drei Akte sind sichtbar getrennt. Vorher schied sie nur eine Haarlinie, und man las die Karte als eine lange Liste statt als drei Schritte. Neu trägt jeder Akt einen getönten Kopfstreifen über die volle Breite mit einer Nummer im Kreis; «Ergebnis» ist durchgehend getönt und trägt die Nummer im Akzent – es ist der Schlusspunkt, nicht ein weiterer Abschnitt. (5) Die Zeitachse neu gezeichnet: kein Kasten mit Überschrift, keine Legende, keine Fusszeile. Stattdessen abgesetzte Kacheln, deren Abstand die Phasengrenze zeigt, dazwischen ein Pfeil für den Übergang, und die Marker (Pensionierung, Ruinalter, erfasste Ist-Jahre) sitzen als Pillen direkt an der Leiste statt darüber zu schweben. (6) Cash verhält sich wie jede andere Zeile. Es war der einzige Eintritts-Punkt, der das rechte Panel öffnete statt aufzuklappen – auf einer Karte, die genau davon wegwollte, ein Fremdkörper. Neu klappt es inline auf, mit denselben Feldern (CashTransitionFields) und eigenem Speichern. |
| 0.42.1 | 2026-08-17 | Claude (Opus 5) | Memo 2026081702 – vier Befunde aus der ersten Testrunde der neuen Oberfläche. (1) Die Kartenansicht war überhaupt nicht erreichbar – der Umschalter dauerhaft ausgegraut. Ursache: Die Bedingung lautete forceMatrix = !!diff, aber computeScenarioDiff liefert immer ein Objekt (bei Gleichheit ein leeres). Auch diff.total > 0 wäre falsch gewesen: Ein Kind-Szenario weicht nach der ersten Änderung immer von seiner Vorlage ab, Karten wären dort für immer gesperrt. Die Diff-Markierungen sind eine Verzierung der Matrix, kein Modus. Neu erzwingt nur die Ist-Ansicht die Matrix (das ist ein Modus, in den man bewusst wechselt); bei Abweichungen zur Vorlage steht in der Kartenansicht ein Hinweis mit dem Weg dorthin. (2) Die Menüleiste wechselte beim Überfahren die Darstellung – eingeklappt Icons, ausgefahren ein Plan-Baum. Das war kein Übergang, sondern ein Sprung. Neu ist es eine Komponente in zwei Breiten: Die Symbole sitzen in einer festen 3.5-rem-Spalte und bleiben exakt an ihrem Platz, rechts daneben fährt nur die Beschriftung aus. Damit entfällt der Baum aller Pläne; er wird nicht vermisst, weil der Plan über den Wähler oben gewechselt wird und die vier Bereiche sich immer auf den aktiven Plan beziehen. Plan anlegen und löschen sind in den Wähler gewandert, der Szenario-Baum hängt aufklappbar unter «Szenarien». (3) Die Matrix schob sich vor die ausgefahrene Leiste. Ihre sticky Köpfe tragen z-40 und konkurrierten damit global; die Ecke oben links gewann. Die Matrix ist neu ein eigener Stapel-Kontext (relative z-0) – ihre Köpfe stapeln sich nur noch untereinander. (4) Die Kachel «Offene Punkte» zog sich über den ganzen Bildschirm, auch mit einem einzigen Eintrag: ein h-full aus der Zeit, als die vier Kacheln in einem Raster gleich hoch sein mussten. In der rechten Leiste ist das falsch – sie ist jetzt so hoch wie ihr Inhalt. |
| 0.42 | 2026-08-17 | Claude (Opus 5) | Kartenansicht und Drei-Spalten-Shell (neues Kapitel 3.15). Der grösste Eingriff in die Oberfläche seit 0.36 – und er behebt eine Ursache statt Symptome. Ausgemessen war die Lage eindeutig: Der Phasenkopf trug 220 px an Kennzahlen, dauerhaft fixiert; von 675 px Matrixhöhe blieben 430 px zum Arbeiten. Fünf Phasen brauchten 1760 px Breite, sichtbar waren mit offenem Panel 1024 px. Und das Bearbeitungs-Panel (fixed right-0, 416 px) legte sich ausgerechnet über die Spalte, zu der man gerade gescrollt hatte. (1) Eine Karte je Lebensphase ersetzt die Matrix als Arbeitsansicht. Sie erzählt die Phase als Zeitablauf: Eintritt (der Übergang aus der Vorphase; bei Phase 1 der Bestand), Laufend (die Elemente mit ihren Annahmen und die Quote), Ergebnis (Vermögen und Cash am Phasenende). Horizontales Scrollen entfällt vollständig. Der Übergang gehört bewusst zur FOLGEphase – dort wird das frei werdende Kapital verteilt, konsistent mit 0.41. (2) Bearbeitet wird IN der Karte. Ein Klick klappt die Zeile auf, die Felder stehen darunter. Damit kann nichts mehr verdecken, woran man arbeitet – das Panel bleibt nur für die Matrix. Ein Knopf im Kartenkopf klappt alle Zeilen auf oder zu; bei acht Elementen ist das der Unterschied zwischen einmal durchsehen und acht Klicks. (3) Die Zeitachse wird zum Navigator: Das Segment der offenen Karte ist hervorgehoben, ein Klick springt hin. Dazu Pfeilknöpfe unten und die Pfeiltasten (ausser in Eingabefeldern – sonst springt die Karte beim Tippen weg). (4) Die Matrix bleibt als Vergleichsansicht. Umschalter oben rechts; die Wahl gehört zum Benutzer und überlebt die Sitzung. Ein Szenario-Diff oder eine Ist-Ansicht erzwingt die Matrix: Vergleich über alle Phasen kann eine einzelne Karte prinzipiell nicht leisten, und stillschweigend die falsche Darstellung anzubieten wäre schlechter als der Wechsel. (5) Drei-Spalten-Shell. Die Menüleiste links ist eingeklappt eine Icon-Schiene (56 statt 256 px); beim Überfahren fährt sie als Overlay aus, ein Pin hält sie breit. Die vier Kacheln wandern von oben in eine rechte Leiste mit aufklappbaren Abschnitten. Das ist der entscheidende Tausch: Sie kosteten oben vertikalen Platz, rechts kosten sie horizontalen – und Höhe ist auf einem Breitbildschirm die knappe Grösse. Eingeklappt bleibt die Zahl der offenen Punkte am Knopf sichtbar. Zusammen: statt 1184 × 574 rund 1328 × 804 für den Inhalt, also +12 % Breite und +40 % Höhe. (6) Der Anzeige-Umschalter nominal/beide/real steht neu auch in der Werkzeugleiste (ValueModeSwitch, eine Implementierung für beide Orte). Neue Komponente PhaseCards; ElementDetail bekommt eine compact-Variante, Timeline die Navigator-Rolle, Tile eine rahmenlose Variante. Kein Eingriff in den Rechenkern; 337 Tests unverändert grün. |
| 0.41 | 2026-08-17 | Claude (Opus 5) | Memo 2026081701 – zehn Punkte aus einer Testrunde, mehrheitlich Feinschliff, zwei davon inhaltlich. (1) Die Kapitalverwendung heisst neu Schuldentilgung · Investition · Cash, und die Tilgung hat ein wählbares Ziel (capitalUseDebtTargetElementId): eine Immobilie oder eine sonstige Schuld. Bisher floss die Quote stur in die erstbeste Immobilie – wer neben einer Hypothek zu 1,5 % einen Konsumkredit zu 6 % trägt, konnte den Kredit gar nicht zuerst tilgen. Ohne Wahl bleibt es beim alten Verhalten, damit bestehende Pläne unverändert rechnen. Cash ist neu eine gleichrangige Zeile statt einer blassen Fussnote: «ich lasse es liegen» ist ein legitimer Entscheid. Rechnerisch bleibt es der Rest – alle drei frei eintippbar zu machen hiesse, dass beim Tippen still eine andere Zahl wandert. (2) Die Maske gibt es neu auch bei der Säule 3a. Dort stellt sich die Frage sogar zwingender als bei der PK: Ein 3a-Konto wird IMMER vollständig bezogen, es fliesst also in jedem Fall ein grosser Betrag – der bis 0.40 stumm auf dem Cash landete. (3) Je Person entsteht beim Abschluss der Bestandsaufnahme automatisch ein AHV-Element, idempotent (wer die Bestandsaufnahme später korrigiert, bekommt kein zweites). Die AHV ist die einzige Kategorie ohne Bestand – ein Plus-Knopf mit leerer Karte wäre sinnlos, das Element wegzulassen aber auch: Ohne es rechnet der Plan ab der Pensionierung mit einer AHV-Rente von 0. Die Karte in der Bestandsaufnahme sagt neu, warum dort nichts zu erfassen ist. (4) Farben folgen der Bedeutung, nicht dem Zustand. Ein einmaliger Cash-Zufluss am Übergang trug die Akzentfarbe – im warmen Schema ein Orangerot, das eine Erbschaft wie einen Fehler aussehen liess; neu ist er grün, ein Abfluss neutral. Die Knöpfe «Kapital verteilen» und «Sparquote verteilen» sind grün mit Haken, sobald erledigt – vorher sah man einer vollständig verteilten Phase nicht an, dass sie fertig war. (5) Sechs Stellen mit unlesbarer Schrift behoben: text-attention-fg ist weiss und für vollflächigen Attention-Grund gedacht; auf hellem oder 10-%-Grund war der Text im warmen und im hellen Schema praktisch unsichtbar (PK-Einkaufswarnung, 3a-Kollision, fehlendes Anlageziel, zwei Stellen in der Kachel «Offene Punkte»). (6) Der Kapitalanteil-Regler hatte keine sichtbare Schiene: appearance-none schaltet die native Darstellung ab, womit auch accent-color nicht mehr greift – übrig blieb der blosse Knopf. (7) Der Umwandlungssatz verschwindet bei 100 % Kapital, spiegelbildlich zur Kapitalbezugssteuer bei 0 %. Bei vollem Kapitalbezug wird nichts verrentet. (8) Der Dialog «Neue Lebensphase» erklärt die Dauer-Kappung – welches Ereignis sie setzt und warum (die Rechnung leitet Erwerbsstatus und Bezüge am Phasenbeginn ab). Vorher stand dort nur «max. N». (9) Zustand statt Regel nach der Pensionierung: Die Zellen von PK und 3a beschrieben in der Pensionsphase die Regel im Futur («wird berechnet», «wird bezogen»), obwohl der Bezug längst geschehen war. Neu steht dort der gerechnete Stand – bei der PK die Rente in Franken oder «Vollständig bezogen», bei der 3a «Vollständig bezogen». (10) Eine Vokabel für einen Gedanken: Die Bestätigung der Pensionsentscheide heisst neu «angeschaut» wie die der Phasenzellen, und der Erklärtext sagt ausdrücklich, dass der Haken keine Zahl ändert. Zwei Wörter für dieselbe Sache hatten mehr verwirrt als geholfen. Die Fixpunkt-Kappung um AHV-, PK- und 3a-Bezugsalter zu erweitern wurde bewusst zurückgestellt – im Basisszenario ist das Pensionsalter fest 65, das Thema gehört zur Szenario-Art «Frühpensionierung» (Roadmap Nr. 48). 3 Tests ergänzt (334 → 337). |
| 0.40.1 | 2026-08-16 | Claude (Opus 5) | Zwei Fehler aus der Testrunde. (1) «Ein Wert, ein Ort» gilt jetzt im Rechenkern, nicht nur in der Oberfläche. Eine Immobilie zeigte in der ersten Lebensphase Startwert 0 statt Kaufpreis minus Hypothek. Ursache: Der Rechenkern legte die Phasenwerte über die Stammdaten ({...baseData, ...phaseValues}) – eine in 0.39 versehentlich gespeicherte 0 gewann damit gegen die Bestandsaufnahme. Seit 0.40 ist das Feld read-only, wodurch dieser Altwert unerreichbar war und den Kaufpreis dauerhaft verdeckt hätte. Neu gewinnen bei den fünf Bestandsfeldern (amount, currentValue, startValue, purchasePrice, mortgage) immer die Stammdaten, wo sie einen Wert tragen (firstPhaseValues); der Rückfall auf den Phasenwert bleibt nur, solange die Stammdaten leer sind – sonst fielen Pläne aus der Zeit vor 0.36 schlagartig auf 0. Die Annahmen (Rendite, Zins, Wertsteigerung, Teuerung) bleiben in Phase 1 überschreibbar: Sie gelten für einen Zeitraum, nicht für den Anfangsbestand. Damit heilen bestehende Daten von selbst. (2) «Alle bestätigen» war zu grob. Der Knopf in der Annahmen-Maske schrieb confirmed: true auf jede Zeile – man konnte ihn drücken, ohne je gescrollt zu haben, also genau das Durchwinken, das der Mechanismus verhindern soll. Neu trägt jede Zeile ein Häkchen «Angeschaut», anfangs leer; wer ein Feld ändert, hakt es automatisch an (bearbeiten IST anschauen); oben markiert ein Klick alle auf einmal, aber als sichtbarer Akt. Gespeichert werden die Werte aller Zeilen, bestätigt nur die angehakten – der Knopf nennt die Zahl. 4 Tests ergänzt (330 → 334). |
| 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), 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) 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. |
| 0.37 | 2026-08-16 | Claude (Opus 5) | Nachbesserungen zur Bestandsaufnahme (aus einer Testrunde des Nutzers). (1) Die Knöpfe tragen die Kategorienamen (Einkommen · Ausgaben · Pensionskasse · Säule 3a · Sonstiges Vermögen · Immobilie · Sonstige Schulden), die Alltagssprache steht als Erklärzeile darunter. Vorher hatte der Assistent eigene Vokabeln erfunden – am deutlichsten «Lebenshaltung», hinter der sich ein Feld «Ausgaben pro Jahr» verbarg. Wer hier eine andere Sprache lernt als die, die Matrix, Rechenwege und Bericht sprechen, sucht sie später vergeblich. Der Elementname darf konkret bleiben: Kategorie «Ausgaben», Zeile «Lebenshaltung». (2) Kein Speichern-Knopf je Element mehr. Der Entwurf liegt neu auf Ebene des ganzen Schritts statt in der einzelnen Karte – nur so überlebt er den Wechsel des Reiters, denn dabei verschwinden die Karten des vorigen Bereichs samt ihrem Zustand. «Schritt abschliessen» schreibt alles in einem Zug. Weil damit alle Zahlen an einem Ort liegen, zeigt der Dialog neu die Summe «Vermögen heute» live beim Tippen – die Rückmeldung, die sonst mit dem Speichern-Knopf verloren gegangen wäre. Wer den Dialog mit ungespeicherten Eingaben schliesst, wird gefragt. (3) «Zurück/Weiter» entfällt in Schritt 1 und 2: Die Reiter oben sind der Weg durch die Bereiche, zwei Navigationen für dieselbe Bewegung sind eine zu viel. (4) Der 3a-Hinweis zum Höchstbetrag ist gestrichen – er beantwortete an dieser Stelle eine Frage, die niemand stellt. (5) Neue Matrix-Spalte «Start» und: die Matrix erscheint, sobald es Elemente gibt – Lebensphasen sind dafür nicht mehr nötig. Das war ein Fehler in 0.36: Die Stammdaten wurden eigens dafür eingeführt, dass eine Bestandsaufnahme ohne Zeitachse möglich ist, und dann rendete die Matrix nichts, weil sie ganz an den Phasen hing. Die Spezifikation behauptete das Richtige, der Code hielt es nicht. Die Spalte löst zugleich ein zweites Problem: Sie ist der Ort, an dem ein Startwert änderbar ist. Ohne sie hätte man ihn in Phase 1 bearbeitet und dabei still einen Phasenwert geschrieben, der die Stammdaten überdeckt – dieselbe Zahl an zwei Orten. (6) Der Text des leeren Zustands ist korrigiert: Seit dem Umbau kommen die Elemente zuerst und die Phasen danach. Neue gemeinsame Komponente BaseFields (Assistent und Matrix nutzen dieselben Felder), neues Panel für die Stammdaten. 2 Tests ergänzt (334 → 336). |
| 0.36 | 2026-07-26 | Claude (Opus 5) | Der Assistent wird das Herzstück (neues Kapitel 3.14). Ein Umbau von Onboarding, Bildschirmaufbau und Führung. (1) Ein Weg hinein. Die Übersicht zeigt im leeren Zustand nur noch «Meinen ersten Finanzplan anlegen»; der geführte Start und der Beispielplan entfallen. Drei Knöpfe waren eine Wahl, die niemand treffen kann, der das Tool noch nicht kennt. Der Plan-Dialog fragt nur noch sechs Dinge: Name, Haushaltsform, Personennamen, Startjahr, Alter, Inflation. Das Pensionsalter wird nicht mehr abgefragt – es ist kein Stammdatum, sondern der erste Entscheid der Pensionsplanung, und es erzeugt eine Phasengrenze. Bis dahin gilt das Referenzalter. (2) Element-Stammdaten (FinancialElement.baseData): Bestand bei Planbeginn und Ausgangs-Annahmen hängen neu am ELEMENT statt in Phase 1. Zwei Gründe – ein Startwert ist nicht «phase-1-spezifisch», sondern schlicht der Stand am Anfang; und Elemente lassen sich damit erfassen, bevor es Lebensphasen gibt. Genau das braucht die Bestandsaufnahme als erster Schritt. Zugleich sind die Stammdaten die Wurzel der Feld-Vererbung: Phase 1 hatte bisher nichts, von dem sie hätte erben können, und fiel auf 0. (3) Aus einem Fixpunkt werden bis zu vier je Person. phaseplan.ts kannte nur das Erwerbsende. Da AHV, Pensionskasse und jedes 3a-Konto eigene Bezugsalter haben (pkWithdrawalAge neu), erzwingt jeder Bezugsbeginn eine Phasengrenze – sonst fiele er mitten in eine Phase und rutschte auf die nächste Grenze, unter Umständen Jahre später. Die Phasendauer-Kappung zählt sie mit; Ereignisse im selben Jahr teilen sich eine Grenze. (4) Planungshorizont in JAHREN am Szenario (planningHorizonYears) statt als Endalter je Person: eine Zahl statt zweier, die bei einem Paar auseinanderlaufen könnten; die Endalter sind die Ableitung. Ersetzt Person.planningHorizonAge aus 0.35. (5) Neuer Szenario-Bildschirm. Zwei farblich getrennte Hälften: oben die Steuerung in vier Kacheln (Grundeinstellungen mit «Pensionsplanung» je Person · Kennzahlen inkl. neuem «Vermögen heute» · Schnellaktionen · Assistent) plus die Zeitachse über die volle Breite; unten die Matrix. Die Aktionsleiste über der Matrix ist verschwunden: «+ Element», «+ Phase», der Nominal/Real-Umschalter und «Alle auf-/zuklappen» sitzen jetzt in der Ecke oben links der Matrix – sie steuern die Matrix und lagen vorher lose darüber wie Aktionen der ganzen Seite. (6) Der FPT-Assistent ersetzt die Karte «Nächste Schritte». Sieben Schritte von der Bestandsaufnahme bis zum Feinschliff. Jeder öffnet ein Popup, das zuerst erklärt (welche Fragen der Schritt beantwortet, was man wissen sollte) und danach das Werkzeug anbietet; «Selbst erledigen» überspringt beides. Der Haken ist manuell – das Tool masst sich nicht an zu wissen, wann jemand fertig ist –, aber daneben steht der abgeleitete Stand («0 Lebensphasen»), damit ein abgehakter Schritt ohne Substanz auffällt. Erledigte rutschen nach unten und werden blass; die Kachel ist gelb, bis alle sieben stehen, dann grün. (7) Neue Tour: ein grosses Popup mit einem nachgebauten Bildschirm und erfundenen Zahlen, schrittweise erklärt. Das frühere Spotlight lag über der echten Ansicht – und hatte auf einem frisch angelegten, leeren Plan nichts hervorzuheben, also gerade dann nicht, wenn es am nötigsten war. Der Preis ist, dass die Attrappe bei UI-Änderungen nachzuführen ist. (8) Entfallen: PlanWizard (der Assistent führt jetzt IM Plan statt davor) und demoplan.ts. Neue Module assistant.ts, neue Komponenten Assistant, AssistantStepDialog, AssistantSteps; neue Endpunkte PUT /api/elements/<id>/base und POST /api/scenarios/<id>/assistant. Keine Datenmigration (Pläne wurden vorgängig gelöscht). 12 Tests ergänzt (322 → 334). |
| 0.35 | 2026-07-26 | Claude (Opus 5) | Die Pensionierung ist eine Eigenschaft der PERSON, nicht der Zeitachse (neues Kapitel 3.13). Der grösste Eingriff seit V7. Bisher hing jeder Bezugs-Entscheid an transitionValues[phaseId] – am Schlüssel Element × Phasen-ID. Daraus folgte fast alles, was an der Pensionsplanung störte: Entscheide, die inhaltlich eine Frage sind, lagen in drei weit auseinander liegenden Matrix-Zellen; das Alter zu ändern war ein struktureller Eingriff, bei dem Entscheide über mergeTransition verlustbehaftet von Grenze zu Grenze gerettet werden mussten; ein Szenario nur für ein anderes Pensionsalter hiess, alles neu zu entscheiden; und der Ziel-Solver (Roadmap Nr. 21) hätte nichts zum Anfassen gehabt. Neu liegt der Entscheid am ELEMENT (FinancialElement.retirementDecision, ohne Phasenbezug) und überlebt damit jede Verschiebung der Zeitachse. (1) Neuer Pensionierungs-Bildschirm gleichrangig neben der Matrix, mit der Rentenlücke als Leitzahl – keine neue Rechnung, sondern die Verzehrquote im ersten voll pensionierten Jahr; sie fehlte bisher nur als Begriff. Gerechnet im Rechenkern (PlanComputed.retirement), damit Bildschirm und PDF-Bericht nicht auseinanderlaufen. Die Matrix-Zellen am Pensions-Übergang bleiben bedienbar und nutzen dieselbe Komponente (RetirementFields) – zwei Ansichten auf ein Objekt, kein Duplikat. (2) AHV-Vorbezug und -Aufschub werden gerechnet (Kap. 4.4.7 neu geschrieben): Kürzung 6,8 %/Jahr, Zuschlag +5,2/10,8/17,1/24,0/31,5 % nach 1–5 Jahren, Teilbezug 20–80 %. Bis 0.34 startete die Rente immer mit 65 – wer mit 62 aufhörte, bekam die ungekürzte Rente drei Jahre später, wer bis 68 arbeitete, verschenkte den Zuschlag. Dabei wurde eine fachliche Trennung eingeführt, die es vorher gar nicht gab: Rentenbeginn und Beitragspflicht sind zwei verschiedene Alter. Wer mit 62 aufhört und ab 63 vorbezieht, bezieht ab 63 und zahlt bis 65 weiter als Nichterwerbstätige(r). (3) Pensionskasse: ein Regler statt eines Modus. payoutMode (PENSION/CAPITAL/COMBI) und der absolute capitalAmount entfallen zugunsten von capitalSharePct (0–100 %). Als Quote, weil sich das Guthaben mit dem Pensionsalter ändert – ein fixer Betrag bedeutete beim Verschieben still ein anderes Verhältnis. (4) Säule 3a: wählbares Bezugsalter (60–70) statt starr am Pensions-Übergang. Ein Konto lässt sich nur ganz auflösen, und alle Bezüge desselben Jahres werden steuerlich zusammengezählt – gestaffelt wird deshalb über Konten und Jahre. Gezogen wird an der ersten Phasengrenze bei oder nach dem Wunschalter. (5) Planungshorizont (Person.planningHorizonAge): Bisher ergab sich das Planende stillschweigend aus der Summe der Phasendauern – zwei Szenarien konnten unbemerkt verschieden weit rechnen und waren nicht vergleichbar. Neue Funktion planHorizonChange, neuer Endpunkt POST /api/scenarios/<id>/horizon. (6) Ampel mit drei Zuständen (Kap. 3.5.3 neu): unbeantwortet · auf Vorgabe · bestätigt. Mit durchgängigen Vorgaben – bewusst, damit niemand am Anfang Fragen beantworten muss, die er erst am Ende beantworten kann – entstand ein Zustand, den das Modell nicht kannte: Das System hat eine Antwort, nur nicht die des Benutzers. Eine Vorgabe wie «volle Rente statt Kapitalbezug» als beantwortet zu zählen hiesse, sie unbemerkt durchgehen zu lassen. Sie zählt deshalb mit, aber getrennt benannt: «2 offene Entscheide · 3 Vorgaben ungeprüft», bestätigt wird je Säule. (7) Drei neue Treiber in Tornado und Live-Simulation: PK-Kapitalanteil, AHV-Vorbezug/Aufschub (in Monaten, neue Einheit delta_months) – und das bestehende Pensionsalter wird endlich korrekt, weil der AHV-Beginn jetzt mitzieht. (8) Nebenbei zwei Vereinfachungen: Die Beitragskarriere vor Planbeginn lag an zwei Orten (Übergangszelle bzw. Phasenzelle für bereits Pensionierte) mit zwei Codepfaden – jetzt an einem. Und Szenario-Kopie, Versionierung und Diff tragen den Entscheid mit; ohne das wäre die Kopie genau für den Zweck unbrauchbar, für den man sie am häufigsten anlegt. Keine Datenmigration (Testdaten wurden vorgängig gelöscht); alte Werte in transitionValues werden ignoriert, betroffene Elemente erscheinen als «Vorgabe ungeprüft». (9) Assistent: neuer Überblicksschritt «Deine Pensionierung» (Kap. 3.2.8) – er ZEIGT Rentenlücke und Reichweite, statt Fragen zu stellen, die zu diesem Zeitpunkt niemand beantworten kann. Die Vorschau wird gerechnet, bevor der Plan existiert; Vorschau und Anlage speisen sich aus EINER Element-Liste, damit sie nicht auseinanderlaufen. Neues Modul retirement-decision.ts, neue Komponenten RetirementPanel und RetirementFields. 23 Tests ergänzt (292 → 315). |
| 0.34 | 2026-07-25 | Claude (Opus 5) | Modul-Review 4, Nachbesserungen: die Übergangs-Entscheide bis ans Ende durchgezogen. (1) Zuordnung überall dort, wo Elemente über ihren Namen angeboten werden. Zwei Personen nennen ihre Guthaben typischerweise gleich («Säule 3a», «ETF»); ohne die Person wählt man im Dropdown blind. Betroffen waren das Ziel der Anlage-Quote beim Kapitalbezug (dort mit hoher Folgewirkung: Ein Fehlgriff leitet das Alterskapital in das Depot der falschen Person) und die Zeilen im Dialog «Kapital verteilen». Die Klartext-Zuordnung liegt neu als ownerLabel in src/lib/elements.ts und wird von allen drei Stellen genutzt. (2) Herkunft des umgeleiteten Alterskapitals wird ausgewiesen. Fliessen PK und 3a in dasselbe Vermögens-Element, stand dort bisher nur eine Summe – ob wirklich beide angekommen sind, liess sich nicht prüfen. Carry und ElementPhaseComputed führen neu capitalInSources bzw. capitalFromTransferSources mit: Betrag je Quelle, benannt mit Element und Person. Sichtbar am Ziel-Element und im Dialog «Kapital verteilen». Das Feld heisst neu «Zusatzinvestition aus Kapitalbezug» (vorher «Davon aus Kapitalbezug (PK/3a)» – irreführend, weil es kein Anteil an der manuell erfassten Zusatzinvestition ist, sondern ein zweiter, davon unabhängiger Betrag). (3) Der Dialog «Kapital verteilen» zeigt das bereits Zugeteilte. Vorher stand dort eine 0, obwohl die Quote geflossen war – das Feld führt nur den manuell erfassten Teil. Neu erscheint darüber eine read-only Zeile mit dem aus dem Bezugs-Entscheid stammenden Betrag samt Aufschlüsselung, darunter das editierbare Feld und die Summe beider. (4) Bezogene Vorsorge-Guthaben werden in beiden Verteil-Dialogen nicht mehr angeboten. Nach der Pensionierung ignoriert die Rechnung Beiträge und Zusatzeinlagen in PK und Säule 3a – die Dialoge boten sie trotzdem an, inklusive eines aus der Vorphase geerbten 3a-Beitrags, der dort als aktive Rate erschien. Der Filter prüfte nur den status (ACTIVE), und der bleibt nach dem Bezug bestehen. Neu setzt die Rechnung selbst das Kennzeichen acceptsCapital: false; die Dialoge lesen es, statt die Regel ein zweites Mal nachzubauen. 4 Tests ergänzt (288 → 292). |
| 0.33 | 2026-07-25 | Claude (Opus 5) | Modul-Review 4 (Matrix: Phasen und Elemente). (1) Kapitalverwendung neu am Vorsorge-Element (Punkt C aus Roadmap Nr. 44): Die Prozent-Aufteilung des bezogenen Alterskapitals hing am Cash-Übergang – dem falschen Ort, denn mit zwei Guthaben (PK und 3a) liess sie sich dort gar nicht getrennt beantworten. Sie steht jetzt beim Bezugs-Entscheid der Pensionskasse (nur bei Kapitalbezug) bzw. der Säule 3a. Beide Dialoge führen neu brutto → Steuersatz → netto und darunter die Verteilung. Der zugeteilte Betrag fliesst über den regulären Weg (Carry.capitalIn → Zusatzeinlage der Folgephase) und ist damit überall sichtbar: am Ziel-Element, in der Cash-Brücke als Investition und im «Kapital verteilen»-Dialog. Vorher erhöhte er still den Bestand, weshalb Element und Dialog eine 0 zeigten. Die Säule 3a ist am Pensions-Übergang neu ein offener Entscheid (Steuersatz und Verwendung); vorher galt sie als automatisch beantwortet. (2) Phasendauer: die Folgephase gleicht aus (Kap. 3.3.2). Bis 0.32 prüfte die Kappung nur die bearbeitete Phase – wurde Phase 1 von 10 auf 12 Jahre verlängert, überspannte danach Phase 2 die Pensionierung, und die tragende Invariante aus Roadmap Nr. 44 kippte. Neu trägt die Folgephase die Differenz (Gesamtdauer bleibt gleich, wie beim Verschieben des Pensionsalters); passt sie nicht, wird blockiert; vorher erscheint eine Rückfrage. Neue reine Funktion planDurationChange. (3) Element und Phase direkt bedienbar: In der Matrix tragen Element-Zeile und Phasenkopf neu Stift (umbenennen, beim Element inkl. Zuordnung) und Papierkorb; das Expand-Symbol ist immer sichtbar statt nur bei Mouseover. PATCH /api/elements/<id> nimmt dafür neu auch ownerRole (bleibt für AHV/PK/3a personengebunden). (4) Hilfetexte werden über ein Portal gezeichnet – in scrollenden Dialogen schnitt der Container sie vorher ab; sie klappen nach oben, wenn unten kein Platz ist. (5) Verteil-Dialoge: Zeilen zeigen die Zuordnung (Person A/B/Gemeinsam) und sind nach «vom Cash»/«ins Cash» gruppiert; die Vorbelegung nutzt neu den effektiven Wert inklusive Vererbung aus der Vorphase – ein geerbter 3a-Beitrag erschien vorher als 0. (6) Matrix: alle Phasenspalten gleich breit, bei vielen Phasen wird horizontal gescrollt; «Alle auf-/zuklappen»; eine zugeklappte Kategorie zeigt je Phase die Summe ihrer Elemente. (7) Phasen-Detailansicht nutzt die neue Aufteilungs-Grafik (Fläche + Ring) statt der alten Balken. (8) Übersicht: «Leer starten» steht neu auch im leeren Zustand zur Wahl. (9) Nebenbei: dritte vom Umlaut-Sweep verstümmelte Hex-Farbe (#7c3äd) repariert, das Phasen-Panel nutzt den eigenen Bestätigungs-Dialog statt window.confirm. 10 Tests ergänzt (278 → 288). |
| 0.32 | 2026-07-25 | Claude (Opus 5) | Modul-Review 3, Nachbesserungen – darunter ein gravierender Rechenfehler bei den effektiven Werten. (1) Immobilien-Bugfix (Kap. 3.9): Der Ist-Wizard belegte den Immobilienwert mit dem Eigenkapital vor (ElementYearPoint.value), während Erfassung und Rechenkern den Verkehrswert erwarten. Der Rechenkern setzte den vorbelegten Wert als Verkehrswert ein, liess die Hypothek aber stehen – das Eigenkapital brach im Ist-Jahr schlagartig ein, typischerweise ins Negative. Sichtbar wurde das als negative Gesamt-Abweichung, obwohl nur ein Lohn erhöht wurde, und als «wegbrechendes» Wohneigentum in der Vermögensaufteilung. Neu wird propertyValue vorbelegt; das Feld ist als «Verkehrswert + Restschuld» beschriftet. Drei Regressionstests. (2) Ist-Datensätze bearbeitbar: Ein Klick auf die Zeile (oder «Bearbeiten») öffnet den erfassten Satz erneut; neuer Endpunkt PUT /api/plans/<id>/actuals/<setId>. Beim Bearbeiten überschreiben die Planwerte die erfassten Zahlen nicht mehr. (3) Ring-Klick in der Vermögensaufteilung repariert: Recharts 3 reicht im Klick-Parameter kein activePayload mehr durch (nur noch activeIndex) – der Handler feuerte nie, der Ring zeigte immer das Planende. (4) Seitenleiste sauber dreistufig: Ebene 1 Pläne, Ebene 2 die vier Bereiche (Szenarien, Effektive Werte, Analysen, Berichte) mit bündigen Symbolen, Ebene 3 nur die Szenarien – verschachtelt nach Herkunft. (5) Die Szenario-Liste zeigt neben der Version deren Kommentar. |
| 0.31 | 2026-07-25 | Claude (Opus 5) | Modul-Review 3 (Plan-/Szenario-Struktur, Dashboard, Grafiken). (1) Versionierung startet bei 0.1 statt 1.0 (Kap. 3.8): Ein Szenario läuft in der 0er-Reihe (0.1, 0.2, … 0.137), bis eine Hauptversion gesetzt wird – erst dann entsteht 1.0. Vorher begann jedes Szenario bereits bei 1.0, wodurch die Hauptversion ihre Bedeutung verlor. Die Szenario-Liste zeigt neu die echte Version statt «1.x», dazu eine Spalte Phasen. (2) Plan-Dashboard: Kacheln sind anklickbar und führen in ihren Bereich, neu inkl. Berichte; der Plan lässt sich über ein Stift-Symbol umbenennen; die Ist-Abweichung nennt das Jahr des jüngsten Ist-Datensatzes und ist bei einer positiven Abweichung grün statt rot. (3) Szenario-Liste: Ein Klick auf die Zeile öffnet die Matrix (der «Matrix»-Knopf entfällt), dazu je Zeile Kopie und Löschen; die Kopiervorlage ist damit frei wählbar und nicht mehr auf das Basisszenario festgelegt. Nach einer Löschung lädt die Liste neu (zeigte vorher den alten Stand). (4) Seitenleiste: Szenarien werden wieder verschachtelt dargestellt (Tiefe = Herkunftskette); «Effektive Werte», «Analysen» und «Berichte» stehen neu bündig zum Knoten «Szenarien» statt auf Höhe der einzelnen Szenarien. (5) CSV-Export vollständig neu (Kap. 3.6.5, neues Modul csv.ts): vier Blöcke – Kopf, Lebensphasen, die ganze Matrix (Elemente × Phasen mit Beginn/Ende und den Übergangs-Entscheiden im Klartext) und Jahreswerte; mit BOM, damit Excel die Umlaute erkennt. Vorher enthielt die Datei kein einziges finanzielles Element. (6) Grafiken: Ein Szenario-Wähler gilt neu für alle drei Grafiken (vorher nur der Vermögensverlauf, und der nur additiv). Der Vergleichs-Fehler ist behoben: WealthChart benutzte den Szenario-Namen als Datenschlüssel, wodurch sich gleichnamige Szenarien gegenseitig überschrieben (Legende zeigte beide, der Chart nur eine) – neu die ID; die stille Deckelung auf vier Serien entfällt. Die Legende ist eigenständig, erlaubt eine freie Farbwahl je Serie und erklärt den Linienstil (gestrichelt = Plan, durchgezogen = effektiv). Die Vermögensaufteilung ist neu eine gestapelte Fläche über alle Planjahre plus ein Ring für die relative Aufteilung zu einem wählbaren Zeitpunkt (vorher gestapelte Balken je Phase mit schräger Beschriftung). Alle Diagrammfarben kommen aus neuen Theme-Tokens (--chart-1 … --chart-6, --chart-grid) statt fester Hex-Werte. (7) Dokumentation nachgezogen: Die Kapitel 2.1, 3.2.2–3.2.7 und 3.10 beschrieben noch den Stand vor V7 (Grundprofil am Szenario, parentPlanId, Scenario.startYear, window.confirm, drei Sidebar-Unterpunkte). (8) Nebenbei: verstümmelte Hex-Farbe --danger-soft im Warm-Schema repariert, deutsche Plural-/Umlautfehler in den Übersichts-Kacheln, Dateiname des CSV-Exports transliteriert Umlaute statt sie zu _ zu machen. 8 Tests ergänzt (267 → 275). |
| 0.30 | 2026-07-24 | Claude (Opus 4.8) | Tour-Korrekturen und 3a-Verschiebung. (1) Der Tour-Spotlight wird neu aus vier fixed-Flächen um die Bounding-Box des Ziels gezeichnet (Kap. 9.24) statt aus einem box-shadow-Trick. Grund: Der Schatten liess sticky Matrix-Köpfe (hoher z-index) hell durchscheinen und wurde im Matrix-Scrollbereich abgeschnitten (dann blieb fast alles hell). Die vier Flächen funktionieren unabhängig von z-index und overflow und folgen dem Ziel per requestAnimationFrame. (2) Tour-Schritte überarbeitet: neu erklärt sind Zeitachse und Endvermögen; der vormals «Analysen»-Schritt beschreibt jetzt korrekt die obere Funktions-Leiste (die Analyse-Werkzeuge sind dort nicht mehr), und ein neuer Schritt zeigt das linke Menü (Analysen, Berichte, Effektive Werte). Unsichtbare Ziele (z. B. das Menü auf schmalen Screens) werden übersprungen. (3) Der Schalter «Selbstständig – grosse Säule 3a» wandert im Assistenten von Schritt 4 zu Schritt 5, weil er die 3a-Einzahlung (also die Sparraten-Verteilung) betrifft. Kein Eingriff in den Rechenkern; 267 Tests unverändert grün. |
| 0.29 | 2026-07-24 | Claude (Opus 4.8) | Modul-Review 2, Feinschliff (Assistent, Tour, Ansicht). (1) Assistent: neuer Willkommens-Screen vor Schritt 1 mit dem Gesamtbild der fünf Schritte, danach eine persistente Schritt-Leiste (links im breiten Modal, mobil als Fortschrittsbalken) – der aktuelle Schritt hervorgehoben, erledigte mit Haken, kommende gedämpft. Schritt «Vorsorge & Vermögen» fragt neu auch die erwartete Rendite bei PK, 3a und Wertschriften ab (vorher fest verdrahtet). Im Schritt «Sparen & Verteilen» rechnet die Sparquote-Vorschau Hypothekarzinsen, die als «noch nicht in den Ausgaben» markiert sind, korrekt zu den Ausgaben dazu (wie der Rechenkern bei interestHandling: ADD); der noch nicht verteilte Rest steht neu prominent oben (zwischen PK und Verteilung), und die Verteilung ist nach Gemeinsam / Person A / Person B gruppiert. (2) Tour (Kap. 9.24): statt der bisher bewusst schlichten Hervorhebung jetzt ein Spotlight – der Rest der Ansicht wird abgedunkelt (ein 9999-px-Kastenschatten, kein separates Overlay), der pulsierende Rahmen ist deutlich stärker, die Karte ist grösser und springt auf die dem Ziel gegenüberliegende Bildschirmhälfte (verdeckt es nie); neu mit «Überspringen»-Knopf. (3) Szenario-Ansicht (Kap. 3.7.7): Grundprofil, Zeitachse und die Kennzahlen (Endvermögen nominal + real, Ruinalter) bilden neu einen kompakten Block aus drei Spalten (25 / 50 / 25 %) statt zweier über die ganze Breite gezogener Zeilen. Neue Modal-Grösse xwide. Kein Eingriff in den Rechenkern; 267 Tests unverändert grün. |
| 0.28 | 2026-07-24 | Claude (Opus 4.8) | Modul-Review 2 (Onboarding & Ansicht): Layout-Umbau, Tour und Assistenten-Redesign. (1) Szenario-Ansicht neu geordnet (Kap. 3.7.7): oben eine schlanke Funktions-Leiste (Änderungshistorie · Tour · Neues Szenario · Rechenwege · CSV-Export · Löschen · Abweichungs-Badge), darunter Nächste Schritte → Grundprofil → Zeitachse → Anzeige-Umschalter → Matrix. Grafiken, Effektive Werte, Live-Simulation, Monte-Carlo und Einflussfaktoren sind aus der Leiste entfernt – sie laufen über die eigenen Menüpunkte (Analysen / Effektive Werte). (2) Tour liegt neu in AppShell (statt PlanView) und startet nach jeder Plan-Erstellung – Assistent, Beispielplan und leerer Plan – unabhängig davon, ob sie schon einmal beendet wurde (Kap. 3.7.8); die Erfolgsmeldung hält damit ihr Versprechen. (3) Assistent durchgehend in Du-Form im Einzelmodus (Paarmodus weiter «ihr» / je Person). (4) Schritt-Redesign (Kap. 3.2.8): Schritt «Vorsorge & Vermögen» erfasst nur noch Bestandswerte; ein neuer Schritt «Sparen & Verteilen» zeigt die Sparquote (Nettoeinkommen − Ausgaben) und lässt sie auf 3a, Wertschriften, Amortisation und Schuldtilgung verteilen – der Rest bleibt sichtbar auf dem Cash-Konto. Die PK-Einzahlung steht dort bewusst separat, mit dem Hinweis, dass sie vom Bruttolohn kommt und die Sparquote nicht schmälert (deckt sich mit dem Rechenkern). Immobilien fragen im Assistenten neu Wertsteigerung und den Zins-in-Ausgaben-Schalter ab; der Lohn bekommt 1 % Default-Erhöhung. (5) «Grosse Säule 3a» für Selbstständige (Roadmap-Feedback): ein Schalter am 3a-Element (und im Assistenten) hebt die Beitrags-Obergrenze von 7’258 auf ca. 36’288 CHF an (neue Konstante PILLAR_3A_MAX_SELF_EMPLOYED, 2026-Wert zu verifizieren; neues Feld selfEmployed3a). (6) Nebenbei: POST /plans liefert bei Validierungsfehlern eine lesbare Meldung statt eines rohen zod-Objekts. Keine Änderung am Rechenkern; 267 Tests unverändert grün. |
| 0.27 | 2026-07-24 | Claude (Opus 4.8) | Modul-Review 1 (Zugang & App-Rahmen): fünf Nachbesserungen an der Authentifizierung. Ergebnis der ersten gemeinsamen Test- und Review-Runde. (1) Timing-Ausgleich beim Login: Ein unbekannter Benutzer wird neu gegen einen Dummy-bcrypt-Hash geprüft, damit die Antwortzeit dieselbe ist wie bei einem bekannten -- vorher liess sich aus der Dauer ablesen, ob ein Benutzername existiert (Kap. 3.1.2). (2) Zurück-Knopf nach Logout: Die aus dem Browser-Cache (bfcache) zurückgeholte, eingefrorene Ansicht prüft neu beim pageshow die Session und leitet ohne Anmeldung sofort auf /login -- vorher blieb die alte Ansicht sichtbar (Kap. 3.1.3). (3) Rate-Limiting: Login (10/15 min je IP), Registrierung (5/h je IP) und Passwortänderung (10/15 min je Benutzer) sind gegen Durchprobieren gebremst; neues In-Memory-Modul rate-limit.ts, 429 mit Retry-After (Kap. 3.1.6). Der Login-Zähler läuft je IP und Benutzername -- ein Konto sperrt nicht die anderen Konten derselben IP. (4) Passwort-Dialog läuft neu über die zentrale Modal-Komponente und schliesst damit auf Esc (Fokus-Falle, aria-modal inklusive) -- war zuvor von Hand gebaut (Kap. 3.1.4). (5) Registrierungs-Fehler werden sauber getrennt: nur der belegte Benutzername ist ein 409 mit freundlicher Meldung, jeder andere Fehler ein 500 statt einer rohen Prisma-Meldung als Konflikt. 6 Tests ergänzt (261 → 267). Offen für Roadmap #34: kein Passwort-Längen-Maximum (bcrypt-72-Byte-Grenze), keine ARIA-Labels auf Login/Palette. |
| 0.26 | 2026-07-21 | Claude (Opus 4.8) | Pensionsalter anpassen (Roadmap Nr. 44, neue Kapitel 3.12, 4.4.7 und 4.16). Bisher war das Pensionsalter faktisch unantastbar: Es bestimmt, wo eine Lebensphase endet – ein frei geändertes Alter hätte die Phasengrenze zerrissen. Neu wird nicht das Alter geändert, sondern die Grenze verschoben: Die Phase davor wird länger, die danach kürzer, die Gesamtdauer bleibt gleich. Der Spielraum endet dort, wo eine angrenzende Phase unter ein Jahr fiele; ein Schritt weiter entfällt sie ganz, was vorher bestätigt wird, weil dabei zwei Übergänge zusammengelegt werden (bereits getroffene Entscheide bleiben, leere Felder werden aus dem entfallenden Übergang ergänzt, einmalige Cash-Beträge werden addiert – der Steuersatz betragsgewichtet). Das Pensionsalter ist damit auch Treiber im Tornado und Regler in der Live-Simulation. AHV-Referenzalter (4.4.7): Die Rente beginnt neu immer mit 65, unabhängig vom Pensionsalter – wer länger arbeitet, erhält sie zusätzlich zum Lohn; wer früher aufhört, zahlt bis 65 einen Beitrag als Nichterwerbstätige(r), der als laufende Ausgabe auf die Verzehrquote schlägt und mit 65 wegfällt (neues Feld ahvContribution, ohne Default, Hilfetext nennt die reale Bandbreite von rund 530 bis 26'500 CHF pro Jahr). Beide Wechsel können innerhalb einer Phase liegen, die AHV wird deshalb jahresweise statt phasenweise gerechnet. Punkt B: Ein Einkommen, das einer Person zugeordnet ist, fällt bei deren Pensionierung auf 0 – bisher lief der Lohn stillschweigend in die Pension weiter. Gemeinsame Einkommen (Mieterträge o. Ä.) bleiben; ein ausdrücklich erfasster Betrag gewinnt, damit ein Teilzeitpensum modellierbar bleibt. Punkt A: Wiederkehr-Parameter (Raten, Beiträge, Amortisation, Wertsteigerung, Zinssatz) werden neu live aus der Vorphase geerbt statt beim Anlegen der Phase kopiert – sichtbar als angehaktes «Aus Vorphase übernehmen» je Feld. Vorher blieb die Kopie stehen, wenn man die Vorphase später änderte. Punkt C: Der Kapitalzufluss am Pensions-Übergang (PK, 3a, Verkaufserlös) lässt sich in Prozent auf Amortisation, Anlage und Cash aufteilen – bewusst nicht in Franken, weil sich der Betrag mit dem Pensionsalter ändert und eine Quote mitskaliert. Nebenbei ein echter Fehler behoben: Der fortgeschriebene Basiswert für Einkommen und Ausgaben wurde vor der Jahresschleife berechnet – effektive Werte kamen dadurch nie in der Folgephase an. Neues Modul retirement.ts, neuer Endpunkt POST /api/scenarios/<id>/retirement; 40 Tests ergänzt (221 → 261). |
| 0.25 | 2026-07-21 | Claude (Opus 4.8) | PDF-Berichte (Roadmap Nr. 11, neues Kapitel 3.11). Neuer Unterpunkt «Berichte» auf Plan-Ebene: Liste der erzeugten Berichte plus Assistent zum Anlegen (Titel, Notiz, nominal oder real, Plan- oder effektive Daten, bis zu drei Szenarien, beliebige gespeicherte Analysen). Das Layout ist immer gleich; die Auswahl bestimmt nur, welche Bausteine erscheinen: Deckblatt mit Zusammenfassung und drei Kernaussagen, dann je Szenario Kennzahlen, Vermögensverlauf, Lebensphasen und Annahmen, danach Vergleich, Plan/Ist, Analysen und die Hinweise. Die PDF-Datei wird als Datei abgelegt (BYTEA in Postgres, nicht im Container-Dateisystem, das jeder Deploy neu baut): Ein Bericht muss in drei Jahren byte-identisch wieder herunterladbar sein – eine Neuerzeugung könnte das nach Änderungen an Plan, Rechenkern oder Layout nicht garantieren. Kennzahlen je Szenario: Endvermögen, Kapitalreichweite, Vermögen und Vorsorgekapital bei Pensionierung, AHV- und PK-Rente sowie die offenen Entscheide – die einzige unmittelbar handlungsleitende Zahl. Damit Bericht und Matrix nie verschiedene Zahlen nennen, liegt deren Zählung neu als reine Funktion in decisions.ts, die beide benutzen. Zu jeder Kennzahl steht ihre Grundlage als kurzer Verweis; die vollständigen Annahmen (Startwerte, Renditen, Raten je Element) stehen einmal je Szenario, statt bei jeder Kennzahl wiederholt zu werden. Ein Haftungsausschluss ist verpflichtend und durch einen Test gesichert – ein formal gesetztes PDF wird sonst als Beratung gelesen. Technik: pdfkit in der Node-Runtime statt Headless-Browser (kein Chromium im Image); @react-pdf/renderer schied aus, weil es mit React 19 / Next 16 bricht. Diagramme entstehen als echte Vektoren aus den gespeicherten Zahlen – genau dafür wurden die Analysen in 0.24 als Zahlen und nicht als Bilder abgelegt. pdfkit ist als externes Paket deklariert, weil es Font-Metriken über Dateipfade lädt und gebündelt erst in der Produktion bräche. Neue Tabelle Report, Endpunkte unter /api/plans/<id>/reports, neue Module report.ts, report-pdf.ts, decisions.ts; 9 Tests ergänzt (212 → 221). |
| 0.24 | 2026-07-20 | Claude (Opus 4.8) | Navigation auf Plan-Ebene und gespeicherte Analysen (neues Kapitel 3.10). Die Seitenleiste ist neu zweistufig: Unter jedem Plan liegen die Unterpunkte Szenarien, Effektive Werte und Analysen; ein Klick auf den Plan-Namen öffnet ein Plan-Dashboard (Kennzahlen – Haushaltsdaten direkt, gerechnete Werte ausdrücklich «laut Basisszenario», dazu die Ist-Abweichung, falls erfasst). Die Szenario-Liste zeigt je Szenario Version, Elementzahl, Endvermögen und Ruinalter mit den Aktionen Historie und Matrix; das Basisszenario ist hervorgehoben, der Szenario-Baum in der Seitenleiste bleibt daneben erhalten. Die Analysen-Ansicht bietet vier umklappende Kacheln (Grafiken, Live-Simulation, Monte-Carlo, Einflussfaktoren – Umklappen auch per Antippen für Touch). Grafiken öffnen neu nicht mehr alle drei Diagramme, sondern lassen zuerst eines wählen; der Szenario-Vergleich zieht mit zu den Grafiken, der CSV-Export auf die Matrix. Gespeicherte Analysen: Jede Grafik, MC-Simulation und Einflussfaktoren-Berechnung kann festgehalten werden – als Zahlen, nicht als Bild (read-only, es wird nichts neu gerechnet). Das hält den Datensatz klein und macht ihn druckfähig: Der spätere PDF-Bericht (Roadmap Nr. 11) zeichnet daraus vektoriell in Druckauflösung, was ein Screenshot nicht könnte. Bewusst nicht gespeichert wird finalWealthSorted (megabyteweise). Jedes Werkzeug schreibt sein Ergebnis in dieselbe generische Form, sodass eine Nur-Lese-Ansicht genügt. Neue Tabelle SavedAnalysis, neue Endpunkte unter /api/plans/<id>/analyses und /dashboard; neue Komponenten PlanViews, SavedAnalysisView, SaveAnalysisButton, neues Modul analyses.ts. Kein Eingriff in den Rechenkern; Testbestand 212 (Migration um SavedAnalysis erweitert). |
| 0.23 | 2026-07-20 | Claude (Opus 4.8) | V7: Der Haushalt liegt am Plan, die Annahmen am Szenario. Haushaltsform, Personen (Name, Alter) und Planstartjahr wandern vom Szenario auf den Plan; das Pensionsalter bleibt szenario-eigen (es ist der Kern jedes Früh-/Spätpensionierungs-Szenarios), ebenso Inflation und Cash-Anfangswert. Begründung: Diese Angaben beschreiben den Haushalt, nicht eine Planungsvariante – unterscheiden sie sich, ist es ein anderer Plan, kein anderes Szenario. Neue Tabelle PlanPerson (Rolle, Name, Alter je Plan); Person behält nur noch Rolle und Pensionsalter; Plan bekommt householdType und startYear. Der Rechenkern bleibt unberührt: toPlanInput() fügt Plan- und Szenario-Ebene wieder zu einem unveränderten PlanInput zusammen, die 43 Golden Tests laufen durch. Nebeneffekt, der ein reales Problem löst: Weil das Startjahr nun plan-weit ist, landet ein erfasster Ist-Satz für 2031 in allen Szenarien zwingend auf demselben Planjahr – vorher war das nicht garantiert. Das Wiederherstellen einer Szenario-Version setzt folgerichtig nur noch das Szenario-Eigene zurück; plan-weite Angaben über eine Version eines Szenarios zu überschreiben, hätte die übrigen stillschweigend mitverändert. Der Profil-Dialog kennzeichnet neu je Feld, ob es plan-weit oder nur dieses Szenario gilt. Die Migration übernimmt die Werte aus dem Basisszenario; zwei neue Tests spielen dafür echte V6-Daten ein und prüfen die Übernahme inkl. abweichender Nebenszenarien (210 → 212). |
| 0.22 | 2026-07-20 | Claude (Opus 4.8) | Fehlerbehebung: Cash-Vorbelegung im Ist-Wizard. Der Wizard für die effektiven Werte zeigte als geplanten Cash-Bestand den Stand am Phasenende statt am gewählten Stichtag -- in einer Phase von 2026 bis 2036 also für 2031 den Wert von 2036. Ursache: Der Dialog las cashBridge.cashEnd, weil computePlan den Cash-Bestand bisher nur je Phase auswies. YearPoint trägt neu ein Feld cash (Stand am Jahresende), analog zu wealthNominal; der Wizard liest daraus. Die Vorbelegung der ELEMENTE war nie betroffen -- die stammte schon immer aus dem Jahresverlauf. Zwei Regressionstests decken den gemeldeten Fall ab (208 -> 210). Rein additiv, die 43 Golden Tests laufen unverändert. |
| 0.21 | 2026-07-20 | Claude (Opus 4.8) | Effektive Werte / Plan-Ist-Vergleich (Roadmap Nr. 5, neue Kapitel 3.9 und 9.29). Macht aus dem Planer ein Monitoring-Werkzeug. Neuer Knopf «Effektive Werte» auf Plan-Ebene: Liste der Erfassungen plus Wizard in zwei Schritten (Stichtag, dann alle Elemente aller Szenarien inkl. Cash, Einkommen und Ausgaben, vorbelegt mit dem Planwert für dieses Jahr). Ein Ist-Satz hängt am Plan, nicht am Szenario – die Wirklichkeit ist dieselbe, egal wogegen man sie hält; die Zuordnung läuft über die Herkunfts-Kette sourceElementId. Das exakte Datum steht in Liste und Zeitachse, für die Rechnung zählt nur die Jahreszahl. Zweiter Rechenlauf: computePlan nimmt neu { actuals }; die Werte schnappen in jedem erfassten Jahr auf die Realität und laufen von dort planmässig weiter (Lücken fallen auf die Plandaten zurück). Ohne die Option verhält sich die Funktion exakt wie bisher – die 43 Golden Tests laufen unverändert. Der Sprung wird als eigene Brückenposition actualsCorrection geführt (Vermögens- und Cash-Brücke): Eine Planabweichung ist keine Rendite, und ohne diese Zeile ginge die Zerlegung im Ist-Jahr nicht mehr auf. Matrix: zweiter Umschalter «Plan» / «Effektiv»; im Ist-Modus steht neben dem Wert die Abweichung zum Plan, farbig – bewusst kein «beide», das wären mit nominal/real acht Zahlen je Zelle (Begründung 9.29). Zeitachse: Marker je erfasstem Jahr, der jüngste farbig, ältere blass. Alle vier Analysewerkzeuge erhalten eine einheitliche Leiste (nominal/real als Einfachauswahl, Plan/Effektiv); im Vermögensverlauf kommt die Planlinie gestrichelt als Referenz dazu, max. vier Serien. Monte-Carlo: Der Zielbetrag dreht mit real/nominal mit und wird entsprechend beschriftet; eine Zeile weist aus, ab welchem Jahr simuliert wird – die Jahre davor sind durch Ist-Werte belegt und werden nicht gewürfelt. Das Startjahr ist abgeleitet, nicht eingebbar: Ein frei gesetztes Jahr würde Jahre als sicher behandeln, die nie erfasst wurden. Ein Ist-Satz erzeugt keine Szenario-Version – er ist eine Beobachtung, keine Planänderung. Neue Tabelle ActualsSet (Migration gegen echtes Postgres verifiziert), neue Endpunkte unter /api/plans/<id>/actuals, neue Module actuals.ts und dataview.ts; 27 Tests ergänzt (181 → 208). |
| 0.20 | 2026-07-19 | Claude (Opus 4.8) | Raten über Lebensphasen übernehmen (neues Kapitel 3.6.11) und Rate in der Verlaufsgrafik. (1) Ändert man ein Ratenfeld, fragt das Bearbeitungspanel neu nach der Reichweite: nur diese Phase (Vorgabe, bisheriges Verhalten), diese + folgende oder alle Phasen. Anlass war, dass eine geänderte Rendite bisher nur für die eine Phase galt und viermal eingetippt werden musste. Als Ratenfelder gelten expectedReturn (PK, 3a, Sonstiges Vermögen), valueGrowth und interestRate (Immobilie) sowie teuerungsausgleich (Einkommen, Ausgaben); AHV und Schulden haben keine. Die Rückfrage erscheint inline und erst beim Speichern wirksam, nicht als Modal – das Zahlenfeld löst bei jedem Tastendruck aus, ein Dialog erschiene bei «5.2» viermal. Sie erscheint nur bei tatsächlich veränderten Raten («nicht gesetzt» und 0 gelten als gleich). Beim Übertragen bleiben die übrigen Werte der Zielphasen erhalten – der Endpunkt ersetzt den ganzen Werte-Satz, ein blosses Kopieren des Entwurfs hätte dort Beträge, Sparraten und Bezüge gelöscht (durch Test abgesichert). Phasen, in denen der Wert schon stimmt, werden übersprungen. Kein neuer Schreibpfad: ein PUT je Zielphase über den bestehenden Endpunkt, alle in einer Bearbeitungssitzung und damit in einer Nebenversion. (2) ElementYearPoint führt neu ein Feld rate mit – additiv, es wird nur durchgereicht, was die Rechnung ohnehin benutzt; die 43 Golden Tests laufen unverändert. Die Verlaufsgrafik der Element-Detailansicht zeigt die Rate damit auf einer zweiten Y-Achse rechts in Prozent, als Stufenlinie (innerhalb einer Phase konstant, Sprung an der Phasengrenze). Neues Modul ratefields.ts; 17 Tests ergänzt (164 → 181). Keine DB- oder API-Änderung. |
| 0.19 | 2026-07-19 | Claude (Opus 4.8) | Versionierung und Änderungshistorie je Szenario (neues Kapitel 3.8). Jedes Szenario trägt eine Version A.B: B entsteht automatisch, A manuell mit Pflichtkommentar. Der zentrale Entwurfsentscheid: FPT hat keinen Speichern-Knopf – jede Änderung schreibt sofort, ein Assistenten-Durchlauf macht ~14 Schreibvorgänge, ein Verteil-Klick einen je Zielelement. Eine Version je Schreibvorgang wäre ein Tastenprotokoll gewesen; stattdessen werden alle Schreibvorgänge innerhalb von 10 Minuten zu einer Nebenversion zusammengefasst, inhaltlich unveränderte Stände erzeugen gar keine, und verschiedene Benutzer laufen nie in einer Version zusammen. Eine Version hält den vollständigen Zustand als JSON in der Form PlanInput – dadurch ist die Versionsauswahl in allen vier Analysewerkzeugen (Grafiken, Live-Simulation, Monte-Carlo, Einflussfaktoren) fast kostenlos; bei Monte-Carlo je Szenario einzeln, weil dort mehrere gleichzeitig laufen. Wiederherstellen ist ungefährlich gebaut: Es legt den zurückgesetzten Stand selbst als neue Version an («Wiederhergestellt aus A.B»), löscht also nichts, und erhält die IDs von Phasen und Elementen – sonst verlören alle Kind-Szenarien ihre Diff-Basis und zeigten schlagartig alles als «neu». Wo ein Bezug trotzdem bricht (der alte Stand kannte das Element noch nicht), warnt der Dialog vorher namentlich. Die destruktive Logik liegt als reine Funktion planRestore vor und ist dort getestet; versioning-db.ts führt sie nur aus. Ein statischer Wächter-Test liest alle Route-Dateien und verlangt, dass jeder schreibende Endpunkt eine Version auslöst – eine vergessene Stelle wäre eine stille Lücke. Neue Tabelle ScenarioVersion + Scenario.currentMajor (Migration gegen echtes Postgres verifiziert), neue Endpunkte unter /api/scenarios/<id>/versions. Neue Kapitel 3.8 und 9.28; 25 Tests ergänzt (139 → 164). |
| 0.18 | 2026-07-19 | Claude (Opus 4.8) | Live-Simulation (Roadmap Nr. 22). Neuer Button und Dialog als Zweispalter: links Schieberegler, rechts eine wählbare Grafik, darüber eine Kennzahlenleiste. Dreht man an einem Regler, wird der Plan sofort neu gerechnet – ohne für jede Variante eine Szenario-Kopie anzulegen. Keine eigene Rechenlogik: Die Regler benutzen dieselben Transformationen wie der Tornado (applyDriver), können also gar nicht etwas anderes zeigen als die Einflussfaktoren-Analyse. Neu ist nur applyElementDriver – dieselbe Verschiebung auf ein einzelnes Element statt auf eine ganze Kategorie: Standardmässig gibt es einen Sammelregler «Rendite», ein Klick auf «Renditen einzeln aufschlüsseln» ersetzt ihn durch je einen Regler pro Anlage (der Sammelregler wird dabei entfernt, nicht ergänzt, sonst zählte eine Bewegung doppelt; ein Test sichert ab, dass beide Ansichten denselben Plan beschreiben). Der unveränderte Plan wird als Referenzlinie mitgezeichnet, und die Kennzahlenleiste weist Endvermögen nominal/real mit Differenz zum Plan aus sowie – als eigene Karte – ob das Kapital reicht; ein gekippter Plan ist einer Verlaufslinie sonst nicht anzusehen. Gemessene Laufzeit von computePlan: 0.2 ms auf einem 60-Jahres-Plan mit 10 Elementen, also rund 1 % des 16-ms-Frame-Budgets – deshalb wird synchron gerechnet, ohne Debounce und ohne Worker. Anders als der Tornado haben die Regler Standardbereiche (Begründung des scheinbaren Widerspruchs zu 9.18: neues Kapitel 9.27), beide Enden editierbar. Das Pensionsalter fehlt weiterhin (9.18, eigener Roadmap-Punkt); «Als Szenario speichern» ist bewusst zurückgestellt, ersatzweise zeigt der Dialog die aktive Einstellung als lesbare Zeile. Die Vermögensaufteilung wurde als AllocationChart aus dem Dashboard herausgelöst, damit beide sie nutzen. Neue Kapitel 4.15 und 9.27; 15 Tests ergänzt (124 → 139). Keine API-, DB- oder Schreib-Änderung – das Feature liest ausschliesslich. Nebenbei dieselbe vom Umlaut-Sweep verstümmelte Hex-Farbe (#7c3aed) wie in 0.17, diesmal im Dashboard. |
| 0.17 | 2026-07-19 | Claude (Opus 4.8) | Monte-Carlo: zwei Welten, vier Fälle. Behebt einen Darstellungs-Widerspruch: Zuvor konnte «Planung 69 % erreicht» neben «Ziel 3 Mio nur 41 %» stehen, obwohl 3 Mio unter dem Plan-Endbetrag von 3.7 Mio lag – die beiden Zahlen stammten aus verschiedenen simulierten Welten. Neu läuft die Simulation immer zweimal (historische Renditen / geplante Werte, gemeinsamer Seed) und liest aus jeder Verteilung beide Schwellen ab: Plan-Endbetrag und Zielbetrag. Fall 1 und Fall 3 stammen damit aus derselben Verteilung, wodurch ein tieferes Ziel nie unwahrscheinlicher sein kann als ein höheres – der Widerspruch ist strukturell ausgeschlossen (Test). Zweite Korrektur: Der Nullpunkt für das Urteil ist nicht 50 %, sondern Fall 2 (derselbe Schwellwert in der eigenen geplanten Welt); durch den Volatilitäts-Drag liegt der je nach Streuung bei 27–48 %. Verglichen wird Fall 1 gegen Fall 2 mit ± 5 pp Toleranzband → «zurückhaltend / realistisch / zu optimistisch». Darstellung: Fall 1 prominent mit Urteil, Fall 3+4 als Satzpaar untergeordnet, Fall 2 und die Mediane klein als Referenz. Der Drei-Wege-Umschalter aus 0.16 entfällt; historische Mittelwerte und Zielbetrag sind jetzt beide Pflicht. Technisch: MonteCarloResult.finalWealthSorted (alle Endvermögen sortiert) plus neuer Helfer probabilityAtLeast (Binärsuche) – vier Zahlen aus zwei Läufen statt vier Läufen. Kapitel 4.12.7 und 9.26 neu gefasst; 3 Tests ergänzt (121 → 124). Keine Änderung am Rechenkern. |
| 0.16 | 2026-07-19 | Claude (Opus 4.8) | Monte-Carlo mit zwei Fragestellungen (Roadmap Nr. 46). Ein Umschalter oben trennt: «Planung prüfen» (Fall 1, wie bisher) würfelt um die historischen Renditen und prüft gegen den Planungs-Endbetrag (read-only) – «wie realistisch ist meine Planung?». «Ziel prüfen» (Fall 2, neu) würfelt um die geplanten Werte aus dem Plan und prüft gegen einen manuellen Zielbetrag – «erreiche ich mein Ziel?»; hier sind keine historischen Mittelwerte nötig, nur die Streuung. «Beides» rechnet beide Durchgänge gleichzeitig (gemeinsamer Seed). Neue Ergebnis-Deutungstexte je Fall (weich formuliert wegen des Volatilitäts-Drags). Der Fächer stammt aus dem historischen Durchgang; der Zielbetrag ist in Fall 2 einer für alle Szenarien. runMonteCarloMulti nimmt neu die Inflation je Szenario (inflationMeanFor); neuer Helfer plannedReturnOf. Kapitel 4.12 überarbeitet, 4.12.7 und 9.26 neu; 8 Tests ergänzt (119 → 121). Nebenbei eine vom Umlaut-Sweep verstümmelte Hex-Farbe (#7c3aed) korrigiert. Keine Änderung am Rechenkern. |
| 0.15 | 2026-07-19 | Claude (Opus 4.8) | Plan-Assistent überarbeitet (Schritt 2 und 4). Rein an der Oberfläche, keine Änderung an Berechnung, Datenmodell oder API. (Schritt 2 – Lebensphasen): Die Lebenslinie zerfällt neu an den fixen Pensionierungszeitpunkten in Abschnitte (neues reines Modul phaseplan.ts, planSegments): Erwerb (alle arbeiten), Misch (eine pensioniert, eine arbeitet), Pension (alle pensioniert) – jeweils mit kurzer Definition. In den durch eine Pensionierung fest begrenzten Abschnitten verteilt der Nutzer beliebig viele Phasen mit +/Papierkorb und eigenem Namen je Phase; eine Live-Summe erzwingt, dass die Phasendauern exakt aufgehen («Weiter» ist bis dahin gesperrt). Der letzte Pensions-Abschnitt ist offen (Lebensdauer frei). Die Anzahl Abschnitte wird abgeleitet – Einzelplan: 2 (Erwerb, Pension); Paar mit unterschiedlichem Pensionsalter: 3. Neue Zeitachse mit Pensionierungs-Flaggen und nummerierter Beschriftung unter dem Balken (auch kurze Phasen bleiben lesbar). Behebt den Fehler, dass die Erwerbsphase zuvor beliebig über die Pensionierung hinaus gesetzt werden konnte. (Schritt 4 – Vorsorge & Vermögen): bei Paaren aufgeteilt in Gemeinsam / Person A / Person B; PK und 3a sind je Person, Wertschriften/Wohneigentum/Schulden je Bereich (gemeinsam oder pro Person). Neue Kapitel 3.2.8 überarbeitet; 8 Tests ergänzt (111 → 119). |
| 0.14 | 2026-07-18 | Claude (Opus 4.8) | Phasenkopf überarbeitet und zwei Verteil-Werkzeuge. (1) Zweizeilige Wertdarstellung: Im Anzeigemodus «Beide» steht der Realwert neu in Klammern in einer eigenen Zeile unter dem nominalen Wert statt daneben – im Phasenkopf und in den Matrix-Zellen. Der Pfeil wiederholt sich auf der zweiten Zeile, damit der Bezug Start → Ende erhalten bleibt. Nebeneffekt: Die Spalten werden schmaler, wodurch jede Kennzahl umbruchfrei (whitespace-nowrap) dargestellt werden kann. (2) Die Kennzahl heisst korrekt «Sparquote» bzw. «Verzehrquote» statt «Quote»/«Verzehr». (3) Neuer Block «Verfügbares Kapital» im Phasenkopf (ab Phase 2, nur wenn > 0): der beim Übergang zugeflossene Topf mit «davon verteilt» und «Rest auf Cash». (4) Zwei neue Werkzeuge als eigene Popups: «Kapital verteilen» (Zusatzeinlagen in PK/3a/Vermögen, Sonderamortisation, Sofort-Tilgung) und «Sparquote/Bezug verteilen» (jährliche Raten), beide mit Live-Vorschau über eine erneute computePlan-Rechnung im Browser – die angezeigte Wirkung ist dadurch per Konstruktion exakt die spätere, inklusive aller Kappungen. Der Quoten-Dialog weist neben erstem und letztem Jahr die absolute Quote über die ganze Phase aus und warnt, wenn die Quote über die Phase sinkt. Neues reines Modul distribution.ts. Neue Kapitel 3.6.9, 3.6.10, 9.25; 3.6.1 und 3.6.3 überarbeitet. 8 Tests ergänzt (103 → 111). Keine Änderung an Berechnung, Datenmodell oder API – beide Werkzeuge schreiben ausschliesslich bestehende Felder über bestehende Endpunkte. |
| 0.13 | 2026-07-18 | Claude (Fable 5) | UI-Gesamtumbau – rein an der Oberfläche, Berechnung, Datenmodell und API-Semantik unverändert. (A) Fundament: durchgehende Du-Form und echte Umlaute in allen sichtbaren Texten (inkl. API-Fehlermeldungen); neue UI-Primitiven in ui.tsx (Button, Modal mit ESC/Fokus-Falle/Animation, Bestätigungs-Dialog statt window.confirm, Toasts statt alert, Skeleton-Loader, EmptyState); eigene Attention-Farbe (Amber) für offene Entscheide, getrennt vom Akzent; Micro-Interactions mit prefers-reduced-motion-Fallback. (B) Onboarding (Roadmap Nr. 10): geführter Plan-Assistent in fünf Schritten (reine Orchestrierung bestehender Endpunkte, Einkommen bewusst pro Person – räumt die 9.9-Falle aus), Beispielplan mit einem Klick (Übergänge absichtlich offen – die Ampel lehrt sich selbst), interaktive Tour über die Planansicht, abgeleitete «Nächste Schritte»-Karte. (C) Struktur: Einzel-Bearbeitungen laufen neu über ein rechtes Inspector-Panel statt Modals (Matrix bleibt sichtbar; Klick auf andere Zelle wechselt den Inhalt); Phasenkopf entschlackt auf vier Kern-Infos (Rest wohnt in der Detailansicht aus 0.11); Matrix mit eigenem Scrollbereich und beidachsig fixierten Köpfen; Sidebar-Gruppen «Meine Pläne»/«Wissen» («So rechnet FPT», Systemparameter); Terminologie-Fix «Szenario-Profil» statt «Plan-Einstellungen»; Aktions-Icons auch ohne Hover sichtbar (Touch). (D) Extras: Sparklines je Element-Zeile (aus den 0.11-Verlaufswerten, keine Neuberechnung), Befehls-Palette (Ctrl/Cmd+K), Ruin-Banner verlinkt auf die Einflussfaktoren. Neue Kapitel 3.2.8, 3.7.6–3.7.9, 9.23, 9.24; 9.17 bereinigt (der Selection-Rest und der ProfileMenu-Lint-Fehler sind behoben – npm run lint ist erstmals fehlerfrei). Testbestand unverändert 103. |
| 0.12 | 2026-07-18 | Claude (Opus 4.8) | Lesbarkeit der Wasserfälle, Verkaufspreis-Abgleich und Erklärung wirkungsloser Tornado-Treiber. (1) Die beiden Wasserfälle werden nicht mehr mit Recharts gezeichnet, sondern als eigene liegende Darstellung: Verbindungslinien zwischen den Balken, Wertbeschriftung an jedem Schritt, Abschnitts-Überschriften („Am Übergang" / „Innerhalb der Phase") und eine aufklappbare Tabelle mit laufendem Zwischenstand. Anlass war, dass die bisherige Darstellung faktisch nicht lesbar war – die Zahlen waren korrekt, die Grafik nicht. (2) Der Restposten beider Brücken wird bei Abweichung neu als Fehlermeldung ausgewiesen statt als beiläufige „Rundungsdifferenz"; eine nicht aufgehende Zerlegung ist ein Rechenfehler und kein Schönheitsproblem. (3) Verkaufspreis einer Immobilie wird beim Wechsel auf „Verkaufen" neu mit dem modellierten Verkehrswert vorbelegt; der Dialog weist Verkehrswert und Abweichung aus und warnt ab 10 % Differenz (Kap. 3.5.8, 9.22). Damit fällt auf, wenn angenommene Wertsteigerung und erwarteter Verkaufspreis nicht zusammenpassen. (4) Der Tornado erklärt neu Nullbalken statt sie stumm zu zeigen – insbesondere den Fall, dass die Immobilien-Wertsteigerung bei einem Verkauf nachweislich wirkungslos ist (ineffectiveReason, Kap. 4.13.5). Neue Kapitel 3.5.8, 4.13.5, 9.22; 11 Tests ergänzt (92 → 103), darunter die Invariante residual === 0 über sieben Plankonstellationen. Keine DB-Änderung, keine Änderung an der Berechnung. |
| 0.11 | 2026-07-18 | Claude (Opus 4.8) | Detailansichten (Roadmap Nr. 43) und vollständige Offenlegung der Berechnungslogiken (Roadmap Nr. 41). (1) Neue Systemparameter-Ansicht in der Seitenleiste: alle fest hinterlegten Grössen mit Wert, Bedeutung, Herleitung, Quelle und Stand – als strukturierte Daten aus constants.ts, also aus derselben Quelle, aus der gerechnet wird. (2) Nur-Lese-Detailansicht je Element und je Lebensphase über ein Expand-Icon: Element mit Verlaufsgrafik über alle Planjahre (dafür führt computePlan neu ElementPhaseComputed.yearly je Element mit), Phase mit Vermögensaufteilung und zwei Wasserfällen. (3) Die Wasserfälle sind bewusst getrennt: Der Vermögens-Wasserfall zeigt nur echte Zu- und Abgänge (Quote, Kapitalerträge, Wertsteigerung, PK-Beiträge, Steuern, Verrentung, Einmalposten); Sparraten, Amortisationen und Investitionen sind Umbuchungen und erscheinen ausschliesslich im Cash-Wasserfall – als Vermögensabgang gezeichnet würden sie einen Verlust vortäuschen, den es nicht gibt. Neue Strukturen WealthBridge / CashBridge inkl. Restposten als Kontrollgrösse. (4) Rechenweg-Protokoll: computePlan(plan, sample?, { explain }) protokolliert die Schritte, die es ohnehin ausführt – Formel, eingesetzte Zahlen, Ergebnis und Hinweis auf geltende Vereinfachungen. Abdeckung über alle Ebenen (Element je Phase, Element je Übergang, Phasen-Kennzahlen, Plan-Ebene). Standardmässig aus, damit die Monte-Carlo-Simulation unberührt bleibt. Jeder Rechenweg verlinkt in das passende Kapitel dieser Spezifikation; ein Test prüft, dass alle Verweise eine existierende Überschrift treffen. Neue Kapitel 3.6.7, 3.6.8, 4.14, 9.20, 9.21; 12 Tests ergänzt (80 → 92). Keine DB-Änderung; die 43 Golden Tests laufen unverändert. |
| 0.10 | 2026-07-18 | Claude (Opus 4.8) | Szenario-Vergleich in der Monte-Carlo-Simulation und Sensitivitätsanalyse / Tornado (Roadmap Nr. 20). (1) Die MC-Simulation rechnet neu mehrere Szenarien desselben Plans in einem Lauf. Die historischen Annahmen werden dabei nur einmal je logischem Element erfasst – die Zuordnung über die Herkunfts-Kette sourceElementId, dieselbe Grundlage wie beim Diff (neue Funktionen resolveRootElementId, buildElementGroups, paramsForScenario, runMonteCarloMulti). Alle Szenarien laufen mit demselben Seed (Common Random Numbers), damit Unterschiede strukturell und nicht zufällig sind. Der Zielbetrag bleibt szenario-eigen (vorbelegt mit dem jeweils geplanten Endvermögen) – nur so misst die Erfolgswahrscheinlichkeit, wie oft ein Szenario sein eigenes Versprechen hält. Ergebnis als Vergleichstabelle plus Median-Linien je Szenario; bei einem einzelnen Szenario unverändert der bisherige Fächer. (2) Neuer Bereich Einflussfaktoren (eigener Button, eigener Dialog) mit einem Tornado-Chart nach dem One-at-a-time-Verfahren: neues reines Modul sensitivity.ts mit sieben Treibern, je Treiber an-/abwählbar und mit pflichtiger, frei definierbarer Bandbreite ohne Default. Das Pensionsalter ist bewusst nicht enthalten (Begründung: 9.18). Neue Kapitel 3.6.6, 4.12.6, 4.13, 9.18; 20 Tests ergänzt (60 → 80). Keine DB-Änderung, keine Verhaltensänderung bestehender Pläne. Ausserdem vier Dokumentationsfehler korrigiert: Kap. 1.2 nannte noch Monte-Carlo, Hypothekarzinsen und Immobilien-Wertsteigerung als nicht umgesetzt (seit 0.5/0.7 vorhanden); Kap. 4.6.5 endete mit einem widersprüchlichen Restsatz zur fehlenden Wertsteigerung; Kap. 5.2/5.3/5.5.2/8.1 waren bei Migrationszahl, Dateiliste, Komponentenliste und Testzahlen veraltet; der Glossar-Eintrag „Szenario" beschrieb noch das Modell vor V6. |
| 0.9 | 2026-07-18 | Claude (Opus 4.8) | UI-Umbau und Planstart. (1) Die Grafiken liegen neu im eigenen Bereich Grafiken (Dialog, Button oben) statt unter der Matrix. (2) Kennzahl Geschätzter Nachlass entfernt – sie war identisch mit dem nominalen Endvermögen. (3) Monte-Carlo-Button nach oben zu den Szenario-Aktionen verschoben. (4) Neues Profilfeld Planstart (Jahr) (Scenario.startYear, Migration; reine Anzeige, Berechnung bleibt in relativen Jahren) – erscheint auf der Zeitachse. (5) Zeitachse zeigt neu die Lebensphasen als Segmente (Breite = Dauer, Einfärbung nach Phasentyp, Jahresspanne). (6) Lebensphase bearbeiten neu als Popup statt Panel unter der Tabelle. (7) Vermögensverlauf über alle Jahre statt nur über die Phasengrenzen – dafür führt computePlan das Vermögen neu pro Jahr mit (YearPoint.wealthNominal/wealthReal). Zwei Tests ergänzt (58 → 60). |
| 0.8 | 2026-07-18 | Claude (Opus 4.8) | Szenario-Hierarchie (V6) – grösste Umstrukturierung bisher. Der Plan ist neu ein schlanker Behälter ohne Finanzdaten; die berechenbare Einheit ist das Szenario, das Grundprofil (inkl. Pensionsalter → Frühpensionierungs-Szenarien), Phasen und Elemente trägt. Jeder Plan erhält beim Anlegen automatisch ein Basisszenario; weitere Szenarien entstehen als vollständige Kopie eines beliebigen Szenarios und hängen als Baum darunter (Sidebar zeigt die Verschachtelung). Kopierte Phasen/Elemente tragen Herkunfts-Verweise (sourcePhaseId, sourceElementId) – darauf beruht die Abweichungs-Markierung: geändert = gelb, neu = grün, entfernt = graue Geisterzeile (eigene Theme-Tokens für alle drei Farbschemata). Charts vergleichen neu die Geschwister-Szenarien. Datenmodell: neue Tabelle Plan, bisheriger Plan → Scenario (IDs erhalten), planId → scenarioId in Person/Phase/FinancialElement. API neu unter /api/scenarios/*. Neue Kapitel 2.1, 3.2, 4.13; 9 Diff-Tests + Migrations-Test (48 → 58). Migration mit echtem Postgres (PGlite) verifiziert, inkl. verschachtelter Szenarien und Cascade. |
| 0.7 | 2026-07-17 | Claude (Opus 4.8) | Monte-Carlo-Simulation (Roadmap Nr. 19, Stufe A). Button in der Planansicht → Dialog mit Erklärung, Eingaben und Ergebnis. Statt einer festen Rendite/Inflation werden tausende Zufallspfade gerechnet; ausgewiesen werden Ruinwahrscheinlichkeit, Erfolgswahrscheinlichkeit (P(Endvermögen ≥ Zielbetrag)) und ein Fächer (10 %/Median/90 %) plus die deterministische Linie. Läuft komplett im Browser (computePlan ist rein). Modell: fettschwänzige Verteilung (Student-t, ν=5), gemeinsamer Marktschock (ρ=0.7), Böden 0 % für PK/3a und −100 % sonst. Pro renditetragendem Element und für die Inflation je: historischer Ø (Pflicht), Streuungsstufe, σ. Neue Datei montecarlo.ts + optionaler sample-Parameter in computePlan (Inflation neu als kumulatives Array; deterministisch identisch). Neues Kapitel 4.12; 7 MC-Tests + Refactor-Absicherung (41 → 48). Keine Verhaltensänderung, keine DB-Änderung. |
| 0.6 | 2026-07-17 | Claude (Opus 4.8) | Teilverkauf von Sonstigem Vermögen (Roadmap Nr. 42) und Sonderamortisation der Hypothek (Roadmap Nr. 15). OTHER_ASSET am Übergang neu: Halten / Verkaufen / Teilverkauf – ein Betrag fliesst ins Cash (erscheint als „Kapitalzufluss" im Phasenkopf), der Rest bleibt investiert. REAL_ESTATE im Halten-Fall neu mit Einmaltilgung aus dem Cash (analog zur Sofort-Tilgung bei Schulden; erscheint als „Kapitalinvestition"). Damit lässt sich die indirekte Amortisation via 3a mechanisch nachbilden. Die eigentliche Roadmap Nr. 15 (Steuerwirkung der indirekten Amortisation) bleibt mit dem Steuer-Bündel 13/14/23 zurückgestellt – siehe 9.14. Kapitel 4.9.3/4.9.4 ergänzt. Fünf Regressionstests (36 → 41). Keine Verhaltensänderung für bestehende Pläne. |
| 0.5 | 2026-07-17 | Claude (Opus 4.8) | Netto/Brutto geklärt (Roadmap Nr. 9, reduziert) und Immobilien-Modul erweitert (Roadmap Nr. 8). Einkommen ist neu explizit als Nettolohn definiert (Label und Hilfetext); für die AHV rechnet das Tool intern mit AHV_GROSS_FROM_NET_FACTOR = 1.12 auf den Bruttolohn hoch – die AHV bemisst sich am Brutto, die bisherige Netto-Basis unterschätzte die Rente um bis zu ~1'900/Jahr. Immobilie neu mit Hypothekarzins (% der Restschuld, sinkt mit der Amortisation, mit Doppelzählungs-Schalter) und Wertsteigerung (auf die Liegenschaft, nicht auf das Eigenkapital – Hebeleffekt). Grundstückgewinnsteuer bemisst sich neu explizit am ursprünglichen Kaufpreis. Keine Aufschlüsselung bei Einkommen oder Ausgaben, keine Steuerschätzung (Begründung: 9.14). Neues Kapitel 4.4.5; 4.6.5 und 4.9.4 überarbeitet; Abschnitt 9 um zwei Punkte ergänzt. Sechs Regressionstests (30 → 36). Verhaltensänderung: siehe 9.13. |
| 0.4 | 2026-07-17 | Claude (Opus 4.8) | AHV-Rente einkommensabhängig (Roadmap Nr. 3) und Fortschreibungs-Warnhinweis (Roadmap Nr. 4). Die AHV-Rente folgt neu der amtlichen Rentenformel (Skala 44) über das massgebende durchschnittliche Jahreseinkommen statt pauschal der Maximalrente; verifiziert gegen die amtliche Tabelle 318.117.1 (51/51 Zeilen). Prüfung der Beitragskarriere am Pensions-Übergang, mit Zusatzfeldern für die Jahre vor Planbeginn (ab Alter 21). Alles real gerechnet. Neue Konstanten AHV_MIN_MONTHLY_FULL, AHV_PENSION_MONTHS, AHV_CONTRIBUTION_START_AGE; AHV_MAX_ANNUAL_SINGLE neu abgeleitet. Warnhinweis in Phasenzellen und Phasen-Detail, wenn Folgephasen existieren. Neue Kapitel 3.5.6, 4.4; Abschnitt 9 um zwei Punkte ergänzt. Zwölf Regressionstests (18 → 30). Verhaltensänderung: siehe 9.10. |
| 0.3 | 2026-07-16 | Claude (Opus 4.8) | Einmalige Sonderein-/ausgaben umgesetzt (Roadmap Nr. 1). Der Cash-Übergang zwischen zwei Phasen ist neu ein eigener Entscheid: 1:1 übernehmen · einmaliger Zufluss · einmalige Kosten · beides. Beträge gehen direkt aufs Cash-Konto. Zufluss wird nominal erfasst (real angezeigt) mit optionalem Steuersatz (Default 0 %), Kosten werden real erfasst (nominal angezeigt); beide bleiben aus der Sparquote heraus. Der Entscheid startet unbeantwortet und zählt im „offen"-Badge mit. Neue Spalte Phase.cashTransition (JSON) + Migration, neue Route PUT /api/phases/<id>/cash-transition, eigene Kennzahlen im Phasenkopf. Fünf Regressionstests ergänzt (13 → 18). Neue Kapitel 3.5.5, 4.9.6; Abschnitt 9 um zwei Grenzen ergänzt. |
| 0.2 | 2026-07-16 | Claude (Opus 4.8) | Drei Fixes umgesetzt und dokumentiert: (1) Phase.inflationRate ersatzlos entfernt (DB-Migration, API, Typen, UI) – die Inflation liegt seit V5 plan-weit; das Feld war wirkungslos. (2) Amortisation und Tilgung stoppen neu, sobald Hypothek bzw. Schuld abbezahlt sind – sie belasten danach weder Cash noch Sparquote (Kap. 4.6.5, 4.6.7, 4.7, 4.8). (3) Kapitalbezugssteuer greift neu auch bei PK-/3a-Vorbezügen vor der Pensionierung, inkl. Steuerfeld und Netto-Vorschau im UI (Kap. 3.5.2, 4.9.1, 4.9.2). Kennzahl plannedSaveRate ist neu die Rate des ersten Phasenjahres. Drei Regressionstests ergänzt (10 → 13). Abschnitt 9 neu nummeriert (erledigte Punkte entfernt, „Sparraten werden nicht indexiert" ergänzt). |
| 0.1 | 2026-07-16 | Claude (Opus 4.8) | Erstfassung. Vollständige Neuerstellung aus dem Code (Stand f768e01). Ersetzt die bisherigen FDD/TDD-Dokumente v1–v5 vollständig. |
Pflegehinweis: Dieses Dokument ist ein lebendes Dokument. Bei jeder Aktualisierung wird (a) die Änderungshistorie um eine Zeile ergänzt, (b) die Versionsnummer im Kopf hochgezählt und (c) der referenzierte Codestand (Commit) aktualisiert.
Die Versionsnummer ist ein Zähler nach dem Punkt, keine Dezimalzahl: auf 0.9 folgt 0.10, darauf 0.11 und so weiter. Ein Sprung auf 1.0 ist damit dem Erreichen eines echten Meilensteins vorbehalten und passiert nicht als Nebeneffekt des Hochzählens.
Inhaltsverzeichnis
- Einleitung und Abgrenzung
- Fachliche Grundkonzepte
- Funktionale Spezifikation
- Berechnungsmodell
- Technische Spezifikation
- API-Referenz
- Betrieb und Deployment
- Qualitätssicherung
- Bekannte Einschränkungen und Modellentscheide
- Glossar
1. Einleitung und Abgrenzung
1.1 Zweck des Tools
Das FPT ist eine Webanwendung zur persönlichen Finanzplanung über Lebensabschnitte hinweg, ausgelegt auf Schweizer Verhältnisse (AHV, Pensionskasse, Säule 3a, Grundstückgewinnsteuer). Ein Benutzer bildet seine finanzielle Situation als Kette von Lebensphasen ab, hinterlegt finanzielle Elemente (Einkommen, Ausgaben, Vorsorge, Immobilien, Vermögen, Schulden) und trifft an den Übergängen zwischen den Phasen bewusste Entscheide (halten, verkaufen, Kapital beziehen, verrenten). Das Tool rechnet daraus Jahr für Jahr das Vermögen, den Cash-Bestand, Spar- bzw. Verzehrquoten sowie ein allfälliges Ruinalter fort.
1.2 Was das Tool nicht ist
Aus dem Code direkt ableitbare Abgrenzungen:
- Keine Steuerberechnung ausser den drei explizit modellierten Sätzen (Kapitalbezugssteuer, Grundstückgewinnsteuer, PK-Umwandlungssatz). Einkommens- und Vermögenssteuern sind nicht modelliert und müssen vom Benutzer in den Ausgaben erfasst werden (Begründung: 9.14).
- Keine Nebenkosten, kein Eigenmietwert, keine Mieteinnahmen bei Immobilien (siehe 9.3).
- Keine automatische Deckung von Liquiditätslücken. Negatives Cash wird gemeldet, aber nicht korrigiert (siehe 9.1).
- Keine Mehrbenutzer-Kollaboration. Pläne gehören genau einem Benutzer.
Hinweis zur Dokumenthistorie: Bis Version 0.9 stand hier zusätzlich „keine Monte-Carlo-Simulation", „keine Hypothekarzinsen" und „keine Wertentwicklung von Immobilien". Alle drei sind seit Version 0.5 bzw. 0.7 umgesetzt (4.4.5, 4.6.5, 4.12); die Abgrenzung war versehentlich stehen geblieben.
Die deterministische Rechnung bleibt der Kern des Tools; Stochastik kommt ausschliesslich in der Monte-Carlo-Simulation (4.12) und – als reine Was-wäre-wenn- Rechnung – in der Sensitivitätsanalyse (4.13) dazu. Beide verändern die gespeicherten Plandaten nicht.
1.3 Kernprinzip: Plan als selbsttragende Einheit
Seit dem V3-Rework (Migration 20260713150000_profile_to_plan_v3) trägt jeder Plan sein
eigenes Grundprofil: Haushaltsform, Personen (Alter, Pensionsalter, Name) und
Inflationsannahme. Es gibt keine übergeordnete Haushalts-Entität mehr. Ein Szenario ist
deshalb eine vollständige Deep-Copy und vom Ursprungsplan unabhängig veränderbar.
Referenz: prisma/schema.prisma Zeilen 8–10, src/lib/types.ts Zeilen 38–49.
2. Fachliche Grundkonzepte
2.1 Die Ebenen
User
└── Plan (der HAUSHALT: Name, Haushaltsform, Startjahr)
├── PlanPerson[] (Rolle, Name, Alter — gelten für ALLE Szenarien)
└── Scenario[] (die berechenbare Einheit = eine Planungsvariante)
│ szenario-eigen: Inflation, Cash-Startwert
│ isBase = genau eines je Plan · parentScenarioId = Baum + Vergleichsbasis
├── Person[] (nur das PENSIONSALTER je Rolle — szenario-eigen)
├── Phase[] (Kette 1..n; sourcePhaseId = Gegenstück in der Vorlage)
└── FinancialElement[] (szenario-weit; sourceElementId = Gegenstück in der Vorlage)
├── ElementPhaseValue[] (Werte je Phase, JSON)
└── ElementTransitionValue[] (Entscheide je Übergang, JSON)
Zwei Designprinzipien tragen dieses Modell:
(1) Ein finanzielles Element ist über alle Lebensphasen hinweg dieselbe Entität. Eine Pensionskasse „PK Arbeitgeber" existiert einmal pro Szenario; sie hat pro Phase einen Werte-Satz und pro Phasenübergang einen Entscheid-Satz. Dadurch bleibt die Identität eines Vermögensgegenstands über die Zeit erhalten – Voraussetzung für die Fortschreibung (Carry) und die Vermögensaufteilungs-Grafik.
(2) Der Haushalt liegt am Plan, die Annahmen am Szenario (V7, siehe 9.30). Haushaltsform, Personen (Name, Alter) und Planstartjahr beschreiben den Haushalt – sie gelten für alle Szenarien. Unterscheiden sie sich, ist es ein anderer Plan. Szenario-eigen sind die Annahmen: Inflation, Cash-Startwert und – am wichtigsten – das Pensionsalter je Person. Nur deshalb sind Frühpensionierungs-Szenarien überhaupt möglich („Was, wenn ich mit 62 statt 65 aufhöre?").
Der Plan trägt zudem den Eigentümer (userId). Ownership von Szenario/Phase/Element läuft
über die Kette Scenario → Plan → User.
Referenz: prisma/schema.prisma.
2.2 Die Matrix als Leitmetapher
Die Hauptansicht ist eine Tabelle:
- Zeilen = finanzielle Elemente, gruppiert nach Kategorie; zuoberst die systemseitige, read-only Zeile Cash.
- Spalten = abwechselnd Phasenspalten und Übergangsspalten (Phase 1 → Übergang → Phase 2 → Übergang → Phase 3 …). Der letzten Phase folgt keine Übergangsspalte.
- Zellen = anklickbar; Phasenzellen öffnen die Werte-Eingabe, Übergangszellen den Entscheid-Dialog.
Referenz: src/components/PlanView.tsx Zeilen 111–120 (Spaltenaufbau), 333–477 (Matrix).
2.3 Phasentyp – abgeleitet, nicht gespeichert
Der Typ einer Phase wird nie gespeichert, sondern in jeder Berechnung aus Alter und Pensionsalter der Personen abgeleitet:
| Bedingung (zu Phasenbeginn) | Typ |
|---|---|
alle Personen startAge < retirementAge |
ERWERB |
alle Personen startAge >= retirementAge |
PENSION |
| gemischt | MIXED |
Referenz: src/lib/calculations.ts Zeilen 158–172; Kommentar in prisma/schema.prisma Zeilen 94–95.
2.4 Cash als Ausgleichskonto
Cash ist kein vom Benutzer erfassbares Element, sondern das systemseitige Ausgleichskonto:
- Es startet mit
Plan.initialCash(Phase 1). - Es nimmt jährlich die Spar-/Verzehrquote (Einkommen − nominale Ausgaben) auf.
- Es finanziert die geplanten Sparraten (3a-Beiträge, Sparbeiträge, Amortisationen, Tilgungen).
- Es empfängt die Bezugsraten aus Sonstigem Vermögen.
- Es empfängt an Übergängen Kapitalzuflüsse (Verkäufe, PK-/3a-Bezüge) und finanziert Sofort-Tilgungen sowie Zusatzinvestitionen der Folgephase.
- Es nimmt an Übergängen einmalige Sonderein-/ausgaben auf (Erbschaft, Poolbau, Autokauf) – siehe 3.5.5.
- Es darf negativ werden – dies ist die Definition einer Liquiditätslücke und wird rot markiert, aber nicht automatisch korrigiert.
Cash ist die einzige Zeile der Matrix ohne FinancialElement-Datensatz. Zwei Zellen sind
dennoch bearbeitbar: die erste Phasenzelle (Cash-Anfangswert) und jede Übergangszelle
(einmalige Sonderein-/ausgaben).
Referenz: src/lib/calculations.ts (Jahresschleife und Übergang).
3. Funktionale Spezifikation
3.1 Authentifizierung und Benutzerkonto
3.1.1 Registrierung
- Öffentlich zugänglich (kein Einladungscode, keine Freischaltung).
- Benutzername: Muster
^[a-zA-Z0-9._-]{3,32}$– 3 bis 32 Zeichen, Buchstaben, Zahlen, Punkt, Unterstrich, Bindestrich. Muss plan-übergreifend eindeutig sein (User.username @unique). - Passwort: mindestens 6 Zeichen; wird mit bcrypt (Cost-Faktor 12) gehasht.
- Bei Erfolg wird sofort eine Session gesetzt (Login inbegriffen), HTTP 201.
- Bei belegtem Benutzernamen: HTTP 409 mit Meldung „Dieser Benutzername ist bereits vergeben."
Referenz: src/lib/users.ts Zeilen 6–22, src/app/api/auth/register/route.ts.
3.1.2 Anmeldung
- Benutzername + Passwort; Prüfung via
bcrypt.compare. - Fehlermeldung ist bewusst unspezifisch: „Benutzername oder Passwort falsch." (HTTP 401) –
verrät nicht, ob der Benutzer existiert. Auch die Antwortzeit verrät es nicht: Ein
unbekannter Benutzer wird gegen einen Dummy-bcrypt-Hash geprüft, damit die Dauer dieselbe
ist wie bei einem bekannten mit falschem Passwort (
TIMING_DUMMY_HASHinusers.ts). - Bei Erfolg: JWT (HS256, Payload
{ userId }, Gültigkeit 30 Tage) im HttpOnly-Cookiefpt_session(sameSite=lax,securenur in Produktion,maxAge30 Tage).
Referenz: src/app/api/auth/login/route.ts, src/lib/auth.ts.
3.1.3 Abmeldung
POST /api/auth/logout löscht das Cookie. Da die Session zustandslos ist (JWT), bleibt ein
bereits kopiertes Token bis zum Ablauf technisch gültig – es gibt keine serverseitige
Token-Sperrliste.
Nach dem Abmelden führt der Zurück-Knopf des Browsers zur zuletzt gezeigten Ansicht aus
dem bfcache zurück -- ohne neue Anfrage, also ohne dass Middleware oder Session-Prüfung
greifen. Damit die eingefrorene, scheinbar noch angemeldete Ansicht nicht stehen bleibt,
prüft die Hauptseite beim pageshow-Ereignis (nur bei einer aus dem bfcache zurückgeholten
Seite) erneut /api/auth/me und leitet ohne gültige Session sofort auf /login. Es sind
dabei nie echte Daten freigegeben -- das Cookie ist gelöscht --, aber die alten Zahlen sollen
auf einem geteilten Rechner gar nicht erst wieder sichtbar werden. Referenz: src/app/page.tsx.
3.1.4 Passwortänderung
Im Profilmenü. Erfordert das aktuelle Passwort; das neue Passwort muss ≥ 6 Zeichen haben und
wird im Dialog gegen eine Wiederholung geprüft (Client-seitig). Nach Erfolg erscheint 1.2 s lang
„Passwort geändert.", dann schliesst der Dialog. Der Dialog läuft über die zentrale
Modal-Komponente und schliesst damit auch auf Esc (Fokus-Falle und aria-modal
inklusive, siehe 3.7.6).
Referenz: src/app/api/auth/change-password/route.ts, src/components/ProfileMenu.tsx Zeilen 117–140.
3.1.5 Zugriffsschutz
Zweistufig:
- Middleware (
src/middleware.ts, Edge-Runtime): schützt alle Pfade ausser/login,/api/auth/login,/api/auth/register,/_next/*,/favicon*. Ohne gültiges Token → API-Aufrufe erhalten HTTP 401, Seitenaufrufe werden nach/login?next=<pfad>umgeleitet. - Ownership-Check in jeder API-Route:
getCurrentUserId()plus eine Abfrage, die den Datensatz nur zurückgibt, wenn er dem Benutzer gehört (getOwnedPlan,getOwnedPhase,getOwnedElementinsrc/lib/queries.ts). Ein fremder Datensatz führt zu HTTP 404 (nicht 403) – die Existenz wird nicht preisgegeben.
3.1.6 Missbrauchsschutz (Rate-Limiting)
Login, Registrierung und Passwortänderung sind gegen wiederholtes Durchprobieren gebremst
(src/lib/rate-limit.ts):
| Endpunkt | Grenze | Schlüssel |
|---|---|---|
POST /api/auth/login |
10 / 15 min | Client-IP + Benutzername |
POST /api/auth/register |
5 / 60 min | Client-IP |
POST /api/auth/change-password |
10 / 15 min | Benutzer-ID |
Bei Überschreitung: HTTP 429 mit lesbarer Meldung und Retry-After-Header. Der Zähler ist
ein In-Memory-Fixed-Window – bewusst einfach, weil das Tool in einem einzigen Container
läuft; nach einem Deploy ist er leer. Die Client-IP kommt aus X-Forwarded-For (Traefik);
ohne Header fallen alle auf denselben Eimer, was im Zweifel eher zu stark als zu schwach
bremst. Für einen Mehrinstanz-Betrieb müsste der Zähler nach Redis o. Ä. wandern (Roadmap #34).
Bewusst offen (Roadmap #34, Pentest): kein Passwort-Längen-Maximum (bcrypt prüft still nur die ersten 72 Byte), keine ARIA-Labels auf der Login-Maske und der Befehls-Palette.
3.2 Plan-Verwaltung
3.2.1 Plan erstellen
Es gibt genau einen Weg: den Knopf «Meinen ersten Finanzplan anlegen» in der Übersicht bzw. das «+» in der Seitenleiste. Beide öffnen denselben Dialog. Zur Begründung, warum die frühere Auswahl aus drei Wegen entfallen ist, siehe 3.2.8.
Der Dialog fragt Name plus Grundprofil -- ohne Pensionsalter, das gehört in die Pensionsplanung:
| Feld | Typ | Default | Wertebereich |
|---|---|---|---|
| Name des Plans | Text | „Basisplan" | 1–120 Zeichen |
| Haushaltsform | Auswahl | SINGLE |
SINGLE / COUPLE |
| Name je Person | Text (optional) | leer | ≤ 60 Zeichen |
| Aktuelles Alter | Zahl | 35 | 0–120 |
| Pensionierungsalter | Zahl | 65 | 30–100 |
| Erwartete Inflationsrate (%) | Zahl | 1.5 | −20 bis 50 |
Konsistenzregel: SINGLE erfordert genau eine Person, COUPLE genau zwei (Person A und B).
Verletzung → HTTP 400 mit Klartextmeldung.
Referenz: src/components/PlanProfileFields.tsx, src/app/api/plans/route.ts.
Ein neu erstellter Plan hat keine Phasen und keine Elemente; initialCash ist 0.
3.2.2 Grundprofil ändern
Über «Profil bearbeiten» im Grundprofil-Block der Szenario-Ansicht (Panel «Szenario-Profil»).
Der Endpunkt ist PATCH /api/scenarios/<scenarioId>: Er schreibt Haushaltsform, Personen (Name,
Alter) und Startjahr an den Plan, Inflation und Pensionsalter ans Szenario – die Trennung
aus V7 (9.30). Das Pensionsalter ist dort nur noch
Anzeige; verschoben wird es über «Pensionsalter anpassen» (3.12),
weil es auf einer Phasengrenze liegt.
Wichtig: Die Personen werden serverseitig in einer Transaktion gelöscht und neu angelegt
(deleteMany + create). Die Person-IDs ändern sich dadurch. Da Elemente über ownerRole
(nicht über personId) zugeordnet sind, bleibt die Zuordnung erhalten.
Ein Wechsel von COUPLE auf SINGLE entfernt Person B. Elemente mit ownerRole = PERSON_B
bleiben in der Datenbank bestehen, finden aber keinen Owner mehr – siehe
9.2.
Plan umbenennen: über das Stift-Symbol neben dem Plannamen im Plan-Dashboard
(PATCH /api/plans/<planId>, nur das Feld name).
Referenz: src/app/api/scenarios/[scenarioId]/route.ts, src/app/api/plans/[planId]/route.ts.
3.2.3 Cash-Anfangswert
Klick auf die erste Zelle der Cash-Zeile öffnet den Dialog „Cash-Anfangswert". Wertebereich 0 bis 1'000'000'000, wird auf ganze Franken gerundet.
Referenz: src/components/PlanView.tsx, PATCH /api/scenarios/<scenarioId> (Feld initialCash).
3.2.4 Plan löschen
Aus der Übersichtskachel oder der Seitenleiste, mit dem eigenen Bestätigungs-Dialog
(useConfirm, siehe 3.7.6 – kein Browser-confirm() mehr).
Löscht per Datenbank-Cascade alle Szenarien des Plans und darüber Personen, Phasen, Elemente
und sämtliche Werte, dazu die Ist-Datensätze, gespeicherten Analysen und Berichte des Plans.
Referenz: prisma/schema.prisma, src/app/api/plans/[planId]/route.ts.
3.2.5 Szenarien
Beim Anlegen eines Plans entsteht automatisch das Basisszenario (isBase = true,
Name „Basisszenario"). Vom erfassten Profil landen Haushaltsform, Personen und Startjahr am
Plan, Inflation und Pensionsalter am Basisszenario (V7).
Ein weiteres Szenario ist eine vollständige Kopie eines beliebigen bestehenden Szenarios (nicht nur des Basisszenarios – die Vorlage ist in der Szenario-Liste je Zeile über «Kopie» wählbar). Kopiert werden die szenario-eigenen Angaben (Inflation, Cash-Startwert, Pensionsalter), alle Phasen, alle Elemente sowie sämtliche Phasen- und Übergangswerte. Der Haushalt wird nicht kopiert – er liegt am Plan und gilt ohnehin für alle Szenarien.
Gesetzt werden dabei:
parentScenarioId= das kopierte Szenario → ergibt den Baum in der Seitenleiste und die Vergleichsbasis für die Abweichungs-MarkierungsourcePhaseId/sourceElementIdje kopierter Phase bzw. Element → die Identität, über die der Diff „dieselbe Zelle" wiederfindet
Da jede Kopie wieder kopierbar ist, entstehen Sub-Szenarien beliebiger Tiefe; die Seitenleiste rückt sie entsprechend ein.
Löschen: Ein Szenario lässt sich löschen, das Basisszenario nicht (dafür den ganzen Plan löschen). Das Löschen eines Plans entfernt per Cascade alle seine Szenarien.
Referenz: src/app/api/scenarios/[scenarioId]/copy/route.ts.
3.2.6 Abweichungs-Markierung (Diff)
Jedes Szenario ausser dem Basisszenario wird live gegen sein Eltern-Szenario verglichen. Abweichende Stellen sind in der Matrix farblich markiert:
| Zustand | Darstellung |
|---|---|
| geändert | gelb hinterlegt (--diff / --diff-soft) |
| neu (in der Vorlage nicht vorhanden) | grün hinterlegt + Badge „neu" (--diff-added) |
| entfernt (in der Vorlage vorhanden, hier gelöscht) | graue Geisterzeile, durchgestrichen (--diff-removed) |
Markiert werden: Phasenzellen, Übergangszellen, Element-Zeilen (Name/Zuordnung), Phasenköpfe (Name/Dauer), der Cash-Anfangswert, Cash-Übergänge und das Grundprofil-Banner. Über der Matrix steht die Gesamtzahl der Abweichungen.
Die Farben sind semantische Tokens und für Hell, Dunkel und Warm getrennt abgestimmt – ein fester Gelbwert würde im Dunkelschema unbrauchbar aussehen.
Zwei bewusste Eigenschaften:
- Der Vergleich läuft gegen das direkte Eltern-Szenario, nicht immer gegen die Basis. Bei einem Sub-Szenario ist das genau „was habe ich gegenüber der Vorlage geändert".
- Der Diff ist live: Ändert man die Vorlage, verschiebt sich die Markierung im Kind rückwirkend (siehe 9.16).
Referenz: src/lib/diff.ts.
3.2.7 Planstart (Kalenderjahr)
Das Grundprofil enthält das Feld Planstart (Jahr) – das Kalenderjahr, in dem Jahr 1 der
Planung liegt (Plan.startYear, seit V7 plan-weit). Es dient ausschliesslich der Darstellung: Zeitachse
und Grafiken beschriften damit Jahre statt nur Alter. Die Berechnung rechnet unverändert in
relativen Jahren ab Planbeginn – startYear fliesst in keine Formel ein.
Beim Anlegen wird das laufende Jahr vorbelegt; bestehende Pläne wurden per Migration darauf
gesetzt. Kalenderjahr eines Planjahrs: startYear + (Jahr − 1).
3.2.8 Der Einstieg: ein Weg, eine Tour, eine Bestandsaufnahme
Bis 0.35 standen im leeren Zustand drei Knöpfe: geführt starten, Beispielplan ansehen, leer starten. Das ist eine Wahl, die niemand treffen kann, der das Tool noch nicht kennt -- und sie kostete den ersten Eindruck.
Seit 0.36 gibt es genau einen Weg:
"Meinen ersten Finanzplan anlegen"
|
v
Plan-Dialog (sechs Felder)
|
v
Basisszenario, leer -> Tour (Demo-Popup) -> Bestandsaufnahme
Der Plan-Dialog fragt nur noch: Name des Plans, Haushaltsform, Namen der Personen (freiwillig), Startjahr, Alter, Inflation. Dasselbe Fenster öffnet auch das Plus in der Seitenleiste.
Das Pensionsalter wird bewusst NICHT gefragt. Es ist kein Stammdatum, sondern der erste Entscheid der Pensionsplanung -- und es erzeugt eine Phasengrenze (3.14.5). Im Basisszenario gilt durchgehend das Referenzalter 65.
Der frühere Plan-Assistent (PlanWizard) und der Beispielplan sind entfallen. Der Wizard
führte VOR dem Plan durch ein Formular und liess einen danach mit der Matrix allein. Was der
Beispielplan leistete -- einmal sehen, wie ein gefüllter Plan aussieht --, übernimmt die Tour;
was danach zu tun ist, sagt die Übersicht der offenen Punkte
(3.14).
3.3.1 Phase anlegen
Neue Phasen werden immer am Ende der Kette angehängt (sequenceNumber = Anzahl + 1).
Automatische Dauer-Kappung: Die Dauer wird ans nächste Pensionsereignis gekappt. Formel
(maxPhaseDuration): für jede Person, die zu Phasenbeginn noch erwerbstätig ist, gilt
retirementAge − (age + yearsBefore); das Minimum dieser Werte ist die Obergrenze. Ist keine
Person mehr erwerbstätig, gibt es keine Obergrenze (null). Diese Kappung wird
serverseitig erzwungen.
Fachliche Begründung: Eine Phase darf keine Pensionierung überspannen, weil der Phasentyp und die AHV-/PK-Renten am Phasenbeginn ausgewertet werden.
Der Dialog nennt seit 0.41 den Grund, nicht nur die Zahl. Vorher stand dort «max. 12» – eine Sperre ohne Erklärung wirkt willkürlich, und man vermutet einen Fehler. Neu steht im Klartext, welches Ereignis die Grenze setzt («Anna geht mit 65 in Pension»), warum das so ist (die Rechnung wertet Erwerbsstatus und Renten am Phasenbeginn aus) und dass es weitergeht («danach legst du einfach die nächste Phase an»). Sind alle Personen bereits pensioniert, sagt der Dialog stattdessen, dass die Dauer frei ist.
Bekannte Lücke.
maxPhaseDurationkann zusätzliche Fixpunkte entgegennehmen (AHV-Beginn, PK-Bezug, jedes 3a-Konto, siehe 3.14.5), aber weder der Dialog noch der Endpunkt übergeben sie – gekappt wird nur am Erwerbsende. Im Basisszenario fällt das nicht ins Gewicht, weil das Pensionsalter dort fest 65 ist. Wer im Übergangs-Dialog ein abweichendes 3a-Bezugsalter setzt, kann die Grenze jedoch überspannen; der Bezug rutscht dann auf die nächste Phasengrenze. Bewusst zurückgestellt bis zur Szenario-Art «Frühpensionierung» (Roadmap Nr. 48), wo abweichende Bezugsalter zum eigentlichen Thema werden.
Default-Dauer: die Kappung, sonst 10 Jahre. Default-Name: Phase 1 → „Erste Lebensphase"; sonst „Pensionsphase" wenn zu Phasenbeginn mindestens eine Person pensioniert ist, sonst „Erwerbsphase".
Vorbelegung der Elemente: Beim Anlegen einer Phase wird für jedes noch aktive Element
(nicht SOLD, nicht SETTLED) ein ElementPhaseValue erzeugt – seit 0.28 aber fast leer
(buildCarryData):
| Kategorie | Kopierte Felder |
|---|---|
AHV |
gapYears: 0 – Ausfalljahre gelten für genau eine Phase, ein geerbter Wert würde eine Lücke erfinden |
REAL_ESTATE |
purchasePrice (eine Tatsache, Basis der Grundstückgewinnsteuer) und interestHandling (ein Schalter ohne Zahlenwert) |
| alle übrigen | nichts |
Alles andere wird nicht kopiert, sondern live vererbt: Beträge und Bestände werden aus der Vorphase fortgeschrieben, die Wiederkehr-Parameter (Raten, Beiträge, Amortisation, Wertsteigerung, Zinssatz) gelten weiter, solange das Feld leer bleibt (3.12.4). Der Unterschied zeigt sich, sobald man eine frühe Phase nachträglich ändert: Eine Kopie bliebe stehen, die Vererbung zieht mit.
Referenz: src/app/api/scenarios/[scenarioId]/phases/route.ts.
3.3.2 Phase bearbeiten
Klick auf einen Phasenkopf (oder auf dessen Stift-Symbol) öffnet das Panel „Lebensphase: " mit Bezeichnung und Dauer. Eine phasenspezifische Inflationsrate gibt es nicht; das Panel weist darauf hin.
Die Folgephase gleicht eine geänderte Dauer aus. Wird eine Phase um N Jahre verlängert, wird die nächste um N Jahre kürzer – die Gesamtdauer des Plans bleibt gleich. Das Panel zeigt die Auswirkung live und fragt vor dem Speichern nach.
Warum das nötig ist: Bis 0.32 kappte das Tool nur die bearbeitete Phase am nächsten
Pensionsereignis. Das ist richtig, aber nicht ausreichend. Beispiel: Phase 1 (10 J.) + Phase 2
(10 J.), Pensionierung im Jahr 20. Eine Verlängerung von Phase 1 auf 12 Jahre ist für Phase 1
zulässig – danach lief Phase 2 aber von Jahr 12 bis 22 und überspannte die Pensionierung.
Da computePlan die Dauer nicht nachkappt und den Phasentyp am Phasenbeginn ableitet, wurde
die Pensionierung faktisch übersprungen; die tragende Invariante aus
4.16.1 war verletzt.
Passt der Ausgleich nicht (die Folgephase fiele unter ein Jahr), wird die Änderung blockiert mit dem Hinweis, um wie viel sie höchstens möglich wäre. Die letzte Phase hat keine Nachfolgerin – sie verlängert oder verkürzt den Plan tatsächlich.
Referenz: planDurationChange in src/lib/phaseplan.ts, src/components/PhaseDetail.tsx,
src/app/api/phases/[phaseId]/route.ts (setzt beide Dauern in einer Transaktion).
3.3.3 Phase löschen
Nur die letzte Phase kann gelöscht werden – serverseitig geprüft (HTTP 400: „Nur die letzte
Phase kann gelöscht werden."). Damit bleibt die Kette der sequenceNumber lückenlos. Der
Löschen-Button erscheint im Detail-Panel nur bei der letzten Phase.
Referenz: src/app/api/phases/[phaseId]/route.ts.
3.4 Finanzielle Elemente
3.4.1 Kategorien und Personenzuordnung
| Kategorie | Label | Zuordnung |
|---|---|---|
INCOME |
Einkommen | frei: Gemeinsam / Person A / Person B |
EXPENSE |
Ausgaben | frei |
AHV |
AHV | zwingend genau eine Person |
PENSION_FUND |
Pensionskasse | zwingend genau eine Person |
PILLAR_3A |
Säule 3a | zwingend genau eine Person |
REAL_ESTATE |
Immobilie | frei |
OTHER_ASSET |
Sonstiges Vermögen | frei |
OTHER_DEBT |
Sonstige Schulden | frei |
Die Vorsorge-Kategorien (PERSON_ONLY_CATEGORIES) sind zwingend personengebunden, weil AHV-Rente,
PK-Verrentung und 3a-Bezug am individuellen Pensionierungszeitpunkt hängen. Wird für diese
Kategorien kein PERSON_A/PERSON_B übergeben → HTTP 400.
Für die übrigen Kategorien gilt: fehlt die Zuordnung, wird serverseitig HOUSEHOLD gesetzt.
Referenz: src/lib/elements.ts (PERSON_ONLY_CATEGORIES), src/app/api/scenarios/[scenarioId]/elements/route.ts.
3.4.2 Element anlegen
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). 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.
Referenz: AddElementDialog in src/components/PlanView.tsx.
3.4.3 Element bearbeiten und löschen
Ein Klick auf eine Phasenzelle öffnet das Panel mit den kategorie- und kontextabhängigen Feldern (siehe 3.4.4) – dort stehen die Werte dieser Phase.
Die Eigenschaften des Elements selbst (Bezeichnung, Zuordnung) sind phasenunabhängig und haben deshalb ihren eigenen Ort: In der Element-Zeile ganz links stehen drei Symbole – Stift (umbenennen und Zuordnung ändern), Papierkorb (löschen) und Expand (Detailansicht). Alle drei sind immer sichtbar, nicht erst bei Mouseover; auf einem Touch-Gerät gäbe es sonst keinen Weg dorthin. Denselben Aufbau trägt der Phasenkopf.
Die Zuordnung läuft über ownerRole, nicht über eine Person-ID – deshalb übersteht sie auch
das Neuanlegen der Personen im Profil-Dialog. Für AHV, Pensionskasse und Säule 3a bleibt sie
zwingend personengebunden (HTTP 400 bei „Gemeinsam").
Löschen entfernt das Element aus allen Phasen (eigener Bestätigungs-Dialog, dann Cascade auf
ElementPhaseValue und ElementTransitionValue).
Referenz: PATCH /api/elements/<id> (Felder name, ownerRole), ElementMetaPanel in
src/components/PlanView.tsx.
3.4.4 Feldkatalog je Kategorie
Die angezeigten Felder hängen von drei Kontextgrössen ab:
carried– ob der Basiswert aus der Vorphase fortgeschrieben wird (ab Phase 2)ownerWorking– ob der zugeordnete Besitzer in dieser Phase erwerbstätig istdurationYears– Phasendauer (begrenzt z. B. die Ausfalljahre)
Referenz: ElementPhaseFields in src/components/ElementDetail.tsx.
INCOME (Einkommen)
Einkommen ist der NETTOLOHN – der Betrag, der nach allen Lohnabzügen (AHV/ALV, Pensionskasse, NBU) tatsächlich aufs Konto kommt. Das ist die für den Cash-Fluss richtige Grösse und die, in der man denkt.
| Feld | JSON | Semantik |
|---|---|---|
| Jahreseinkommen NETTO, nominal (erstes Jahr) | amount |
Basiswert Jahr 1. Ab Phase 2 vorbelegt mit dem fortgeschriebenen Wert, bewusst änderbar (Teilzeit, Beförderung). |
| ≈ real (heutige Kaufkraft) | – | Read-only Info: amount / deflatorStart |
| Nominale Lohnerhöhung (%/Jahr) | teuerungsausgleich |
Default 0 %. 0 % = nominal gleichbleibend, real sinkend. |
Für die AHV rechnet das Tool intern auf den Bruttolohn zurück – siehe
4.4.5. Warum die Definition überhaupt nötig war:
Für den Cash-Fluss sind beide Konventionen gleichwertig (brutto − Ausgaben inkl. Abzüge
≡ netto − Ausgaben ohne Abzüge), weshalb die Ambiguität lange folgenlos blieb. Mit der
einkommensabhängigen AHV hängt daran aber eine Rente.
EXPENSE (Ausgaben)
Hinweistext: „Ausgaben werden REAL erfasst (in heutiger Kaufkraft)."
| Feld | JSON | Semantik |
|---|---|---|
| Jahresausgaben REAL (erstes Jahr) | amount |
Basiswert in heutiger Kaufkraft. |
| ≈ nominal (in diesem Jahr) | – | Read-only Info: amount * deflatorStart |
| Reale Mehrausgaben (%/Jahr) | teuerungsausgleich |
Zusätzlich zur Inflation. 0 % = gleicher Lebensstandard. |
Die Asymmetrie (Einkommen nominal, Ausgaben real) ist der Kern des V5-Modells: Man kennt seinen Lohn nominal, aber seinen Lebensstandard real.
AHV
| Zustand | Anzeige |
|---|---|
| Besitzer erwerbstätig | Eingabefeld Geplante Ausfalljahre (gapYears), 0 bis Phasendauer. Hilfetext: „Jedes Ausfalljahr kuerzt die spaetere Rente um 1/44." |
| Besitzer pensioniert | Nur Hinweistext – die Rente wird automatisch berechnet. |
PENSION_FUND (Pensionskasse)
| Zustand | Felder |
|---|---|
| erwerbstätig, Phase 1 | Aktueller PK-Wert (currentValue), Jährliche Einzahlung (annualContribution), Erwartete Rendite (expectedReturn) |
| erwerbstätig, ab Phase 2 | Startwert (fortgeschrieben) (read-only), Zusatzeinlage aus Kapital (additionalInvestment), Einzahlung, Rendite |
| pensioniert | Hinweistext (Rente aus Umwandlungssatz bzw. „Vollständig bezogen") |
Wichtig (Hilfetext im UI): Die PK-Einzahlung „Fliesst NICHT in die Sparquote ein (bereits in den Ausgaben beruecksichtigt)" – Lohnabzüge sind im Nettoeinkommen bereits weg.
PILLAR_3A (Säule 3a)
Wie PK, aber:
- Die Einzahlung zählt zur Sparquote (verlässt das Cash).
- Das Feld ist auf
PILLAR_3A_MAX_ANNUAL= 7'258 CHF (2026, mit PK) hart geklammert – mit dem Schalter «Selbstständig ohne PK» aufPILLAR_3A_MAX_SELF_EMPLOYED(FeldselfEmployed3a, siehe 4.11). - Bei Pensionierung: „Die Saeule 3a wird beim Pensions-Uebergang vollstaendig bezogen."
REAL_ESTATE (Immobilie)
| Feld | JSON | Semantik |
|---|---|---|
| Kaufpreis | purchasePrice |
nur Phase 1 / Neukauf; ab Phase 2 read-only fortgeschrieben |
| Hypothek | mortgage |
dito |
| Startwert Netto (fortgeschrieben) | – | read-only ab Phase 2: Verkehrswert − Resthypothek |
| Resthypothek (fortgeschrieben) | – | read-only ab Phase 2 |
| Amortisation (CHF/Jahr) | amortization |
endet, sobald die Hypothek abbezahlt ist |
| Hypothekarzins (%/Jahr) | interestRate |
Zinssatz auf der Restschuld |
| Hypothekarzins-Betrag (Beginn → Ende) | – | read-only: Zinsbetrag im ersten und im letzten Jahr der Phase |
| Geschätzte Wertsteigerung (%/Jahr) | valueGrowth |
wirkt auf die Liegenschaft |
| Sind die Zinsen bereits in den Ausgaben enthalten? | interestHandling |
INCLUDED (Default) / ADD |
Der Zinsbetrag sinkt automatisch mit der Amortisation – das kann kein manueller
Ausgabenposten. Beispiel: Hypothek 1'000'000, Amortisation 10'000/Jahr, Zins 1 %, 10 Jahre →
Anzeige 10'000 → 9'100 (Jahr 1 auf 1'000'000, Jahr 10 auf 910'000).
Der Schalter interestHandling verhindert die Doppelzählung: Bestehende Pläne haben die
Zinsen im Ausgabenbetrag, deshalb ist INCLUDED der Default und das Tool zieht nichts ab.
Erst ADD rechnet die Zinsen dazu – dann gehören sie aus dem Ausgabenbetrag entfernt. Der
Schalter sitzt bewusst auf der Immobilie und nicht auf dem Ausgaben-Element: Bei mehreren
Ausgaben-Elementen wäre sonst unklar, welches die Zinsen trägt (und zwei auf „Ja" würden
doppelt zählen).
OTHER_ASSET (Sonstiges Vermögen)
| Feld | JSON | Semantik |
|---|---|---|
| Startwert / Startwert (fortgeschrieben) | startValue |
ab Phase 2 read-only |
| Zusatzinvestition aus Kapital | additionalInvestment |
nur ab Phase 2 |
| Erwartete Rendite (%/Jahr) | expectedReturn |
|
| Jährlicher Sparbeitrag | annualContribution |
fliesst ins Vermögen, verlässt das Cash → geplante Sparrate |
| Jährliche Bezugsrate | annualWithdrawal |
Entnahme aus dem Vermögen ins Cash → geplante Verzehrrate |
Die Bezugsrate ist das Instrument für den laufenden Kapitalverzehr innerhalb einer Phase. Für einen einmaligen Zugriff am Übergang gibt es zusätzlich den Teilverkauf (3.5.2, 4.9.3).
OTHER_DEBT (Sonstige Schulden)
| Feld | JSON | Semantik |
|---|---|---|
| Restschuld / (fortgeschrieben) | startValue |
ab Phase 2 read-only |
| Jährliche Tilgung | annualRepayment |
verlässt das Cash → geplante Sparrate |
Schulden gehen mit negativem Vorzeichen ins Vermögen ein.
3.5 Übergänge
3.5.1 Konzept
Zwischen zwei Phasen liegt ein Übergang. Er ist der Ort, an dem einmalige Entscheide getroffen
werden. Fünf Kategorien haben Übergangs-Entscheide (TRANSITION_CATEGORIES):
PENSION_FUND, PILLAR_3A, REAL_ESTATE, OTHER_ASSET, OTHER_DEBT. Dazu kommt der
Cash-Entscheid (siehe 3.5.5), der an
jedem Übergang zu treffen ist.
Für INCOME, EXPENSE und AHV erscheint: „Fuer diese Kategorie gibt es im Uebergang keine
Eingaben."
3.5.2 Normaler Übergang vs. Pensions-Übergang
Ein Übergang ist für ein Element ein Pensions-Übergang, wenn dessen Besitzer in der Von-Phase
erwerbstätig und in der Ziel-Phase pensioniert ist (isRetirementTransition). Nur
personenzugeordnete Elemente (nicht HOUSEHOLD) können das auslösen.
| Kategorie | Normaler Übergang (Vorbezug) | Pensions-Übergang |
|---|---|---|
PENSION_FUND |
Bezug? Kein Bezug / Bezug + Bruttobetrag (max. Endwert) + Kapitalbezugssteuer (%) | Bezugsart: Rente / Kapitalbezug / Kombination |
PILLAR_3A |
Bezug? Kein Bezug / Bezug + Bruttobetrag + Kapitalbezugssteuer (%) | Nur Kapitalbezugssteuer (%) – Bezug ist zwingend vollständig |
REAL_ESTATE |
Halten (+ optionale Sonderamortisation) / Verkaufen (+ Verkaufspreis, Grundstückgewinnsteuer) | identisch |
OTHER_ASSET |
Halten / Verkaufen / Teilverkauf (+ Betrag) | identisch |
OTHER_DEBT |
Sofortige Tilgung (CHF) | identisch |
Ein Vorbezug (vor der Pensionierung, z. B. für Wohneigentum oder Selbstständigkeit) ist wie der Bezug bei Pensionierung kapitalbezugssteuerpflichtig. Der eingegebene Betrag ist der Bruttobezug: er wird in voller Höhe dem Vorsorgekapital entnommen, ins Cash fliesst der Betrag nach Abzug der Steuer. Der Dialog zeigt die resultierende Netto-Auszahlung als read-only Vorschau an.
Bei PK-Bezugsart:
- Rente: Feld Umwandlungssatz (Default 6 %)
- Kapitalbezug: Feld Kapitalbezugssteuer (Default 8 %)
- Kombination: beide Felder plus „Davon Kapitalbezug (CHF)", geklammert am Endwert
Referenz: src/components/ElementDetail.tsx Zeilen 342–462.
3.5.3 Ampel-Logik: „offene" Entscheide
Seit 0.35 gibt es drei Zustände statt zwei. Der dritte ist der interessanteste.
| Zustand | Bedeutung |
|---|---|
| unbeantwortet | Das System weiss nichts. Verkauf/Halten, Tilgung, Cash-Übergang. |
| auf Vorgabe | Das System hat eine Antwort – nur nicht die des Benutzers. Gilt für die Pensionierungs-Entscheide (AHV/PK/3a), die seit 0.35 durchgängige Vorgaben haben. |
| bestätigt | Der Benutzer hat hingeschaut (retirementDecision.confirmed), je Säule. Seit 0.41 heisst das im UI durchgehend «angeschaut» – dieselbe Vokabel wie bei den Phasenzellen (3.14.2), weil es derselbe Gedanke ist: dort auf den Werten der Vorphase, hier auf einer Vorgabe des Tools. Zwei Wörter für eine Sache hatten mehr verwirrt als geholfen. Der Erklärtext sagt neu ausdrücklich, dass der Haken keine Zahl ändert. |
Warum der dritte Zustand nötig wurde: Die Vorgaben sind Absicht – ohne sie müsste man am Anfang Fragen beantworten, die man erst am Ende beantworten kann, und der Plan wäre bis dahin nicht rechenbar (3.13.2). Eine Vorgabe aber als beantwortet zu zählen hiesse, eine stillschweigend gesetzte Annahme wie «volle Rente statt Kapitalbezug» durchgehen zu lassen, obwohl sie das Ergebnis massiv verändert. Sie zählt deshalb mit – aber in einem eigenen Topf, damit sie sich nicht wie ein vergessenes Eingabefeld liest:
2 offene Entscheide · 3 Vorgaben ungeprüft
openTransitionCount und totalOpenDecisions liefern deshalb ein Paar DecisionCounts
({ open, unconfirmed }) statt einer Zahl. decisionsText formuliert es. Ein Entscheid gilt
weiterhin als beantwortet (isTransitionAnswered), wenn das jeweilige Feld gesetzt ist:
| Kategorie | Beantwortet, wenn |
|---|---|
REAL_ESTATE, OTHER_ASSET |
decision gesetzt (inkl. PARTIAL) |
PENSION_FUND, PILLAR_3A, AHV |
am Pensions-Übergang: über confirmed (siehe oben); sonst: withdrawalMode gesetzt |
| Cash | mode gesetzt (isCashTransitionAnswered) |
| alle anderen | immer beantwortet |
Der Übergangs-Spaltenkopf zeigt „N offen" (Akzentfarbe), „N Vorgaben" (gedeckt, weil es kein Handlungsdefizit ist) oder „geprüft" (grün, Häkchen). Offene Zellen sind hervorgehoben und zeigen „?".
Seit 0.39 gilt derselbe Mechanismus auch für Phasenzellen (PhaseData.confirmed,
3.14.2). Eine neue Phase übernimmt die Werte
der Vorphase; sie gelten als unbestätigt, bis jemand hingeschaut hat. Damit ist die Ampel
nicht mehr auf die Übergänge beschränkt, sondern deckt die ganze Matrix ab -- die Zählung
liegt in review.ts, die Anzeige teilt sich die Attention-Farbe mit den Übergängen.
Auswirkung auf den PDF-Bericht: Die Kennzahl «Offene Entscheide» weist beide Töpfe zusammen aus. Berichte von vor 0.35 sind deshalb nicht direkt vergleichbar.
Ein Element ist am Übergang inaktiv (transitionInactive), wenn es bereits verkauft/getilgt
ist oder wenn es eine PK/3a ist, deren Besitzer schon zu Beginn der Von-Phase pensioniert war
(dann ist bereits bezogen/verrentet). Inaktive Zellen zeigen „–" und sind nicht anklickbar.
Referenz: src/lib/decisions.ts (openTransitionCount, totalOpenDecisions, decisionsText).
3.5.4 Geführter Übergang (Review-Dialog)
Klick auf einen Übergangs-Spaltenkopf öffnet „Übergang prüfen: → ". Der Dialog listet zuoberst den Cash-Entscheid (einmalige Sonderein-/ausgaben, betrifft jeden Übergang) und darunter alle noch aktiven Elemente der Übergangs-Kategorien mit ihren Entscheidfeldern und kontextabhängigen Hinweisen:
- PK/3a, normaler Übergang: „Hier könnten Sie optional Kapital beziehen."
- PK, Pensionierung: „Pensionierung: Bezugsart wählen (Rente / Kapital / Kombination)."
- 3a, Pensionierung: „Wird bei Pensionierung vollständig bezogen."
„Alle speichern" schreibt jeden Entscheid einzeln per PUT. Entscheidend: Die Formulare sind mit
withTransitionDefaults vorbelegt (Halten / Kein Bezug / Rente), damit ein blosses Speichern den
sichtbaren Default auch tatsächlich persistiert und die Ampel auf grün geht.
Referenz: src/components/PlanView.tsx (TransitionReviewDialog), src/components/ElementDetail.tsx
(withTransitionDefaults).
3.5.5 Cash-Übergang: einmalige Sonderein-/ausgaben
Einmalige Ereignisse (Erbschaft, Poolbau, Autokauf, grössere Anschaffung) werden nicht als finanzielles Element modelliert, sondern als Entscheid auf dem Cash-Konto am Phasenübergang. Sie belasten bzw. speisen das Cash direkt.
Der Entscheid hat vier Ausprägungen (CashTransitionMode):
| Modus | Bedeutung | Felder |
|---|---|---|
NONE |
1:1 übernehmen – Cash läuft unverändert weiter (Default) | – |
INFLOW |
Einmaliger Zufluss | Bezeichnung, Betrag (nominal), Steuer (%) |
OUTFLOW |
Einmalige Kosten | Bezeichnung, Betrag (real) |
BOTH |
Zufluss und Kosten am selben Übergang | beide Feldgruppen |
Erfassungs-Konventionen – bewusst analog zu den laufenden Flows:
- Zufluss: nominal erfasst, real angezeigt (wie Einkommen). Man kennt den Betrag, der effektiv aufs Konto kommt. Der Realwert erscheint read-only als Info.
- Kosten: real erfasst, nominal angezeigt (wie Ausgaben). Man denkt „ein Pool kostet heute 20'000"; die Inflation rechnet daraus den Betrag zum Ereigniszeitpunkt. Der Nominalwert erscheint read-only als Info.
- Steuersatz nur beim Zufluss, Default 0 % (Erbschaften an direkte Nachkommen sind in den meisten Kantonen steuerfrei). Ins Cash fliesst der Betrag nach Abzug der Steuer.
Weder Zufluss noch Kosten gehen in die Spar-/Verzehrquote. Sie sind keine laufenden Flows; eine Erbschaft von 250'000 würde die Quote zu einem sinnlosen Ausschlag treiben. Sie wirken ausschliesslich auf das Cash und damit auf Vermögensverlauf, Endvermögen und Ruinalter.
Bedienung: Klick auf eine Übergangszelle der Cash-Zeile öffnet den Dialog „Uebergang: Cash".
Die Zelle zeigt 1:1, +100'000, −20'000 bzw. +100'000 / −20'000, solange offen ein ?.
Der Entscheid ist Teil des geführten Übergangs (3.5.4) und zählt im „offen"-Badge mit – eine
neu angelegte Phase erzeugt damit automatisch einen offenen Cash-Entscheid am neuen Übergang.
Referenz: src/components/ElementDetail.tsx (CashTransitionFields), src/components/PlanView.tsx
(CashTransitionDialog).
3.5.6 AHV-Prüfung am Pensions-Übergang
Die AHV-Rente hängt vom massgebenden durchschnittlichen Jahreseinkommen (mdJE) über die ganze Beitragsdauer ab. Diese Grösse kann das Tool nicht allein aus dem Plan bestimmen: Die Beitragspflicht beginnt mit 21, der Plan aber erst beim heutigen Alter. Bei einer 45-jährigen Person liegen 24 Beitragsjahre vor dem Planbeginn.
Deshalb ist die Beitragskarriere am Pensions-Übergang zu prüfen – dort, wo bereits die PK-Bezugsart und der 3a-Bezug entschieden werden. Der Dialog zeigt:
| Feld | Art |
|---|---|
| Geplantes Durchschnittseinkommen (aus dem Plan) | read-only, real |
| Beitragsjahre im Plan | read-only |
| Durchschnittseinkommen vor Planbeginn (real) | Eingabe – nur wenn Alter bei Planbeginn > 21 |
| Ausfalljahre vor Planbeginn | Eingabe – nur wenn Alter bei Planbeginn > 21 |
| Massgebendes durchschnittliches Jahreseinkommen | read-only, live berechnet |
| Resultierende AHV-Rente pro Jahr | read-only, live berechnet |
Die beiden Eingabefelder erscheinen also nur, wenn sie fachlich gebraucht werden. Die Zelle
zeigt Geprueft bzw. ?; der Entscheid zählt im „offen"-Badge mit. An allen anderen
Übergängen ist die AHV-Zelle inaktiv (–).
Wichtig zum Feld „vor Planbeginn": Der Wert ist real (heutige Kaufkraft). Der AHV-Kontoauszug listet Einkommen historisch-nominal – ein Lohn von 2008 steht dort mit dem Betrag von 2008 und wäre zu tief. Die Zahl stammt idealerweise aus der Rentenvorausberechnung, dort ist die Aufwertung bereits enthalten. Der Hilfetext im Feld sagt das.
Sonderfall „bei Planbeginn bereits pensioniert": Dann gibt es keinen Pensions-Übergang. Die gleichen Felder erscheinen stattdessen in der AHV-Phasenzelle der ersten Phase, zusammen mit der resultierenden Rente als Live-Vorschau.
Referenz: src/components/ElementDetail.tsx (AhvReviewFields), Formeln in
src/lib/calculations.ts (ahvMdje, ahvAnnualPension).
3.5.7 Warnhinweis bei Änderungen in früheren Phasen
Seit dem V3-Rework werden Werte live fortgeschrieben (Endwert = Startwert der Folgephase). Eine Änderung in einer frühen Phase wirkt damit bis ans Planende durch – oft unbemerkt. Seit Roadmap Nr. 3 gilt das verschärft: Ein geändertes Einkommen in Phase 1 verschiebt über das mdJE auch die AHV-Rente in Phase 5.
Beim Bearbeiten einer Phase, der noch Phasen folgen, erscheint deshalb ein rot abgesetzter Warnhinweis mit der Anzahl betroffener Folgephasen. Er erscheint in:
- Phasenzellen (Werte eines Elements in einer Phase)
- Phasen-Detail (Bezeichnung/Dauer – eine geänderte Dauer verschiebt alle Folgephasen)
Nicht in Übergangs-Dialogen (dort ist die Wirkung auf die Folgephase offensichtlich) und nicht beim Anlegen eines Elements (dort gibt es noch nichts zu überschreiben).
Referenz: src/components/ElementDetail.tsx (CarryWarning).
3.5.8 Verkaufspreis und modellierter Verkehrswert
Das Modell führt zwei Immobilienwerte getrennt: den Verkehrswert, der mit valueGrowth
wächst, und den ursprünglichen Kaufpreis als Basis der Grundstückgewinnsteuer
(4.6.5). Beim Verkauf zählt jedoch ausschliesslich der vom
Benutzer erfasste Verkaufspreis (4.9.4).
Daraus ergab sich eine stille Inkonsistenz: Man konnte 2 % jährliche Wertsteigerung annehmen und die Immobilie trotzdem zum Kaufpreis verkaufen, ohne dass das Tool widersprach.
Deshalb gilt seit Version 0.12:
- Beim Wechsel auf Verkaufen wird der Verkaufspreis mit dem modellierten Verkehrswert am Phasenende vorbelegt – aber nur, wenn noch keiner erfasst ist (bestehende Pläne bleiben unverändert).
- Der Dialog zeigt den Verkehrswert daneben read-only an und beziffert die Abweichung in Franken und Prozent.
- Ab 10 % Abweichung wird der Hinweis rot abgesetzt, mit der Aufforderung zu prüfen, ob Wertsteigerungsannahme und erwarteter Verkaufspreis zusammenpassen.
Der erfasste Preis bleibt massgebend – die Vorbelegung ist eine Hilfe, keine Bevormundung. Ein bewusst abweichender Preis (Notverkauf, Liebhaberpreis, Verkauf an Nachkommen) bleibt möglich. Die Berechnung ist unverändert.
Referenz: src/components/ElementDetail.tsx (ElementTransitionFields, REAL_ESTATE).
3.6 Auswertung und Visualisierung
3.6.1 Anzeigemodus nominal / beide / real
Ein Umschalter oben in der Planansicht steuert die Darstellung aller Geldbeträge in Matrix und Phasenköpfen:
| Modus | Darstellung |
|---|---|
| Nominal | 1'234'567 |
| Beide | nominal oben, real in Klammern darunter (siehe unten) |
| Real | 890'123 |
Im Modus Beide steht der Realwert seit 0.14 in einer eigenen Zeile unter dem nominalen Wert, nicht mehr daneben. Bei Start-/Endwerten wiederholt sich der Pfeil, damit der zeitliche Bezug erhalten bleibt:
30'000 → 10'000
(29'557) → (7'430)
Grund: Nebeneinander wird die Zeile so lang, dass die Spalten unnötig breit werden und Kennzahlen umbrechen. Untereinander bleiben die Spalten schmal – erst dadurch lässt sich jede Kennzahl umbruchfrei darstellen. Die Regel gilt im Phasenkopf und in den Matrix-Zellen; in den Modi «Nominal» und «Real» bleibt alles einzeilig.
„real" bedeutet kaufkraftbereinigt auf den Planbeginn: nominal / Deflator. Die Wahl wird in
localStorage unter fpt-value-mode gespeichert.
Es werden zwei verschiedene Deflatoren verwendet – siehe 4.5.3.
Referenz: src/components/PlanView.tsx (Anzeige-Umschalter über der Matrix).
3.6.2 Zeitachse
Horizontale Achse über das Alter (von jüngster Person bis Planende) mit:
- den Lebensphasen als Segmente: Breite proportional zur Dauer, Einfärbung nach Phasentyp (Erwerb kräftig, Misch mittel, Pension hell), beschriftet mit Name, Dauer und – sofern ein Planstart gesetzt ist – der Jahresspanne (z. B. „2026–2046")
- Flaggen-Marker je Person am Pensionsalter (Farbe: Person A indigo, Person B hellblau)
- rotem „Ruin "-Marker, falls zutreffend
- Alters- und Jahres-Beschriftung an beiden Enden
Die Kalenderjahre stammen aus dem Profilfeld Planstart (Scenario.startYear). Ist es nicht
gesetzt, zeigt die Achse nur Alter – die Berechnung ist davon nie betroffen (siehe 3.2.7).
Referenz: src/components/Timeline.tsx.
3.6.3 Phasenkopf-Kennzahlen
Seit 0.13 ist der Phasenkopf bewusst schlank – zwölf Kennzahlen in 10-Pixel-Schrift liest niemand (Progressive Disclosure):
| Kennzahl | Bedeutung |
|---|---|
| Name + Status-Icon | grünes Häkchen oder rotes Warnsymbol (Liquiditätslücke) |
| Typ-Badge + Dauer | Erwerb / Pension / Misch, „N J." |
| Alter je Person | <Name> <StartAlter> → <EndAlter> |
| Verfügbares Kapital | ab Phase 2 und nur wenn > 0: Topf, davon verteilt, Rest auf Cash (3.6.9) |
| Sparquote bzw. Verzehrquote | Einkommen − Ausgaben; Label wechselt auf „Verzehrquote", wenn Jahr 1 negativ; rot bei Verzehr |
| Vermögen | Start → Ende (inkl. Cash), hervorgehoben |
| Einmalposten | nur als Kurzhinweis (Bezeichnung), wenn vorhanden |
Die beiden Blöcke «Verfügbares Kapital» und «Sparquote» tragen je einen Knopf, der das passende Verteil-Werkzeug öffnet (3.6.10).
Alles Weitere – Einkommen/Ausgaben Jahr 1 → letztes Jahr, geplante Spar-/Verzehrrate, Kapitalzufluss und -investitionen, die vollen Einmalposten – wohnt in der Phasen-Detailansicht (seit 0.11, 3.6.7), erreichbar über das Expand-Icon im Kopf. Die Kennzahlen selbst sind unverändert berechnet; nur der Anzeigeort hat gewechselt.
Referenz: src/components/PlanView.tsx Zeilen 701–769.
3.6.4 Analyse-Bereich „Grafiken"
Die Auswertungen liegen nicht unter der Matrix, sondern in einem eigenen Bereich: Die Kachel Grafiken unter dem Menüpunkt Analysen (3.10.2) öffnet sie als breiten Dialog. So bleibt die Matrix die ruhige Hauptansicht.
Szenario-Wahl: Ganz oben steht ein Auswahlfeld, welches Szenario die Grafiken zeigen – es gilt für alle drei. Vorher war der Bereich fest an das geöffnete Szenario gebunden, und nur der Vermögensverlauf konnte weitere überlagern.
Kennzahl-Karten: Endvermögen nominal und Endvermögen real. (Die frühere Karte „Geschätzter Nachlass" ist entfallen – sie war rechnerisch identisch mit dem nominalen Endvermögen und suggerierte eine zusätzliche Information, die es nicht gab.)
Grafik 1 – Einkommen vs. Ausgaben pro Jahr (SparquoteChart): Ein Datenpunkt pro Jahr
über alle Phasen. Grüne Linie = Einkommen nominal (inkl. Renten), rote Linie = Ausgaben nominal,
graue gestrichelte Linie = Ausgaben real. Die Fläche zwischen Einkommen und nominalen Ausgaben
ist grün (Sparquote) oder rot (Verzehr) eingefärbt. Der Keil zwischen roter und grauer Linie ist
anschaulich „das, was die Inflation frisst".
Grafik 2 – Vermögensverlauf nach Alter (WealthChart): Liniendiagramm über das Alter von
Person A. Datenpunkte: jedes Planjahr (nicht nur die Phasengrenzen) – dadurch werden Verläufe
innerhalb einer Phase sichtbar, etwa das Abschmelzen im Kapitalverzehr. Grundlage ist
YearPoint.wealthNominal/wealthReal (4.10). Über Checkboxen lassen sich weitere Szenarien
überlagern; deren Daten werden bei Bedarf nachgeladen und im Client zwischengespeichert.
Der Datenschlüssel je Serie ist die Szenario-ID, nicht der Name. Bis 0.30 diente der Name als Schlüssel – zwei gleichnamige Szenarien überschrieben sich dadurch gegenseitig, und die Legende zeigte eine Linie an, die es im Diagramm gar nicht gab. Die Legende ist eigenständig (nicht die von Recharts): Sie erlaubt je Serie eine freie Farbwahl und erklärt den Linienstil (gestrichelt = reine Planwerte, durchgezogen = mit effektiven Werten).
Grafik 3 – Vermögensaufteilung im Zeitverlauf: Links eine gestapelte Fläche über alle
Planjahre, die Phasengrenzen als feine gestrichelte Senkrechte; rechts ein Ring, der die
relative Aufteilung zu einem wählbaren Zeitpunkt zeigt (Klick in die Fläche wählt das Jahr,
Vorgabe ist das Planende). Gestapelt werden alle Elemente der Kategorien PK, 3a, Immobilie,
Sonstiges Vermögen, die irgendwann einen positiven Wert haben. Stapelung nach elementId (nicht
Name), damit gleichnamige Elemente nicht kollidieren. Negative Werte werden auf 0 geklammert.
Bis 0.30 waren das gestapelte Balken mit zwei Säulen je Phase und schräg gestellter
Beschriftung – ab drei Phasen kaum mehr lesbar, und die relative Aufteilung war gar nicht
ablesbar. Die Fläche zeigt den Verlauf statt zweier Stichproben je Phase; die Jahreswerte dafür
liegen seit 0.11 ohnehin vor (ElementPhaseComputed.yearly), es wird nichts neu gerechnet.
Referenz: src/components/Dashboard.tsx, src/components/WealthChart.tsx,
src/components/SparquoteChart.tsx, src/components/AllocationChart.tsx.
3.6.5 CSV-Export
GET /api/scenarios/<scenarioId>/export liefert die vollständige Matrix als
semikolon-getrennte CSV. Bis 0.30 war das eine reine Phasen-Zusammenfassung – zehn Spalten, eine
Zeile je Phase, kein einziges finanzielles Element; was am Bildschirm stand, liess sich damit
nicht weiterverarbeiten.
Die Datei enthält vier Blöcke untereinander (Excel kommt damit zurecht, und es ist sofort erkennbar, was zusammengehört):
| Block | Inhalt |
|---|---|
| Kopf | Szenario, Haushaltsform, Personen (Alter/Pensionsalter), Planstart, Inflation, Cash-Anfangswert, Erzeugungszeitpunkt |
| LEBENSPHASEN | je Phase: Typ, Dauer, Altersspanne, Einkommen/Ausgaben, Quote (Beginn/Ende), Cash (Beginn/Ende), Vermögen (Beginn/Ende, nominal + real) |
| MATRIX | Zeilen = Cash + alle Elemente; je Phase zwei Spalten (Beginn/Ende), dazwischen je Übergang eine Spalte mit dem Entscheid im Klartext (z. B. «Kapitalbezug (Steuer 8 %)») |
| JAHRESWERTE | je Planjahr: Kalenderjahr, Alter, Vermögen nominal/real, Cash, Einkommen, Ausgaben, Sparquote |
Der Jahreswerte-Block ist der, mit dem sich in Excel selbst weiterrechnen lässt; die Matrix bildet 1:1 die Bildschirm-Ansicht ab.
Zwei Details, die vorher Ärger machten: Die Datei beginnt mit einem BOM – ohne dieses
Zeichen zerlegt Excel unter Windows alle Umlaute –, und der Dateiname transliteriert Umlaute
(ä→ae) statt sie durch _ zu ersetzen. Bei vorhandenem Ruin folgt eine Schlusszeile.
Referenz: src/lib/csv.ts (rein, ohne I/O), src/app/api/scenarios/[scenarioId]/export/route.ts.
3.6.6 Analyse-Bereich „Einflussfaktoren"
Der Button Einflussfaktoren berechnen in der oberen Aktionsleiste (neben „Grafiken" und „Monte-Carlo-Simulation") öffnet die Sensitivitätsanalyse als eigenen Dialog. Aufbau bewusst analog zur Monte-Carlo-Simulation:
- Erklärung – was die Analyse beantwortet, wie sie rechnet, was man davon hat, und die zwei ehrlichen Grenzen (Bandbreiten-Abhängigkeit, keine Wechselwirkungen).
- Zielgrösse – Endvermögen real (Default) oder nominal.
- Parameter – je Treiber eine Checkbox; erst angehakt erscheinen die beiden Pflichtfelder „tief" und „hoch" in der Einheit des Treibers, mit Hilfe-Bubble zu plausiblen Bandbreiten. Nicht anwendbare Treiber werden gar nicht erst angezeigt.
- Ergebnis – Basisfall, Tornado-Chart und Tabelle.
Der Dialog ist bewusst nicht Teil des Grafiken-Bereichs: Dieser ist reine Ausgabe, während der Tornado Pflichteingaben braucht. Seit 0.26 gehört auch das Pensionsalter je Person zu den Treibern – es erscheint nur, wenn die Phasengrenze überhaupt Spielraum hat (siehe 9.18 und 4.16).
Referenz: src/components/SensitivityDialog.tsx, src/components/AppShell.tsx.
3.6.7 Detailansichten je Element und je Lebensphase
Jede Element-Zeile und jeder Phasenkopf trägt oben rechts ein Expand-Icon (erscheint beim Überfahren). Es öffnet eine Nur-Lese-Detailansicht mit Reitern.
Element – zwei Reiter:
| Reiter | Inhalt |
|---|---|
| Verlauf | Liniendiagramm über alle Planjahre, nicht nur die Phasengrenzen. Bei einer Immobilie drei Reihen (Eigenkapital, Verkehrswert, Restschuld). Zusätzlich die wirksame Rate auf einer zweiten Y-Achse rechts in Prozent – als Stufenlinie, weil sie innerhalb einer Phase konstant ist und an der Phasengrenze springt; eine interpolierte Kurve würde einen gleitenden Übergang suggerieren, den die Berechnung nicht macht. Die Achse erscheint nur, wenn das Element überhaupt eine Rate trägt. Darunter eine Tabelle Beginn/Ende je Lebensphase. |
| Rechenweg | Die Herleitung je Phase und je Übergang (siehe 4.14) |
Lebensphase – drei Reiter:
| Reiter | Inhalt |
|---|---|
| Übersicht | Typ, Dauer, Vermögen Beginn/Ende; Vermögensaufteilung als Fläche über die Planjahre mit Ring für den gewählten Zeitpunkt |
| Wasserfall | Vermögens- und Cash-Wasserfall (4.14.2) |
| Rechenweg | Quote, Spar-/Verzehrrate, Cash-Fortschreibung, Vermögen, Phasentyp, Cash-Übergang |
Der Phasenkopf reagiert bereits auf Klick (öffnet das Bearbeiten-Popup); das Expand-Icon stoppt deshalb die Ereignis-Weitergabe, sonst gingen beide Dialoge gleichzeitig auf.
Zusätzlich öffnet der Button Rechenwege in der Aktionsleiste die plan-weiten Grössen: Deflatoren, AHV-Beitragskarriere je Person und Ruinalter.
Referenz: src/components/DetailView.tsx, src/components/PlanView.tsx.
3.6.8 Systemparameter-Ansicht
Eigener Eintrag in der Seitenleiste (über der SPEZIFIKATION). Zeigt alle fest hinterlegten Systemgrössen, gruppiert nach AHV, Vorsorge und Steuern, je mit Wert, Bedeutung, Herleitung, Quelle und Stand.
Die Einträge stammen aus SYSTEM_PARAMETERS in src/lib/constants.ts – derselben Datei, aus
der die Berechnung liest. Ein Auseinanderlaufen von angezeigtem und gerechnetem Wert ist damit
konstruktiv ausgeschlossen. Abgeleitete Grössen wie AHV_MAX_ANNUAL_SINGLE zeigen ihre
Herleitung (2 × R0 × 13) statt nur das Ergebnis.
Referenz: src/components/SystemParametersView.tsx, src/lib/constants.ts.
3.6.9 Verfügbares Kapital im Phasenkopf
Für die Planung einer Phase ist die zentrale Frage: Wie viel Kapital steht überhaupt zur Verfügung? Der Phasenkopf weist das ab Phase 2 als eigenen Block aus (nur wenn > 0):
| Zeile | Bedeutung |
|---|---|
| Verfügbares Kapital | der gesamte Topf beim Übergang in diese Phase |
| davon verteilt | Zusatzeinlagen + Sonderamortisation + Sofort-Tilgung |
| Rest auf Cash | was auf dem Cash-Konto liegen bleibt (= cashStart) |
Der Topf ist vollständig aus der Cash-Brücke (4.14.2) ableitbar – es braucht keine zusätzliche Berechnung:
Topf = Cash-Ende der Vorphase + Kapitalzufluss + einmaliger Zufluss − einmalige Kosten
= cashStart + Investitionen + Sofort-Tilgungen
Nur ab Phase 2: In der ersten Phase ignoriert die Berechnung additionalInvestment – dort
tragen die Elemente ihren Startwert direkt (4.6.3). Ein «Verteilen» hätte
dort eine andere Bedeutung, deshalb wird es gar nicht erst angeboten.
Referenz: src/lib/distribution.ts (capitalPot).
3.6.10 Verteil-Werkzeuge
Zwei Popups, erreichbar über je einen Knopf im Phasenkopf. Beide schreiben ausschliesslich bestehende Felder über die bestehenden Endpunkte – an der Berechnung ändert sich nichts.
«Kapital verteilen» verteilt den Topf aus 3.6.9 auf:
| Ziel | geschriebenes Feld | liegt an |
|---|---|---|
| PK, Säule 3a, Sonstiges Vermögen | additionalInvestment |
dieser Phase |
| 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
Einlage, ein Eingabefeld wäre also folgenlos.
Dass zwei verschiedene Objekte beschrieben werden, ist eine Folge des Datenmodells: Die
Zusatzeinlage ist ein Phasenwert, Sonderamortisation und Sofort-Tilgung sind Übergangs-Entscheide.
Beide zehren aber vom selben Topf. Betragsfelder werden dabei in die bestehenden Daten
hineingemischt – vorhandene Entscheide (decision, salePrice, payoutMode …) bleiben
erhalten.
Was nicht verteilt wird, bleibt automatisch auf dem Cash – dafür braucht es keine Logik, das 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 grün mit Haken, sobald etwas zugeteilt ist, und trägt sonst die Attention-Farbe. Vorher sah man einer fertig verteilten Phase nicht an, dass sie fertig war – der Knopf sah immer gleich aus. «Erledigt» heisst hier: Es ist Kapital da und etwas davon ist zugeteilt; wer bewusst nur einen Teil verteilt und den Rest liegen lässt, hat entschieden. Anders als bei der Quote gibt es dafür kein gespeichertes Kennzeichen – das Kapital ist keine Frage, die man beantworten MUSS, sondern eine Gelegenheit.
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), liegt daneben und ist nach Quelle aufgeschlüsselt. Bis 0.33 fehlte diese Zeile: Man sah eine 0 und hielt die Quote für wirkungslos.
«Sparquote verteilen» (bzw. «Bezug verteilen» bei Verzehr) verteilt die laufende Quote auf
jährliche Raten: annualContribution (3a, Sonstiges Vermögen), annualWithdrawal (Sonstiges
Vermögen), amortization (Immobilie), annualRepayment (Schulden).
Seit 0.40 sind diese Felder ausschliesslich hier erfassbar; in der Phasenzelle stehen sie read-only mit Absprung hierher (3.14.3).
Die Pensionskasse ist dabei – aber nicht in der Quote. Ihr Beitrag stammt aus dem Bruttolohn und belastet das Cash-Konto nicht (4.6.3); 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
Vorphase geerbten Beitrag, der als aktive Rate aussah, obwohl die Rechnung ihn längst verwarf.
Der Dialog weist drei Bezugsgrössen aus: Quote im ersten Jahr, im letzten Jahr und – entscheidend – die absolute Quote über die ganze Phase. Letztere ist die Grösse, gegen die sich eine flache Jahresrate sinnvoll verteilen lässt (siehe 9.25).
Live-Vorschau: Beide Dialoge kopieren den Plan mit den Entwurfswerten und rechnen ihn erneut
durch computePlan – im Browser, ohne API-Aufruf. Die angezeigte Wirkung ist dadurch per
Konstruktion exakt die spätere, inklusive aller Kappungen (Bezugsrate am Bestand, Amortisation
an der Restschuld). Eine Nebenrechnung im UI hätte hier dieselbe Driftgefahr wie bei den
Rechenwegen (4.14.3).
Beide Dialoge zeigen den Fortschreibungs-Warnhinweis (3.5.7), wenn Folgephasen existieren.
Referenz: src/components/DistributionDialogs.tsx, src/lib/distribution.ts.
3.6.11 Raten über Lebensphasen übernehmen
Werte liegen je Lebensphase vor. Bei Beträgen ist das richtig – das Einkommen ändert sich, der Vermögensstand ohnehin. Bei Raten ist es meist nicht gemeint: Wer die erwartete Rendite seines ETF auf 5 % setzt, meint fast nie «nur in Phase 2». Ohne Übernahme muss derselbe Wert vier- oder fünfmal eingetippt werden, und dabei wird zuverlässig eine Phase übersehen.
Ändert man ein Ratenfeld, erscheint deshalb im Bearbeitungspanel eine Rückfrage mit drei Möglichkeiten:
| Auswahl | Wirkung |
|---|---|
| Nur diese Phase (Vorgabe) | bisheriges Verhalten, andere Phasen bleiben unberührt |
| Diese + folgende | ab der bearbeiteten Phase vorwärts – Vergangenes bleibt stehen |
| Alle Phasen | der Wert gilt für den ganzen Plan |
Als Ratenfelder gelten:
| Feld | Kategorien |
|---|---|
expectedReturn |
Pensionskasse, Säule 3a, Sonstiges Vermögen |
valueGrowth |
Immobilie (Wertsteigerung) |
interestRate |
Immobilie (Hypothekarzins) |
teuerungsausgleich |
Einkommen (Lohnentwicklung), Ausgaben (reale Mehrausgaben) |
AHV und Schulden haben keine: Die AHV-Rente folgt der amtlichen Formel (4.4), Schulden tragen ihren Zins nicht als eigenes Feld.
Vier Entwurfsentscheide:
Die Rückfrage erscheint beim Bearbeiten, nicht als Modal. Das Zahlenfeld löst bei jedem Tastendruck aus – ein Dialog erschiene bei der Eingabe «5.2» viermal. Stattdessen taucht die Auswahl inline unter den Feldern auf, sobald sich eine Rate tatsächlich vom gespeicherten Wert unterscheidet, und wird beim Speichern ausgeführt.
Nur veränderte Raten lösen sie aus. Wer bloss einen Betrag anpasst, bekommt keine Rückfrage. «Nicht gesetzt» und «0» gelten dabei als gleich – sonst meldete schon das Öffnen eines Panels mit leerem Feld eine Änderung.
Die Zielphasen behalten ihre übrigen Werte. Der Endpunkt ersetzt den ganzen Werte-Satz einer Phase. Würde man den Entwurf der bearbeiteten Phase einfach hinüberkopieren, verlöre jede andere Phase ihre Beträge, Sparraten und Bezüge. Übertragen wird deshalb ausschliesslich das geänderte Ratenfeld, in die bestehenden Werte hineingemischt. Ein Test sichert genau das ab.
Phasen, in denen der Wert schon stimmt, werden übersprungen – das spart Schreibvorgänge und verhindert eine Version, obwohl sich inhaltlich nichts geändert hat.
Technisch ist die Übernahme ein Schreibvorgang je Zielphase über den bestehenden Endpunkt; es gibt keinen neuen Schreibpfad. Alle fallen in dieselbe Bearbeitungssitzung und ergeben deshalb eine Nebenversion, nicht eine je Phase (3.8.1).
Referenz: src/lib/ratefields.ts, src/components/ElementDetail.tsx.
3.7 Bedienoberfläche
3.7.1 Layout
- Sidebar (Desktop ab
lgfix, mobil als Overlay): Logo, Ctrl-K-Hinweis, „Übersicht", Gruppe «Meine Pläne» (Szenario-Baum, «+»), Gruppe «Wissen» («So rechnet FPT» = gerenderte Spezifikation, «Systemparameter»). - Header: Menü-Button (mobil), Plantitel, Profilmenü.
- Hauptbereich: Übersicht (Plan-Kacheln bzw. EmptyState mit «Geführt starten» / «Beispielplan») oder Planansicht (Umschalter, Zeitachse, Szenario-Profil-Leiste, «Nächste Schritte», Ruin-Banner, Matrix, Inspector-Panel rechts).
- Die Matrix hat einen eigenen Scrollbereich (max. 75 vh): Phasen-Köpfe (oben) und Elementnamen (links) bleiben beim Scrollen in beiden Achsen fixiert.
- Alle Aktions-Icons (Kopieren, Löschen, Detailansicht) sind auch ohne Hover leicht sichtbar – auf Touch-Geräten gibt es kein Hover.
3.7.2 Farbschemata
Der Akzent ist keine Bedeutung (Lehre aus 0.41). Im warmen Schema ist er ein Orangerot, im dunklen ein Indigo. Wer ihn benutzt, um «erledigt» oder «hier ist Geld hereingekommen» zu sagen, sagt im einen Schema etwas anderes als im anderen: Ein einmaliger Erbschafts-Zufluss sah warm aus wie ein Fehler. Bedeutung trägt deshalb Grün (positiv, erledigt), Attention (hier fehlt etwas) und Danger (hier ist etwas falsch) – der Akzent nur «hier kannst du klicken».
Ebenso hat jede Farbe zwei Vordergrund-Töne, und sie sind nicht austauschbar:
--attention-fgist weiss und gehört auf vollflächigen Attention-Grund;--attention-soft-fgist dunkel und gehört auf den weichen. Verwechselt man sie, ist die Schrift im warmen und im hellen Schema praktisch unsichtbar – in 0.41 an sechs Stellen behoben.
Drei Themes: Hell, Dunkel, Warm (cremefarben, Koralle-Akzent). Wahl im Profilmenü,
persistiert in localStorage (fpt-theme), gesetzt als data-theme am <html>. Ohne
gespeicherte Wahl folgt die Oberfläche prefers-color-scheme. Ein Inline-Script im <head>
setzt das Attribut vor dem ersten Paint (verhindert FOUC).
Alle Farben laufen über semantische CSS-Variablen (--bg, --surface, --accent, --danger, …).
Referenz: src/lib/theme.ts, src/app/globals.css, src/app/layout.tsx Zeilen 1181–1196.
3.7.3 Geldeingabefelder
Die MoneyInput-Komponente ist ein spezialisiertes Betragsfeld:
- Unfokussiert Anzeige mit Apostroph-Tausendertrennung (
1'234'567), fokussiert reine Ziffern - Ein Default-Wert 0 wird beim Fokussieren geleert, sonst der Text markiert
- Pfeiltasten: ↑/↓ = ±1, Shift+↑/↓ = ±100
- Pfeil-Buttons mit Klick-und-Halten-Beschleunigung: nach 400 ms Wiederholung im 70-ms-Takt, Schrittweite wächst mit der Haltedauer (1 → 10 → 100 → 1'000 → 10'000)
- Optionale harte Klammerung über
min(Default 0) undmax
Zahlenformat: Bewusst nicht über toLocaleString("de-CH"), weil dessen Trennzeichen das
typografische Apostroph (U+2019) ist. Das Tool verwendet durchgehend das gerade Apostroph.
Referenz: src/components/FormField.tsx Zeilen 60–200, src/lib/format.ts.
3.7.4 Hilfe-Bubbles
Feldbeschriftungen können ein Info-Icon tragen (InfoBubble), das per Hover oder Klick einen
Erklärtext einblendet. Wird durchgehend für die fachlich heiklen Felder verwendet.
3.7.5 Reaktivität
Die Anwendung rechnet nicht im Client. Jede Änderung führt zu einem PUT/PATCH/POST,
gefolgt von onChanged() → loadDetail(planId, silent = true). Der „silent"-Refresh lädt Plan
und Berechnung neu, ohne die PlanView zu demontieren – so bleibt die Scrollposition nach dem
Schliessen eines Popups erhalten.
Referenz: src/components/AppShell.tsx.
3.7.6 Sprache und UI-Primitiven
Seit 0.13 gilt durchgehend die Du-Form (wie moderne CH-Finanz-Apps) und es werden überall echte Umlaute verwendet – zuvor mischte das UI «Übergang prüfen» mit «Uebersicht» und Sie- mit Du-Anrede. Der Sweep umfasst auch die API-Fehlermeldungen (z. B. „Nur die letzte Phase kann gelöscht werden.").
Alle Bausteine liegen zentral in src/components/ui.tsx:
| Baustein | Ersetzt / leistet |
|---|---|
Button |
vier Varianten (primary/secondary/danger/ghost), einheitliche Hover-/Active-Zustände |
Modal |
Dialog mit ESC, Fokus-Falle, aria-modal, Einblend-Animation |
ConfirmProvider / useConfirm |
ersetzt window.confirm() – Browser-Systemdialoge folgen keinem Farbschema |
ToastProvider / useToast |
ersetzt alert(); Erfolgs- und Fehlermeldungen unten rechts |
Skeleton / PlanSkeleton |
Struktur-Platzhalter statt «Lädt…»-Text |
EmptyState |
einheitliche leere Zustände mit Erklärung und Aktion |
InspectorShell |
das rechte Bearbeiten-Panel (3.7.7) |
Mehrere Schichten (Panel + Dialog) teilen sich einen ESC-Stapel: ESC schliesst immer nur
die oberste. Animationen (150–220 ms) respektieren prefers-reduced-motion.
Attention-Farbe: Offene Entscheide tragen neu einen eigenen Amber-Ton (--attention,
je Farbschema abgestimmt) statt der Akzentfarbe – «hier fehlt eine Eingabe» und «hier kannst
du klicken» sind damit unterscheidbar.
Diagramm-Palette: Seit 0.31 kommen auch die Farben aller Diagramme aus Tokens
(--chart-1 … --chart-6, dazu --chart-grid) statt aus festen Hex-Werten – je Farbschema
gedämpft abgestimmt. Recharts bekommt die Variablen direkt als Attributwert (var(--chart-1)),
der Browser löst sie im SVG auf. Vorher stachen die Diagramme mit greller Standardpalette aus
dem Schema heraus und sahen im Dunkelmodus falsch aus.
3.7.7 Inspector-Panel statt Modals
Seit 0.42 gilt das nur noch für die Matrix. In der Kartenansicht wird in der Karte selbst bearbeitet (3.15.2) – das Panel legte sich als
fixed right-0ausgerechnet über die Spalte, zu der man gerade gescrollt hatte.
Alle Einzel-Bearbeitungen – Phasenzelle, Übergangszelle, Cash-Übergang, Cash-Anfangswert,
Phase (Name/Dauer), Szenario-Profil – öffnen sich als rechtes Seitenpanel (InspectorShell,
26 rem, mobil volle Breite) statt als Modal. Bewusst ohne Backdrop: Die Matrix bleibt
sichtbar und klickbar, ein Klick auf eine andere Zelle wechselt einfach den Panel-Inhalt –
der Kontext, den Modals nehmen, bleibt erhalten. Genau ein Panel kann offen sein (eine
Panel-Union ersetzt die früheren Einzel-Zustände); der key erzwingt beim Wechsel den
Neuaufbau des Formulars wie zuvor bei den Dialogen.
Modals bleiben für Erstell-Flows (Element, Phase, Plan, Bestandsaufnahme), den geführten Übergang (mehrere Objekte auf einmal), die Analysen und die Detailansichten.
Der frühere Dialog «Plan-Einstellungen» heisst im Panel korrekt «Szenario-Profil» – er bearbeitet seit V6 das Szenario, nicht den Plan.
Aufbau der Szenario-Ansicht (seit 0.28/0.29): von oben nach unten eine schlanke Funktions-Leiste (Änderungshistorie · Tour · Neues Szenario aus diesem · Rechenwege · CSV-Export · Szenario löschen · Abweichungs-Badge), dann Nächste Schritte, danach ein kompakter Block aus drei Spalten – Grundprofil (25 %, Angaben untereinander, «Bearbeiten» oben rechts), Zeitachse (50 %) und Kennzahlen (25 %: Endvermögen nominal und real, bei Ruin das Ruinalter) –, dann der Anzeige-Umschalter (Nominal/Beide/Real, Plan/Ist) und zuunterst die Matrix mit ihren «Element»-/«Lebensphase»-Knöpfen. Der Drei-Spalten-Block ersetzt das früher über die ganze Breite gezogene Grundprofil, das auf breiten Bildschirmen unnötig lang wirkte. Auf schmalen Screens stapeln die drei Spalten. Die Analyse-Werkzeuge (Grafiken, Live-Simulation, Monte-Carlo, Einflussfaktoren) und die Effektiven Werte sind hier bewusst nicht mehr verlinkt – sie laufen ausschliesslich über ihre eigenen Menüpunkte (3.10), damit es je Funktion genau einen Ort gibt.
3.7.8 Tour und «Nächste Schritte»
Die Tour (Tour.tsx) führt in bis zu sechs Schritten über Profil, Zeitachse, Matrix,
Übergänge, Cash und Analysen – als Spotlight: das jeweilige Ziel bleibt hell, der Rest wird
abgedunkelt, dazu ein pulsierender Rahmen (data-tour-Attribute); die Karte springt auf die
gegenüberliegende Bildschirmhälfte und lässt sich überspringen (Umsetzung und Grenze: 9.24).
Schritte ohne vorhandenes Ziel werden übersprungen; der «Tour»-Knopf in der oberen
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, 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
Szenario-Ansicht bereits gerendert ist; der Auslöser feuert deshalb erst, wenn das Szenario
geladen ist.
Die Karte «Nächste Schritte» über der Matrix leitet aus den vorhandenen Daten ab, was sinnvollerweise als Nächstes ansteht – offene Übergangs-Entscheide (mit Direktsprung in den geführten Übergang), fehlendes Einkommen/fehlende Ausgaben, ein Plan, der vor der Pensionierung endet, und bei Ruin der Sprung zu den Einflussfaktoren. Kein eigener Zustand, kein API-Aufruf; die Karte verschwindet von selbst, wenn nichts offen ist. Auch das Ruin-Banner verlinkt neu auf die Einflussfaktoren («Wirksamsten Hebel finden»).
3.7.9 Befehls-Palette und Sparklines
Ctrl/Cmd+K öffnet die Befehls-Palette (CommandPalette.tsx): Springen zu jedem
Plan/Szenario und die wichtigsten Aktionen (Analysen, neues Szenario, neuer Plan), mit
Tastatur-Navigation – der Effizienz-Pfad für geübte Nutzer.
Jede Element-Zeile der Matrix trägt eine Sparkline (Sparkline.tsx): der Wertverlauf
über alle Planjahre als Mini-SVG, gespeist aus den per-Jahr-Werten der Detailansichten
(ElementPhaseComputed.yearly, seit 0.11) – keine Neuberechnung, reine Darstellung. Flache
Verläufe werden nicht gezeichnet (keine Information).
3.8 Versionierung und Änderungshistorie
Jedes Szenario trägt eine Version A.B und eine vollständige Änderungshistorie. Zugang über den Knopf «Änderungshistorie» in der Szenario-Leiste.
| Bedeutung | Entsteht | |
|---|---|---|
| B (Nebenversion) | ein Bearbeitungsstand | automatisch, eine je Bearbeitungssitzung |
| A (Hauptversion) | ein bewusst gesetzter Meilenstein | manuell, mit Pflichtkommentar; setzt B auf 0 |
3.8.1 Eine Version je Sitzung – nicht je Änderung
FPT hat keinen Speichern-Knopf: Jede Änderung schreibt sofort. Eine Version je Schreibvorgang wäre deshalb ein Tastenprotokoll und keine Historie – ein Durchlauf des Bestandsaufnahme-Dialogs macht rund 14 Schreibvorgänge, ein Klick im Verteil-Dialog einen je Zielelement.
Stattdessen werden alle Schreibvorgänge innerhalb eines Zeitfensters von 10 Minuten zu einer Nebenversion zusammengefasst: Der erste legt sie an, alle weiteren aktualisieren sie. Zwei Sicherungen ergänzen das:
- Unverändert = keine Version. Ergibt ein Schreibvorgang inhaltlich denselben Stand (Dialog geöffnet und unverändert geschlossen), entsteht nichts. Verglichen wird über eine Serialisierung mit sortierten Schlüsseln – ohne das würden identische Stände als verschieden gelten, weil die Reihenfolge der Werte aus der Datenbank-Zeilenfolge stammt.
- Benutzerwechsel trennt immer. Änderungen verschiedener Benutzer laufen nie in einer Version zusammen, auch nicht innerhalb des Fensters – Vorbereitung auf die spätere Freigabe an einen Finanzberater.
Eine Hauptversion wird nie zusammengefasst und nie nachträglich verändert; die nächste Änderung beginnt bei A.1.
Ein Szenario startet bei 0.1. Die Nebenversion zählt beliebig weit hoch (0.1, 0.2, … 0.137);
die 0 bedeutet «noch kein verabschiedeter Stand». Erst eine bewusst gesetzte Hauptversion
macht daraus 1.0. Bis 0.30 begann jedes Szenario bereits bei 1.0 – damit war die Hauptversion
bedeutungslos, weil jeder Plan schon eine hatte. Bestehende Historien werden nicht
umnummeriert; nur der Startwert ist neu (Migration 20260724120000_version_zero_start).
3.8.2 Was die Historie zeigt
Je Version: wer (Benutzername), wann (Zeitpunkt der letzten Änderung dieser Sitzung) und – bei Hauptversionen – der Kommentar. Die oberste Zeile ist der aktuelle Stand.
Zwei Aktionen je Version:
- Anzeigen – ein Nur-Lese-Fenster mit der Matrix dieses Standes (VersionMatrix), Phasen als Spalten, Elemente als Zeilen, dazu Endvermögen und Ruinalter. Bewusst nicht die Bearbeitungs-Matrix: Inspector, Übergangs-Ampeln und Diff-Markierung haben für einen alten Stand keine Bedeutung.
- Wiederherstellen – setzt das Szenario vollständig auf diesen Stand zurück.
3.8.3 Wiederherstellen
Zwei Eigenschaften machen den Knopf ungefährlich:
Es geht nichts verloren. Der wiederhergestellte Stand wird selbst als neue Version festgehalten, mit dem Kommentar «Wiederhergestellt aus A.B». Die bisherige Historie bleibt vollständig – auch alles, was nach dem Zielstand kam.
IDs bleiben erhalten. Phasen und Elemente behalten ihre IDs (der Snapshot trägt sie mit).
Das ist keine Kosmetik: Kind-Szenarien zeigen über sourceElementId / sourcePhaseId auf
genau diese IDs, und daran hängt die Abweichungs-Markierung
(3.2.6). Würden neue IDs entstehen, erschiene in
jedem Kind-Szenario schlagartig alles als «neu» statt als «geändert».
Restlos vermeiden lässt sich das nicht: Geht man auf einen Stand zurück, in dem ein Element noch gar nicht existierte, auf das ein Kind verweist, bricht dieser eine Bezug zwangsläufig. Der Dialog prüft das vorher und benennt die betroffenen Szenarien namentlich, statt es stillschweigend zu tun.
3.8.4 Analyse auf einer bestimmten Version
Alle vier Analysewerkzeuge – Grafiken, Live-Simulation, Monte-Carlo und Einflussfaktoren – haben oben einen Wähler «Berechnungsgrundlage»: aktueller Stand (Vorgabe) oder eine festgehaltene Version.
In der Monte-Carlo-Simulation ist die Auswahl zweidimensional: Sie rechnet mehrere Szenarien gleichzeitig, und der Wähler hängt deshalb an der jeweiligen Szenario-Zeile – man kann Szenario A in Version 1.2 gegen Szenario B in Version 2.0 stellen.
Das war fast kostenlos: Der Snapshot ist bereits ein PlanInput, also genau der Typ, den
alle Werkzeuge ohnehin entgegennehmen. Es genügt, den Arbeitsstand durch den Snapshot zu
ersetzen; das Gerechnete entsteht lokal über computePlan.
Eine Ausnahme ist beschriftet: Der CSV-Export liest immer das Szenario aus der Datenbank und liefert deshalb den aktuellen Stand, nicht die betrachtete Version. Der Knopf sagt das in diesem Fall ausdrücklich.
3.8.5 Vollständigkeit der Historie
Die Versionierung hängt daran, dass jeder inhaltsverändernde Endpunkt sie auslöst
(touchScenario). Ein vergessener Pfad fiele nicht auf – er erzeugte still keine Version, und
die Lücke bemerkte man erst Wochen später. Ein Test liest deshalb alle Route-Dateien und
verlangt, dass jeder schreibende Endpunkt entweder touchScenario aufruft oder mit Begründung
in einer Ausnahmeliste steht; verwaiste Ausnahmen meldet er ebenfalls.
Das Festhalten einer Version ist bewusst fehlertolerant: Schlägt es fehl, wird es protokolliert, die eigentliche Änderung des Nutzers aber nicht zurückgewiesen. Die Historie ist Begleitinformation, nicht der Zweck der Anfrage.
Referenz: src/lib/versioning.ts (reine Logik), src/lib/versioning-db.ts (Datenbank),
src/components/VersionHistoryDialog.tsx, src/components/VersionMatrix.tsx,
src/components/VersionPicker.tsx.
3.9 Effektive Werte (Plan-/Ist-Vergleich)
Roadmap Nr. 5. Macht aus dem Planer ein Monitoring-Werkzeug: Was ist tatsächlich eingetreten, und was heisst das für den Rest der Planung?
Knopf «Effektive Werte» auf Plan-Ebene → Liste der bisherigen Erfassungen → Wizard in zwei Schritten.
3.9.1 Ein Ist-Satz gehört zum Plan, nicht zum Szenario
Das tatsächliche PK-Guthaben am 18.8.2026 ist eine Zahl – unabhängig davon, gegen welches
Szenario man sie hält. Ein Ist-Satz hängt deshalb am Plan und wird über dieselbe
Herkunfts-Kette (sourceElementId) auf die szenario-eigenen Element-IDs abgebildet, die auch
der Diff (3.2.6) und die Monte-Carlo-Gruppierung benutzen.
Erfasst wird also je Wurzel-Element.
3.9.2 Der Wizard
Schritt 1 – Stichtag. Exaktes Datum (z. B. 18. August 2026) plus optionale Notiz. Das Datum erscheint in der Liste und auf der Zeitachse; für die Rechnung zählt nur die Jahreszahl, weil der Rechenkern in ganzen Jahren ab Planbeginn arbeitet. Der Dialog sagt das ausdrücklich.
Schritt 2 – Werte. Alle Elemente aller Szenarien dieses Plans, zusammengefasst auf ihre Wurzel, dazu das Cash-Konto. Vorbelegt mit dem Stand, den der Plan für dieses Jahr vorsieht (aus dem Basisszenario; fehlt das Element dort, aus dem erstbesten Szenario, das es kennt). Der Nutzer überschreibt nur, was tatsächlich abweicht.
Zwei Arten von Werten, die sich verschieden verhalten:
| Art | Elemente | Wirkung |
|---|---|---|
| Bestand | PK, 3a, Sonstiges Vermögen, Schulden, Cash | ersetzt den laufenden Stand |
| Verkehrswert + Schuld | Immobilie | zwei Felder: Wert und Resthypothek getrennt |
| Fluss | Einkommen, Ausgaben | nominaler Jahresbetrag; ersetzt die Basis für alle Folgejahre |
Die AHV erscheint nur, wenn die Rente zum Stichtag bereits läuft. Vorher gibt es keinen Stand, den man ablesen könnte – die Rente folgt der amtlichen Formel aus der Beitragskarriere (4.4).
Ist-Werte erfassen Werte, keine Entscheide. Wenn der Plan die Immobilie verkauft, du sie aber behalten hast, lässt sich das hier nicht ausdrücken – dafür ist ein Szenario da.
3.9.3 Die zweite Berechnung
Der Plan-Lauf bleibt unangetastet. Parallel läuft ein zweiter mit derselben Mechanik, aber korrigierter Ausgangsbasis: In jedem Jahr, für das ein Ist-Satz erfasst wurde, schnappen die Werte auf die Realität und laufen von dort planmässig weiter. Alle Sätze gehen ein, nicht nur der jüngste.
Beispiel aus der Anforderung: Fonds startet 2020 mit 100'000 bei 5 %. Ohne Ist-Daten steht 2022 rechnerisch 115'763. Wird für 2022 ein Ist-Wert von 120'000 erfasst, rechnet die Ist-Sicht ab dort weiter und steht 2024 bei 132'300. Kommt für 2024 ein Wert von 140'000 dazu, springt sie erneut. Genau das ist als Test hinterlegt.
Lücken fallen auf die Plandaten zurück. Ein Element ohne erfassten Ist-Wert läuft unverändert auf seiner Planlinie weiter – man muss nicht alles wissen, um etwas zu erfassen.
Technisch: computePlan(plan, sample?, { actuals }). Ohne die Option verhält sich die
Funktion exakt wie bisher; die 43 Golden Tests laufen unverändert.
Der Sprung ist keine Rendite. Die Differenz zwischen Plan und Wirklichkeit wird als eigene
Brückenposition actualsCorrection geführt (Vermögens- und Cash-Brücke). Würde man sie den
Kapitalerträgen zuschlagen, erschiene ein Planrückstand als Anlageverlust – und die Zerlegung
ginge im Ist-Jahr nicht mehr auf (3.6.8).
3.9.4 Anzeige in der Matrix
Ein zweiter Umschalter neben nominal/real/beide, aber mit nur zwei Möglichkeiten:
| Auswahl | Wirkung |
|---|---|
| Plan | wie bisher |
| Effektiv | die Ist-Zahlen, jeweils mit der Abweichung zum Plan daneben |
Warum keine dritte Möglichkeit «beide»: Plan und Ist als Rohwerte nebeneinander wären bei zusätzlich aktivem nominal/real acht Zahlen je Zelle. Stattdessen zeigt die Ist-Ansicht den Wert und daneben klein die Differenz, grün oder rot (9.29). Der Umschalter erscheint nur, wenn überhaupt Ist-Werte erfasst sind.
Nur die Abweichung trägt Farbe. Die Beträge selbst bleiben neutral – sonst wird die Matrix zum Ampelteppich, in dem nichts mehr heraussticht.
3.9.5 Zeitachse
Je erfasstem Jahr ein Marker. Der jüngste ist farbig, ältere blass – sie sind überholt, aber nicht bedeutungslos. Ohne gesetztes Planstartjahr entfallen die Marker, weil es dann keinen Kalenderbezug gibt.
3.9.6 Die vier Analysewerkzeuge
Grafiken, Live-Simulation, Monte-Carlo und Einflussfaktoren erhalten dieselbe Leiste: Werte (nominal/real) und Grundlage (Plan/Effektiv), dazu die schon bestehende Versionswahl. «Effektiv» ist deaktiviert, solange nichts erfasst ist.
Nominal/real ist in den Werkzeugen eine Einfachauswahl, kein «beide» – anders als in der Matrix. Begründung siehe 9.29.
Besonderheiten:
- Vermögensverlauf: Im Ist-Modus kommt die reine Planlinie gestrichelt als Referenz dazu. Maximal vier Serien.
- Monte-Carlo: Der Zielbetrag dreht mit der gewählten Grösse (real/nominal) und wird entsprechend beschriftet – sonst prüft man einen nominalen Zielbetrag gegen ein reales Endvermögen. Ausserdem eine Zeile: Simuliert ab ‹Jahr›; die Jahre davor sind durch deine effektiven Werte belegt und werden nicht gewürfelt. Das Startjahr ist abgeleitet, nicht eingebbar – ein frei gesetztes Jahr würde Jahre als sicher behandeln, die nie erfasst wurden.
- Einflussfaktoren: Mit Ist-Werten wirken die Treiber nur noch auf die nicht belegten Jahre. Die Balken fallen dadurch zu Recht kürzer aus.
3.9.7 Verhältnis zur Versionierung
Ein Ist-Satz ist eine Beobachtung, keine Planänderung: Er erzeugt keine Szenario-Version (3.8), und es gibt kein Wiederherstellen. Löschen entfernt ihn ersatzlos; der Plan bleibt unberührt. Die beiden Historien bleiben getrennt.
Referenz: src/lib/actuals.ts, src/lib/dataview.ts, src/components/ActualsDialog.tsx,
src/components/AnalysisControls.tsx.
3.9.6 Immobilien: Verkehrswert, nicht Eigenkapital
Bei einer Immobilie führt der Rechenkern zwei Grössen: den Verkehrswert der Liegenschaft
und die Restschuld. Was die Matrix zeigt und was ElementYearPoint.value trägt, ist die
Differenz – das Eigenkapital. Der Verkehrswert steht separat in propertyValue.
Der Ist-Wizard erfasst Verkehrswert und Restschuld getrennt, nie das Eigenkapital. Bis 0.31
belegte er das Wertfeld irrtümlich mit value (dem Eigenkapital) vor: Der Rechenkern setzte
diese Zahl als Verkehrswert ein und liess die Hypothek unverändert, wodurch das Eigenkapital im
Ist-Jahr um genau die Hypothek einbrach – meist ins Negative. Weil ein Ist-Satz alle Zeilen
mitschreibt (auch die unveränderten), traf das jeden Satz, selbst wenn nur ein Lohn angepasst
wurde. Drei Regressionstests in actuals.test.ts halten die Trennung fest.
3.9.7 Ist-Datensätze bearbeiten
Ein erfasster Satz lässt sich über einen Klick auf seine Zeile (oder «Bearbeiten») erneut öffnen
und korrigieren – Stichtag, Kommentar, Cash und alle Werte. Der Wizard läuft dabei in beiden
Schritten wie beim Erfassen, überschreibt die bereits erfassten Zahlen aber nicht mit den
Planwerten; nur Zeilen ohne erfassten Wert (z. B. ein später hinzugekommenes Element) werden
ergänzt. Endpunkt: PUT /api/plans/<planId>/actuals/<setId>.
Wie das Anlegen erzeugt auch das Bearbeiten keine Szenario-Version – ein Ist-Satz ist eine Beobachtung, keine Planänderung. Der ursprüngliche Erfasser bleibt vermerkt.
3.10 Navigation auf Plan-Ebene und gespeicherte Analysen
Die Seitenleiste ist zweistufig: Unter jedem Plan liegen vier Unterpunkte. Ein Klick auf den Plan-Namen öffnet dessen Dashboard.
| Ort | Führt zu |
|---|---|
| Plan-Name | Plan-Dashboard (Kennzahlen, Umbenennen) |
| Szenarien | Szenario-Liste – darunter der Szenario-Baum, verschachtelt nach Herkunft; seine Einträge führen direkt in die Matrix |
| Effektive Werte | Liste + Wizard der Ist-Werte (3.9) |
| Analysen | Vier Werkzeug-Kacheln + Liste gespeicherter Analysen |
| Berichte | PDF-Berichte erzeugen und wieder herunterladen (3.11) |
Die Seitenleiste ist damit dreistufig:
| Ebene | Inhalt |
|---|---|
| 1 | die Pläne |
| 2 | je Plan die vier Bereiche: Szenarien · Effektive Werte · Analysen · Berichte – ihre Symbole stehen auf einer Linie (der Knoten «Szenarien» trägt zusätzlich den Auf-/Zuklapp-Pfeil) |
| 3 | nur unter «Szenarien»: die einzelnen Szenarien, verschachtelt nach ihrer Herkunftskette (beliebig tief) |
So ist in der Seitenleiste sichtbar, woraus ein Szenario entstanden ist; die Szenario-Liste zeigt dasselbe zusätzlich als Spalte «aus …».
3.10.1 Plan- vs. Szenario-Ebene der Kennzahlen
Seit V7 (9.30) liegen Haushaltsform, Personen und Startjahr am Plan; Endvermögen, Ruinalter, Phasen und Elemente sind dagegen szenario-eigen. Das Plan-Dashboard zeigt deshalb die Haushaltsdaten direkt, alle gerechneten Kennzahlen aber ausdrücklich als «laut Basisszenario» – das Basisszenario ist der kanonische Vertreter. Die vier Kennzahl-Kacheln (Szenarien, Effektive Werte, Analysen, Berichte) sind anklickbar und führen in ihren Bereich. Über ein Stift-Symbol neben dem Plannamen lässt sich der Plan umbenennen.
Falls Ist-Werte erfasst sind, weist das Dashboard die Abweichung des Endvermögens gegenüber dem Plan aus – benannt mit dem Jahr des jüngsten Ist-Datensatzes («Mit den effektiven Werten von 2032 …») und grün, wenn die Realität besser ist als der Plan, sonst rot.
Die Szenario-Liste zeigt je Szenario Name, die aktuelle Version (z. B. «0.17») samt ihrem Kommentar (sofern vorhanden – gesetzt wird er bei Hauptversionen und beim Wiederherstellen), Anzahl Phasen und Elemente, das Endvermögen und ob das Kapital reicht; das Basisszenario ist farblich hervorgehoben, und die Herkunft (aus welchem Szenario kopiert) steht darunter. Ein Klick auf die Zeile öffnet die Matrix; je Zeile gibt es zusätzlich Historie, Kopie (macht dieses Szenario zur Vorlage) und – ausser beim Basisszenario – Löschen.
3.10.2 Analysen: vier Kacheln
Die Werkzeuge Grafiken, Live-Simulation, Monte-Carlo und Einflussfaktoren liegen als Kacheln vor, die beim Darüberfahren – oder Antippen (Touch hat kein Hover) – umklappen und einen Erklärtext zeigen. Ein Klick startet das jeweilige Werkzeug auf dem Basisszenario; Szenario und Version lassen sich darin weiterhin umstellen.
Grafiken öffnen neu nicht mehr alle drei Diagramme, sondern lassen zuerst eine wählen (Vermögensverlauf / Einkommen vs. Ausgaben / Vermögensaufteilung), dazu wie bisher nominal/real und Plan/effektiv. Der Szenario-Vergleich wandert mit zu den Grafiken; der CSV-Export zieht dorthin, wo er hingehört – auf die Matrix.
3.10.3 Gespeicherte Analysen
Jede Grafik, Monte-Carlo-Simulation und Einflussfaktoren-Berechnung kann gespeichert werden (die Live-Simulation vorerst nicht – sie ist bewusst flüchtig). Festgehalten werden Eingaben und Ergebnis als Zahlen, read-only: Beim Öffnen wird nichts neu gerechnet, die gespeicherten Werte werden nur gezeichnet.
Zahlen statt Bild – und zwar bewusst: Ein Bildschirm-Abbild wäre im Druck unscharf (300 dpi
gegen 96), im Seitenformat fix und im Dunkelmodus falsch eingefärbt. Aus Zahlen zeichnet der
spätere PDF-Bericht (Roadmap Nr. 11) die Grafik vektoriell und in Druckauflösung neu –
und braucht die Zahlen für Tabellen und Fliesstext ohnehin. Bewusst nicht gespeichert wird
finalWealthSorted aus der Monte-Carlo-Simulation (ein Eintrag je Lauf, megabyteweise); für die
Anzeige genügen die abgelesenen Wahrscheinlichkeiten und die Bänder.
Jedes Werkzeug schreibt sein Ergebnis in dieselbe generische Form (Parameter, Kernzahlen, Tabelle, Grafik). Dadurch braucht die Nur-Lese-Ansicht nur einen Renderer, und der PDF-Bericht findet überall dieselbe Struktur vor. Der Name wird automatisch vorgeschlagen («Typ · Szenario Version · Datum»), ist aber überschreibbar.
Die Liste je Plan zeigt Typ, Zeitpunkt, Szenario/Version, nominal/real und Plan/effektiv. Ein gespeicherter Datensatz ist eine Momentaufnahme, keine Planänderung: Er erzeugt keine Version, und Löschen entfernt ihn ersatzlos.
Referenz: src/components/PlanViews.tsx, src/components/SavedAnalysisView.tsx,
src/components/SaveAnalysisButton.tsx, src/lib/analyses.ts.
3.11 PDF-Berichte
Roadmap Nr. 11. Neuer Unterpunkt «Berichte» auf Plan-Ebene: eine Liste der erzeugten Berichte, von dort aus lässt sich ein neuer anlegen.
3.11.1 Was ein Bericht ist
Ein Bericht ist ein festes Dokument, kein Blick auf den aktuellen Stand. Die erzeugte PDF-Datei wird als Datei abgelegt und lässt sich jederzeit unverändert wieder herunterladen – auch nachdem der Plan weiterentwickelt wurde. Das ist der Zweck: ein verlässlicher Audit-Trail. Wer heute einen Bericht verschickt, muss in drei Jahren exakt dasselbe Dokument vorweisen können.
Deshalb wird nicht die Definition gespeichert und das PDF bei Bedarf neu erzeugt: Eine Neuerzeugung könnte nach Änderungen an Plan, Rechenkern oder Layout nicht mehr dieselben Bytes liefern.
Abgelegt werden drei Dinge: die gewählten Parameter, das eingefrorene Berichtsmodell (alle Zahlen, für die Liste und zur Nachvollziehbarkeit) und die PDF-Datei selbst. In der Datenbank, nicht im Dateisystem – der Anwendungscontainer wird bei jedem Deploy neu gebaut, nur das Datenbank-Volume überlebt.
3.11.2 Auswahl beim Anlegen
| Angabe | Möglichkeiten |
|---|---|
| Titel und Notiz | frei; die Notiz erscheint auf dem Deckblatt |
| Werte | nominal oder real – eine Leitgrösse für den ganzen Bericht |
| Grundlage | Plandaten oder effektive Werte (nur wenn erfasst) |
| Szenarien | 1 bis 3 (Konstante MAX_REPORT_SCENARIOS) |
| Analysen | beliebig viele der gespeicherten Auswertungen |
Zwei bewusste Begrenzungen:
Nur eine Leitgrösse. «Beides» würde jede Tabellenspalte verdoppeln – dasselbe Problem wie in der Matrix (9.29).
Höchstens drei Szenarien. Darüber wird die Vergleichstabelle unlesbar; vergleichbare Werkzeuge stellen bewusst nur zwei gegenüber. Das Basisszenario führt den Bericht an und liefert die Zusammenfassung.
3.11.3 Aufbau des Dokuments
Das Layout ist immer gleich; die Auswahl bestimmt nur, welche Bausteine erscheinen.
| Seite | Inhalt |
|---|---|
| 1 | Deckblatt · Das Wichtigste in Kürze (Kennzahlen + drei Kernaussagen) · Ausgangslage |
| je Szenario | Kennzahlen · Vermögensverlauf · Lebensphasen · Annahmen |
| bei ≥ 2 Szenarien | Vergleichstabelle |
| bei Ist-Daten | Plan gegenüber effektiven Werten inkl. Abweichung |
| bei gewählten Analysen | die eingefrorenen Auswertungen |
| Schluss | Wichtige Hinweise (Haftungsausschluss) |
Die Zusammenfassung zuerst ist kein Geschmacksentscheid: Überkomplexität ist die häufigste Kritik an Beraterberichten, weshalb sich die einseitige Übersicht als eigenes Format etabliert hat.
3.11.4 Kennzahlen und ihre Grundlage
Je Szenario: Endvermögen · Kapital reicht bis · Vermögen bei Pensionierung · Vorsorgekapital bei Pensionierung (PK + 3a) · AHV-Rente · PK-Rente · offene Entscheide.
Die letzte ist die einzige unmittelbar handlungsleitende Zahl – sie zählt die noch nicht
getroffenen Übergangs-Entscheide. Damit Bericht und Matrix nie verschiedene Zahlen nennen,
liegt die Zählung neu als reine Funktion in lib/decisions.ts, die beide benutzen.
Zu jeder Kennzahl steht ihre Grundlage – aber als kurzer Verweis, nicht als wiederholte Tabelle. Die vollständigen Annahmen (Inflation, Pensionsalter, Startwerte, Renditen, Sparraten je Element) stehen einmal je Szenario in einem eigenen Abschnitt. Sie bei jeder Kennzahl zu wiederholen würde den Bericht aufblähen, ohne etwas hinzuzufügen.
3.11.5 Haftungsausschluss
Ein formal gesetztes PDF wird als Beratung gelesen. Der Bericht schliesst deshalb verpflichtend mit Hinweisen: dass es sich um eine Projektion auf Basis eigener Annahmen handelt, keine Anlage-, Steuer- oder Vorsorgeberatung; dass Abweichungen erheblich sein können; dass laufende Einkommens- und Vermögenssteuern nicht modelliert sind (9.14); und dass Monte-Carlo-Wahrscheinlichkeiten die Streuung um die Annahmen messen, nicht deren Richtigkeit. Ein Test stellt sicher, dass der Abschnitt nicht wegfallen kann.
3.11.6 Technik
Erzeugt wird mit pdfkit in der Node-Runtime. Bewusst kein Headless-Browser: Chromium
würde das Image um Hunderte Megabyte und Systembibliotheken aufblähen. @react-pdf/renderer
schied aus, weil es React-Interna nutzt, die mit React 19 / Next 16 brechen.
Weil das Layout ohnehin fix ist, kostet programmatisches Setzen nichts – und die Diagramme entstehen als echte Vektoren aus den gespeicherten Zahlen. Genau dafür wurden die gespeicherten Analysen als Zahlen und nicht als Bilder abgelegt (3.10.3): Ein Bildschirm-Abbild wäre im Druck unscharf.
pdfkit ist in next.config.ts als externes Paket deklariert. Es lädt seine
Font-Metriken zur Laufzeit über Dateipfade; gebündelt stimmt __dirname nicht mehr und der
Bericht bräche erst in der Produktion.
Die Trennung ist zweistufig: report.ts baut aus Plan und Konfiguration ein reines
Berichtsmodell (ohne PDF-Kenntnisse, testbar), report-pdf.ts zeichnet es. Damit ist
das, was eingefroren wird, dasselbe, was geprüft wird.
Referenz: src/lib/report.ts, src/lib/report-pdf.ts, src/lib/decisions.ts,
src/components/ReportsView.tsx.
3.12 Pensionsalter anpassen
Roadmap Nr. 44. Bedienung im Szenario-Profil, Abschnitt «Pensionsalter anpassen».
3.12.1 Warum das kein Zahlenfeld ist
Das Pensionsalter bestimmt, wo eine Lebensphase endet: maxPhaseDuration kappt jede
Phasendauer beim nächsten Pensionierungsereignis, jede Pensionierung liegt deshalb zwangsläufig
auf einer Phasengrenze. Ein frei änderbares Alter hätte diese Zusammengehörigkeit zerrissen –
die Phasen blieben, wo sie sind, und der Phasentyp (Erwerb/Misch/Pension) käme mitten in einer
Phase ins Rutschen.
Deshalb ändert die Bedienung nicht das Alter, sondern verschiebt die Grenze:
| vorher | nachher (−4 Jahre) | |
|---|---|---|
| Phase 3 «Misch» | 10 Jahre | 6 Jahre |
| Phase 4 «Pension» | 15 Jahre | 19 Jahre |
| Gesamtdauer | 60 Jahre | 60 Jahre |
Die Gesamtdauer des Plans bleibt immer gleich – es wird Zeit umverteilt, nicht hinzugefügt.
3.12.2 Spielraum und Sperren
Angezeigt wird je Person das aktuelle Pensionsalter, Knöpfe für ±1 Jahr, Kurzwahl-Chips für grössere Schritte und eine Hilfebox, die den Spielraum benennt. Ein deaktivierter Knopf ohne Erklärung wirkt wie ein Fehler; die Box sagt deshalb immer, was möglich ist und warum nicht mehr.
Der Spielraum ist −(Dauer der Vorphase − 1) bis +(Dauer der Folgephase − 1): Eine
Lebensphase muss mindestens ein Jahr dauern.
Gesperrt wird ganz, wenn:
| Situation | Meldung |
|---|---|
| Person ist bei Planbeginn schon pensioniert | es gibt keine Grenze zu verschieben |
| Pensionierung liegt am oder nach dem Planende | zuerst eine Lebensphase anhängen |
| Pensionierung liegt nicht auf einer Phasengrenze | zuerst die Lebensphasen anpassen (Altdaten) |
| Beide Personen teilen dieselbe Grenze | zuerst eine Lebensphase einfügen, um sie zu trennen |
Der letzte Fall ist der Kehrseite der Zusammenlegung: Sind zwei Pensionierungen auf demselben Zeitpunkt, liesse sich die eine nicht bewegen, ohne die andere mitzunehmen.
3.12.3 Zusammenlegung: wenn eine Lebensphase entfällt
Genau ein Jahr über den Spielraum hinaus fällt die angrenzende Phase auf 0 und verschwindet. Das ist erlaubt – es ist der Fall «beide werden gleichzeitig pensioniert, die Mischphase gibt es nicht mehr» –, wird aber vorher bestätigt, weil dabei zwei Übergänge zu einem werden.
Regel für die Zusammenführung (überlebende Grenze ist die des Vorgängers der entfallenden Phase):
| Regel | Begründung | |
|---|---|---|
| Element-Entscheide | Was am überlebenden Übergang schon entschieden ist, bleibt. Nur leere Felder werden aus dem entfallenden ergänzt. | Ein bewusster Entscheid darf nie von einem anderen überschrieben werden. |
| Einmalige Cash-Beträge | werden addiert | Beide Ereignisse finden weiterhin statt, nur zum selben Zeitpunkt. |
| Steuersatz auf dem Zufluss | betragsgewichteter Mischsatz | Nur so bleibt der Netto-Zufluss derselbe wie vorher. |
| Bezeichnungen | mit « + » verbunden | damit nachvollziehbar bleibt, woraus die Summe entstand |
Die Verschiebung ist nicht über die Undo-Funktion rückgängig zu machen – sie erzeugt wie jede Änderung eine neue Nebenversion, aus der sich der alte Stand wiederherstellen lässt (3.8).
3.12.4 Punkt A: «Aus Vorphase übernehmen»
Wiederkehr-Parameter – Teuerungsausgleich, erwartete Rendite, jährliche Einzahlung, Bezugsrate, Amortisation, Wertsteigerung, Hypothekarzins, Tilgung – galten bisher als Kopie: Beim Anlegen einer Phase wurde der Wert der Vorphase hineingeschrieben. Änderte man die Vorphase später, blieb die Kopie stehen.
Neu ist das Feld in Folgephasen standardmässig leer und wird live geerbt; darunter steht ein angehaktes Kästchen «Aus Vorphase übernehmen» mit dem geerbten Wert daneben. Ein Häkchen weg macht das Feld editierbar und den Wert phasen-eigen.
Nicht vererbt werden bewusst: Ausfalljahre (sie gelten für genau eine Phase – ein geerbter Wert würde eine Lücke erfinden), der Kaufpreis einer Immobilie (eine Tatsache, keine Annahme) und die Zins-Behandlung (ein Schalter ohne Zahlenwert).
Bestehende Pläne verhalten sich unverändert: Dort sind die Werte gespeichert und gewinnen daher gegen die Vererbung, bis man das Häkchen aktiv setzt.
3.12.5 Punkt C: Verwendung des bezogenen Alterskapitals
Am Pensions-Übergang kommt oft ein grosser Betrag auf einmal herein (PK-Kapital, Säule 3a). Ihn vollständig als Cash liegen zu lassen ist selten die Absicht. Beim Bezugs-Entscheid des jeweiligen Guthabens lässt sich deshalb erfassen, wie viel in Prozent wohin fliesst – seit 0.41 in drei gleichrangigen Zeilen:
| Zeile | Ziel | wirkt als |
|---|---|---|
| Schuldentilgung | wählbar: eine Immobilie oder eine sonstige Schuld | Sonderamortisation bzw. Sofort-Tilgung |
| Investition | wählbar: ein Element «Sonstiges Vermögen» | Zusatzeinlage in der Folgephase |
| Cash | – | bleibt liegen |
Das Tilgungs-Ziel ist wählbar, seit es Schulden ausserhalb der Hypothek gibt. Bis 0.40 floss die Quote stur in die erstbeste Immobilie; wer neben einer Hypothek zu 1,5 % einen Konsumkredit zu 6 % trägt, konnte den Kredit gar nicht zuerst tilgen. Ohne getroffene Wahl bleibt es beim alten Verhalten (Immobilien der Reihe nach) – ein Bestandsschutz, damit bestehende Pläne unverändert rechnen; ein Schuld-Element wird nur getilgt, wenn es ausdrücklich gewählt ist.
Cash ist eine Zeile, kein Rest. Rechnerisch ist es beides – die Zahl ergibt sich aus 100 % minus den beiden anderen und ist deshalb nicht eintippbar. Dargestellt wird sie trotzdem gleichrangig: «ich lasse es liegen und entscheide später» ist ein Entscheid, keine Unterlassung. Alle drei frei eintippbar zu machen hiesse, dass beim Tippen still eine der anderen Zahlen wandert.
Die Frage steht seit 0.33 dort, wo der Bezug entschieden wird – bei der Pensionskasse (nur wenn Kapital bezogen wird) und bei der Säule 3a. Bei der 3a fehlte sie bis 0.40 allerdings in der Oberfläche, obwohl sie sich dort zwingender stellt: Ein 3a-Konto lässt sich nur ganz auflösen, es fliesst also in jedem Fall ein grosser Betrag – und der landete stumm auf dem Cash. Bis 0.32 hing sie am Cash-Übergang: Das war der falsche Ort, weil sich mit zwei Guthaben nicht getrennt festlegen liess, welches wohin fliesst. Beide Dialoge führen deshalb neu in der Reihenfolge, in der man tatsächlich entscheidet: Bezugsart → Betrag brutto → Steuersatz → Betrag netto → Verteilung.
Warum Prozent und nicht Franken: Verschiebt man das Pensionsalter, ändert sich das bezogene Kapital. Ein Frankenbetrag müsste von Hand nachgezogen werden – und würde bis dahin still eine falsche Aufteilung rechnen. Eine Quote skaliert mit.
Die Tilgungs-Quote ist am Restsaldo gekappt; ist sie grösser, bleibt der Rest Cash. Die Anlage-Quote fliesst in ein wählbares Vermögens-Element (Vorgabe: das erste aktive). Beide sind mechanisch nichts Neues – die eine wirkt wie eine Sonderamortisation, die andere wie eine Zusatzeinlage, und beide laufen dadurch korrekt durch die zwei Wasserfall-Brücken.
Der zugeteilte Betrag ist überall sichtbar. Er wandert über Carry.capitalIn in die
Zusatzeinlage der Folgephase und erscheint dadurch am Ziel-Element (als eigene, read-only
Zeile «Zusatzinvestition aus Kapitalbezug»), in der Cash-Brücke als Investition und im
Dialog «Kapital verteilen» (dort über dem Eingabefeld, das nur den manuell erfassten Teil
führt). Bis 0.32 erhöhte die Verteilung direkt den internen Bestand – Element und Dialog
zeigten deshalb eine 0, obwohl das Geld geflossen war; bis 0.33 fehlte sie im Dialog.
Die Herkunft wird mitgeführt. Carry.capitalIn hat als Gegenstück capitalInSources –
Betrag je Bezug, benannt mit Element und Person; am Ziel-Element liegt das Ergebnis als
capitalFromTransferSources. Ohne diese Aufschlüsselung ist bei zwei Guthaben (PK und 3a, ggf.
beider Personen) im selben Ziel nicht prüfbar, ob alles angekommen ist – der Grund, weshalb
das Ziel-Dropdown die Zuordnung nennen muss: Zwei gleichnamige Depots sind sonst nicht
unterscheidbar, und ein Fehlgriff leitet das Alterskapital an die falsche Person.
Bezogene Guthaben nehmen nichts mehr auf. Nach der Pensionierung ignoriert die Rechnung
Beiträge und Zusatzeinlagen in PK und Säule 3a. Damit die Verteil-Dialoge diese Regel nicht ein
zweites Mal nachbauen (und dabei abweichen), setzt die Rechnung das Kennzeichen
ElementPhaseComputed.acceptsCapital = false; die Dialoge blenden solche Zeilen aus. Der
frühere Filter auf status === "ACTIVE" griff nicht: Der Status bleibt nach dem Bezug bestehen.
Die Säule 3a ist am Pensions-Übergang ein offener Entscheid. Der Bezug selbst steht fest (sie wird immer ausbezahlt), zu entscheiden sind Steuersatz und Verwendung. Vorher galt der Übergang als automatisch beantwortet, wodurch die Verwendungsfrage nie gestellt wurde.
Referenz: src/lib/retirement.ts, src/components/RetirementAdjuster.tsx,
src/components/FormField.tsx (InheritableField).
3.13 Pensionierung
3.13.1 Warum sie eine Eigenschaft der Person ist
Bis 0.34 war die Pensionierung als Eigenschaft der Zeitachse modelliert: ein Phasenübergang,
an dem verstreut in drei Matrix-Zellen (AHV, PK, 3a) je ein Entscheid hing – gespeichert unter
transitionValues[phaseId], also am Schlüssel Element × Phasen-ID. Daraus folgte fast alles,
was an der Pensionsplanung störte:
- Entscheide, die inhaltlich eine Frage sind, lagen räumlich weit auseinander.
- Das Alter zu ändern war ein struktureller Eingriff. Beim Zusammenlegen zweier Übergänge
mussten Entscheide über
mergeTransitiongerettet werden – verlustbehaftet. - Ein Szenario nur für ein anderes Pensionsalter hiess: alle Entscheide erneut treffen. Genau dafür legt man aber Szenarien an.
- Ziel-Solver (Roadmap Nr. 21) und Live-Simulation hatten nichts zum Anfassen.
Der Denkfehler: Die Pensionierung ist keine Eigenschaft der Zeitachse. Sie ist eine Eigenschaft der Person – die Zeitachse ist die Folge davon.
Seit 0.35 liegt der Entscheid deshalb am Element (FinancialElement.retirementDecision,
ohne Phasenbezug) und gilt für die Pensionierung des Besitzers, wo immer die gerade liegt.
Ohne Phasen-ID im Schlüssel überlebt er jede Zeitachsen-Änderung: Alter verschieben,
Phase zusammenlegen, Szenario kopieren.
Was nicht dort hineingehört: Vorbezüge (PK/3a vor der Pensionierung), Verkäufe, Tilgungen,
der Cash-Übergang. Das sind echte Ereignisse an einer bestimmten Grenze und bleiben in
transitionValues.
3.13.2 Kein leeres Formular
Ein Widerspruch steckt in der Sache: Der Zeitpunkt muss früh feststehen (er definiert die Phasengrenze), die Bezugsentscheide lassen sich aber erst beurteilen, wenn bis dahin geplant ist. Ein Formular, das am Anfang leer dasteht, verlangt also Antworten, die noch niemand geben kann – und der Plan wäre bis dahin nicht einmal rechenbar.
Auflösung: ein vollständiger Vorschlag, den man korrigiert. withRetirementDefaults füllt
jeden Entscheid mit einer Vorgabe, die für die meisten Fälle richtig ist:
| Säule | Vorgabe | warum diese |
|---|---|---|
| AHV | Bezug ab Referenzalter | der gesetzliche Normalfall |
| Pensionskasse | capitalSharePct = 0 (volle Rente) |
die Rente ist die Regel; ein Kapitalbezug ist der begründungspflichtige Fall |
| Säule 3a | withdrawalAge = Pensionsalter, gekappt auf 60–70 |
wer mit 58 aufhört, kann die 3a trotzdem erst mit 60 beziehen |
Die Rentenlücke steht damit ab der ersten Sekunde da und wird mit jeder geplanten Phase genauer. Dass eine Vorgabe nicht dasselbe ist wie ein Entscheid, hält die dreistufige Ampel fest (3.5.3).
3.13.3 Der Bildschirm
Eigene Ansicht, gleichrangig neben der Matrix (Umschalter darüber), plus eine Kurzfassung in der Matrix-Ansicht, die dorthin führt. Aufbau in der Reihenfolge, in der man tatsächlich denkt: Wann höre ich auf? → Was kommt dann rein? → Was habe ich auf einen Schlag? → Reicht das? → Was mache ich mit dem Haufen?
Die Leitzahl ist die Rentenlücke. Sie ist keine neue Rechnung, sondern die Verzehrquote im
ersten Jahr, in dem niemand mehr arbeitet – Renteneinkommen minus Ausgaben. Genau das rechnet
die Jahresschleife ohnehin; es fehlte nur der Begriff. Gerechnet wird sie im Rechenkern
(PlanComputed.retirement), nicht im UI: Eine Nebenrechnung in der Komponente hätte dieselbe
Driftgefahr wie bei den Rechenwegen (4.14.3) und fiele im
PDF-Bericht anders aus.
Daneben steht die zweite Zahl, wie lange das Vermögen trägt (ruinAge, bzw. «über die ganze
Planung»).
Die Matrix-Zellen bleiben bedienbar. AHV, PK und 3a am Pensions-Übergang zeigen exakt
dieselbe Komponente (RetirementFields) – zwei Ansichten auf dasselbe Objekt, kein Duplikat.
Wären es zwei Implementierungen, liefen sie auseinander.
3.13.4 Der Feldsatz
| Säule | Felder |
|---|---|
| AHV | ahvDraw (EARLY/REFERENCE/DEFERRED), ahvMonths, ahvSharePct (Teilbezug 20–80 %); dazu abgesetzt als Grundlage der Schätzung: avgIncomeBefore, gapYearsBefore |
| Pensionskasse | capitalSharePct (0–100 %), conversionRate, capitalTaxRate, recentBuyIn (Hinweis-Flag), Kapitalverwendung capitalUse* |
| Säule 3a | withdrawalAge (60–70), capitalTaxRate, Kapitalverwendung capitalUse* |
| alle | confirmed |
Kürzung, Zuschlag und die Aufteilung Kapital/Rente sind gerechnet, nicht erfasst – ihre Formeln stehen in 4.4.7 und 4.9.1.
Die Beitragskarriere vor Planbeginn lag bis 0.34 an zwei Orten – in der Übergangszelle und, für bei Planbeginn bereits Pensionierte, in der Phasenzelle der ersten Phase – mit zwei Codepfaden für dieselbe Frage. Jetzt an einem.
3.13.5 Planungshorizont
Der Planungshorizont ist die Summe der Lebensphasen -- eine abgeleitete Grösse, kein Feld. Er steht in den Grundeinstellungen, zusammen mit dem Endalter je Person; ohne Phasen ist er leer.
Das war der dritte Anlauf. In 0.35 stand er als Endalter je Person, in 0.36 als Jahreszahl am Szenario, der die letzte Phase folgte. Beide Male war es eine zweite Wahrheit über dieselbe Sache: Wer seine Lebensphasen einzeln plant, hat den Horizont damit bereits bestimmt. Ihn zusätzlich zu erfassen hiess nur, zwei Zahlen synchron halten zu müssen -- und beim Verschieben einer Phase zu entscheiden, welche der beiden gewinnt.
Der ursprüngliche Anlass bleibt gültig: Zwei Szenarien dürfen nicht unbemerkt verschieden weit rechnen. Sichtbar wird das jetzt über die Anzeige in den Grundeinstellungen statt über ein eigenes Feld.
3.13.6 Was bewusst nicht abgebildet wird
- Teilpensionierung in Schritten. Gesetzlich seit AHV 21 in bis zu drei Schritten möglich (Art. 13a BVG). Voll modelliert hiesse, dass eine Person teilweise erwerbstätig ist – das bricht die Invariante, dass jede Pensionierung auf einer Phasengrenze liegt (4.16.1). Wer stufenweise aufhört, bildet das heute über einen Teilzeit-Lohn und eine Kapitalquote ab.
- Progressive Kapitalbezugssteuer. Alle Bezüge desselben Jahres werden zusammengezählt, bei Ehepaaren auch die des Partners. Das Tool rechnet mit einem Pauschalsatz und weist auf die Staffelung hin (Roadmap Nr. 14, siehe 9.14).
- Die 3-Jahres-Sperrfrist nach einem PK-Einkauf (Art. 79b Abs. 3 BVG). Das Tool kennt keine Einkäufe und kann die Frist deshalb nicht prüfen – statt einer Automatik gibt es die Frage «in den letzten drei Jahren eingekauft?» und einen Warnhinweis.
- Reglementarische Grenzen (Mindestalter 58 vs. 60, Kapitalquote 25 % vs. 100 %, Aufschubmöglichkeit) werden als Hinweis gezeigt, nicht als Sperre: Ein Planungstool, das den Fall verbietet, den die eigene Kasse erlaubt, wäre falsch.
Referenz: src/lib/retirement-decision.ts, src/components/RetirementPanel.tsx,
src/components/RetirementFields.tsx, src/lib/retirement.ts (planHorizonChange).
3.14 Bestandsaufnahme und offene Punkte
3.14.1 Warum aus dem Assistenten eine Übersicht wurde
Ein Assistent ist ein Ablauf: tu dies, dann das, dann bist du fertig. Das trägt genau einmal -- beim ersten Aufsetzen. Wer einen bestehenden Plan öffnete, bekam Schritte angeboten, die längst erledigt waren, und ein Häkchen, das nichts wusste. Vier Anläufe (sieben Schritte, dann zwei) haben dasselbe Grundproblem nur kleiner gemacht.
Eine Übersicht ist ein Zustand: das ist noch offen. Sie wird bei jedem Rendern aus dem
Plan abgeleitet (reviewPlan), speichert nichts und kann deshalb nie veralten. Sie trägt bei
jedem Plan, in jeder Reihenfolge, auch beim zwanzigsten Szenario.
Sie steht oben rechts, wo vorher der Assistent stand, und nennt je Lebensphase und je Übergang, was dort fehlt -- jede Zeile ist ein Sprung an die Stelle:
| Zustand | Was die Kachel zeigt |
|---|---|
NO_ELEMENTS |
«Noch nichts erfasst» + grosser Knopf Bestandsaufnahme |
NO_PHASES |
«Noch keine Zeitachse» + Knopf Erste Lebensphase anlegen |
OPEN |
die Liste der Gruppen mit Anzahl und Grund |
DONE |
grün: «Alles angeschaut» |
Warum zwei Startzustände statt einer Zahl. Ohne Elemente und ohne Phasen gibt es naturgemäss nichts Offenes -- der Plan ist trotzdem leer. Eine «0 offene Punkte» wäre dort eine Lüge, und ein grünes Häkchen die falscheste Auskunft überhaupt. Deshalb zählt die Kachel in diesen beiden Fällen nicht, sondern fordert zum nächsten Handgriff auf.
Tatsache und Annahme gehören getrennt
Dieser Schnitt ist geblieben; er ist der Grund, warum die Bestandsaufnahme vor der Zeitachse kommt:
Tatsachen (baseData) |
Annahmen (phaseValues) |
|---|---|
| Nettolohn, Ausgaben pro Jahr | Lohnerhöhung, Teuerung |
| PK-Guthaben, 3a-Guthaben, Wert der Wertschriften | Verzinsung, erwartete Renditen, PK-Beitrag |
| Kaufpreis, Hypothek, Restschuld | Hypothekarzins, Wertsteigerung, Zins-in-Ausgaben |
Links steht, was man nachschlagen kann. Rechts steht, was man vermutet -- und eine Vermutung gilt immer nur für einen Zeitraum. Bis 0.37 standen beide zusammen; dadurch sah eine Annahme aus wie eine Tatsache, und man traf sie, bevor überhaupt feststand, für welche Jahre sie gelten sollte.
Der PK-Beitrag steht bewusst bei den Annahmen und nicht in der Sparquoten-Verteilung: Er stammt aus dem Bruttolohn und belastet das Cash-Konto nicht (4.6.3) -- aus der Quote liesse er sich gar nicht verteilen.
Beim Abschliessen entsteht je Person ein AHV-Element (seit 0.41), idempotent – wer die Bestandsaufnahme später korrigiert und erneut abschliesst, bekommt kein zweites. Die AHV ist die einzige Kategorie ohne Bestand: Es gibt kein Guthaben zum Nachschlagen, nur eine Beitragskarriere, die FPT aus den Einkommen dieses Plans ableitet. Ein Plus-Knopf mit einer leeren Karte darunter wäre deshalb sinnlos gewesen – sie wegzulassen aber auch: In der Schweiz hat jede Person eine AHV, und ohne das Element rechnet der Plan ab der Pensionierung mit einer Rente von 0. Erscheint die Karte später in der Liste, sagt sie, warum dort nichts zu erfassen ist. Wer die Bestandsaufnahme nie öffnet, legt das Element selbst an.
Die Bestandsaufnahme bleibt als eigener Dialog (InventoryDialog): 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. Der Entwurf
liegt auf Ebene des ganzen Dialogs, nicht in der einzelnen Karte; nur so überlebt er den
Wechsel des Reiters. «Fertig» schreibt alles in einem Zug, und die Summe «Vermögen heute»
läuft beim Tippen mit.
3.14.2 Bestätigen heisst «ich habe hingeschaut»
Eine neue Lebensphase übernimmt alle Werte der Vorphase. Das ist richtig -- ohne Vorbelegung müsste man in jeder Phase alles neu eintippen. Es ist aber auch gefährlich: Eine Rendite von 5 %, die aus dem Erwerbsleben stillschweigend in die Pension weiterläuft, ist keine Entscheidung, sondern ein Versehen.
Deshalb gilt seit 0.39 in der ganzen Matrix derselbe Mechanismus, den die Übergänge seit 0.35
haben: vorbelegen ja, stillschweigend übernehmen nein. Jede Phasenzelle trägt ein
confirmed-Flag; solange es fehlt, ist die Zelle mit der Attention-Farbe markiert und zählt
in der Übersicht.
Der Haken steht NEBEN den Werten, nicht statt ihrer. Bestätigen kopiert nichts und friert nichts ein -- eine bestätigte Phase erbt weiterhin live aus der Vorphase (3.12.4). Würde die Bestätigung die geerbten Werte materialisieren, wäre der ganze Punkt-A-Mechanismus hinfällig: Eine Änderung in Phase 1 erreichte Phase 3 nicht mehr. Ein Test sichert genau das.
Was gilt überhaupt als offen? Nur Zellen, die in dieser Phase noch etwas beitragen
(needsConfirmation prüft status === "ACTIVE"). Ein verkauftes Haus trägt in der Folgephase
keine Annahmen mehr und verlangt auch keine Bestätigung.
Die Spar-/Verzehrquote zählt eigens (Phase.ratesConfirmed). Sie ist keine Eigenschaft
eines Elements, sondern der Phase, und sie lässt sich nicht aus den Zellen ableiten: Man kann
jede einzelne 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», während das Geld unverzinst herumliegt. Das Flag wird gesetzt, sobald der
Verteil-Dialog einmal gespeichert hat.
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).
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.
Bestätigt wird zeilenweise. Jede Zeile trägt ein Häkchen «Angeschaut», anfangs leer;
gespeichert werden die Werte aller Zeilen, confirmed aber nur bei den angehakten. Ein Knopf,
der pauschal alles bestätigt, liesse sich drücken, ohne je gescrollt zu haben -- der erste
Anlauf in 0.40 tat genau das. Zwei Dinge halten den Aufwand trotzdem klein:
- Wer ein Feld ändert, hakt es automatisch an. Bearbeiten IST anschauen.
- Oben markiert ein Klick alle Zeilen -- aber als sichtbarer Akt, nicht als Nebenwirkung des Speicherns.
Das ist nötig, weil es sonst bei sechs Elementen und fünf Phasen dreissig einzelne Klicks wären, und das erzieht wieder zum Durchklicken. Wer eine einzelne Zelle in der Matrix öffnet und speichert, bestätigt sie dabei ohnehin.
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); 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 legte 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.
Die Regel steht deshalb seit 0.40.1 im Rechenkern (firstPhaseValues), nicht nur in der
Oberfläche: Bei den fünf Bestandsfeldern (amount, currentValue, startValue,
purchasePrice, mortgage) gewinnen in der ersten Phase immer die Stammdaten, wo sie
einen Wert tragen. Der Anlass war ein handfester Fehler: Eine in 0.39 versehentlich
gespeicherte 0 verdeckte den Kaufpreis einer Immobilie – und weil das Feld seit 0.40 read-only
ist, war sie nicht mehr zu entfernen. Eine Sperre in der Oberfläche schützt eben nur vor
neuen Eingaben, nicht vor alten.
Der Rückfall auf den Phasenwert bleibt, solange die Stammdaten leer sind; sonst fielen Pläne aus der Zeit vor 0.36 schlagartig auf 0. Die Annahmen sind ausdrücklich nicht betroffen: Eine Rendite gilt für einen Zeitraum, nicht für den Anfangsbestand, und darf in Phase 1 abweichen.
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). 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).
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.
Seit 0.36 trennt FinancialElement.baseData zwei Dinge, die nie dasselbe waren:
gehört zum Element (baseData) |
gehört zur Phase (phaseValues) |
|---|---|
| Bestand bei Planbeginn, Kaufpreis, Anfangshypothek, Restschuld | Sparraten, Amortisation, Bezugsraten |
| Ausgangs-Annahmen: Rendite, Wertsteigerung, Zinssatz | abweichende Werte einzelner Phasen |
Das ist nicht nur ein Kunstgriff für Schritt 1. Ein Startwert war nie «phase-1-spezifisch» -- er ist der Stand am Anfang der Planung, und dass er in Phase 1 stand, war eine Eigenheit der Speicherung. Nebenbei löst der Umbau eine alte Unstimmigkeit: Die Feld-Vererbung (3.12.4) hatte in Phase 1 nichts, von dem sie hätte erben können, und fiel auf 0. Die Stammdaten sind jetzt die Wurzel dieser Kette:
eigener Phasenwert -> aus der Vorphase geerbt -> Stammdaten -> 0
Die Matrix erscheint, sobald es Elemente gibt -- Lebensphasen sind dafür nicht nötig. Ihre erste Spalte heisst «Start» und zeigt den Bestand bei Planbeginn; Endwerte gibt es erst, wenn eine Phase eine Dauer vorgibt. Solange keine Phasen bestehen, steht rechts davon der Hinweis auf den nächsten Schritt.
Die Spalte ist auch mit Phasen dauerhaft sichtbar, und zwar aus einem zweiten Grund: Sie ist der einzige Ort, an dem ein Startwert änderbar ist. Bearbeitete man ihn stattdessen in Phase 1, entstünde dort ein Phasenwert, der die Stammdaten überdeckt -- dieselbe Zahl an zwei Orten, genau das Muster, das dieser Umbau beseitigt hat.
0.36 hielt das nicht. Die Stammdaten waren eingeführt, die Matrix hing aber weiterhin vollständig an den Phasen -- die Bestandsaufnahme verschwand nach dem Speichern im Nichts. Behoben in 0.37.
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:
| Fixpunkt | Quelle |
|---|---|
| Erwerbsende | Person.retirementAge |
| AHV-Rentenbeginn | ahvStartAge(retirementDecision) |
| PK-Bezug | pkWithdrawalAge (neu in 0.36) |
| je 3a-Konto | withdrawalAge |
Der Grund ist derselbe wie beim Erwerbsende: Die Rechnung leitet Erwerbsstatus und Bezüge am
Phasenbeginn ab. Fiele ein Bezug mitten in eine Phase, rutschte er auf die nächste Grenze
-- unter Umständen Jahre später, und die Zahlen wären still falsch. maxPhaseDuration zählt
die Fixpunkte deshalb mit. Ereignisse im selben Jahr teilen sich eine Grenze und werden
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.6 Der Bildschirm
Zwei farblich getrennte Hälften, damit sichtbar ist: oben stellst du ein, unten siehst du das Ergebnis.
Oben vier gleichrangige Kacheln plus die Zeitachse über die volle Breite:
| Kachel | Inhalt |
|---|---|
| Grundeinstellungen | plan-weit (Personen, Startjahr, Inflation) und szenario-eigen (Horizont, Endjahr, Endalter, Pensionsalter). Stift zum Bearbeiten; je Person ein Knopf «Pensionsplanung» |
| Kennzahlen | Vermögen heute (Summe der Stammdaten -- die einzige Zahl, die schon vor jeder Zeitplanung etwas aussagt), Endvermögen nominal und real, Reichweite |
| Schnellaktionen | Neues Szenario · Tour · Änderungshistorie · Rechenwege · CSV-Export |
| Offene Punkte | siehe 3.14.1 |
Das Pensionsalter ist im Basisszenario auf 65 festgelegt und nicht änderbar. Vorbezug, Aufschub, eigene Bezugsalter für Pensionskasse und Säule 3a sowie die daraus folgenden Fixpunkte sind im Rechenkern vollständig vorhanden und getestet -- sie werden hier bewusst nicht angeboten.
Der Grund ist nicht technisch, sondern didaktisch: Wer einen Plan zum ersten Mal aufsetzt, soll nicht zwischen fünf Pensionsmodellen wählen müssen, bevor er weiss, wie sein Plan überhaupt aussieht. Abweichende Modelle gehören zu einer eigenen Szenario-Art -- man legt ein zweites Szenario an und wählt dort «Frühpensionierung». Bis dahin weisen die Grundeinstellungen darauf hin.
Unten die Matrix. Die frühere Aktionsleiste darüber ist verschwunden: «+ Element», «+ Phase», der Nominal/Real-Umschalter, die Plan/Ist-Umschaltung und «Alle auf-/zuklappen» 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.7 Die Tour
Ein grosses Popup mit einem nachgebauten Bildschirm und erfundenen Zahlen, in neun Schritten erklärt. Der letzte führt zur Bestandsaufnahme.
Das frühere Spotlight legte sich über die echte Ansicht. Zwei Nachteile liessen sich nicht beheben: Auf einem frisch angelegten, leeren Plan gab es kaum etwas hervorzuheben -- also gerade dann nicht, wenn die Tour am nötigsten ist. Und das Ausschneiden kämpfte dauernd mit Scroll-Containern und Z-Ebenen (9.24).
Der Preis ist bekannt und bewusst in Kauf genommen: Die Attrappe muss bei UI-Änderungen nachgeführt werden. Dafür funktioniert die Tour ab der ersten Sekunde und unabhängig davon, was im Plan schon steht.
Referenz: src/lib/review.ts, src/components/ReviewTile.tsx,
src/components/InventoryDialog.tsx, src/components/Tour.tsx.
3.15 Der Bildschirm: drei Spalten, zwei Sichten
3.15.1 Was gemessen wurde
Die Matrix war ab der vierten Lebensphase nicht mehr zu bedienen. Der Befund in Zahlen, bevor irgendetwas entworfen wurde:
| Grösse | Wert | Folge |
|---|---|---|
| Phasenkopf | 220 px, dauerhaft fixiert | von 675 px Matrixhöhe blieben 430 px |
| Fünf Phasen | 224 + 112 + 5×208 + 4×96 = 1760 px | passt auf keinen Laptop |
| Bearbeitungs-Panel | fixed right-0, 416 px |
legte sich über die Spalte, zu der man gescrollt hatte |
| Kacheln und Zeitachse | `230 px über der Matrix | ein zweiter Kopf über dem ersten |
Drei davon haben dieselbe Wurzel: Der Phasenkopf war ein Kennzahlen-Dashboard. Die 208 px Spaltenbreite waren nicht die Breite, die eine Zelle braucht, sondern die, die der Kennzahlenblock brauchte.
3.15.2 Eine Karte je Lebensphase
Die Matrix stellt Phasen nebeneinander – gut zum Vergleichen, schlecht zum Arbeiten. Eine Karte zeigt genau eine Phase über die ganze Breite. Damit wird horizontales Scrollen, das eine Web-App am zuverlässigsten unbenutzbar macht, gegen vertikales getauscht, das jeder beherrscht.
Die Karte erzählt die Phase als Zeitablauf:
| Abschnitt | Inhalt |
|---|---|
| Eintritt | der Übergang aus der Vorphase: Verkauf oder Halten, Kapitalbezug, einmalige Cash-Ereignisse, die Kapitalverteilung. Bei Phase 1 stattdessen der Start aus der Bestandsaufnahme |
| Laufend | die Elemente mit ihren Annahmen, dazu die Spar- bzw. Verzehrquote und ihre Verteilung |
| Ergebnis | Vermögen und Cash am Phasenende |
Der Übergang gehört zur Folgephase, nicht ans Ende der Vorphase. Das dabei frei werdende Kapital wird in der Folgephase verteilt – dieselbe Zuordnung, die 0.41 schon für Sonderamortisation und Sofort-Tilgung eingeführt hat (3.12.5). Der Preis ist bekannt: Wer den Hausverkauf gedanklich am Ende von Phase N trifft, findet ihn auf Karte N+1. Bewusst in Kauf genommen, weil dort die Folgen stehen.
Die Karte ist auf 46 rem begrenzt und steht zentriert. Die volle Breite auszunutzen war verlockend und falsch: Eine Zeile aus 1300 px liest sich schlecht, weil das Auge beim Zeilenwechsel den Anschluss verliert und Beschriftung und Wert so weit auseinanderdriften, dass man sie nicht mehr zusammenbringt. Die Luft links und rechts ist kein Verlust – sie trägt die beiden grossen Pfeile zur Nachbarphase, ein Bedienelement ohne eigenen Platzbedarf.
Die drei Akte sind sichtbar getrennt. Jeder trägt einen getönten Kopfstreifen über die volle Breite mit einer Nummer im Kreis; «Ergebnis» ist durchgehend getönt und trägt die Nummer im Akzent – es ist der Schlusspunkt, nicht ein weiterer Abschnitt. Der erste Anlauf trennte sie nur mit einer Haarlinie, und man las die Karte als eine lange Liste statt als drei Schritte.
Bearbeitet wird in der Karte. Ein Klick klappt die Zeile auf, die Felder stehen darunter –
dieselben wie im Panel (ElementDetail mit compact), also keine zweite Implementierung.
Damit kann nichts mehr verdecken, woran man arbeitet; das Panel bleibt der Matrix vorbehalten.
Ein Knopf im Kartenkopf klappt alle Zeilen auf oder zu: Bei acht Elementen ist das der
Unterschied zwischen einmal durchsehen und acht Klicks.
Das gilt ausnahmslos, auch für den Cash-Übergang. Er öffnete bis 0.42.1 als einziger das
rechte Panel – auf einer Karte, die genau davon wegwollte, ein Fremdkörper. Die Felder sind
dieselben wie im Panel (CashTransitionFields); zwei Implementierungen wären garantiert
auseinandergelaufen.
3.15.3 Die Zeitachse ist der Navigator
Sie war bis 0.41 Dekoration. Neu ist das Segment der offenen Karte hervorgehoben, und ein Klick springt hin. Das ist der Punkt, an dem sich Aussehen und Verständlichkeit treffen: Zeit läuft von links nach rechts, das braucht keine Erklärung.
Gezeichnet ist sie als klassischer Zeitstrahl (0.42.3): ein durchgehender dunkler Balken, dessen Segmente sich die Breite streng nach Dauer teilen, und Beschriftungen abwechselnd ober- und unterhalb, verbunden über ein Dreieck und eine dünne Linie.
Das Abwechseln ist kein Stilmittel, sondern löst ein Platzproblem: Nebeneinander gesetzte Beschriftungen kollidieren, sobald eine Phase kurz ist. Abwechselnd liegen Nachbarn auf derselben Seite zwei Phasen auseinander und haben damit den doppelten Raum. Jede Beschriftung nennt Jahresspanne, Phasenname und Dauer.
| Element | Gestaltung |
|---|---|
| Aktive Phase | Segment, Dreieck, Linie und Beschriftung im Akzent |
| Startjahr | grosse Zahl am linken Ende – der Nullpunkt, auf den sich die reale Kaufkraft bezieht |
| Planende | kleiner am rechten Ende |
| Pensionierung, Ruinalter, Ist-Jahre | Marker auf dem Balken (oberhalb kollidierten sie mit den Beschriftungen) |
Entfallen sind die Überschrift (eine Zeitachse erkennt man), der Rahmen und die Legende.
Drei Wege durch die Phasen, weil verschiedene Leute verschieden arbeiten:
- Klick auf die Zeitachse – gezielt zu einer bestimmten Phase.
- Pfeilknöpfe unter der Karte, beschriftet mit dem Namen der Nachbarphase. Auf der letzten Karte steht dort stattdessen «+ Lebensphase» – am Ende der Kette baut man weiter.
- Pfeiltasten links/rechts, ausser der Fokus steht in einem Eingabefeld. Ohne diese Ausnahme spränge die Karte beim Bearbeiten eines Betrags weg.
3.15.4 Zwei Sichten auf dasselbe Material
Die Matrix wird nicht ersetzt. Ihr Wert ist der Vergleich über Phasen hinweg, und den kann eine Karte prinzipiell nicht leisten. Konkret hängen drei Dinge daran: der Szenario-Diff (was ist anders als in der Vorlage), der Plan/Ist-Vergleich und die schlichte Frage, wie sich eine Grösse über alle Phasen entwickelt.
Der Umschalter steht oben rechts; die Wahl gehört zum Benutzer und überlebt die Sitzung
(localStorage) – sie ist eine Arbeitsgewohnheit, keine Eigenschaft des Plans.
Was die Matrix erzwingt und was nicht (korrigiert in 0.42.1):
| Fall | Verhalten | Warum |
|---|---|---|
| Ist-Ansicht aktiv | Matrix erzwungen | Plan gegen Ist ist eine Frage über alle Phasen – und es ist ein Modus, in den man bewusst wechselt |
| Keine Lebensphase | Matrix erzwungen | Eine Karte ohne Phase gibt es nicht |
| Abweichungen zur Vorlage | Karten bleiben, mit Hinweis | Die Diff-Markierungen sind eine Verzierung der Matrix, kein Modus |
Der erste Anlauf sperrte die Karten vollständig. Die Bedingung lautete
!!diff, undcomputeScenarioDiffliefert immer ein Objekt – bei Gleichheit ein leeres. Der Umschalter war damit dauerhaft ausgegraut. Aber auchdiff.total > 0wäre falsch gewesen: Ein Kind-Szenario weicht nach der ersten Änderung immer ab, und Karten wären dort für immer gesperrt. Eine Verzierung darf keinen Modus erzwingen.
3.15.5 Drei Spalten
[Icon-Schiene 56px] [ Karten oder Matrix ] [ Steuerung 304px ]
^ Pin haelt sie breit ^ einklappbar
Links die Menüleiste: Haus, Plan-Wähler und die vier Bereiche des aktiven Plans, darunter das Wissen sowie Suchen und Anpinnen. Beim Überfahren fährt sie aus und schiebt den Inhalt; der Pin entscheidet, ob sie so bleibt.
Ausfahren heisst schieben, nicht überlagern (korrigiert in 0.42.2). Der erste Anlauf liess sie als Overlay ausfahren, damit sie im Ruhezustand keinen Platz kostet. Dabei verschwand aber jedes Mal ein Streifen des Inhalts, und das las sich wie ein Fehler. Aufgeklappt sieht es jetzt genau so aus wie angepinnt – der einzige Unterschied ist, ob es so bleibt.
Suchen und Anpinnen stehen direkt unter den Systemparametern, nicht am Fuss der Leiste. Die
Leiste ist so hoch wie die Seite, und mit einer aufgeklappten Karte wird die sehr lang – am
Fuss wären die beiden ausserhalb des Blickfelds. Der Inhalt beider Leisten klebt zudem am
oberen Bildschirmrand (sticky): Steuerung, die beim Arbeiten wegscrollt, ist keine.
Eine Komponente in zwei Breiten, nicht zwei Darstellungen (korrigiert in 0.42.1). Die Symbole sitzen in einer festen 3.5-rem-Spalte und bleiben beim Ausfahren exakt an ihrem Platz; rechts daneben kommt nur die Beschriftung dazu. Der erste Anlauf zeigte eingeklappt Icons und ausgefahren einen Plan-Baum – das war kein Übergang, sondern ein Sprung, und man verlor jedes Mal die Orientierung.
Damit entfällt der Baum aller Pläne. Er wird nicht vermisst: Der Plan wird über den Wähler gewechselt, und die vier Bereiche beziehen sich immer auf den aktiven Plan. Statt einer Liste von Listen bleiben vier Knöpfe. Was daran hing, hat einen neuen Ort:
| Vorher im Baum | Neu |
|---|---|
| Plan wechseln | Plan-Wähler (Klapp-Menü) |
| Plan anlegen, Plan löschen | ebenfalls im Plan-Wähler |
| Szenario-Baum mit Kopieren/Löschen | aufklappbar unter «Szenarien» (nur ausgefahren – er braucht Text) |
Der Plan-Wähler zeigt eingeklappt die Initialen des aktiven Plans. Ohne diesen Bezug wüsste man nach dem Umschalten nicht mehr, worin man ist – und die vier Knöpfe darunter wären gegenstandslos.
Die Matrix ist ein eigener Stapel-Kontext (relative z-0). Ihre sticky Köpfe tragen z-40
und konkurrierten sonst global mit der ausgefahrenen Leiste – die Ecke oben links schob sich
vor das Menü. Innerhalb der Matrix stapeln sich die Köpfe unverändert; nach aussen tritt sie
als eine Ebene auf.
Rechts die vier bisherigen Kacheln als aufklappbare Abschnitte. Das ist der entscheidende Tausch: Oben kosteten sie vertikalen Platz, rechts kosten sie horizontalen – und auf einem Breitbildschirm ist Höhe die knappe Grösse. Eingeklappt bleibt die Zahl der offenen Punkte am Knopf sichtbar; sonst klappt man die Leiste zu und vergisst den halben Plan.
Unter 1280 px verschwindet die rechte Leiste, unter 1024 px die linke Schiene (dort greift der bestehende Overlay-Drawer). Ergebnis auf 1440 × 900: statt 1184 × 574 rund 1328 × 804 für den Inhalt – 12 % mehr Breite und 40 % mehr Höhe.
3.15.6 Grundsätze, die daraus folgen
Aus dem Umbau sind drei Regeln geworden, die für neue Oberfläche gelten:
- Höhe ist knapper als Breite. Was dauerhaft sichtbar sein muss, gehört an den Rand, nicht nach oben.
- Was fixiert ist, muss sich rechtfertigen. Ein sticky Element kostet seine Höhe auf jedem Bildschirm, in jeder Sitzung, für jeden Benutzer.
- Nichts legt sich über das, was man bearbeitet. Overlays sind für Dinge, die den Arbeitsfluss unterbrechen sollen – nicht für die Arbeit selbst.
- Ein Übergang verschiebt, er ersetzt nicht. Wenn ein Element beim Aus- oder Aufklappen seinen Platz behält und nur Beiwerk dazukommt, folgt das Auge mit. Wechselt stattdessen die ganze Darstellung, ist es ein Sprung – und man sucht sich neu zurecht.
- Wer
z-indexvergibt, baut einen Stapel-Kontext. Sonst konkurrieren Zahlen aus verschiedenen Bauteilen miteinander, und wer gewinnt, ist Zufall der Reihenfolge im DOM.
Referenz: src/components/PhaseCards.tsx, src/components/Timeline.tsx,
IconRail in src/components/AppShell.tsx, RailSection und ValueModeSwitch in
src/components/PlanView.tsx.
4. Berechnungsmodell
Dies ist der fachliche Kern des Tools. Der gesamte Abschnitt beschreibt
src/lib/calculations.ts, FunktioncomputePlan(plan: PlanInput): PlanComputed. Die Funktion ist rein (keine I/O, keine Datenbank) und dadurch isoliert testbar.
4.1 Ablauf im Überblick
für jede Phase i (nach sequenceNumber sortiert):
1. Personen-Infos berechnen (Alter, erwerbstätig?), Phasentyp ableiten
2. AHV: Ausfalljahre kumulieren, Renten der Pensionierten berechnen, plafonieren
3. Element-Setup: je Element Startwerte/Raten bestimmen, in Arbeitslisten einordnen
4. Investitionen vom Cash abziehen → cashStart
5. Jahresschleife t = 1..duration:
Flows indexieren, Vermögen verzinsen, Bezugsraten entnehmen,
Cash fortschreiben, Ruin prüfen, YearPoint anfügen
6. Endwerte je Element setzen, Phasen-Kennzahlen zusammenstellen
7. Übergang: Entscheide anwenden, Carry aktualisieren, Cash der Folgephase bilden
4.2 Zustand über Phasengrenzen: der Carry
Zwischen den Phasen wird pro Element ein Carry-Objekt fortgeschrieben:
| Feld | Bedeutung |
|---|---|
status |
ACTIVE / SOLD / SETTLED |
value |
Aktiven-Saldo (PK / 3a / Sonstiges Vermögen) am Ende der Vorphase |
mortgage |
Immobilie: Resthypothek |
owed |
Schulden: Restschuld (positiv geführt) |
pkPensionAnnual |
PK: jährliche Rente nach Verrentung |
flowBasis |
Einkommen/Ausgaben: indexierter Basiswert der nächsten Phase |
hasCarry |
ob überhaupt eine Vorphase existiert (steuert read-only vs. Eingabe im UI) |
Zusätzlich laufen phasenübergreifend mit: cashCarryIn, cumulativeInflation,
gapYearsByPerson, yearsBefore, ruinAge, incomingInflow, incomingImmediateRepay.
Referenz: src/lib/calculations.ts Zeilen 103–115, 132–147.
4.3 Personen und Phasentyp
Für jede Person und jede Phase:
startAge = person.age + yearsBefore
endAge = startAge + duration
working = startAge < retirementAge
retiresAtStart = startAge === retirementAge
yearsBefore ist die Summe der Dauern aller vorangehenden Phasen. Alter wird also relativ
gezählt – es gibt keine Kalenderdaten im Modell, nur „Jahre ab Planbeginn".
maxDurationYears = Minimum über retirementAge − startAge aller noch erwerbstätigen Personen
(nur positive Werte), sonst null.
4.4 AHV-Rente
Die Rente hängt an zwei Achsen: der Beitragsdauer (Rentenskala 44) und dem massgebenden durchschnittlichen Jahreseinkommen (mdJE).
4.4.1 Die amtliche Rentenformel (Skala 44)
Alle Schwellen sind Vielfache von R0, dem Mindestbetrag der vollen Monatsrente
(AHV_MIN_MONTHLY_FULL = 1'260). Mit x = mdJE / (12 × R0):
mdJE ≤ 12 × R0 (= 15'120) → Rente = R0 (Mindestrente 1'260)
12 × R0 < mdJE ≤ 36 × R0 → Rente = R0 × (0.74 + 0.26 × x) Formel 1
36 × R0 < mdJE < 72 × R0 → Rente = R0 × (1.04 + 0.16 × x) Formel 2
mdJE ≥ 72 × R0 (= 90'720) → Rente = 2 × R0 (Maximalrente 2'520)
Der Wendepunkt liegt bei 36 × R0 = 45'360 → 1'915/Monat; beide Formelteile sind dort stetig.
Quelle und Verifikation: BSV/MAS „Berechnungsvorschriften der AHV/IV-Renten", gültig ab
1.1.2026 (liefert die Schwellenstruktur 12/36/72 × R0), und die amtliche Tabelle
318.117.1 – Monatliche Vollrenten, Skala 44. Die implementierte Formel reproduziert alle 51
Zeilen dieser Tabelle exakt; Stützstellen sind als Golden Tests hinterlegt (Kap. 8.2).
Die Funktion ahvMonthlyFullPension(mdJE) rechnet bewusst stetig. Amtlich wird das mdJE auf
Vielfache von 1.2 × R0 gestuft (daher die 51 Tabellenzeilen); für eine Planung ist der stetige
Wert näher an der Wahrheit, die Abweichung liegt unter 20/Monat.
4.4.2 Beitragskarriere und mdJE
Pro Person wird über die Phasen hinweg akkumuliert (AhvCareer):
| Feld | Bedeutung |
|---|---|
plannedAvgGrossIncome |
reales Brutto-Durchschnittseinkommen der Beitragsjahre im Plan (siehe 4.4.5) |
planYears |
Beitragsjahre im Plan = Σ (Phasendauer − Ausfalljahre der Phase) |
yearsBeforePlan |
max(0, Alter bei Planbeginn − 21) |
gapYearsInPlan |
Summe der Ausfalljahre im Plan |
Das mdJE ist der gewichtete Mittelwert über alle Beitragsjahre:
yearsBefore = max(0, yearsBeforePlan − gapYearsBefore)
mdJE = (avgIncomeBefore × yearsBefore + plannedAvgIncome × planYears)
/ (yearsBefore + planYears)
Ausfalljahre reduzieren die Gewichtung (und die Skala), nicht das Durchschnittseinkommen – genau wie in der echten AHV: Wer zwei Jahre aussetzt, hat deswegen kein tieferes Durchschnittseinkommen, aber weniger Beitragsjahre.
4.4.3 Warum real gerechnet wird
Sämtliche Einkommen gehen real (Kaufkraft bei Planbeginn) in das mdJE ein, und die Schwellen sind heutige Werte. Das ist kein Vereinfachungs-, sondern ein Genauigkeitsentscheid: Die echte AHV wertet vergangene Einkommen auf (Lohnindex) und indexiert die Schwellen (Mischindex). Beide Bewegungen heben sich in realer Betrachtung weitgehend auf – wer nominal mittelt und gegen heutige Schwellen hält, vergleicht Franken von 2046 mit Schwellen von 2026 und überschätzt die Rente systematisch.
Grössenordnung: 45-jährig, 85'000 Lohn, +1.5 %/Jahr bei 2 % Inflation, 20 Erwerbsjahre. Nominal gemittelt: mdJE 98'276 → Maximalrente 32'760. Real gemittelt: mdJE 81'156 → 31'096. Differenz 1'664/Jahr, über 25 Rentenjahre rund 41'600 – und der Fehler geht immer nach oben.
Die reale Berechnung eines Phasen-Durchschnitts erfolgt analytisch (avgRealFlow) als
geometrische Reihe mit q = (1 + Lohnerhöhung) / (1 + Inflation).
Bekannte Unschärfe: Die Schwellen folgen dem Mischindex, die Aufwertung dem Lohnindex. Da Löhne langfristig schneller steigen als Preise, ist die Deflationierung mit der Preisinflation leicht konservativ. Bewusst in Kauf genommen, statt eine dritte Indexannahme einzuführen.
4.4.4 Woher die Karriere-Werte kommen
| Situation | Quelle |
|---|---|
| Person retires innerhalb des Plans | transitionValues des AHV-Elements am Pensions-Übergang |
| Person bei Planbeginn bereits pensioniert | phaseValues des AHV-Elements in der ersten Phase |
| Prüfung noch nicht erfolgt | Fallback: avgIncomeBefore = plannedAvgIncome |
Der Fallback ist bewusst gewählt: Ohne erfassten Wert gilt der geplante Durchschnitt als Schätzung für die Jahre davor – exakt der Wert, den der Dialog vorbelegt. Ein Fallback auf 0 würde die Rente still und massiv zu tief rechnen (bei einer 45-jährigen Person auf rund 45 %).
Einkommen wird einer Person nur zugerechnet, wenn das INCOME-Element ihr zugeordnet ist. Bei
einem Einzelplan zählt „Gemeinsam" (HOUSEHOLD) zur Person A; bei einem Paar-Plan nicht
(siehe 9.9).
4.4.5 Netto-Brutto-Umrechnung für die AHV
Das Tool erfasst das Einkommen netto, die AHV bemisst sich am Bruttolohn. Beim Aufbau der Karriere wird deshalb hochgerechnet:
plannedAvgGrossIncome = avgRealFlow(...) × AHV_GROSS_FROM_NET_FACTOR // = 1.12
Sämtliche Werte in AhvCareer, im Prüf-Dialog und im mdJE sind damit brutto – auch das
Feld „Durchschnittliches Bruttoeinkommen vor Planbeginn", das der Benutzer aus der
Rentenvorausberechnung übernimmt (die ohnehin brutto-basiert ist). Eine Einheit im ganzen
Ablauf, keine Umrechnung an der Feldgrenze.
Herleitung des Faktors (Arbeitnehmer-Abzüge in % des Bruttolohns):
| Abzug | Satz | Bemerkung |
|---|---|---|
| AHV/IV/EO | 5.30 % | 10.6 % total, hälftig geteilt |
| ALV | 1.10 % | 2.2 % total, hälftig geteilt, bis 148'200 |
| NBU | ~1.00 % | variiert 0.4–1.6 %, teils vom Arbeitgeber getragen |
| PK | ~2–5 % | BVG-Altersgutschrift 7/10/15/18 % auf den koordinierten Lohn (Brutto − 26'460, max. 90'720), Arbeitnehmer ≥ die Hälfte |
Total ~9–13 % je nach Alter und Lohn → Faktor 1/(1−q) = 1.10 bis 1.16, Mittel 1.12.
Warum ein fixer Faktor genügt: Das mdJE ist selbst ein Durchschnitt über die ganze Beitragskarriere (44 Jahre). Der altersabhängige PK-Satz mittelt sich dabei heraus – ein Karriere-Faktor gegen einen Karriere-Durchschnitt ist konzeptionell stimmig, nicht bloss eine Näherung. Die Restunschärfe (~3 %) ist deutlich kleiner als der Fehler, den die Umrechnung behebt (~11 %, siehe 9.13).
Grenzen: Überobligatorische PK-Pläne, vom Arbeitgeber getragene NBU, Selbstständige und Löhne über 148'200 (ALV sinkt auf 0.5 %) weichen ab. Der Faktor ist heute eine nicht sichtbare Konstante; mit Roadmap Nr. 41 wird er in der Formel-Erklärung ausgewiesen.
Quellen: BSV „Beträge gültig ab 1.1.2026" (Koordinationsabzug 26'460, obere Limite 90'720, „keine Änderung gegenüber 2025"); AHV-Merkblätter 2.01 (AHV/IV/EO) und 2.08 (ALV).
4.4.6 Jahresrente, Skala und Plafonierung
factor = max(0, (44 − Ausfalljahre total) / 44) // Rentenskala 44
rente = round(ahvMonthlyFullPension(mdJE) × 13 × factor)
AHV_PENSION_MONTHS = 13 – seit 1.1.2026 gibt es die 13. Altersrente (Art. 34bis AHVG).
Die Formel liefert Monatsrenten; der Jahresbetrag ist deshalb × 13, nicht × 12.
AHV_MAX_ANNUAL_SINGLE ist neu abgeleitet: 2 × R0 × 13 = 32'760.
Ehepaar-Plafonierung – nur bei householdType = COUPLE und wenn für beide Personen
eine Rente vorliegt:
cap = 32'760 × 1.5 = 49'140
falls (renteA + renteB) > cap:
beide Renten proportional kürzen: rente × cap / summe
Die Rente ist danach nominal fix – sie wird über die Phasen hinweg nicht indexiert und verliert damit real an Kaufkraft (siehe 9.11). Wann sie fliesst, entscheidet seit 0.26 nicht mehr der Phasentyp, sondern das Alter im jeweiligen Jahr – siehe 4.4.7.
Referenz: src/lib/calculations.ts (ahvMonthlyFullPension, ahvMdje, ahvAnnualPension),
src/lib/constants.ts.
4.4.7 Referenzalter: Die Rente beginnt mit 65, nicht mit der Pensionierung
Bis Version 0.25 galt implizit «pensioniert = Rente». Sobald sich das Pensionsalter verschieben
lässt (4.16), ist das falsch: Die AHV-Rente hängt am
Referenzalter (AHV_REFERENCE_AGE = 65), nicht daran, wann jemand aufhört zu arbeiten.
Daraus folgen drei Fälle:
| Fall | Was passiert |
|---|---|
| Pensionierung mit 65 | unverändert – die Rente fliesst ab Beginn der Pensionsphase |
| Pensionierung nach 65 | Die Erwerbsphase deckt das Referenzalter ab. Die Rente fliesst ab 65 zusätzlich zum Lohn. Erfasst werden nur noch die Ausfalljahre bis 65. |
| Pensionierung vor 65 | Die Pensionsphase beginnt vor 65. Bis dahin ist die Person beitragspflichtig als Nichterwerbstätige(r); der Beitrag (ahvContribution) ist eine laufende Ausgabe. Mit 65 fällt er weg und die Rente setzt ein. |
Beide Wechsel können innerhalb derselben Lebensphase stattfinden. Die AHV wird deshalb jahresweise ausgewertet statt als Phasenkonstante.
Vorbezug und Aufschub (seit 0.35)
Bis 0.34 begann die Rente immer mit 65. Das war schlicht falsch: Wer mit 62 aufhörte, bekam die ungekürzte Rente erst drei Jahre später; wer bis 68 arbeitete, verschenkte den Zuschlag. Der Bezugszeitpunkt ist seither Teil des Pensionierungs-Entscheids (3.13) und wird gerechnet:
| Regel | Grenzen | |
|---|---|---|
| Vorbezug | −6,8 % pro Jahr, lebenslang, monatlich anteilig | höchstens 36 Monate (ab 62) |
| Aufschub | +5,2 / 10,8 / 17,1 / 24,0 / 31,5 % nach 1–5 Jahren; dazwischen linear interpoliert | 12 bis 60 Monate |
| Teilbezug | Kürzung bzw. Zuschlag wirken nur auf den bezogenen Anteil | 20–80 % |
Der Faktor greift nach der Ehepaar-Plafonierung: Der Plafond gilt für die ordentlichen Renten, die individuelle Kürzung setzt darauf auf.
Rentenbeginn und Beitragspflicht sind zwei verschiedene Alter – eine Trennung, die es vor 0.35 gar nicht gab. Der Vorbezug zieht nur den Rentenbeginn vor; die Beitragspflicht als Nichterwerbstätige(r) endet unabhängig davon erst mit dem Referenzalter. Wer mit 62 aufhört und ab 63 vorbezieht, bezieht ab 63 und zahlt bis 65 weiter:
für jedes Jahr t der Phase:
alterImJahr = alterZuPhasenbeginn + t − 1
alterImJahr ≥ individuellesRentenalter → Rente fliesst (Einkommen)
nicht erwerbstätig ∧ alterImJahr < 65 → Beitrag fällt an (Ausgabe)
Beide Bedingungen können im selben Jahr gelten. Der Verlaufspunkt des AHV-Elements zeigt die Differenz; ein reiner Beitrag erscheint negativ, damit in der Grafik sichtbar ist, dass die AHV in diesen Jahren Geld kostet, statt welches zu bringen.
Für das Jahresraster wird das Rentenalter gerundet. Die Monatsgenauigkeit steckt im Faktor, nicht im Auszahlungszeitpunkt – FPT rechnet durchgehend jahresweise.
Nicht abgebildet: die seit 1.1.2025 tieferen, einkommensabhängigen Kürzungssätze für Frauen der Übergangsgeneration (Jahrgänge 1961–1969). Sie laufen aus, und ihre Nachbildung erforderte eine zweite, jahrgangsabhängige Rentenformel.
Der Beitrag als Nichterwerbstätige(r) bemisst sich am Vermögen und am Renteneinkommen, nicht am Lohn. Die Bandbreite ist entsprechend enorm: vom Mindestbeitrag von rund 530 CHF/Jahr bis zum Höchstbeitrag von rund 26'500 CHF/Jahr. Deshalb gibt es hier keinen Default – ein stiller Vorschlag würde nicht hinterfragt (vgl. 9.15). Der Hilfetext nennt die Bandbreite ausdrücklich, damit auch jemand ohne Vorwissen ein Gefühl für die Grössenordnung bekommt.
Beitragsjahre: Wer den Beitrag zahlt, hat keine Ausfalljahre – die Rentenskala bleibt unberührt. Das Modell kürzt die Rente bei Frühpensionierung deshalb nicht; eine Kürzung entsteht nur, wenn man die Jahre ausdrücklich als Ausfalljahre erfasst.
Referenz: src/lib/calculations.ts (ahvItems in der Jahresschleife), AHV_REFERENCE_AGE in
src/lib/constants.ts.
4.5 Nominal, real und die Deflatoren
4.5.1 Das V5-Modell
| Grösse | Erfassung | Indexierung über die Phasenjahre |
|---|---|---|
| Einkommen | nominal | basis × (1 + Lohnerhöhung)^(t−1) |
| Renten (AHV, PK) | nominal | keine – konstant |
| Ausgaben | real | real: basis × (1 + reale Mehrausgaben)^(t−1), dann × kumulierte Inflation |
Die Inflation ist seit V5 plan-weit (plan.inflationRateDefault) und gilt einheitlich für
alle Phasen; eine phasenspezifische Überschreibung existiert nicht (mehr).
4.5.2 Kumulierte Inflation
cumInflStart(Phase 1) = 1
cumInflStart(Phase n) = cumInflStart(Phase n−1) × (1 + infl/100)^duration(n−1)
4.5.3 Die drei Deflatoren
Ein subtiler, aber wichtiger Punkt: Bestandswerte und Flow-Werte haben am Phasenende nicht
denselben Deflator, weil ein Flow im Jahr duration anfällt, ein Bestand aber nach dem
Jahr duration gemessen wird.
| Deflator | Formel | Verwendung |
|---|---|---|
cumulativeInflationStart |
s. o. | Bestände zu Phasenbeginn, Flows im Jahr 1 |
cumulativeInflationEnd |
cumInflStart × (1+infl)^duration |
Bestände am Phasenende (Cash, Vermögen) |
flowDeflatorEnd |
cumInflStart × (1+infl)^(duration−1) |
Flow-Endwerte (Einkommen, Ausgaben, Quote) – eine Kaufkraft-Stufe weniger |
Referenz: src/lib/calculations.ts Zeilen 427–429; Anwendung in src/components/PlanView.tsx Zeilen 686–687, 716–717.
4.6 Element-Setup je Kategorie
Für jedes Element (sortiert nach orderIndex) wird ein ElementPhaseComputed erzeugt.
Vorab-Abbruch: Ist der Carry-Status SOLD → Notiz „Verkauft"; ist er SETTLED und die Kategorie
OTHER_DEBT → „Getilgt". Solche Elemente werden nicht weiter gerechnet.
4.6.1 INCOME / EXPENSE
idx = phaseData.teuerungsausgleich ?? carry.rates.teuerungsausgleich ?? 0 // Punkt A
baseValue = hasCarry ? round(carry.flowBasis) : round(phaseData.amount)
basis = !hasCarry → round(phaseData.amount)
phaseData.amount ist Zahl → round(phaseData.amount) // bewusster Override
sonst → baseValue // live vererbt
// Punkt B: Besitzer ist eine PERSON und in dieser Phase pensioniert,
// und es ist kein Betrag erfasst → basis = 0
// NACH der Jahresschleife (siehe 4.16.6):
carry.flowBasis = basis × (1 + idx/100)^duration // für die Folgephase
Die Vererbungsregel (src/components/ElementDetail.tsx Zeilen 480–490): Beim Speichern wird
das Feld amount gelöscht, wenn es exakt dem fortgeschriebenen Wert entspricht. Dadurch
bleibt der Wert „live vererbt" – eine spätere Änderung in einer früheren Phase wirkt sich weiter
durch. Nur ein bewusst abweichender Wert wird fix gespeichert.
Man beachte den Exponenten-Unterschied: der Endwert der Phase nutzt duration − 1
(letztes Jahr), der Carry für die Folgephase nutzt duration (ein Jahr weiter).
Die Fortschreibung passiert seit 0.26 nach der Jahresschleife – sonst kämen effektive Werte nie in der Folgephase an (4.16.6).
4.6.2 AHV
Seit 0.26 jahresweise statt phasenweise (4.4.7):
Das Element meldet Rente, Beitrag und Startalter des Besitzers an die Jahresschleife an, die je
Jahr entscheidet, was fliesst. startValue / endValue zeigen den Stand im ersten bzw. letzten
Jahr der Phase; die Zusammenfassung nennt beide Zustände, wenn das Referenzalter mitten in der
Phase liegt («Beitrag … → Rente …»). Die Rente läuft nicht mehr über renteTotal, weil sie
innerhalb einer Phase einsetzen kann.
4.6.3 PENSION_FUND
| Fall | Verhalten |
|---|---|
pensioniert, pkPensionAnnual > 0 |
Rente: startValue = endValue = pkPensionAnnual, Summand in renteTotal |
| pensioniert, keine Rente | Notiz „Vollständig bezogen" |
| erwerbstätig | base = hasCarry ? carry.value : phaseData.currentValue; topUp = hasCarry ? additionalInvestment : 0; start = base + topUp; Rate = annualContribution |
Die PK-Rate wird nicht zu fixedRatesTotal addiert – sie belastet das Cash nicht.
topUp wird ab Phase 2 zu investmentsFromCash addiert.
4.6.4 PILLAR_3A
Identisch zu PK, mit zwei Unterschieden:
- Die Rate wird zu
fixedRatesTotaladdiert (belastet das Cash). - Ist der Besitzer pensioniert, gibt es nie eine Rente, nur „Vollständig bezogen".
4.6.5 REAL_ESTATE (Immobilie)
purchase = hasCarry ? carry.propertyPurchase : round(phaseData.purchasePrice) // Kaufpreis
valueStart = hasCarry ? carry.propertyValue : round(phaseData.purchasePrice) // Verkehrswert
mortgage = hasCarry ? carry.mortgage : round(phaseData.mortgage)
amort = round(phaseData.amortization) // wird JÄHRLICH am Restsaldo gekappt
equity = valueStart − mortgage → startValue, wealthStart
Kaufpreis und Verkehrswert laufen getrennt. Der Verkehrswert wächst mit valueGrowth, der
Kaufpreis bleibt der ursprüngliche – er ist die Basis der Grundstückgewinnsteuer beim Verkauf
(4.9.4). Beide werden über die Phasen fortgeschrieben (carry.propertyValue,
carry.propertyPurchase); die Hypothek läuft als Saldo in der Jahresschleife (4.7).
Die Wertsteigerung wirkt auf die Liegenschaft, nicht auf das Eigenkapital. Das ist der Hebel, der Wohneigentum finanziell auszeichnet: 1 % von 1'000'000 sind 10'000 im Jahr – bei einem Eigenkapital von 100'000 also 10 % darauf. Würde man die Wertsteigerung auf das Eigenkapital rechnen, ergäbe dieselbe Annahme nur 1'000 im Jahr:
Kaufpreis 1'000'000 · Hypothek 900'000 · EK 100'000 · 1 %/J. · Amortisation 10'000/J. · 10 J. → korrekt 304'622; auf das Eigenkapital gerechnet nur 210'462. Differenz 94'160, und sie wächst mit dem Belehnungsgrad.
Der angezeigte Elementwert bleibt das Eigenkapital (Verkehrswert − Restschuld); nur die
Basis der Verzinsung ist die Liegenschaft.
Falls nicht fortgeschrieben und nicht Phase 1 (= Neukauf in einer späteren Phase):
investmentsFromCash += max(0, equity) – das Eigenkapital wird aus dem Cash finanziert.
4.6.6 OTHER_ASSET
base = hasCarry ? carry.value : round(phaseData.startValue)
topUp = hasCarry ? round(phaseData.additionalInvestment) : 0
start = base + topUp
rate = round(phaseData.annualContribution) → fixedRatesTotal += rate
withdrawal = round(phaseData.annualWithdrawal) → plannedWithdrawTotal += withdrawal
4.6.7 OTHER_DEBT
owedStart = hasCarry ? carry.owed : round(phaseData.startValue)
repay = round(phaseData.annualRepayment) // wird JÄHRLICH am Restsaldo gekappt
startValue = −owedStart // negatives Vorzeichen im Vermögen
Wie bei der Immobilie ist die Restschuld ein laufender Saldo; ist sie getilgt, entfällt die Tilgungsrate.
4.7 Die Jahresschleife
Zunächst wird der Cash-Startwert gebildet:
cash = cashCarryIn − (isFirstPhase ? 0 : investmentsFromCash)
cashStart = cash
Die Investitionen werden also am Phasenanfang abgezogen. Grund (Kommentar im Code, Fix in
Commit f768e01): Der Cash-Startwert zeigt damit den Bestand nach den Investitionen – die
investierten Mittel erscheinen im Vermögen und nicht doppelt auch im Cash.
Dann für t = 1 .. duration:
// 1. Einkommen (nominal)
incomeFlow = renteTotal + Σ (inc.basis × (1 + inc.idx/100)^(t−1))
// 2. Ausgaben (real → nominal) + Hypothekarzins
inflFactor = cumInflStart × (1 + infl/100)^(t−1)
expenseRealBase = Σ (exp.basis × (1 + exp.idx/100)^(t−1))
interestNominal = Σ (re.mortgage × re.interestRate/100) // nur wo interestHandling = ADD
expenseNominal = expenseRealBase × inflFactor + interestNominal
expenseReal = expenseRealBase + interestNominal / inflFactor
// 3. Quote
quote = incomeFlow − expenseNominal
// 4. YearPoint anlegen (year, age Person A, income, expenseNominal, expenseReal);
// wealthNominal/wealthReal werden nach Schritt 8 nachgetragen
// 5. Vermögen: verzinsen, Sparbeitrag, Bezugsrate
für jedes Asset a:
grown = a.value × (1 + a.r/100) + a.rate // Zins zuerst, dann Einzahlung (nachschüssig)
w = min(a.withdrawal, max(0, grown)) // Bezug am Bestand gekappt
a.value = grown − w
cashFromWithdraw += w
// 6. Amortisation und Tilgung – jeweils am Restsaldo gekappt
debtRates = 0
für jede Immobilie re:
pay = min(re.amort, re.mortgage) // nie mehr als die Restschuld
re.mortgage −= pay
debtRates += pay
re.value ×= (1 + re.growth/100) // Wertsteigerung auf die LIEGENSCHAFT
für jede Schuld d:
pay = min(d.repay, d.owed)
d.owed −= pay
debtRates += pay
falls t === 1 → plannedSaveRate = fixedRatesTotal + debtRates
// 7. Cash fortschreiben
cash += quote − fixedRatesTotal − debtRates + cashFromWithdraw
falls cash < 0 → cashNegative = true
// 8. Ruin prüfen (Gesamtvermögen zum Jahresende)
total = cash + Σ asset.value + Σ (re.value − re.mortgage) + Σ (−d.owed)
yearPoint.wealthNominal = round(total) // Grundlage des Verlaufs je Jahr
yearPoint.wealthReal = round(total / cumInfl[Jahr])
falls ruinAge === null und total < 0 → ruinAge = age(Person A) + yearsBefore + t
Wichtige Details:
- Verzinsung ist nachschüssig: der Sparbeitrag des Jahres wird nicht mitverzinst.
- Amortisation und Tilgung enden mit der Schuld. Hypothek und Restschuld sind laufende
Salden; die Rate ist pro Jahr auf den Restsaldo gekappt (
min(rate, saldo)). Ist die Schuld abbezahlt, fliesst kein Franken mehr ab – weder aus dem Cash noch in die Sparrate. Im letzten Zahlungsjahr wird nur noch der Restbetrag fällig, nicht die volle Rate. plannedSaveRate(Kopf-Kennzahl) ist die tatsächliche Rate des ersten Phasenjahres. In späteren Jahren kann sie tiefer liegen, wenn eine Schuld ausläuft.cashNegativewird gesetzt, sobald der Cash-Bestand irgendwann innerhalb der Phase unter 0 fällt – auch wenn er am Phasenende wieder positiv ist.- Der Ruin bezieht sich auf das Gesamtvermögen inkl. Immobilien, nicht auf das Cash.
ruinAgewird nur einmal gesetzt (erstes Auftreten, plan-weit).
Referenz: src/lib/calculations.ts Zeilen 365–425.
4.8 Endwerte und Phasen-Kennzahlen
Einkommen: startValue = basis
endValue = basis × (1 + idx/100)^(duration−1)
Ausgaben: startValue = basis × cumInflStart
endValue = basis × (1 + idx/100)^(duration−1) × flowDeflatorEnd
Assets: endValue = a.value (nach der Jahresschleife)
Immobilie: endValue = re.value − re.mortgage (Verkehrswert inkl. Wertsteigerung, minus Restschuld)
Schulden: endValue = −d.owed (0, falls getilgt; + Notiz „Wird getilgt")
Aggregate:
startWealthNominal = Σ Element-Startwerte + cashStart
endWealthNominal = Σ Element-Endwerte + cashEnd
endWealthReal = endWealthNominal / cumulativeInflationEnd
isConsumption = quotaStart < 0
incomplete = cashNegative // „roter Status" = Liquiditätslücke
capitalInflow = incomingInflow // aus dem Übergang IN diese Phase
capitalInvest = investmentsFromCash + incomingImmediateRepay
oneOffInflow = incomingOneOffInflow // einmaliger Zufluss (netto nach Steuer)
oneOffOutflow = incomingOneOffOutflow // einmalige Kosten (nominal)
4.9 Der Übergang
Nach jeder Phase (auch nach der letzten) läuft die Übergangs-Logik. Sie liest
transitionValues[phase.id] – der Übergang ist also am Von-Phasen-Schlüssel gespeichert.
ownerRetiresNext = owner existiert
∧ nextPhase existiert
∧ owner ist in DIESER Phase erwerbstätig
∧ owner.age + yearsBefore + duration >= owner.retirementAge
INCOME/EXPENSE sowie nicht-aktive Elemente überspringen die Logik (nur hasCarry = true).
4.9.1 PENSION_FUND
Pensions-Übergang (ownerRetiresNext), value = ec.endValue. Gelesen wird der
Pensionierungs-Entscheid des Elements (3.13), nicht die Übergangszelle:
sharePct = clamp(retirementDecision.capitalSharePct ?? 0, 0, 100)
capital = round(value × sharePct / 100)
rest = value − capital
falls capital > 0:
netto = round(capital × (1 − capitalTaxRate/100))
txInflow += netto
txTax += capital − netto
-> geht in die Kapitalverwendung (Punkt C, Kap. 3.12.5)
carry.pkPensionAnnual = rest > 0 ? round(rest × conversionRate / 100) : 0
carry.value = 0
Eine Quote statt des früheren Modus (PENSION/CAPITAL/COMBI) plus Frankenbetrag:
0 % ist die volle Rente, 100 % der volle Kapitalbezug, alles dazwischen die Kombination.
Der Grund ist derselbe wie bei Punkt C – verschiebt man das Pensionsalter, ändert sich das
Guthaben. Ein fixer Betrag bedeutete dann still ein anderes Verhältnis, eine Quote skaliert mit.
Normaler Übergang (Vorbezug) – brutto entnommen, netto ins Cash:
withdrawal = min(ec.endValue, round(td.withdrawal)) // brutto
carry.value = ec.endValue − withdrawal
txInflow += round(withdrawal × (1 − capitalTaxRate/100))
4.9.2 PILLAR_3A
Bezug am gewählten Alter (seit 0.35): Der Bezug hängt nicht mehr starr am Pensions-Übergang,
sondern am withdrawalAge des Pensionierungs-Entscheids (60–70, Vorgabe = Pensionsalter).
Gezogen wird an der ersten Phasengrenze bei oder nach diesem Alter; liegt der Wunsch hinter
dem Planende, greift die letzte Grenze, damit kein Guthaben unbezogen liegen bleibt.
netto = round(ec.endValue × (1 − capitalTaxRate/100))
txInflow += netto
carry.value = 0
Warum das Alter und nicht ein Ja/Nein: Ein 3a-Konto lässt sich bei der Pensionierung nur ganz auflösen, und alle Kapitalbezüge desselben Jahres werden steuerlich zusammengezählt. Gestaffelt wird deshalb über mehrere Konten mit unterschiedlichen Bezugsjahren – dieses Feld ist die einzige Stellschraube dafür. Innerhalb einer Phase kennt das Modell kein Einzelereignis; wer exakt staffeln will, setzt eine Phasengrenze.
Normaler Übergang (Vorbezug): unverändert in der Übergangszelle – Bruttoentnahme, Netto-Zufluss nach Kapitalbezugssteuer. Ein Vorbezug ist ein Ereignis an einer bestimmten Grenze und gehört deshalb nicht zum Pensionierungs-Entscheid.
4.9.3 OTHER_ASSET
decision |
Wirkung |
|---|---|
SELL |
txInflow += ec.endValue; carry.status = "SOLD" (kein Steuerabzug) |
PARTIAL |
part = min(ec.endValue, partialSaleAmount); txInflow += part; carry.value = ec.endValue − part – Rest bleibt aktiv und wächst weiter |
HOLD / sonst |
carry.value = ec.endValue |
Der Teilverkauf (Roadmap Nr. 42) fliesst über txInflow ins Cash der Folgephase und erscheint
dort im Phasenkopf als „Kapitalzufluss" (capitalInflow) – das „zu investierende Kapital".
Er ist die einmalige Entnahme am Übergang; die laufende Entnahme während einer Phase
bleibt die jährliche Bezugsrate (annualWithdrawal, 4.6.6). Beide dürfen koexistieren.
4.9.4 REAL_ESTATE
Gelesen werden die laufenden Werte aus der Jahresschleife (re.value, re.mortgage,
re.purchase).
decision = "SELL":
gain = max(0, salePrice − re.purchase) // URSPRÜNGLICHER Kaufpreis, nicht der Verkehrswert
tax = gain × (saleTaxRate / 100)
txInflow += round(salePrice − re.mortgage − tax)
carry.status = "SOLD"
Der Nettoerlös ist Verkaufspreis minus Hypothekenablösung minus Grundstückgewinnsteuer. Ein
Verlustverkauf erzeugt keine Steuer (gain bei 0 geklammert).
Die Steuer bemisst sich am ursprünglichen Kaufpreis, nicht am zwischenzeitlich gestiegenen Verkehrswert – deshalb führt das Modell beide getrennt (4.6.5). Nicht modelliert sind wertvermehrende Investitionen und die Haltedauer-Abstufung (Roadmap Nr. 23).
Halten (decision ≠ SELL) – mit optionaler Sonderamortisation (Roadmap Nr. 15):
extra = min(re.mortgage, extraAmortization) // am Restsaldo gekappt
txImmediateRepay += extra // Einmaltilgung aus dem Cash
carry.mortgage = re.mortgage − extra
carry.propertyValue / carry.propertyPurchase // fortgeschrieben
Die Einmaltilgung bringt REAL_ESTATE auf Augenhöhe mit OTHER_DEBT (dort schon
immediateRepayment, 4.9.5): Sie senkt die Restschuld und damit die künftige Zinslast und
erscheint im Phasenkopf als „Kapitalinvestition" (capitalInvest). Damit lässt sich die
indirekte Amortisation mechanisch nachbilden – 3a wachsen lassen, bei Pensionierung ins
Cash beziehen, dann per Sonderamortisation die Hypothek tilgen.
Die Steuerwirkung der indirekten Amortisation (3a-Abzug, erhaltener Zinsabzug) ist bewusst nicht modelliert; sie gehört zum zurückgestellten Steuer-Bündel (siehe 9.14).
4.9.5 OTHER_DEBT
carry.owed = −ec.endValue
immediate = min(carry.owed, round(td.immediateRepayment))
falls immediate > 0:
carry.owed −= immediate
txImmediateRepay += immediate
falls carry.owed === 0 → carry.status = "SETTLED"
4.9.6 Cash: einmalige Sonderein-/ausgaben
Gelesen wird phase.cashTransition – der Entscheid hängt an der Von-Phase. Er wird nur
ausgewertet, wenn eine Folgephase existiert; nach der letzten Phase gibt es keinen Übergang,
ein dort erfasster Betrag bleibt wirkungslos.
Der Umrechnungskurs zwischen real und nominal ist an dieser Grenze cumulativeInflation, also
der Bestands-Deflator am Phasenende (vgl. 4.5.3) – denn Cash ist
ein Bestand, und das Ereignis fällt exakt auf die Grenze.
mode = cashTransition.mode ?? "NONE"
falls mode ∈ {INFLOW, BOTH}: // nominal erfasst
brutto = round(inflowAmount)
txOneOffInflow = round(brutto × (1 − inflowTaxRate/100))
falls mode ∈ {OUTFLOW, BOTH}: // real erfasst
txOneOffOutflow = round(outflowAmount × cumulativeInflation)
Beide Grössen fliessen ausschliesslich ins Cash der Folgephase (4.9.7) und nicht in die
Jahresschleife – damit bleiben sie per Konstruktion aus Einkommen, Ausgaben und Quote heraus.
Ein Zufluss/eine Kostenposition, die das Cash unter 0 drückt, wird über den bestehenden
Startwert-Check der Folgephase (cashNegative = cash < 0) automatisch als Liquiditätslücke
erkannt.
4.9.7 Abschluss des Übergangs
cashCarryIn = cashEnd + txInflow + txOneOffInflow − txImmediateRepay − txOneOffOutflow
incomingInflow = txInflow // Kopf-Kennzahlen der Folgephase
incomingImmediateRepay = txImmediateRepay
incomingOneOffInflow = txOneOffInflow // inkl. Bezeichnung
incomingOneOffOutflow = txOneOffOutflow // inkl. Bezeichnung
yearsBefore += duration
4.10 Ergebnisstruktur
PlanComputed {
phases: PhaseComputed[] // alle Kennzahlen je Phase, inkl. elements[]
yearly: YearPoint[] // ein Punkt pro Jahr über alle Phasen (inkl. Vermögen je Jahr)
nachlass: number // = endWealthNominal der letzten Phase, sonst 0
ruinAge: number | null // Alter Person A beim ersten Gesamtvermögen < 0
}
4.11 Systemparameter
Zentral in src/lib/constants.ts geführt, weil sie sich periodisch durch Bundesanpassungen ändern:
| Konstante | Wert | Bedeutung |
|---|---|---|
AHV_MIN_MONTHLY_FULL |
1'260 | R0 – Mindestbetrag der vollen Monatsrente (Skala 44). Alle Schwellen der Rentenformel sind Vielfache davon |
AHV_PENSION_MONTHS |
13 | Rentenzahlungen pro Jahr – 13. Altersrente ab 1.1.2026 |
AHV_CONTRIBUTION_START_AGE |
21 | Beitragspflicht ab 1. Januar nach dem 20. Geburtstag |
AHV_MAX_ANNUAL_SINGLE |
32'760 | abgeleitet: 2 × R0 × 13 |
AHV_GROSS_FROM_NET_FACTOR |
1.12 | Netto → Brutto für die AHV; Herleitung siehe 4.4.5 |
AHV_COUPLE_CAP_FACTOR |
1.5 | Ehepaar-Plafonierung: 150 % der Einzel-Maximalrente |
AHV_FULL_CONTRIBUTION_YEARS |
44 | Volle Beitragsdauer (Rentenskala 44) |
AHV_REFERENCE_AGE |
65 | Referenzalter – ab hier fliesst die Rente, unabhängig vom Pensionsalter (4.4.7) |
PILLAR_3A_MAX_ANNUAL |
7'258 | Max. 3a-Beitrag/Jahr für PK-Versicherte (2026) |
PILLAR_3A_MAX_SELF_EMPLOYED |
36'288 | Max. 3a-Beitrag/Jahr «grosse Säule 3a» (Selbstständige ohne PK). 2026-Wert zu verifizieren (2025: 36'288) |
DEFAULT_PK_CONVERSION_RATE |
6 % | Umwandlungssatz |
DEFAULT_CAPITAL_TAX_RATE |
8 % | Kapitalbezugssteuer |
DEFAULT_PROPERTY_GAINS_TAX_RATE |
20 % | Grundstückgewinnsteuer |
Die drei Default-Sätze werden sowohl als UI-Vorschlag als auch in der Berechnung als Fallback
verwendet (num(td.capitalTaxRate, DEFAULT_CAPITAL_TAX_RATE)). Grund laut Code-Kommentar: Damit
ein nicht angetippter Wert nicht fälschlich als 0 gerechnet wird.
4.12 Monte-Carlo-Simulation
Die deterministische Berechnung nimmt pro Anlage eine feste Rendite und eine feste Inflation
an. Real schwanken beide. Die Monte-Carlo-Simulation (Roadmap Nr. 19, Stufe A, montecarlo.ts)
würfelt viele tausend mögliche Verläufe und weist die Erfolgswahrscheinlichkeit des Plans aus.
4.12.1 Die Nahtstelle in computePlan
computePlan(plan, sample?) nimmt optional ein PlanSample:
interface PlanSample {
inflation: number[]; // Inflation %/Jahr (Index 0 = Jahr 1)
assetReturn: (elementId: string, year: number) => number; // Rendite %/Jahr (1-basiert)
}
Ohne sample rechnet die Funktion exakt wie bisher (die geplanten Annahmen). Mit sample
liefert sie einen einzelnen simulierten Pfad. Voraussetzung war eine Umstellung der Inflation
auf ein kumulatives Deflator-Array cumInfl[] (statt der geschlossenen (1+i)^t-Formel),
damit die Inflation pro Jahr variieren kann – deterministisch bitgenau identisch, durch die
Golden Tests abgesichert. Die AHV-Karriere bleibt bewusst auf der festen Plan-Inflation (sie
ist eine Real-Grösse auf Planungsbasis, sie wird nicht mitgewürfelt).
Architektur-Vorteil: computePlan ist eine reine Funktion ohne Server-Abhängigkeiten und
läuft damit im Browser. Die gesamte Simulation rechnet client-seitig – null Serverlast.
~10'000 Läufe in rund 1 Sekunde; die Ausführung gibt alle 500 Läufe die Kontrolle ab
(Fortschrittsbalken, keine eingefrorene Oberfläche).
4.12.2 Das statistische Modell
Pro Jahr ein gemeinsamer Marktschock z_markt; je Element und Jahr:
rendite = mittelwert + σ × (ρ × z_markt + √(1−ρ²) × z_eigen)
z_markt,z_eigen: standardisierte Student-t (ν = 5) – „fette Ränder", damit Extremcrashs realistisch häufig auftreten. Eine Normalverteilung macht ein −40%-Jahr zu einem 1-in-250-Ereignis; real ist es ~1-in-15. Standardisiert auf Einheitsvarianz → die eingegebene Standardabweichung σ bleibt die tatsächliche.ρ = 0.7→ Korrelation zweier riskanter Anlagen ≈ 0.5: alle riskanten Anlagen fallen im Crash gemeinsam. Unabhängiges Würfeln würde das Absturzrisiko systematisch unterschätzen.- Böden:
max(0, rendite)für PK/3a (schreiben keine negative Rendite gut),max(−100, rendite)sonst.
Die Inflation wird analog gezogen (eigener Student-t-Schock, Mittelwert + σ), unabhängig vom Marktschock. Der Zufallsgenerator ist seedbar (reproduzierbare Läufe).
4.12.3 Zwei Renditezahlen — und warum
Pro renditetragendem Element (PK, 3a, Sonstiges Vermögen, Immobilie) gibt es im Simulations-Dialog zwei Renditen mit verschiedenen Rollen:
| Zahl | Rolle |
|---|---|
| Geplante Rendite (im Plan) | zeichnet die deterministische Linie = der Zielbalken |
| Historische Ø-Rendite (im Dialog, Pflicht) | der Mittelpunkt, um den die Simulation streut |
Ohne diese Trennung wäre die Kennzahl „P(erreiche mein geplantes Endvermögen)" immer ~50 %, egal welche Rendite man annimmt (der Zielbetrag wüchse ja mit). Erst weil die Simulation um die historische Rendite streut, während der Zielbalken auf der geplanten steht, wird ein konservativer Plan (tiefe Planannahme) korrekt mit einer höheren Erfolgsquote belohnt als ein optimistischer. Analog auf Plan-Ebene für die Inflation.
Ehrliche Grenze (im Dialog ausgewiesen): Die Simulation misst das Risiko um deine Annahmen herum – sie beurteilt nicht, ob deine Mittelwerte realistisch sind. Ein zu optimistischer Mittelwert liefert eine beruhigende, aber unrealistische Erfolgsquote (siehe 9.15).
4.12.4 Streuungsstufen
| Stufe (Rendite) | σ | Beispiele | Stufe (Inflation) | σ | |
|---|---|---|---|---|---|
| Sehr niedrig | 3 % | Staatsanleihen, Geldmarkt | Sehr niedrig | 1 % | |
| Niedrig | 6 % | Immobilien, defensive Mischportfolios | Niedrig | 2 % | |
| Moderat | 15 % | breit diversifizierte Aktien-ETFs/Fonds | Manuell | frei | |
| Hoch | 25 % | Einzelaktien, Branchen-/Schwellenländerfonds | |||
| Sehr hoch | 55 % | Kryptowährungen, hochspekulative Anlagen | |||
| Manuell | frei | eigene Eingabe |
Default-Stufe je Typ: PK → sehr niedrig · 3a/Immobilie → niedrig · Sonstiges Vermögen → moderat. Bei Inflation gibt es bewusst nur zwei Stufen (höhere wären Hyperinflations-Annahmen).
Quellen der σ-Werte: Anleihen ~6 %, globale Aktien ~15–18 %, Schweizer Immobilien(fonds) ~2 %, Bitcoin ~54 %, Schweizer Inflation SD der letzten 20 J. ~1 %. Belege: BSV/Weltbank sowie Markt-/Volatilitätsstatistiken (recherchiert 2026-07-17).
4.12.5 Ergebnis
| Kennzahl | Bedeutung |
|---|---|
| Ruinwahrscheinlichkeit | Anteil der Läufe mit ruinAge !== null (Vermögen fällt vor Planende unter 0) |
| Erfolgswahrscheinlichkeit | Anteil der Läufe mit Endvermögen ≥ Zielbetrag (nominal, vorbelegt mit dem geplanten Nachlass) |
| Fächer | je Alterspunkt (Phasengrenzen) das 10-/50-/90-Perzentil des Vermögens; dazu die deterministische Planungslinie |
Der Median liegt typischerweise unter der deterministischen Linie – der „Volatilitäts-Drag"
(geometrisch ≈ arithmetisch − σ²/2) macht sichtbar, dass die glatte Ein-Zahl-Planung schon
leicht zu optimistisch ist. Pflichtfelder: ohne die historischen Ø-Werte startet die Simulation
nicht.
4.12.6 Mehrere Szenarien im Vergleich
Der Dialog rechnet auf Wunsch mehrere Szenarien desselben Plans in einem Lauf. Drei Entscheide machen den Vergleich überhaupt aussagekräftig.
(1) Eine Parametereingabe je logischem Element. Die MC-Parameter hängen an der elementId,
und Element-IDs sind szenario-spezifisch – eine Kopie bekommt neue IDs. Ohne Zuordnung müsste
dieselbe Anlage pro Szenario erneut erfasst werden. Das wäre nicht nur mühsam, es würde den
Vergleich zerstören: Mit 5 % im einen und 6 % im anderen Szenario vergleicht man die
Eingaben statt der Szenarien.
Die Zuordnung läuft über die Herkunfts-Kette sourceElementId – dieselbe Grundlage wie beim
Diff (3.2.6). resolveRootElementId folgt ihr bis zum
Ursprung; alle Elemente mit derselben Wurzel bilden eine Gruppe und teilen einen
Parametersatz. Deshalb lädt der Dialog beim Öffnen alle Szenarien des Plans, nicht nur die
ausgewählten: Nur so löst sich die Kette auch über ein übersprungenes Zwischen-Szenario auf
(Basis → S1 → S2 bei Auswahl von Basis und S2). Ein Element, das es nur in einem Szenario gibt,
bildet eine eigene Gruppe und wird im Dialog entsprechend gekennzeichnet.
(2) Gemeinsamer Seed. Alle Szenarien eines Laufs verwenden denselben Zufalls-Seed (Common Random Numbers). Ohne das wären kleine Unterschiede blosses Rauschen: Bei 1'000 Läufen beträgt der Standardfehler der Erfolgswahrscheinlichkeit rund 1.5 Prozentpunkte – zwei identische Szenarien könnten 87 % und 90 % zeigen. Mit gemeinsamem Seed teilen strukturgleiche Szenarien exakt dieselben Marktpfade, und die Unterschiede sind rein strukturell. Einschränkung: Die Pfade sind nur dort identisch, wo die Struktur es ist – abweichende Laufzeit oder Elementzahl verschiebt die Ziehungsreihenfolge.
(3) Zielbetrag je Szenario. Der Zielbetrag ist bewusst nicht gemeinsam, sondern je Szenario mit dessen geplantem Endvermögen vorbelegt (einzeln editierbar). Damit misst die Erfolgswahrscheinlichkeit, wie oft ein Szenario sein eigenes Versprechen hält.
Warum das der entscheidende Punkt ist: In der Simulation wird die geplante Rendite vollständig durch die gewürfelte ersetzt. Unterscheiden sich zwei Szenarien nur in der geplanten Rendite (5 % vs. 6 %), sind ihre simulierten Verteilungen identisch – gleicher Median, gleicher Fächer, gleiche Ruinwahrscheinlichkeit. Der einzige Unterschied ist der Zielbetrag. Mit einem gemeinsamen Zielbetrag zeigte der Vergleich zwei identische Zeilen; mit szenario-eigenem Zielbetrag zeigt er die eigentliche Aussage: Das pessimistisch geplante Szenario erreicht sein tieferes Ziel häufiger und ist damit das belastbarere. Durch einen Test abgedeckt (Kap. 8.2).
Folge für die Darstellung: Die Ruinwahrscheinlichkeit ist zielbetrags-unabhängig und damit die direkt vergleichbare Kennzahl; die Erfolgswahrscheinlichkeit bezieht sich je Zeile auf eine andere Messlatte. Deshalb steht der Zielbetrag als eigene Spalte in der Vergleichstabelle.
Darstellung: eine Vergleichstabelle (Szenario, Ziel, Erfolg, Ruin, P10/Median/P90) als Hauptinstrument, dazu ein Chart mit der Median-Linie je Szenario. Übereinandergelegte 10–90 %-Bänder wären unlesbar; der vollständige Fächer inklusive deterministischer Linie erscheint deshalb nur, wenn genau ein Szenario ausgewählt ist – dann verhält sich der Dialog exakt wie zuvor.
Laufzeit: Die Szenarien laufen sequenziell, der Fortschritt weist Szenario und Gesamtanteil aus. Die Schätzung skaliert mit der Anzahl Szenarien.
Referenz: src/lib/montecarlo.ts (resolveRootElementId, buildElementGroups,
paramsForScenario, runMonteCarloMulti), src/components/MonteCarloDialog.tsx.
4.12.7 Zwei Welten, vier Fälle
Die Simulation läuft immer zweimal – einmal um die historischen Renditen, einmal um die geplanten Werte des Plans. Aus jeder der beiden Verteilungen werden zwei Schwellen abgelesen: der Plan-Endbetrag (read-only, der geplante Nachlass des Szenarios) und der Zielbetrag (manuell, einer für alle Szenarien). Das ergibt vier Zahlen:
| Schwelle Plan-Endbetrag | Schwelle Zielbetrag | |
|---|---|---|
| Welt historisch (Eingabe) | Fall 1 – Realismus der Planung | Fall 3 – Ziel, wenn der Markt wie früher läuft |
| Welt geplant (aus dem Plan) | Fall 2 – Nullpunkt für das Urteil | Fall 4 – Ziel, wenn der Markt wie geplant läuft |
Das baut auf der Trennung der zwei Renditezahlen aus 4.12.3
auf. Die Streuung ist in beiden Welten dieselbe; nur der Mittelpunkt unterscheidet sie. In der
geplanten Welt kommt der Mittelpunkt je Element aus plannedReturnOf (erste Phase, bei Immobilien
die Wertsteigerung) und die Inflation aus plan.inflationRateDefault.
Warum zwei Läufe und nicht vier. Eine Wahrscheinlichkeit P(Endvermögen ≥ X) hängt nur von der
Verteilung und von X ab. Wird das gesamte Endvermögen eines Laufs sortiert aufbewahrt
(finalWealthSorted), lässt sich jede Schwelle nachträglich per Binärsuche ablesen
(probabilityAtLeast). Zwei Läufe genügen also für vier Zahlen – halbe Rechenzeit, und vor allem:
Fall 1 und Fall 3 stammen aus derselben Verteilung. Liegt der Zielbetrag unter dem Plan-Endbetrag, ist Fall 3 damit zwingend ≥ Fall 1. Der Widerspruch, der die alte Darstellung unlesbar machte («Planung 69 %, Ziel 3 Mio nur 41 %» – zwei verschiedene Welten gegeneinander gestellt), ist strukturell nicht mehr möglich. Ein Test sichert das ab.
Der Nullpunkt ist Fall 2, nicht 50 %. Naiv erwartet man, dass der eigene Plan-Endbetrag in der eigenen geplanten Welt in ~50 % der Fälle erreicht wird. Das ist falsch: Der Volatilitäts-Drag drückt den Median unter den deterministischen Pfad, und zwar umso stärker, je höher die Streuung (gemessen: 37 % bei σ = 15 %, 27 % bei σ = 25 %; siehe 9.26). Fall 2 misst genau diesen Nullpunkt und wird deshalb als Referenz ausgewiesen, gegen die Fall 1 verglichen wird – beides derselbe Schwellwert, nur andere Renditeannahme.
Urteil (Toleranzband ± 5 Prozentpunkte):
| Fall 1 vs. Fall 2 | Urteil |
|---|---|
| mehr als 5 pp höher | Planung zurückhaltend gerechnet |
| innerhalb ± 5 pp | Planung realistisch gerechnet |
| mehr als 5 pp tiefer | Planung tendenziell zu optimistisch |
Darstellung (bei einem einzelnen Szenario):
- Prominent: Fall 1 als grosse Prozentzahl mit dem Urteil.
- Untergeordnet: Fall 3 und 4 als Satzpaar, dessen Wortwahl automatisch dreht, je nachdem welche Welt besser dasteht («Wenn der Markt so ‹schlecht› läuft, wie du es in deiner Planung vorgibst, … bei X %. Läuft er so ‹gut› wie in der Vergangenheit, … bei Y %.»).
- Kleingedruckt: Fall 2 als Referenz plus der Median je Welt.
Die Tabelle zeigt je Szenario Fall 1 (Fall 2) und Fall 3 (Fall 4). Ruin, Bandbreite und
Fächer stammen immer aus der historischen Welt, weil das das ehrliche Risikobild ist.
Eingaben: Da beide Welten immer laufen, sind die historischen Mittelwerte und der Zielbetrag stets Pflichtfelder. Der frühere Drei-Wege-Umschalter entfällt.
Technik: kein Eingriff in den Rechenkern. Beide Durchgänge laufen mit demselben Seed (Common Random Numbers), damit der Unterschied zwischen den Welten nicht Rauschen ist.
Referenz: src/lib/montecarlo.ts (finalWealthSorted, probabilityAtLeast, plannedReturnOf),
src/components/MonteCarloDialog.tsx.
4.13 Sensitivitätsanalyse (Tornado)
Die Monte-Carlo-Simulation würfelt alle Unsicherheiten gleichzeitig und beantwortet „wie
wahrscheinlich geht mein Plan auf?". Die Sensitivitätsanalyse (Roadmap Nr. 20, sensitivity.ts)
beantwortet die komplementäre Frage: „Welche meiner Annahmen entscheidet überhaupt über das
Ergebnis?"
4.13.1 Verfahren
One-at-a-time (OAT):
base = Zielgrösse(Plan)
für jeden ausgewählten Treiber d:
lowResult = Zielgrösse(applyDriver(Plan, d, d.low))
highResult = Zielgrösse(applyDriver(Plan, d, d.high))
swing = |highResult − lowResult|
sortiere absteigend nach swing → Trichterform, längster Balken zuoberst
Alle übrigen Parameter bleiben dabei auf dem Planwert. Das sind 2 Aufrufe je Treiber – bei
sieben Treibern 14 computePlan-Aufrufe, also Millisekunden. Wie die Monte-Carlo-Simulation
läuft alles im Browser; applyDriver ist rein und lässt den Ausgangsplan unberührt.
Zielgrösse ist das Endvermögen der letzten Phase, wahlweise real (Default,
kaufkraftbereinigt) oder nominal. Das Ruinalter wäre als Balkengrösse untauglich, weil es in
vielen Plänen null ist.
4.13.2 Die Treiber und ihre Einheiten
Die Einheit ist je Treiber verschieden und lässt sich nicht vereinheitlichen, ohne fachlich falsch zu werden:
| Treiber | Einheit | Wirkung |
|---|---|---|
| Ausgaben | relativ % | skaliert amount aller EXPENSE-Elemente |
| Rendite (PK, 3a, Sonstiges Vermögen) | Δ Prozentpunkte | verschiebt expectedReturn |
| Lebensdauer | Δ Jahre | verlängert/verkürzt die letzte Phase (min. 1 Jahr) |
| Inflation | absolut % | setzt inflationRateDefault |
| Einkommen | relativ % | skaliert amount aller INCOME-Elemente |
| Lohnentwicklung | Δ Prozentpunkte | verschiebt teuerungsausgleich der INCOME-Elemente |
| Wertsteigerung der Immobilie | Δ Prozentpunkte | verschiebt valueGrowth |
| Pensionsalter Person A / B | Δ Jahre | verschiebt die Phasengrenze der Pensionierung (4.16) |
Die Begründungen im Einzelnen:
- Absolut nur bei der Inflation – es gibt genau einen plan-weiten Wert.
- Δ Prozentpunkte bei den Renditen, weil die Elemente je eigene Sätze tragen. Ein absolutes „3 % bis 7 %" würde die PK auf ETF-Rendite plätten.
- Relativ % bei Einkommen und Ausgaben, weil die Elemente je eigene Beträge tragen.
- Immobilien-Wertsteigerung ist ein eigener Treiber und nicht Teil von „Rendite", damit sie nicht doppelt zählt.
Die Skalierung von Einkommen/Ausgaben greift nur dort, wo amount gesetzt ist. Das ist korrekt
und beabsichtigt: Ab Phase 2 ist der Wert in der Regel live vererbt (4.6.1),
und die Fortschreibung leitet ihn aus dem skalierten Basiswert ab – die Skalierung wirkt damit
automatisch über alle Folgephasen.
Ein Treiber erscheint nur, wenn der Plan passende Elemente enthält (applies).
4.13.3 Bandbreiten sind Pflicht – ohne Default
Je Treiber gibt der Benutzer eine tiefe und eine hohe Ausprägung an; Vorgabewerte gibt es bewusst nicht. Grund: Die Balkenlänge hängt direkt von diesen Bandbreiten ab. Ein stiller Default würde nicht hinterfragt, und das Ranking wäre dann eine Aussage über unsere Vorgabe statt über den Plan – dieselbe Begründung wie bei den Monte-Carlo-Mittelwerten (9.15).
Die Hilfe-Bubble je Treiber nennt stattdessen plausible Grössenordnungen. Entscheidend ist, die Bandbreiten ähnlich plausibel zu wählen, nicht ähnlich gross: „±10 % Inflation" (1.5 → 1.65 %) und „±10 % Ausgaben" sind völlig ungleich wahrscheinlich.
Zwei weitere Regeln: mindestens zwei Treiber (ein Tornado ist eine Rangliste – ein einzelner Balken ordnet nichts), und tiefer und hoher Wert dürfen nicht identisch sein (Spannweite 0).
4.13.4 Darstellung
Waagrechtes Balkendiagramm, je Balken die Spanne min…max der Zielgrösse, senkrechte
Referenzlinie beim Basisfall, sortiert nach Spannweite. Darunter eine Tabelle mit der
eingegebenen Bandbreite, den beiden Ergebniswerten und der Spannweite.
Die Richtung kann sich umkehren – tiefe Ausgaben ergeben ein hohes Endvermögen. Der Balken
spannt deshalb über min…max; welche Eingabe zu welchem Ende gehört, zeigt die Tabelle.
Referenz: src/lib/sensitivity.ts, src/components/SensitivityDialog.tsx.
4.13.5 Wirkungslose Treiber werden erklärt
Ein Balken mit Spannweite 0 ohne Erklärung ist die schlechteste Antwort – der Benutzer hält
ihn für einen Fehler. computeTornado hängt deshalb an jeden Nullbalken eine Begründung
(ineffectiveReason).
Der wichtigste Fall ist die Immobilien-Wertsteigerung bei einem Verkauf. Der Verkaufserlös
ist Verkaufspreis − Hypothek − Grundstückgewinnsteuer und hängt damit am erfassten
Verkaufspreis, nicht am modellierten Verkehrswert. Wird die Immobilie vor Planende
verkauft, wird die aufgelaufene Wertsteigerung an dieser Stelle verworfen – der Treiber kann
das Endvermögen dann rechnerisch nicht mehr beeinflussen.
Erkannt wird das daran, dass in der letzten Phase alle REAL_ESTATE-Elemente den Status
SOLD tragen. Andernfalls greift ein allgemeiner Hinweis. Durch Tests abgedeckt: gehalten →
Spannweite > 0 ohne Hinweis; verkauft → Spannweite 0 mit Begründung.
Verwandt: Der Verkaufspreis-Abgleich im Übergangs-Dialog (3.5.8) setzt an derselben Stelle an, nur früher – er verhindert, dass die Annahmen überhaupt auseinanderlaufen.
4.14 Verlaufswerte, Brücken und Rechenwege
Dieses Kapitel beschreibt, was computePlan über die reinen Ergebniswerte hinaus mitführt –
die Grundlage der Detailansichten (Roadmap Nr. 43) und der Transparenz-Offenlegung (Nr. 41).
4.14.1 Verlaufswerte je Element
ElementPhaseComputed.yearly hält einen Punkt pro Phasenjahr je Element (Jahr, Alter, Wert;
bei Immobilien zusätzlich Verkehrswert und Restschuld).
Der Grund ist derselbe wie beim Vermögensverlauf in Version 0.9: Nur Start- und Endwert zu kennen
reicht nicht. Eine Anlage mit 5 % über 20 Jahre wächst konvex – eine Gerade zwischen den
Phasengrenzen wäre sichtbar falsch und würde den Zinseszins optisch unterschlagen. Der Punkt wird
am Jahresende erfasst, nach Verzinsung und Tilgung, konsistent zum YearPoint.
4.14.2 Die beiden Wasserfälle
Ein Wasserfall zerlegt eine Veränderung in ihre Summanden. Der entscheidende Punkt ist, welche Posten dazugehören – und hier liegt eine Falle:
Sparraten, Amortisationen und Zusatzinvestitionen sind Umbuchungen, keine Verluste. Eine 3a-Einzahlung verlässt das Cash und erhöht im selben Zug das 3a-Guthaben; das Vermögen bleibt unverändert. Eine Amortisation senkt das Cash und die Hypothek – das Eigenkapital steigt sogar. Als Abgang im Vermögens-Wasserfall gezeichnet würden diese Posten eine Vermögensminderung vortäuschen, die es nicht gibt.
Deshalb gibt es zwei Zerlegungen mit unterschiedlichen Fragestellungen:
WealthBridge – „Warum hat sich mein Vermögen so entwickelt?"
Vermögen Ende Vorphase
+ einmaliger Zufluss − einmalige Kosten ⎫
− Steuern am Übergang (Kapitalbezug, Grundstückgewinn) ⎬ Vermögensänderungen
− verrentetes PK-Kapital (verlässt die Bilanz) ⎪ AN der Phasengrenze
± Verkaufspreis minus Verkehrswert ⎭
= Vermögen zu Phasenbeginn
+ Spar-/Verzehrquote (Summe über alle Phasenjahre) ⎫
+ Kapitalerträge (PK, 3a, Sonstiges Vermögen) ⎬ INNERHALB der Phase
+ Wertsteigerung der Liegenschaft ⎪
+ PK-Beiträge ⎭
= Vermögen am Phasenende
Zwei Posten verdienen eine Erläuterung:
- PK-Beiträge sind ein echter Zugang. Sie belasten das Cash nicht (im Nettolohn bereits abgezogen), erhöhen aber das Vorsorgekapital – anders als 3a-Beiträge, die aus dem Cash fliessen und deshalb reine Umbuchung sind.
- Verrentetes PK-Kapital verlässt die Bilanz. Bei der Verrentung wird Kapital in einen Rentenstrom umgewandelt; der Saldo fällt auf 0. Ohne diesen Posten ginge die Brücke am Pensions-Übergang nicht auf.
CashBridge – „Wohin ist mein Cash geflossen?"
Cash Ende Vorphase (Phase 1: Cash-Anfangswert)
+ Kapitalzufluss + einmaliger Zufluss
− Sofort-Tilgung/Sonderamortisation − einmalige Kosten
− Investitionen am Phasenanfang
= Cash zu Phasenbeginn
+ Spar-/Verzehrquote − Sparraten − Amortisationen/Tilgungen + Bezugsraten
= Cash am Phasenende
Beide Strukturen führen einen Restposten (residual) mit: die Differenz zwischen dem
gerechneten Endwert und der Summe der Summanden. Er ist die eingebaute Selbstkontrolle – ist die
Zerlegung vollständig und richtig, muss er exakt 0 sein.
src/lib/bridges.test.ts nagelt das über sieben Plankonstellationen fest (Ansparen mit 3a
und Schuldentilgung, Pensionierung mit Verrentung und 3a-Bezug, PK-Kapitalbezug, Immobilie
gehalten, Immobilie verkauft, einmalige Sonderein-/ausgaben, Sofort-Tilgung mit
Sonderamortisation) – je Phase für beide Brücken, zusätzlich der Abgleich der Kontrollpunkte
gegen startWealthNominal / endWealthNominal / cashStart / cashEnd.
Im UI wird ein Restposten über 2 Franken als Fehlermeldung ausgewiesen, nicht als beiläufige Rundungsnotiz: Eine Brücke, die nicht aufgeht, ist ein Rechenfehler und kein Darstellungsproblem.
4.14.2.1 Darstellung der Wasserfälle
Die Wasserfälle werden nicht mit Recharts gezeichnet. Ein Wasserfall lebt von drei Dingen, die dort nicht ohne Weiteres zu bekommen sind:
- Verbindungslinien zwischen den Balken – ohne sie sieht man nicht, dass jeder Balken dort ansetzt, wo der vorherige aufhört, und die Grafik zerfällt in unverbundene Rechtecke.
- Wertbeschriftung an jedem Schritt, statt Beträge aus der Achse zu schätzen.
- Unterscheidung von Zwischenstand und Veränderung. Ein Zwischenstand („Vermögen Phasenbeginn") ist ein absoluter Wert ab Null, eine Veränderung („Kapitalerträge") setzt auf dem laufenden Saldo auf. Sehen beide gleich aus, ist die Grafik nicht lesbar.
Die Darstellung ist deshalb eine eigene HTML/CSS-Konstruktion und liegend statt stehend – die Beschriftungen sind lang und müssten stehend gedreht werden; liegend ist es ausserdem konsistent zum Tornado. Abschnitts-Überschriften trennen „Am Übergang in diese Phase" von „Innerhalb der Phase". Posten mit Wert 0 werden gar nicht erst gezeichnet.
Darunter steht aufklappbar eine Tabelle mit laufendem Zwischenstand. Bei sieben bis zwölf Schritten mit stark unterschiedlichen Grössenordnungen ist sie der Grafik schlicht überlegen – die Grafik zeigt das Verhältnis, die Tabelle die Zahl.
4.14.3 Rechenweg-Protokoll
computePlan(plan, sample?, { explain: true }) legt zu jedem Ergebnis die Herleitung ab:
interface TraceStep { label; formula?; substituted?; result; unit?; note? }
interface Trace { title; specAnchor?; steps: TraceStep[] }
formula ist die abstrakte Regel, substituted dieselbe Regel mit den eingesetzten Zahlen,
note benennt eine an dieser Stelle geltende Vereinfachung.
Der Architekturentscheid dahinter ist der wichtigste Teil dieses Kapitels. Die naheliegende Alternative wäre, die Formeln im UI nachzurechnen und anzuzeigen. Das ergäbe eine zweite Implementierung jeder Formel – und damit die Möglichkeit, dass die angezeigte Herleitung still von der tatsächlichen Rechnung abdriftet. Bei einem Tool, dessen Kernversprechen die rechnerische Korrektheit ist, wäre das die gefährlichste Variante überhaupt: Ein Nutzer, der nachrechnet und eine Abweichung findet, verliert mehr Vertrauen, als eine Black Box je gekostet hätte.
Deshalb entstehen die Schritte innerhalb von computePlan, als Nebenprodukt der Rechnung, die
ohnehin läuft. Die Erklärung ist die Rechnung.
Zwei Konsequenzen daraus:
- Standardmässig aus. Die Monte-Carlo-Simulation ruft
computePlanzehntausendfach auf und darf von der Protokollierung nichts merken. Ein Test prüft, dassexplaindie Ergebniswerte nicht verschiebt. - Die Arithmetik wird nicht umgestellt. Wo für das Protokoll eine Zwischengrösse gebraucht
wird (etwa der Renditeanteil eines Jahres), wird sie als Differenz abgeleitet statt die
Formel umzuformen –
a × (1 + r)unda + a × rsind in Gleitkomma-Arithmetik nicht bitgleich. Die 43 Golden Tests laufen unverändert.
Abdeckung. Vollständig über alle Ebenen: Element je Phase (Einkommen, Ausgaben, PK, 3a, Immobilie, Sonstiges Vermögen, Schulden, AHV-/PK-Renten), Element je Übergang (Bezugsarten, Verkauf inkl. Grundstückgewinnsteuer, Teilverkauf, Sonderamortisation, Sofort-Tilgung), Phasen-Kennzahlen (Quote, Spar-/Verzehrrate, Cash-Fortschreibung, Vermögen, Phasentyp, Cash-Übergang) und Plan-Ebene (Deflatoren, AHV-Karriere je Person, Ruinalter).
Verweis in die Spezifikation. Jeder Trace trägt optional einen specAnchor auf das zugehörige
Kapitel dieses Dokuments; die App springt von dort in die eingebaute SPEZIFIKATION-Ansicht (die
Anker erzeugt rehype-slug). Ein Test liest SPEZIFIKATION.md und prüft, dass jeder Verweis
eine existierende Überschrift trifft – sonst würden die Links bei einer Umbenennung still ins
Leere zeigen.
Wo die Erklärung gerechnet wird. Nicht auf dem Server: computePlan ist rein und läuft im
Browser (wie schon bei Monte Carlo und Sensitivitätsanalyse), und der Client hält den
PlanInput ohnehin. Die Detailansicht rechnet die erklärte Fassung beim Öffnen lokal – kein
API-Umbau, keine grössere Antwort, keine Serverlast, und per Konstruktion identisch zum
Serverergebnis.
Referenz: src/lib/calculations.ts, src/components/DetailView.tsx.
4.15 Live-Simulation (Was-wäre-wenn-Regler)
Roadmap Nr. 22. Beantwortet weder «welche Annahme entscheidet» (das ist der Tornado, 4.13) noch «wie wahrscheinlich ist das» (das ist Monte-Carlo, 4.12), sondern schlicht: «Wie sieht mein Plan aus, wenn ich hier drehe?» – sofort, und ohne für jede Variante eine Szenario-Kopie anzulegen.
Eigener Button «Live-Simulation» in der Szenario-Leiste, Dialog als Zweispalter: links die Regler, rechts die Grafik, darüber eine Kennzahlenleiste.
4.15.1 Keine eigene Rechenlogik
Die Regler benutzen dieselben Transformationen wie der Tornado (applyDriver). Damit kann
die Live-Simulation gar nicht etwas anderes zeigen als die Einflussfaktoren-Analyse – beide
bewegen den Plan identisch. Es entsteht kein zweiter, potenziell abweichender Rechenweg.
Neu hinzu kommt nur applyElementDriver(plan, elementId, deltaPp): dieselbe Verschiebung, aber
auf ein einzelnes Element statt auf eine ganze Kategorie (bei Immobilien auf valueGrowth
statt expectedReturn). Element-IDs sind innerhalb eines Szenarios eindeutig; die
Herkunfts-Verkettung sourceElementId aus der Monte-Carlo-Simulation braucht es hier nicht,
weil die Live-Simulation immer nur auf einem Szenario läuft.
4.15.2 Sammelregler und Aufschlüsselung
Standardmässig gibt es einen Rendite-Regler für alle Anlagen – das hält das Panel ruhig und entspricht dem Tornado. Ein Klick auf «Renditen einzeln aufschlüsseln» ersetzt ihn durch je einen Regler pro renditetragendem Element (PK, 3a, Sonstiges Vermögen, Immobilie). Erst dann lässt sich die eigentliche Spielfrage stellen: Was, wenn mein ETF schlechter läuft, die PK aber wie geplant?
Der Sammelregler wird beim Aufklappen entfernt, nicht bloss ergänzt – sonst würde eine Bewegung doppelt zählen. Aus demselben Grund werden die Rendite-Regler beim Umschalten zurückgesetzt. Ein Test sichert ab, dass beide Ansichten denselben Plan beschreiben: Alle Elemente einzeln um +1 pp zu heben ergibt exakt dasselbe Endvermögen wie der Sammelregler auf +1 pp.
Bewusst nicht aufschlüsselbar sind Ausgaben und Einkommen. «Alle Ausgaben ±20 %» ist die Frage, die man tatsächlich stellt; Ausgaben-Elemente sind typischerweise viele kleine Posten, deren Einzelregler das Panel fluten würden, ohne eine bessere Frage zu ermöglichen.
Das Pensionsalter ist seit 0.26 als Regler dabei (je Person einer, sofern überhaupt Spielraum besteht). Sein Bereich ist plan-abhängig: Er endet dort, wo eine angrenzende Lebensphase unter ein Jahr fiele – ein Regler, der stumm an seiner Grenze klebt, wäre schlechter als keiner. Der Treiber Lebensdauer bleibt daneben bestehen; er beantwortet die andere Frage (wie lange muss es reichen, statt wann höre ich auf).
4.15.3 Referenz und Kennzahlen
Eine wandernde Linie ohne Anker ist wertlos – «ist 2.9 Mio jetzt viel oder wenig?». Deshalb:
- Der unveränderte Plan wird im Vermögensverlauf als blasse Referenzlinie mitgezeichnet.
- Darüber steht eine Kennzahlenleiste mit Endvermögen nominal und real, jeweils mit der
Differenz zum Plan (
3'660'683 → 2'880'100, −780'583). - Eine dritte Karte meldet, ob das Kapital reicht oder in welchem Alter es aufgebraucht ist. Das ist die wichtigste Einzelinformation und einer Verlaufslinie nicht zuverlässig anzusehen: Ein Plan kann optisch plausibel aussehen und trotzdem zwischendurch unter null fallen.
Die Grafik zeigt wann sich etwas ändert, die Leiste wie viel.
Rechts stehen drei Grafiken zur Wahl: Vermögensverlauf (mit Referenzlinie), Vermögensaufteilung je Phase und Einkommen vs. Ausgaben. Die beiden letzteren zeigen nur den simulierten Stand – ein zweiter gestapelter Balkensatz wäre nicht mehr lesbar; darauf weist der Dialog hin.
4.15.4 Laufzeit: synchron, ohne Debounce
Gemessen an einem Plan über 60 Jahre mit 10 Elementen braucht computePlan rund 0.2 ms.
Bei 60 fps stehen 16 ms je Bild zur Verfügung – die Rechnung kostet also etwa 1 % des
Budgets. Deshalb wird bei jeder Reglerbewegung synchron neu gerechnet: kein Debounce, kein
Web Worker, keine Ladeanzeige. Der Engpass ist das Neuzeichnen der Grafik, nicht die Mathematik.
4.15.5 Nichts wird gespeichert
Die Live-Simulation schreibt nicht – keine API, keine Datenbank, kein Schreibpfad. Genau das ist der Punkt der Roadmap-Anforderung («ohne für jede Variante eine Szenario-Kopie anzulegen»).
Ein «Als neues Szenario speichern» ist bewusst noch nicht umgesetzt: Reglerwerte in echte Element- und Phasenwerte zurückzuschreiben hiesse viele einzelne Schreibvorgänge und einen neuen Schreibpfad – eine eigene Ausbaustufe. Als Behelf zeigt der Dialog die aktive Einstellung als lesbare Zeile («Rendite −1.5 pp · Ausgaben +10 % · Lebensdauer +5 J.»), die sich von Hand in ein echtes Szenario übertragen lässt.
Referenz: src/lib/livesim.ts, src/lib/sensitivity.ts (applyElementDriver,
tunableElements), src/components/LiveSimDialog.tsx, src/components/AllocationChart.tsx.
4.16 Pensionsalter verschieben
Modul
src/lib/retirement.ts. Rein: Es entscheidet nur, was geschehen soll; das Schreiben übernimmt der Aufrufer (Panel: API-Aufrufe; Tornado/Live-Simulation: reine Plan-Kopie).
4.16.1 Die tragende Invariante
maxPhaseDuration(persons, yearsBefore) kappt jede Phasendauer beim nächsten
Pensionierungsereignis. Daraus folgt: Jede Pensionierung liegt auf einer Phasengrenze. Genau
das macht die Anpassung überhaupt erst möglich – «Pensionsalter ändern» ist gleichbedeutend mit
«diese eine Grenze verschieben».
retirementBoundaries(plan) liefert je Person:
| Feld | Bedeutung |
|---|---|
planYear |
Planjahr der Pensionierung (retirementAge − age) |
phaseIndex |
Index der Phase vor der Grenze |
minDelta / maxDelta |
Spielraum, ohne dass eine Nachbarphase unter 1 Jahr fällt |
mergeDeltaDown / mergeDeltaUp |
genau der Wert, bei dem eine Phase entfällt |
blocked |
Erklärtext, wenn gar keine Anpassung möglich ist (siehe 3.12.2) |
4.16.2 shiftRetirement(plan, role, delta)
Phase[i].dauer += delta // vor der Grenze
Phase[i+1].dauer −= delta // nach der Grenze
person.retirementAge += delta
Fällt eine der beiden auf 0, wird sie entfernt und die sequenceNumber lückenlos neu vergeben.
Die Übergangsdaten der entfallenden Phase werden nach der Regel aus 3.12.3 in den Vorgänger
gezogen (mergeTransition, mergeCashTransition). Ausserhalb des erlaubten Bereichs liefert
die Funktion null – sie klemmt nicht still.
4.16.3 Als Treiber und Regler
applyDriver(plan, "retirementA" | "retirementB", jahre) benutzt dieselbe Funktion. Zwei
bewusste Abweichungen gegenüber der Bedienung:
- Der Bereich endet bei
minDelta/maxDelta, ohne Zusammenlegung. Eine Zusammenlegung verändert den Plan inhaltlich (zwei Übergänge werden einer) – dafür ist eine Was-wäre-wenn-Betrachtung der falsche Ort. - Eine zu weite Eingabe wird auf das Mögliche gekürzt statt verworfen: Eine Bandbreite von
±5 Jahren soll auch dann etwas zeigen, wenn nur ±2 möglich sind. Bleibt gar kein Spielraum,
erklärt
ineffectiveReasonden Nullbalken.
Der Hebel ist doppelt – länger Einkommen und kürzer Verzehr – und deshalb meist einer der grössten im Tornado.
4.16.4 Punkt B: Einkommen endet mit der Pensionierung
Bis 0.25 lief ein Erwerbseinkommen stillschweigend in die Pensionsphase weiter (der Basiswert wird ja fortgeschrieben). Bei fixem Pensionsalter fiel das kaum auf; sobald sich die Grenze verschieben lässt, ist es ein handfester Fehler – «drei Jahre früher aufhören» hätte sonst gar keine Wirkung gehabt.
Kategorie INCOME, Element ist EINER PERSON zugeordnet,
Person in dieser Phase pensioniert, kein ausdrücklicher Betrag erfasst
→ Basiswert = 0
Drei bewusste Einschränkungen:
- Nur personenzugeordnete Einkommen. «Gemeinsam» (Mieterträge, Ausschüttungen) hängt nicht an der Erwerbstätigkeit einer Person und läuft weiter.
- Ein ausdrücklich erfasster Betrag gewinnt. Sonst liesse sich ein Teilzeitpensum oder eine Überbrückungsrente nach der Pensionierung nicht abbilden.
- Das Element zeigt in dieser Phase den Hinweis, dass es wegen der Pensionierung auf 0 steht.
4.16.5 Vererbte Wiederkehr-Parameter (Punkt A)
Der Carry führt neu eine Tabelle rates mit den zuletzt verwendeten Werten. Ist ein Feld in
einer Phase nicht gesetzt, gilt der Wert aus der Vorphase; der verwendete Wert wird wieder
abgelegt, sodass die Kette über beliebig viele Phasen trägt. Betroffen sind
teuerungsausgleich, expectedReturn, annualContribution, annualWithdrawal,
amortization, valueGrowth, interestRate und annualRepayment.
4.16.6 Fehlerbehebung: fortgeschriebener Basiswert und effektive Werte
Der Basiswert der Folgephase (carry.flowBasis) wurde beim Element-Setup berechnet, also
vor der Jahresschleife. Ein effektiver Wert setzt den Basiswert aber erst in der
Jahresschleife neu (rebaseFlow). Folge: Ein für 2031 erfasster Lohn wirkte bis zum Ende seiner
Phase – und fiel an der Phasengrenze stillschweigend auf den geplanten Wert zurück.
Die Fortschreibung passiert neu nach der Jahresschleife, gemeinsam mit den Endwerten. Zwei
Regressionstests in actuals.test.ts halten den Fall fest.
5. Technische Spezifikation
5.1 Technologie-Stack
| Bereich | Technologie | Version |
|---|---|---|
| Framework | Next.js (App Router) | 16.2.10 |
| UI | React | 19.2.4 |
| Sprache | TypeScript | ^5 |
| Styling | Tailwind CSS | ^4 (via @tailwindcss/postcss) |
| Icons | lucide-react | ^1.24.0 |
| Charts | recharts | ^3.9.2 |
| ORM | Prisma | ^7.8.0 (Client-Output nach src/generated/prisma) |
| Datenbank | PostgreSQL | 16-alpine |
| DB-Treiber | pg + @prisma/adapter-pg |
^8.22.0 / ^7.8.0 |
| Validierung | Zod | ^4.4.3 |
| Auth | jose (JWT) + bcryptjs | ^6.2.3 / ^3.0.3 |
| Tests | Vitest | ^4.1.10 |
| Build | Docker (multi-stage), output: "standalone" |
Hinweis für Entwickler: Gemäss
AGENTS.mdweicht diese Next.js-Version von verbreiteten Konventionen ab. Vor Änderungen ist der relevante Guide unternode_modules/next/dist/docs/zu konsultieren.
5.2 Verzeichnisstruktur
FPT/
├── prisma/
│ ├── schema.prisma Datenmodell
│ └── migrations/ 17 Migrationen (chronologisch, siehe 5.4.6)
├── src/
│ ├── app/
│ │ ├── api/ Route Handlers (siehe Kapitel 6)
│ │ ├── login/page.tsx Login-/Registrierseite
│ │ ├── page.tsx Einstiegsseite (lädt /api/auth/me → AppShell)
│ │ ├── layout.tsx Root-Layout, Theme-Init-Script, Metadata
│ │ └── globals.css Tailwind + semantische Farb-Tokens (3 Themes)
│ ├── components/ 23 React-Komponenten (alle "use client")
│ ├── generated/prisma/ Generierter Prisma-Client (nicht editieren)
│ ├── lib/ Domänenlogik (siehe 5.3)
│ └── middleware.ts Zugriffsschutz (Edge-Runtime)
├── Info Dateien/ Fachdokumente, Roadmap (historisch)
├── docker-compose.yml, Dockerfile, docker-entrypoint.sh
└── .gitea/workflows/deploy.yaml CI/CD
5.3 Schichtenmodell und src/lib
Ein bewusster Entkopplungs-Entscheid (src/lib/types.ts Zeilen 1–4): Die Berechnungslogik
arbeitet auf eigenen Domänentypen, nicht auf den generierten Prisma-Typen. Dadurch ist
computePlan ohne Datenbank testbar.
Prisma-Modelle (DB)
│ toPlanInput() ← queries.ts: parst + validiert die JSON-Felder
▼
PlanInput (types.ts) ← reine Domänentypen
│ computePlan() ← calculations.ts: pure function
▼
PlanComputed ← an den Client geliefert
| Datei | Verantwortung |
|---|---|
calculations.ts |
Berechnungskern. Keine I/O. |
csv.ts |
CSV-Export der vollständigen Matrix (vier Blöcke, Kap. 3.6.5). Rein. |
elements.ts |
Kategorien, Labels, Reihenfolge, JSON-Payload-Typen, Zod-Schemas, num() |
types.ts |
Domänentypen für API und Berechnung |
constants.ts |
Schweizer Systemparameter |
montecarlo.ts |
Monte-Carlo-Simulation (Sampler + Treiber), Szenario-Vergleich und Element-Gruppierung über die Herkunfts-Kette. Keine I/O, läuft im Browser. |
sensitivity.ts |
Sensitivitätsanalyse / Tornado: Treiber-Katalog, Parameter-Transformationen, computeTornado; zusätzlich applyElementDriver / tunableElements für die einzeln regelbaren Element-Renditen. Rein, läuft im Browser. |
actuals.ts |
Effektive Werte: Zuordnung auf die Szenario-Elemente über die Herkunfts-Kette, Einspielen in den Rechenkern, Bestand/Fluss. Rein. |
report.ts |
Berichtsmodell: Kennzahlen, Annahmen, Vergleich, Plan/Ist -- rein, ohne PDF-Kenntnisse (Kap. 3.11). |
report-pdf.ts |
Zeichnet das Berichtsmodell mit pdfkit. Nur serverseitig. |
decisions.ts |
Offene Übergangs-Entscheide -- von Matrix UND Bericht benutzt, damit beide dieselbe Zahl nennen. |
analyses.ts |
Gespeicherte Analysen: Typen, generische Ergebnis-Form, Speicher-Helfer (Kap. 3.10). |
dataview.ts |
Bündelt Plan-Sicht und Ist-Sicht für Matrix, Grafiken und Analysewerkzeuge. Rein. |
ratefields.ts |
Ratenfelder je Kategorie, Reichweite der Übernahme über Lebensphasen, Aufbau der Schreibvorgänge (Kap. 3.6.11). Rein. |
versioning.ts |
Versionierung: Nummerierung A.B, Zusammenfassung je Sitzung, Bauplan des Wiederherstellens, Auswirkung auf Kind-Szenarien. Rein, ohne I/O. |
versioning-db.ts |
Datenbank-Anbindung der Versionierung: Snapshot festhalten, Hauptversion, Wiederherstellen. Führt den Bauplan aus versioning.ts nur aus. |
livesim.ts |
Live-Simulation: Reglerkatalog mit Standardbereichen, Anwenden mehrerer Regler, Kennzahlen (Kap. 4.15). Rein, läuft im Browser. |
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.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) |
queries.ts |
Prisma-Includes, toPlanInput(), Ownership-Abfragen |
db.ts |
Prisma-Singleton (Global-Cache im Dev, verhindert Connection-Leak bei HMR) |
auth.ts |
JWT erzeugen/prüfen (Edge-kompatibel via jose) |
session.ts |
getCurrentUserId() aus dem Cookie (Node-Runtime) |
users.ts |
Registrierung, Credential-Prüfung, Passwortwechsel (bcrypt). Nie aus der Middleware importieren – Edge-Runtime hat keinen DB-Zugriff. |
format.ts |
formatChf / parseChfInput |
theme.ts |
Theme-Verwaltung (localStorage + data-theme) |
api-client.ts |
Typisierter fetch-Wrapper mit einheitlicher Fehlerextraktion |
5.4 Datenmodell
5.4.1 Tabellen
User
| Feld | Typ | Constraints |
|---|---|---|
id |
String | PK, cuid() |
username |
String | unique |
passwordHash |
String | bcrypt, Cost 12 |
createdAt |
DateTime | now() |
Person
| Feld | Typ | Constraints |
|---|---|---|
id |
String | PK, cuid() |
scenarioId |
String | FK → Scenario, Cascade |
role |
PersonRole |
PERSON_A | PERSON_B |
name |
String? | optional |
age |
Int | aktuelles Alter |
retirementAge |
Int | szenario-eigenes Pensionsalter |
@@unique([scenarioId, role]) |
Plan (Behälter – trägt keine Finanzdaten)
| Feld | Typ | Constraints |
|---|---|---|
id |
String | PK, cuid() |
userId |
String | FK → User, Cascade |
name |
String | |
createdAt / updatedAt |
DateTime |
Scenario (die berechenbare Einheit)
| Feld | Typ | Constraints |
|---|---|---|
id |
String | PK, cuid() |
planId |
String | FK → Plan, Cascade |
name |
String | |
isBase |
Boolean | Default false; genau eines je Plan ist true |
parentScenarioId |
String? | FK → Scenario (Self-Relation „ScenarioTree"), SetNull – Baum und Vergleichsbasis |
householdType |
HouseholdType |
SINGLE | COUPLE |
inflationRateDefault |
Float | szenario-weite Inflation in % |
initialCash |
Float | Default 0 |
startYear |
Int? | Kalenderjahr des Planbeginns – nur Darstellung (siehe 3.2.7) |
createdAt / updatedAt |
DateTime |
Phase
| Feld | Typ | Constraints |
|---|---|---|
id |
String | PK, cuid() |
scenarioId |
String | FK → Scenario, Cascade |
sequenceNumber |
Int | 1-basiert, lückenlos |
name |
String | |
durationYears |
Int | 1–80 |
cashTransition |
Json? | Cash-Entscheid beim Übergang nach dieser Phase (siehe 5.4.5) |
ratesConfirmed |
Boolean | Default false – wurde die Spar-/Verzehrquote dieser Phase je verteilt? (3.14.2) |
sourcePhaseId |
String? | Gegenstück in der Vorlage (lose Referenz, kein FK) – Diff-Grundlage |
createdAt / updatedAt |
DateTime | |
@@unique([scenarioId, sequenceNumber]) |
cashTransition liegt an der Phase und nicht in ElementTransitionValue, weil Cash kein
FinancialElement ist und damit keine elementId besitzt. Die Verschlüsselung folgt derselben
Logik wie dort: Der Übergang gehört der Von-Phase.
FinancialElement
| Feld | Typ | Constraints |
|---|---|---|
id |
String | PK, cuid() |
scenarioId |
String | FK → Scenario, Cascade |
category |
ElementCategory |
8 Werte |
name |
String | |
ownerRole |
OwnerRole? |
PERSON_A | PERSON_B | HOUSEHOLD |
orderIndex |
Int | Default 0 |
sourceElementId |
String? | Gegenstück in der Vorlage (lose Referenz, kein FK) – Diff-Grundlage |
createdAt |
DateTime |
Die Herkunfts-Verweise sind bewusst lose (kein Fremdschlüssel): Wird das Gegenstück in der Vorlage gelöscht, soll die Kopie bestehen bleiben und im Diff einfach als „neu" gelten – ein Cascade wäre hier falsch.
ElementPhaseValue
| Feld | Typ | Constraints |
|---|---|---|
id |
String | PK, cuid() |
elementId |
String | FK → FinancialElement, Cascade |
phaseId |
String | FK → Phase, Cascade |
data |
Json | Payload gemäss PhaseData |
@@unique([elementId, phaseId]) |
ElementTransitionValue
| Feld | Typ | Constraints |
|---|---|---|
id |
String | PK, cuid() |
elementId |
String | FK → FinancialElement, Cascade |
fromPhaseId |
String | FK → Phase, Cascade |
data |
Json | Payload gemäss TransitionData |
@@unique([elementId, fromPhaseId]) |
5.4.2 Warum JSON?
Die kategoriespezifischen Felder liegen als JSON, weil sich sonst pro Kategorie eine eigene
Tabelle (oder eine breite Sparse-Tabelle) ergäbe. Die Typisierung und Validierung findet in der
Applikationsschicht statt (src/lib/elements.ts): PhaseData / TransitionData als
TypeScript-Interfaces, phaseDataSchema / transitionDataSchema als Zod-Schemas an der
API-Grenze.
Die Interfaces sind bewusst tolerant getippt (alle Felder optional): die Berechnung liest
defensiv über num(value, fallback), das UI zeigt kontextabhängig nur die relevanten Felder.
Referenz: prisma/schema.prisma Zeilen 4–6, src/lib/elements.ts Zeilen 48–51.
5.4.3 JSON-Payload PhaseData
| Feld | Kategorien | Zod-Regel |
|---|---|---|
amount |
INCOME, EXPENSE | ≥ 0 |
teuerungsausgleich |
INCOME, EXPENSE | −20 bis 50 |
gapYears |
AHV | Integer ≥ 0 |
ahvContribution |
AHV | ≥ 0 – Beitrag als Nichterwerbstätige(r) bis zum Referenzalter (4.4.7) |
avgIncomeBefore |
AHV – nur wenn bei Planbeginn bereits pensioniert | ≥ 0, real |
gapYearsBefore |
AHV – dito | Integer 0–50 |
currentValue |
PENSION_FUND, PILLAR_3A | ≥ 0 |
startValue |
OTHER_ASSET, OTHER_DEBT | ≥ 0 |
expectedReturn |
PK, 3a, OTHER_ASSET | −50 bis 100 |
annualContribution |
PK, 3a, OTHER_ASSET | ≥ 0 |
selfEmployed3a |
PILLAR_3A – «grosse Säule 3a» (Selbstständige ohne PK) → höhere Obergrenze | Boolean |
annualWithdrawal |
OTHER_ASSET | ≥ 0 |
additionalInvestment |
PK, 3a, OTHER_ASSET (ab Phase 2) | ≥ 0 |
purchasePrice |
REAL_ESTATE | ≥ 0 |
mortgage |
REAL_ESTATE | ≥ 0 |
amortization |
REAL_ESTATE | ≥ 0 |
interestRate |
REAL_ESTATE – Hypothekarzins %/Jahr | 0–20 |
interestHandling |
REAL_ESTATE – Doppelzählungs-Schalter | INCLUDED (Default) | ADD |
valueGrowth |
REAL_ESTATE – Wertsteigerung %/Jahr auf die Liegenschaft | −20 bis 20 |
annualRepayment |
OTHER_DEBT | ≥ 0 |
confirmed |
alle – «ich habe hingeschaut» (3.14.2) | Boolean, optional |
Vererbbare Felder (Punkt A, Kap. 3.12.4): teuerungsausgleich, expectedReturn,
annualContribution, annualWithdrawal, amortization, valueGrowth, interestRate und
annualRepayment sind ab Phase 2 in der Regel nicht gesetzt – die Berechnung übernimmt dann
den Wert der Vorphase. Alle übrigen Felder bedeuten «nicht gesetzt = 0» wie bisher.
5.4.4 JSON-Payload TransitionData
| Feld | Kategorien | Zod-Regel |
|---|---|---|
reviewed |
AHV (Pensions-Übergang) – Beitragskarriere geprüft | Boolean |
avgIncomeBefore |
AHV (Pensions-Übergang) | ≥ 0, real |
gapYearsBefore |
AHV (Pensions-Übergang) | Integer 0–50 |
withdrawalMode |
PK, 3a (normal) | NONE | AMOUNT |
withdrawal |
PK, 3a (normal) | ≥ 0, brutto |
payoutMode |
PK (Pensionierung) | CAPITAL | PENSION | COMBI |
capitalAmount |
PK (COMBI) | ≥ 0 |
conversionRate |
PK | 0–20 |
capitalTaxRate |
PK + 3a, sowohl Vorbezug als auch Pensionierung | 0–100 |
decision |
REAL_ESTATE, OTHER_ASSET | HOLD | SELL | PARTIAL |
salePrice |
REAL_ESTATE | ≥ 0 |
partialSaleAmount |
OTHER_ASSET (Teilverkauf) | ≥ 0 |
extraAmortization |
REAL_ESTATE (Sonderamortisation) | ≥ 0 |
saleTaxRate |
REAL_ESTATE | 0–100 |
immediateRepayment |
OTHER_DEBT | ≥ 0 |
capitalUseAmortizationPct |
PENSION_FUND (Kapitalbezug), PILLAR_3A – Anteil des bezogenen Kapitals in die Schuldentilgung | 0–100 |
capitalUseDebtTargetElementId |
Ziel der Tilgungs-Quote: eine Immobilie oder eine sonstige Schuld. Ohne Angabe die Immobilien der Reihe nach (Bestandsschutz); ein Schuld-Element wird nur getilgt, wenn es ausdrücklich gewählt ist | ≤ 60 Zeichen |
capitalUseInvestPct |
dito – Anteil in eine Investition | 0–100 |
capitalUseTargetElementId |
Ziel der Investitions-Quote; ohne Angabe das erste aktive Sonstige Vermögen | ≤ 60 Zeichen |
5.4.5 JSON-Payload CashTransitionData
Liegt in Phase.cashTransition. Validierung über cashTransitionSchema.
| Feld | Bedeutung | Zod-Regel |
|---|---|---|
mode |
Entscheid | NONE | INFLOW | OUTFLOW | BOTH |
inflowLabel |
Bezeichnung des Zuflusses (z. B. „Erbschaft") | ≤ 120 Zeichen |
inflowAmount |
Betrag nominal | ≥ 0 |
inflowTaxRate |
Steuer auf den Zufluss, Default 0 % | 0–100 |
outflowLabel |
Bezeichnung der Kosten (z. B. „Poolbau") | ≤ 120 Zeichen |
outflowAmount |
Betrag real (heutige Kaufkraft) | ≥ 0 |
Pro Übergang ist genau ein Zufluss und eine Kostenposition möglich – siehe 9.7.
Alle Schemas verwenden .strip() – unbekannte Felder werden verworfen, nicht abgelehnt.
Beim Lesen aus der DB gilt zusätzlich: schlägt safeParse fehl, wird {} zurückgegeben
(parsePhaseData / parseTransitionData) – korrupte Daten führen also nie zu einem Absturz,
sondern zu leeren Werten.
5.4.6 Migrationshistorie
| Migration | Inhalt |
|---|---|
20260708171600_init |
Initiales Schema |
20260709010000_add_transition_automation |
Übergangs-Automatik |
20260709120000_rework_realestate |
Immobilien-Überarbeitung |
20260711090000_multi_user |
User-Entität, Ownership |
20260713100000_element_model_rework |
Elemente auf Plan-Ebene, Phase-/Transition-Werte als JSON |
20260713150000_profile_to_plan_v3 |
Grundprofil von Household auf Plan verschoben |
20260714120000_person_name |
Person.name |
20260715120000_plan_initial_cash |
Plan.initialCash |
20260716210000_drop_phase_inflation_rate |
Phase.inflationRate entfernt (Inflation ist plan-weit) |
20260716230000_phase_cash_transition |
Phase.cashTransition (JSONB) für einmalige Sonderein-/ausgaben |
20260718090000_plan_scenario_hierarchy |
V6: Plan → Scenario (IDs erhalten), neuer Behälter Plan, planId → scenarioId, Herkunfts-Verweise |
20260718140000_scenario_start_year |
Scenario.startYear (Kalenderjahr des Planbeginns), bestehende auf das laufende Jahr gesetzt |
20260719210000_scenario_versioning |
Tabelle ScenarioVersion (Snapshot als JSONB, A.B eindeutig je Szenario) und Scenario.currentMajor |
20260720090000_actuals |
Tabelle ActualsSet (effektive Werte je Plan, Werte als JSONB, Cash separat) |
20260720140000_plan_level_profile |
V7: Haushaltsform, Personen (Name/Alter) und Startjahr vom Szenario auf den Plan; neue Tabelle PlanPerson; Person behält nur das Pensionsalter. Datenübernahme aus dem Basisszenario. |
20260721090000_reports |
Tabelle Report: gewählte Parameter, eingefrorenes Modell und die PDF-Datei als BYTEA |
20260724120000_version_zero_start |
Scenario.currentMajor startet bei 0 statt 1 – die Versionierung beginnt bei 0.1 (Kap. 3.8). Nur der Default; bestehende Zeilen bleiben |
20260720160000_saved_analyses |
Tabelle SavedAnalysis (Eingaben + Ergebnis als JSONB, denormalisierte Kerndaten) |
20260816150000_review_state |
Phase.ratesConfirmed (Default false); Scenario.assistantProgress entfällt mit dem Assistenten (Kap. 3.14) |
Zur V6-Migration: Sie benennt die bisherige Plan-Tabelle in Scenario um – dadurch
bleiben alle IDs und damit sämtliche Kind-Fremdschlüssel gültig. Für jedes bisherige
Wurzel-Element entsteht ein neuer Behälter ('plan_' || id, deterministisch ableitbar, daher
ohne Hilfstabelle); der bisherige Plan-Name wandert dorthin, das Szenario heisst „Basisszenario".
Bestehende Szenarien werden per rekursivem CTE demselben Behälter zugeordnet – auch mehrfach
verschachtelte. Ein Sicherheitsnetz fängt verwaiste Szenarien ab und macht sie eigenständig.
Die Migration wurde vor dem Deploy gegen echtes PostgreSQL verifiziert (PGlite, in-process): alle Vorgänger-Migrationen einspielen, realistische Daten inkl. verschachtelter Szenarien anlegen, migrieren, Ergebnis und Cascade prüfen.
5.5 Frontend-Architektur
5.5.1 Datenfluss
page.tsx (Client)
└─ GET /api/auth/me → username
└─ AppShell
├─ GET /api/plans → Planliste (Sidebar, Kacheln)
├─ GET /api/plans/<id> → { plan: PlanInput, computed: PlanComputed }
│ └─ PlanView (Matrix, Dialoge) → onChanged() → silent reload
│ └─ Dashboard (Kennzahlen, 3 Grafiken, Vergleich, Export)
└─ Dialoge: PlanDialog, ScenarioDialog
Es gibt keinen State-Management-Layer (kein Redux/Zustand/React Query). Der Zustand lebt in
AppShell (Planliste, ausgewählter Plan, Detail) und lokal in den Dialogen. Nach jeder Mutation
wird der Plan neu geladen; die Berechnung kommt immer vom Server.
5.5.2 Komponenten
| Komponente | Zeilen | Rolle |
|---|---|---|
AppShell |
433 | Layout, Sidebar, Planliste, Laden, Plan-/Szenario-Dialoge |
PlanView |
1304 | Matrix, Spaltenaufbau, Kontextbildung, alle Zell-Dialoge |
ElementDetail |
548 | Feldgruppen je Kategorie/Kontext, Speicherlogik, Übergangs-Defaults |
Dashboard |
187 | Kennzahlkarten, 3 Grafiken, Planvergleich, CSV-Link |
FormField |
275 | NumberField, MoneyInput/MoneyField, TextField, SelectField |
ProfileMenu |
166 | Benutzer, Theme-Wahl, Passwortwechsel, Logout |
Timeline |
108 | Zeitachse mit Pensions- und Ruin-Markern |
WealthChart |
106 | Vermögensverlauf, mehrere Serien |
PlanProfileFields |
101 | Wiederverwendete Grundprofil-Felder |
PhaseDetail |
95 | Phase bearbeiten/löschen |
SparquoteChart |
64 | Einkommen/Ausgaben pro Jahr mit Spar-/Verzehrbändern |
MonteCarloDialog |
~600 | Monte-Carlo-Dialog: Erklärung, Szenario-Auswahl, Eingaben, Lauf, Vergleichstabelle + Fächer |
VersionHistoryDialog |
~290 | Änderungshistorie: Liste, Hauptversion festlegen, Anzeigen, Wiederherstellen mit Warnung (3.8) |
VersionMatrix |
~110 | Nur-Lese-Matrix eines festgehaltenen Standes |
PlanViews |
~350 | Plan-Dashboard, Szenario-Liste, Analysen-Kacheln + gespeicherte Liste (Kap. 3.10) |
SavedAnalysisView |
~180 | Nur-Lese-Ansicht einer gespeicherten Analyse |
SaveAnalysisButton |
~90 | Speichern-Knopf mit Namensvorschlag |
VersionPicker |
~140 | Wahl der Berechnungsgrundlage in den vier Analysewerkzeugen |
LiveSimDialog |
~330 | Live-Simulation: Regler links, Grafikwahl rechts, Kennzahlenleiste mit Differenz zum Plan (4.15) |
AllocationChart |
~80 | Gestapelte Vermögensaufteilung je Phase; aus dem Dashboard herausgelöst, damit die Live-Simulation sie mitbenutzen kann |
SensitivityDialog |
~330 | Einflussfaktoren: Erklärung, Treiber-Auswahl mit Bandbreiten, Tornado + Tabelle |
DetailView |
~470 | Detailansichten Element/Phase, Wasserfall-Darstellung, Rechenweg-Blöcke, plan-weite Rechenwege |
SystemParametersView |
~90 | Systemparameter mit Wert, Bedeutung, Herleitung, Quelle und Stand |
SpecView |
65 | Rendert SPEZIFIKATION.md (via /api/spec) als lesbares Dokument, inkl. Sprungmarken aus den Rechenwegen |
InfoBubble |
28 | Hilfe-Tooltip |
ui |
~370 | UI-Primitiven: Button, Modal, InspectorShell, Confirm, Toast, Skeleton, EmptyState (3.7.6/3.7.7) |
ReviewTile · InventoryDialog |
~900 | Übersicht der offenen Punkte und der Sammel-Dialog der Bestandsaufnahme (3.14) |
PhaseCards |
~560 | Kartenansicht: eine Karte je Lebensphase, Bearbeitung in der Karte (3.15) |
Tour |
~140 | Interaktive Kurz-Tour über die Planansicht (3.7.8) |
CommandPalette |
~130 | Befehls-Palette Ctrl/Cmd+K (3.7.9) |
DistributionDialogs |
~460 | Verteil-Werkzeuge für Kapital und Spar-/Verzehrquote (3.6.10) |
Sparkline |
~45 | Mini-Verlaufskurve je Element-Zeile (3.7.9) |
5.5.3 Wiederverwendungsmuster
ElementPhaseFields und ElementTransitionFields sind aus ElementDetail exportiert und
werden an drei Stellen wiederverwendet: im Zell-Dialog, im Erstell-Dialog („Werte (erste
Lebensphase)") und im geführten Übergangs-Review. Dadurch gibt es genau eine Definition der
Felder je Kategorie.
Der CellContext ist die einheitliche Kontext-Schnittstelle dieser Feldgruppen; er wird in
PlanView durch buildPhaseContext / buildTransitionContext aus der Berechnung befüllt.
5.5.4 React-Detail: der key auf Dialogen
Zell-Dialoge tragen einen key aus elementId + phaseId. Grund (Kommentar Zeilen 479–480):
Beim Wechsel von Zelle zu Zelle wird ein Neuaufbau erzwungen, damit der lokale Formularzustand
nicht vom vorher geöffneten Element übrig bleibt.
5.6 Sicherheit
| Aspekt | Umsetzung |
|---|---|
| Passwortspeicherung | bcrypt, Cost 12 |
| Session | JWT HS256, 30 Tage, HttpOnly-Cookie, sameSite=lax, secure in Produktion |
| Secret | SESSION_SECRET aus der Umgebung; Fehlen wirft beim ersten Zugriff |
| Autorisierung | Middleware (grob) + Ownership-Query je Route (fein) |
| Information Disclosure | Fremde/nicht existierende Ressourcen → einheitlich 404; Login-Fehler unspezifisch |
| Eingabevalidierung | Zod an jeder API-Grenze; .strip() gegen Mass-Assignment |
| SQL-Injection | Prisma (parametrisiert) |
| XSS | React-Escaping; einziges dangerouslySetInnerHTML ist das statische Theme-Init-Script |
| CSRF | Kein Token. Schutz beruht allein auf sameSite=lax – siehe 9.4 |
5.7 Konfiguration
| Variable | Zweck |
|---|---|
DATABASE_URL |
Postgres-Connection-String |
SESSION_SECRET |
JWT-Signaturschlüssel (z. B. openssl rand -hex 32) |
POSTGRES_PASSWORD |
Nur für docker-compose: Passwort des db-Containers |
NODE_ENV |
Steuert u. a. das secure-Flag des Cookies und den Prisma-Global-Cache |
Es gibt kein konfiguriertes Login-Passwort: Konten werden über die Registrierung angelegt.
6. API-Referenz
Alle Routen liefern JSON. Fehlerformat einheitlich: { "error": "<Meldung>" } (bei
Zod-Fehlern in POST /api/plans: { "error": <flatten()-Objekt> }).
Alle Routen ausser login/register erfordern ein gültiges Session-Cookie.
6.1 Authentifizierung
| Methode | Pfad | Body | Antwort |
|---|---|---|---|
| POST | /api/auth/register |
{ username, password } |
201 { ok, username } + Cookie · 400 Validierung · 409 Name vergeben |
| POST | /api/auth/login |
{ username, password } |
200 { ok, username } + Cookie · 400 · 401 |
| POST | /api/auth/logout |
– | 200 { ok }, Cookie gelöscht |
| GET | /api/auth/me |
– | 200 { user: { id, username, createdAt } } · 401 · 404 |
| POST | /api/auth/change-password |
{ currentPassword, newPassword } |
200 { ok } · 400 · 401 |
6.2 Pläne
GET /api/plans
Liste der eigenen Pläne, sortiert nach createdAt aufsteigend.
{ "plans": [ { "id", "name", "parentPlanId", "branchFromPhaseId", "createdAt",
"phases": [ { "id", "name", "sequenceNumber" } ] } ] }
POST /api/plans
{ "name": "Basisplan", "householdType": "COUPLE", "inflationRateDefault": 1.5,
"persons": [ { "role": "PERSON_A", "name": "Anna", "age": 40, "retirementAge": 65 },
{ "role": "PERSON_B", "name": null, "age": 38, "retirementAge": 64 } ] }
Validierung: name 1–120; inflationRateDefault −20…50; persons 1–2 Einträge;
age 0–120; retirementAge 30–100; name je Person ≤ 60. Zusätzlich Konsistenzregel
SINGLE=1 / COUPLE=2 Personen. Legt Plan und Basisszenario an.
→ 201 { plan: { id }, scenario: { id } }
GET /api/plans liefert die Pläne inkl. Szenario-Kopfdaten:
{ "plans": [ { "id", "name", "createdAt",
"scenarios": [ { "id", "planId", "name", "isBase", "parentScenarioId" } ] } ] }
PATCH /api/plans/<planId>
{ name } – der Plan trägt nur noch den Namen. → 200 { plan: { id, name } }
DELETE /api/plans/<planId>
→ 200 { ok: true }, Cascade über alle Szenarien (inkl. Versionen und Ist-Sätzen).
GET /api/plans/<planId>/actuals
Alle erfassten Ist-Sätze, neueste zuerst (3.9):
{ "sets": [ { "id", "recordedOn": "2026-08-18", "year": 2026, "comment",
"cash", "values": { "<rootElementId>": { "value", "mortgage" } },
"author", "createdAt" } ] }
POST /api/plans/<planId>/actuals
{ recordedOn: "JJJJ-MM-TT", comment?, cash?, values } – year wird aus dem Datum abgeleitet.
→ 201 { set: { id, year } }
DELETE /api/plans/<planId>/actuals/<setId>
→ 200 { ok: true }. Ein Ist-Satz ist eine Beobachtung – es gibt weder Versionierung noch
Wiederherstellung.
GET /api/plans/<planId>/dashboard
Gebündelte Kennzahlen fürs Plan-Dashboard und die Szenario-Liste (3.10): Haushaltsdaten, Zähler (Szenarien/Ist-Sätze/Analysen), Basisszenario-Kennzahlen inkl. Ist-Abweichung, und je Szenario Version/Elementzahl/Endvermögen/Ruinalter.
GET/POST /api/plans/<planId>/analyses
Gespeicherte Analysen – GET listet die Kopfdaten (ohne die grossen JSON-Felder), POST legt eine
Momentaufnahme an ({ name, type, metric, source, scenarioName?, versionLabel?, inputs, result }).
GET/DELETE /api/plans/<planId>/analyses/<analysisId>
GET liefert die vollständige Analyse inkl. der eingefrorenen Zahlen (Eingaben + Ergebnis), DELETE entfernt sie. Read-only – es wird nichts neu gerechnet.
6.3 Szenarien
GET /api/scenarios/<scenarioId>
Liefert Eingabe, Berechnung und die Vergleichsbasis in einem Zug:
{ "plan": <PlanInput>, "computed": <PlanComputed>,
"base": <PlanInput|null>,
"meta": { "id", "planId", "planName", "name", "isBase", "parentScenarioId" } }
base ist das Eltern-Szenario (null beim Basisszenario) – daraus rechnet der Client den Diff.
Dies ist der einzige Endpunkt, der die Berechnung ausführt (neben export). → 404 wenn fremd.
POST /api/scenarios/<scenarioId>/retirement
Verschiebt das Pensionsalter einer Person und damit die zugehörige Phasengrenze (3.12):
{ "role": "PERSON_A", "delta": -4, "confirmMerge": false }
Bewusst kein PATCH auf retirementAge: Die Änderung betrifft immer zwei Phasendauern
gleichzeitig und kann eine Phase entfallen lassen.
→ 200 { ok, removedPhaseId, retirementAge } ·
→ 400 mit Erklärtext, wenn gesperrt oder ausserhalb des Spielraums ·
→ 409 { needsMergeConfirmation: true, removedPhaseId, removedPhaseName, mergedIntoPhaseId },
wenn dabei eine Lebensphase entfiele und confirmMerge nicht gesetzt ist. Der Client fragt
vorher selbst (er kennt den Plan), der 409 ist die serverseitige Absicherung.
PATCH /api/scenarios/<scenarioId>
Akzeptiert eine Union von zwei Formen:
- Vollständiges Profil:
{ householdType, inflationRateDefault, persons[], name? }– ersetzt die Personen in einer Transaktion. - Teilaktualisierung:
{ name?, initialCash? }–initialCash0…1'000'000'000, gerundet.
→ 200 { scenario: { id, name } }
DELETE /api/scenarios/<scenarioId>
→ 200 { ok: true } · 400 wenn es das Basisszenario ist.
POST /api/scenarios/<scenarioId>/copy
{ name } – vollständige Kopie; setzt parentScenarioId sowie die Herkunfts-Verweise.
→ 201 { scenarioId: "<neue Id>" }
GET /api/scenarios/<scenarioId>/export
→ text/csv; charset=utf-8, Content-Disposition: attachment. Liefert immer den aktuellen
Stand, auch wenn im Client eine ältere Version betrachtet wird (3.8.4).
GET /api/scenarios/<scenarioId>/versions
Änderungshistorie, neueste zuerst – ohne die Snapshots (je zig Kilobyte).
{ "currentMajor": 2,
"versions": [ { "id", "major", "minor", "comment", "isMajor",
"createdAt", "updatedAt", "author" } ] }
POST /api/scenarios/<scenarioId>/versions
{ comment } (Pflicht, ≥ 3 Zeichen) – legt den aktuellen Stand als Hauptversion fest und
setzt Scenario.currentMajor. → 201 { version: { major, minor } } · 400 ohne Kommentar.
GET /api/scenarios/<scenarioId>/versions/<versionId>
Ein einzelner Stand samt Berechnung und der Vorwarnung für das Wiederherstellen:
{ "version": { "id", "major", "minor", "comment", "isMajor", "createdAt" },
"plan": <PlanInput>, "computed": <PlanComputed>,
"impact": { "lostElementIds": [], "lostPhaseIds": [], "affectedChildren": [] } }
impact benennt die Kind-Szenarien, die durch ein Wiederherstellen ihre Diff-Basis verlören.
POST /api/scenarios/<scenarioId>/versions/<versionId>
Setzt das Szenario auf diesen Stand zurück – IDs bleiben erhalten, und der wiederhergestellte
Stand wird selbst als neue Version festgehalten («Wiederhergestellt aus A.B»).
→ 200 { version: { major, minor } }
6.4 Phasen
POST /api/scenarios/<scenarioId>/phases
Body optional: { name?, durationYears? }.
Hängt eine Phase am Ende an, kappt die Dauer, vergibt Default-Name, legt vorbelegte
ElementPhaseValue für alle aktiven Elemente an (alles in einer Transaktion).
→ 201 { phase: { id } }
PUT /api/phases/<phaseId>
{ name?, durationYears? } – durationYears 1–80, wird gekappt.
→ 200 { phase: { id } }
DELETE /api/phases/<phaseId>
Nur die letzte Phase. → 200 { ok } · 400 „Nur die letzte Phase kann gelöscht werden."
PUT /api/phases/<phaseId>/cash-transition
Body = CashTransitionData (siehe 5.4.5). Speichert den Cash-Entscheid für den Übergang nach
dieser Phase (einmalige Sonderein-/ausgaben). Schreibt die Spalte Phase.cashTransition.
→ 200 { ok } · 404 wenn die Phase nicht dem Benutzer gehört.
6.5 Elemente
POST /api/scenarios/<scenarioId>/elements
{ category, name, ownerRole? } → 201 { element: { id } }
PERSON_ONLY_CATEGORIESohne Person → 400- fehlendes
ownerRolesonst →HOUSEHOLD orderIndex= Max + 1
PATCH /api/elements/<elementId>
{ name } (1–120). → 200 { ok }
DELETE /api/elements/<elementId>
→ 200 { ok }, Cascade auf alle Phasen-/Übergangswerte.
PUT /api/elements/<elementId>/phase/<phaseId>
Body = PhaseData. Upsert auf @@unique([elementId, phaseId]). Prüft zusätzlich, dass die
Phase zum selben Plan gehört wie das Element. → 200 { ok }
PUT /api/elements/<elementId>/transition/<fromPhaseId>
Body = TransitionData. Upsert auf @@unique([elementId, fromPhaseId]). → 200 { ok }
7. Betrieb und Deployment
7.1 Container
Dockerfile – vierstufiger Multi-Stage-Build auf node:20-alpine:
base– Arbeitsverzeichnis/appdeps–npm ci(mitprisma/für denpostinstall-Hookprisma generate)builder–npx prisma generate+npm run buildrunner– nur Laufzeit-Artefakte;ENTRYPOINT ./docker-entrypoint.sh,CMD npm start, Port 3000
docker-entrypoint.sh führt vor dem Start npx prisma migrate deploy aus – Migrationen
laufen also automatisch bei jedem Container-Start.
7.2 docker-compose
Zwei Services:
app– hängt an zwei Netzwerken:agent-net(extern, Traefik) undinternal(DB).db–postgres:16-alpine, nur iminternal-Netz, persistiert auf Volumefpt_db_data. Die Datenbank ist von aussen nicht erreichbar.
Traefik-Labels:
traefik.enable=true
traefik.docker.network=agent-net ← kritisch, siehe unten
traefik.http.routers.fpt.rule=Host(`fpt.aicds.ch`)
traefik.http.routers.fpt.entrypoints=websecure
traefik.http.routers.fpt.tls.certresolver=myresolver
traefik.http.services.fpt.loadbalancer.server.port=3000
Zwei projektspezifische Fallstricke sind im Code dokumentiert bzw. durch die Konventionen gesetzt:
traefik.docker.network=agent-netist zwingend. Der Container hängt an zwei Netzwerken; ohne diese Angabe wählt Traefik zufällig eines – landet es im internen DB-Netzwerk, ist das Backend unerreichbar (Timeout). (Kommentar indocker-compose.yml.)- Der Router-Name muss projektspezifisch sein (hier
fpt, nichtapp), sonst kollidiert er mit anderen Projekten auf derselben VM.
7.3 CI/CD
.gitea/workflows/deploy.yaml: Push auf main = automatisches Live-Deployment
(bewusst kein Review-Gate). Ablauf:
- Checkout
.envaus den Gitea-SecretsSESSION_SECRETundPOSTGRES_PASSWORDschreiben- Code nach
/opt/aicds/apps/FPT/kopieren docker compose down→docker compose up -d --build→docker image prune -f
Zielumgebung: Hetzner CX23, Traefik als Reverse Proxy, Domain fpt.aicds.ch, Gitea unter
git.aicds.ch.
8. Qualitätssicherung
8.1 Teststrategie
Getestet wird ausschliesslich der Berechnungskern – bewusst, da dort die Fachlogik und das
Regressionsrisiko liegen. Ausgeführt mit Vitest in der Node-Umgebung (vitest.config.ts,
Include src/**/*.test.ts). Es gibt keine Komponenten-, API- oder E2E-Tests.
| Datei | Tests | Schwerpunkt |
|---|---|---|
calculations.test.ts |
53 | AHV-Rentenformel, Immobilie, Teilverkauf, Sonderamortisation, AHV einkommensabhängig, AHV-Referenzalter (Beitrag vor 65, Rente ab 65), Einkommen endet mit der Pensionierung, Vererbung der Wiederkehr-Parameter, „V5 Golden Tests" |
sensitivity.test.ts |
20 | Treiber-Transformationen (Reinheit, Einheiten, Kappung), Verfügbarkeit, Tornado-Sortierung und -Richtung, Erklärung wirkungsloser Treiber, Pensionsalter als Treiber |
explain.test.ts |
12 | Verlaufswerte je Element, Vollständigkeit beider Wasserfall-Zerlegungen, Rechenweg-Protokoll, Gültigkeit der Spezifikations-Verweise |
montecarlo.test.ts |
18 | Determinismus, Volatilität/Vol-Drag, Böden, Reproduzierbarkeit; Element-Gruppierung, Szenario-Vergleich, Inflation je Szenario, plannedReturnOf; Schwellen-Ablesung (probabilityAtLeast), Monotonie über Schwellen, Fall 2 systematisch unter 50 % |
distribution.test.ts |
8 | Kapitaltopf und Quoten-Zerlegung gegen die Cash-Brücke; Entwurfswerte anwenden ohne Verlust bestehender Felder |
actuals.test.ts |
27 | Zuordnung über Kopie-Ketten, Sprung im Ist-Jahr, Weiterrechnen ab dem Ist-Wert, geschlossene Brücken, Fluss-Rückrechnung, Wirkung über die Phasengrenze hinaus |
dataview.test.ts |
7 | beide Sichten in einem Zug, Rückfall ohne Ist-Werte, Abweichungsmessung |
ratefields.test.ts |
17 | Erkennung geänderter Raten, Reichweite (diese/folgende/alle), Erhalt der übrigen Werte in den Zielphasen |
versioning.test.ts |
23 | Nummerierung und Zusammenfassung je Sitzung, unveränderte Stände, Benutzertrennung, Bauplan des Wiederherstellens, Auswirkung auf Kind-Szenarien |
versioning-coverage.test.ts |
3 | statischer Wächter: jeder schreibende Endpunkt löst eine Version aus |
report.test.ts |
9 | Zusammenfassung, Basis-Angabe zu JEDER Kennzahl, Szenario-Deckelung, Plan/Ist-Block, Haftungsausschluss, gültige PDF-Datei |
livesim.test.ts |
15 | Element-Regler bewegt genau ein Element; Aufschlüsselung deckt sich mit dem Sammelregler; neutrale Stellung verändert den Plan nicht; Ruinmeldung |
phaseplan.test.ts |
14 | Ableitung der Lebensabschnitte aus den Pensionierungszeitpunkten (Einzel/Paar/bereits pensioniert); letzter Teil immer offen |
bridges.test.ts |
21 | Vermögens- und Cash-Brücke gehen über acht Plankonstellationen ohne Restgrösse auf; Umbuchungen bleiben aus der Vermögensbrücke heraus; Kapitalverwendung nach Quote (Punkt C) inkl. Herkunft je Quelle und acceptsCapital |
retirement.test.ts |
16 | Spielraum und Sperren je Person, Verschiebung ohne Änderung der Gesamtdauer, Wegfall einer Phase, Zusammenführung der Übergangs-Entscheide |
server-boundary.test.ts |
1 | statischer Wächter: kein Modul unter src/lib importiert aus src/components |
diff.test.ts |
9 | Abweichungs-Erkennung gegen das Eltern-Szenario |
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 | 337 |
8.2 Testfälle
| Test | Prüft |
|---|---|
| Amtliche Tabelle 318.117.1 | ahvMonthlyFullPension reproduziert 11 Stützstellen der amtlichen Rentenskala exakt (Mindestrente, Formel 1, Wendepunkt 45'360 → 1'915, Formel 2, Maximalrente) |
| Kappung / Stetigkeit | unter 15'120 → 1'260, über 90'720 → 2'520; kein Sprung am Wendepunkt |
| AHV volle Karriere | mdJE 100'000 → Maximalrente 32'760 |
| AHV abgestuft / Mindestrente | mdJE 60'000 → Formel 2; mdJE 10'000 → 1'260 × 13 |
| AHV Vorgeschichte dominiert | 39 Jahre à 40'000 + 5 Jahre à 200'000 → mdJE 58'182 |
| AHV Ausfalljahre | 4 Ausfalljahre → 32'760 × 40/44; Ausfalljahre im Plan senken nur die Skala, nicht das mdJE |
| AHV ohne Prüfung | ohne erfassten Wert gilt der geplante Durchschnitt (nicht 0) |
| AHV bereits pensioniert | Karriere aus der Phasenzelle der ersten Phase |
| AHV Plafonierung | zwei Maximalrenten im Paar-Plan → gekappt auf 32'760 × 1.5 |
| AHV Netto → Brutto | mdJE = 70'000 × 1.12, nicht 70'000 |
| Immobilie: Hebel | 1 Mio / 900k Hypothek / 1 % / 10 J. → Endwert 304'622; deutlich mehr als eine Wertsteigerung auf das Eigenkapital ergäbe |
| Immobilie: ohne Wertsteigerung | Verhalten unverändert (1 Mio − 800k = 200'000) |
| Immobilie: Zins-Schalter | INCLUDED → kein Cash-Abzug; ADD → 10 × 1 % von 900'000 = 90'000 |
| Immobilie: Zins sinkt | Jahr 1: 10'000, Jahr 10: 9'100; schlägt auf die Quote durch |
| Immobilie: Verkauf | Grundstückgewinnsteuer auf Verkaufspreis − Kaufpreis, nicht auf den Verkehrswert |
| Teilverkauf (42) | Betrag ins Cash (→ capitalInflow), Rest bleibt aktiv; am Endwert gekappt; Halten/Vollverkauf unverändert |
| Sonderamortisation (15) | Einmaltilgung senkt Restschuld, belastet Cash (→ capitalInvest), am Restsaldo gekappt |
| MC: Determinismus | Streuung 0 reproduziert exakt das deterministische Ergebnis (Bänder kollabieren) |
| MC: Volatilität / Vol-Drag | σ > 0 spreizt p10<median<p90; Median unter dem deterministischen Wert |
| MC: Erfolg / Reproduzierbarkeit | P(≥ Ziel) fällt mit steigendem Ziel; gleicher Seed → identisches Ergebnis |
| MC: Boden / Ruin | 0%-Boden hält PK/3a ≥ Startwert; sicherer Verzehr → Ruinwahrscheinlichkeit 100 % |
| MC: Herkunfts-Kette | resolveRootElementId folgt der Kette bis zum Ursprung; Verweis ins Leere → eigenes Element ist Wurzel; defekte Kette terminiert |
| MC: Element-Gruppen | Kopie und Original ergeben eine Gruppe (nicht zwei); Auflösung auch über ein nicht ausgewähltes Zwischen-Szenario; ein nur in einem Szenario neues Element bildet eine eigene Gruppe |
| MC: Parameter-Übersetzung | paramsForScenario bildet die Gruppen-Parameter auf die Element-Ids des jeweiligen Szenarios ab |
| MC: Szenario-Vergleich | Zwei Szenarien, die sich nur in der geplanten Rendite unterscheiden: identischer Median/P10/Ruin (gleicher Seed, gleiche Struktur), aber höhere Erfolgswahrscheinlichkeit beim pessimistisch geplanten – der einzige Unterschied ist der Zielbetrag |
| Tornado: Reinheit | applyDriver lässt den Ausgangsplan unverändert |
| Tornado: Einheiten | Inflation absolut gesetzt; Rendite/Lohnentwicklung in pp verschoben; Ausgaben relativ skaliert (Einkommen unberührt) |
| Tornado: Lebensdauer | verschiebt nur die letzte Phase; Kappung bei mindestens 1 Jahr |
| Tornado: Verfügbarkeit | Treiber ohne passende Elemente werden ausgeblendet (z. B. Immobilien-Wertsteigerung ohne Immobilie) |
| Tornado: Sortierung/Richtung | absteigend nach Spannweite; Treiber ohne Bandbreite hat Spannweite 0 und steht zuunterst; höhere Ausgaben → tieferes, höhere Rendite → höheres Endvermögen |
| Tornado: wirkungslose Treiber | jeder Nullbalken trägt eine Begründung; verkaufte Immobilie → Wertsteigerung wirkungslos mit konkretem Hinweis, gehaltene Immobilie → Spannweite > 0 ohne Hinweis |
| Brücken: Restgrösse | residual === 0 je Phase für Vermögens- und Cash-Brücke über sieben Plankonstellationen (Ansparen, Verrentung, Kapitalbezug, Immobilie gehalten/verkauft, Einmalposten, Sofort-Tilgung mit Sonderamortisation) |
| Brücken: Kontrollpunkte | startWealth/endWealth/cashStart/cashEnd der Brücken stimmen mit den offiziellen Phasen-Kennzahlen überein |
| Brücken: Umbuchungen | Sparraten und Amortisationen erscheinen nur in der Cash-Brücke; die Vermögensänderung erklärt sich exakt aus Quote + Erträgen + Wertsteigerung + PK-Beiträgen |
| Brücken: Verrentung/Verkauf | verrentetes PK-Kapital erscheint als Vermögensabgang am Übergang; Verkaufsdifferenz und Grundstückgewinnsteuer nur beim Verkauf, nicht beim Halten |
| Verlauf: ein Punkt je Jahr | je aktivem Element genau durationYears Punkte pro Phase |
| Verlauf: Konvexität | 200'000 @ 5 % + 10'000 Sparbeitrag: Jahr 1 = 220'000, und die Jahreszuwächse wachsen – eine Gerade zwischen den Phasengrenzen hätte konstante Zuwächse |
| Verlauf: Immobilie | Verkehrswert, Restschuld und Eigenkapital werden getrennt geführt; Eigenkapital = Verkehrswert − Restschuld |
| Brücke: Vermögen geht auf | Restposten ≤ 5 CHF über einen Plan mit allen Element-Arten und Übergangs-Entscheiden |
| Brücke: Cash geht auf | dito; Start- und Endwert stimmen mit cashStart/cashEnd überein |
| Brücke: Umbuchung | 5 × 12'000 Sparbeitrag: Cash −60'000, Vermögen unverändert – die Sparrate erscheint nur im Cash-Wasserfall |
| Brücke: PK-Beiträge | 4 × 10'000 PK-Beitrag: Vermögen +40'000, Cash unbelastet |
| Brücke: Übergangsposten | Verrentung, Kapitalbezugssteuer und Einmalposten erscheinen in der Brücke der Folgephase |
| explain: Standardmässig aus | ohne Option keine Traces; mit Option identische Ergebniswerte (Endvermögen, Cash, Ruinalter) |
| explain: Abdeckung | Traces auf Element-, Übergangs-, Phasen- und Plan-Ebene vorhanden |
| explain: eingesetzte Zahlen | jeder Schritt trägt die Formel und die substituierte Fassung; das Resultat stimmt mit dem Ergebniswert überein |
| explain: Spec-Verweise | jeder specAnchor trifft eine existierende Überschrift in SPEZIFIKATION.md (gegen die echte Datei geprüft) |
| Vermögen je Jahr | 100k @ 10 % über 3 J. → 110k/121k/133.1k je Jahrespunkt; Endjahr = Phasen-Endvermögen |
| Vermögen real | 100k bei 10 % Inflation → real 90'909 |
| Test 1 – Ansparen | Einkommen +2 % nominal, Ausgaben real flach: Endvermögen 761'654 nominal / 565'928 real (±1 %), kein negatives Cash |
| Test 2 – Verzehr/Ruin | Rente nominal fix 60k, Ausgaben real 100k, Vermögen 900k @3 %: ruinAge === 94 |
| Test 3 – Cash-Ausgleich | Sparrate 6'364: cashEnd === 5472, nie negativ |
| Test 4 – Liquiditätslücke | Sparrate 10'000: cashEnd === -5436, cashNegative === true |
| Ausgaben real → nominal | Jahr 1 nominal = real; Jahr 5 = 100'000 × 1.02^4 |
| Einkommen flach | Lohnerhöhung 0 % → incomeStart === incomeEnd === 80'000 |
| Bezugsrate | 3 × 10'000 Entnahme → cashEnd === 30'000, Asset-Endwert 0, plannedWithdrawRate === 10'000 |
| Kapitalzufluss/-investition | Verkauf in P1, Reinvestition in P2: capitalInflow === 10'000, capitalInvest === 10'000, cashStart === 0, startWealthNominal === 10'000 (kein Doppelzählen) |
| Cash-Anfangswert | initialCash 50'000 fliesst in Phase 1 ein |
| Tilgung stoppt | Schuld 25'000, Tilgung 10'000/J., 5 Jahre: Gesamtabfluss 25'000 (nicht 50'000), cashEnd === 75'000, Restschuld 0 |
| Amortisation stoppt | Hypothek 15'000, Amortisation 10'000/J., 4 Jahre: cashEnd === 85'000 (nicht 60'000), Immobilie schuldenfrei |
| Vorbezugssteuer | PK-Vorbezug 100'000 brutto @ 8 %: capitalInflow === 92'000, Restkapital 200'000 (brutto entnommen) |
| Einmaliger Zufluss | 100'000 nominal @ 10 % Steuer → oneOffInflow === 90'000, Cash-Start Folgephase +90'000; Phase 1 hat keinen Zufluss |
| Einmalige Kosten | 20'000 real, 2 % Inflation, Grenze nach 10 J. → 20'000 × 1.02^10, entsprechend vom Cash abgezogen |
| Zufluss + Kosten / NONE | BOTH: +50'000 −20'000 → Cash-Start 30'000. NONE mit erfassten Beträgen → keine Wirkung |
| Liquiditätslücke durch Kosten | Kosten 25'000 bei Cash 10'000 → cashStart === −15'000, cashNegative, incomplete |
| Letzte Phase | Cash-Entscheid der letzten Phase bleibt wirkungslos (kein Übergang mehr) |
| Fortschreibung | Einkommens-Basiswert P1 → Startwert P2 = 100'000 × 1.02^5; cashStart(P2) === cashEnd(P1) |
8.3 Ausführung
npm test # vitest run
npm run lint # eslint
npm run build # next build
Verifikationseinschränkung: Lokal steht keine Datenbank/Docker zur Verfügung. Die
Verifikation erfolgt über npm run build und die isolierten Berechnungstests; End-to-End-Prüfung
erst gegen das Deployment (fpt.aicds.ch).
9. Bekannte Einschränkungen und Modellentscheide
Dieser Abschnitt hält fest, was im Code steht und beim Weiterentwickeln bekannt sein muss.
9.1 Cash wird nicht automatisch ausgeglichen
Wird das Cash negativ, meldet das Tool eine Liquiditätslücke (rotes Icon, incomplete = true),
greift aber nicht ein – es wird kein Vermögen automatisch verkauft und kein Kredit aufgenommen.
Negatives Cash geht mit negativem Vorzeichen ins Gesamtvermögen ein.
9.2 Verwaiste PERSON_B-Elemente
Die Haushaltsform ist eine Szenario-Eigenschaft und lässt sich im Dialog „Plan-Einstellungen"
auch bei einem bestehenden Szenario nachträglich ändern (nicht pro Phase – innerhalb eines Szenarios
gilt sie durchgehend). Wechselt ein Szenario dabei von COUPLE auf SINGLE, schneidet
PlanProfileFields die Personen auf eine zusammen und PATCH /api/scenarios/<id> löscht Person B
aus der Datenbank. Elemente mit ownerRole = "PERSON_B" bleiben bestehen. In der Berechnung
liefert personByRole dann null:
AHV: wird perif (!owner || …) continue;übersprungen – keine Rente.PENSION_FUND/PILLAR_3A:owneristnull, deshalb fälltownerWorkingaufanyWorkingzurück, d. h. das Element folgt dem Erwerbsstatus des Haushalts statt der (nicht mehr existierenden) Person.
Es gibt keine Bereinigung und keine Warnung im UI. Da die Haushaltsform normalerweise nicht nachträglich geändert wird, ist der Fall selten – er ist aber erreichbar.
9.3 Immobilien: was noch fehlt
Seit Version 0.5 kennt das Modul Hypothekarzins und Wertsteigerung. Nicht modelliert sind:
- Nebenkosten und Unterhalt (Faustregel ~1 % des Werts). Lassen sich heute als normales Ausgaben-Element erfassen.
- Eigenmietwert – ohne echte Steuerlogik (Roadmap Nr. 23) nur halb wirksam.
- Mieteinnahmen (Renditeliegenschaften) – anderer Anwendungsfall.
- Wertvermehrende Investitionen und die Haltedauer-Abstufung der Grundstückgewinnsteuer (kantonal, teils stark degressiv).
- Zinsänderungsrisiko: Der Zinssatz gilt für die ganze Phase. Ein Zinsschock lässt sich nur abbilden, indem man an dieser Stelle eine Phasengrenze zieht und den Satz neu setzt.
9.4 Kein CSRF-Token
Zustandsändernde Requests sind allein durch sameSite=lax geschützt. Das deckt klassische
Cross-Site-Formular-POSTs ab, ist aber schwächer als ein expliziter Token.
9.5 Logout invalidiert das Token nicht serverseitig
Das JWT ist zustandslos und bis zu 30 Tage gültig. logout löscht nur das Cookie.
9.6 Spar- und Bezugsraten werden nicht indexiert
annualContribution, annualWithdrawal, amortization und annualRepayment sind flache
Nominalbeträge, die über die Phasenjahre konstant bleiben. Eine Sparrate von 10'000 bleibt
20 Jahre lang 10'000 nominal und verliert dabei real an Gewicht. Wer eine mitwachsende Rate
abbilden will, muss die Phase teilen und den Betrag in der Folgephase erhöhen.
9.7 Nur ein Zufluss und eine Kostenposition pro Übergang
CashTransitionData hält genau ein Zufluss- und ein Kostenpaar (Bezeichnung + Betrag).
„Erbschaft + Autoverkauf + Poolbau + Küche" am selben Übergang lässt sich nur durch
Zusammenfassen abbilden („Diverses, 45'000") – die Aufschlüsselung geht dabei verloren.
Bewusster Entscheid zugunsten eines einfachen UI; erweiterbar auf Listen.
9.8 Einmalige Ereignisse nur an Phasengrenzen
Ein Ereignis kann nur an einem Phasenübergang liegen. Ein Poolbau in Jahr 3 einer 10-jährigen Phase ist nur abbildbar, wenn dort eine Phasengrenze gezogen wird. Nach der letzten Phase gibt es keinen Übergang – ein dort erfasster Betrag bleibt wirkungslos (durch Test abgedeckt).
9.9 Gemeinsames Einkommen zählt bei Paaren nicht für die AHV
Das mdJE ist eine personenbezogene Grösse. Einkommen wird deshalb nur einer Person
zugerechnet, wenn das INCOME-Element ihr zugeordnet ist (PERSON_A/PERSON_B). Bei einem
Einzelplan zählt HOUSEHOLD zur Person A – es gibt ja nur eine. Bei einem Paar-Plan
bleibt HOUSEHOLD-Einkommen für die AHV unberücksichtigt.
Wer in einem Paar-Plan den Lohn als „Gemeinsam" erfasst, sieht deshalb im Prüf-Dialog ein geplantes Durchschnittseinkommen von 0 und bekäme die Mindestrente. Der Dialog zeigt den Wert prominent an, sodass der Fehler auffällt – aber es gibt keine aktive Warnung. Erwerbseinkommen sollte in Paar-Plänen immer personenscharf erfasst werden.
Ebenfalls nicht modelliert: das Einkommenssplitting verheirateter Paare (während der Ehe werden die Einkommen hälftig geteilt) sowie Erziehungs- und Betreuungsgutschriften. Beides würde das mdJE real beeinflussen und wäre der nächste Ausbauschritt.
9.10 Verhaltensänderung: AHV-Rente bestehender Pläne
Bis Version 0.3 erhielt jede AHV-Position pauschal die Maximalrente (32'760), gekürzt nur um Ausfalljahre – unabhängig vom Einkommen. Seit 0.4 folgt sie der Rentenformel. Bestehende Pläne zeigen dadurch eine andere, in der Regel tiefere AHV-Rente, sobald das geplante Durchschnittseinkommen unter 90'720 liegt. Das ist keine Regression, sondern die Korrektur einer zu optimistischen Pauschale.
Zwei Fälle brauchen Aufmerksamkeit:
- Bereits pensionierte Personen (bei Planbeginn): Ohne erfasstes Durchschnittseinkommen ergibt das mdJE 0 → Mindestrente. Vorher war es die Maximalrente. Die Felder stehen in der AHV-Phasenzelle der ersten Phase; solange sie leer sind, ist die Rente bewusst konservativ.
- Paar-Pläne mit
HOUSEHOLD-Einkommen: siehe 9.9.
9.11 AHV-Rente wird nach der Pensionierung nicht indexiert
Die Rente wird zum Pensionierungszeitpunkt in heutigem Geld berechnet und danach nominal eingefroren. Die echte AHV wird alle zwei Jahre an den Mischindex angepasst. Über 25 Rentenjahre verliert die modellierte Rente damit real spürbar an Wert – das Modell ist an dieser Stelle deutlich konservativ. Bewusster Alt-Entscheid, unabhängig von der Rentenformel; der grösste verbliebene Hebel im AHV-Modell.
9.12 PILLAR_3A_MAX_ANNUAL wird nur im UI erzwungen
Das Feld ist per max-Prop hart geklammert. Das Zod-Schema kennt für annualContribution nur
≥ 0 – ein direkter API-Aufruf kann die Obergrenze überschreiten.
9.13 Verhaltensänderung: Nettolohn-Klarstellung und AHV
Bis Version 0.4 war nirgends definiert, ob INCOME netto oder brutto ist – der Hilfetext sagte
nur „die Zahl auf dem Lohnausweis", und dort stehen beide. Für den Cash-Fluss war das folgenlos,
weil sich beide Konventionen aufheben (brutto − Ausgaben inkl. Abzüge ≡ netto − Ausgaben ohne Abzüge). Mit der einkommensabhängigen AHV (0.4) hing daran aber plötzlich eine Rente.
Seit 0.5 gilt: amount ist der Nettolohn, und die AHV rechnet mit × 1.12 auf brutto hoch.
Konsequenzen:
- Wer bisher netto erfasst hat (die dokumentierte Absicht): Die AHV-Rente steigt – sie war zuvor um bis zu ~1'900/Jahr zu tief, am stärksten bei mittleren Einkommen (bei 80'000 brutto: 28'974 statt 30'902). Über 90'720 brutto verschwindet der Effekt, weil beide Werte in die Maximalrente laufen.
- Wer brutto erfasst hat: Cash-Fluss und AHV sind nun beide zu hoch. Der Einkommensbetrag gehört auf netto korrigiert und die Lohnabzüge aus dem Ausgabenbetrag entfernt.
9.14 Keine Steuerschätzung
Bewusst nicht umgesetzt: eine automatische Schätzung von Einkommens- und Vermögenssteuer.
Die Bemessungsgrundlage ist das steuerbare Einkommen, nicht der Nettolohn – also brutto minus PK, 3a, Berufsauslagen, Versicherungs- und Kinderabzüge. Darauf kommen drei Ebenen (Bund, Kanton, Gemeinde); allein der Gemeindesteuerfuss variiert innerhalb eines Kantons um rund den Faktor zwei. Dazu Zivilstand, Kinder, Konfession und für die Vermögenssteuer 26 kantonale Tarife mit eigenen Freibeträgen. Das Tool kennt weder Wohnort noch Kinder.
Eine Schätzung daraus läge im Einzelfall schnell 30–50 % daneben – bei vielen Haushalten dem grössten Ausgabenposten. Eine selbst berechnete Zahl wirkt zudem autoritativ und wird nicht hinterfragt. Der Benutzer kennt seine Steuerrechnung dagegen exakt aus der letzten Veranlagung.
Heutiger Weg: ein normales Ausgaben-Element „Steuern" – dafür braucht es kein neues Feld. Echte Steuerlogik ist Roadmap Nr. 23, mit Kanton und Gemeinde als Eingabe.
9.15 Monte-Carlo misst Risiko um die Annahmen, nicht deren Richtigkeit
Die Simulation streut um die eingegebenen Mittelwerte (historische Rendite/Inflation). Sie beurteilt nicht, ob diese Mittelwerte realistisch sind. Ein zu optimistischer Mittelwert liefert eine beruhigende, aber unrealistische Erfolgsquote – die präzise Prozentzahl täuscht dann Sicherheit vor. Der Dialog weist das explizit aus; die Mittelwerte sind Pflichtfelder ohne Default, damit sie bewusst gesetzt werden.
Weitere bewusste Vereinfachungen der Stufe A: Inflation und Renditen werden unabhängig gezogen (real sind sie negativ korreliert); die Normalverteilungs-Alternative wird gar nicht angeboten (fette Ränder fest eingebaut); die Simulationsparameter werden nicht persistiert (ephemer im Dialog). Ein historischer Backtest (Stufe B) und korrelierte/vollständigere Modelle (Stufe C) sind offen.
9.16 Diff ist live gegen die Vorlage
Die Abweichungs-Markierung vergleicht immer den aktuellen Stand des Eltern-Szenarios. Ändert man die Vorlage nachträglich, verschiebt sich die Markierung in allen Kindern rückwirkend: Setzt man in der Vorlage einen Wert auf das, was ein Szenario ohnehin hatte, verschwindet dort die gelbe Markierung, ohne dass das Szenario angefasst wurde.
Das ist logisch korrekt („weicht ab von der Vorlage"), kann aber überraschen. Die Alternative – ein eingefrorener Snapshot beim Kopieren – wäre schnell veraltet und würde Abweichungen anzeigen, die keine mehr sind. Bewusster Entscheid zugunsten des Live-Vergleichs.
Verwandt: Wird ein Element in der Vorlage gelöscht, gilt das Gegenstück im Kind ab dann als neu (der Herkunfts-Verweis zeigt ins Leere). Auch das folgt aus dem Live-Vergleich.
9.17 Kleinere Beobachtungen
planToCsv(plan, computed)erhält denplan-Parameter, verwendet ihn aber nicht.Plan.branchFromPhaseIdhat keine Fremdschlüssel-Constraint; die Phase kann gelöscht werden, ohne dass das Feld bereinigt wird.- Die Element-Umbenennung ist als API vorhanden, im UI aber nicht erreichbar.
- Die
README.mdist noch der unverändertecreate-next-app-Text.
(Der frühere Selection-Rest in PlanView und der ProfileMenu-Lint-Fehler sind seit 0.13
behoben – npm run lint läuft fehlerfrei.)
9.18 Tornado: was der Chart nicht leistet
Die Balkenlänge hängt von den eingegebenen Bandbreiten ab. Wer Renditen mit ±2 Prozentpunkten und Ausgaben mit ±20 % variiert, misst zu einem Teil die eigene Wahl dieser Bandbreiten. Deshalb sind sie Pflichteingabe ohne Default und im Ergebnis sichtbar (4.13.3). Aussagekräftig ist die Reihenfolge, nicht der absolute Betrag.
One-at-a-time sieht keine Wechselwirkungen. Schlechte Renditen und hohe Ausgaben treffen härter als die Summe der Einzelbalken – weil in der Folge Kapital verzehrt wird, das später zur Verzinsung fehlt. Für Kombinationen ist die Monte-Carlo-Simulation zuständig.
Das Pensionsalter ist seit 0.26 enthalten – zuvor bewusst nicht, und die Begründung von damals erklärt, warum die heutige Umsetzung so aussieht, wie sie aussieht.
Ein isoliert verändertes retirementAge wäre nicht ungenau gewesen, sondern irreführend.
Beispiel: Person 45, Pension mit 65, Phase 1 = 20 Jahre (45→65), Phase 2 = 20 Jahre.
- Auf 62 gesetzt: Phase 1 beginnt mit 45, also
45 < 62→ die Phase bliebe vollständig Erwerbsphase. Die Person arbeitete im Modell weiterhin bis 65. Wirkung: praktisch null. - Auf 68 gesetzt: Phase 2 beginnt mit 65, also
65 < 68→ Phase 2 würde zur Erwerbsphase. Zugleich wäreownerRetiresNextan der Grenze nach Phase 1 falsch, womit der Pensions-Übergang komplett entfiele: keine PK-Verrentung, kein 3a-Bezug. Der Balken wäre riesig – er misst aber den Ausfall der Vorsorgelogik, nicht „drei Jahre länger arbeiten".
Der Treiber verschiebt deshalb retirementAge und die Phasengrenze gemeinsam
(4.16). Zwei Eigenheiten bleiben und sind im Dialog benannt:
Der Spielraum endet bei den angrenzenden Phasendauern (eine Zusammenlegung findet im Tornado
bewusst nicht statt), und eine zu weite Bandbreite wird auf das Mögliche gekürzt statt verworfen.
Der verwandte Treiber Lebensdauer (Dauer der letzten Phase) bleibt daneben bestehen – er
beantwortet die andere Frage: nicht „wann höre ich auf", sondern „wie lange muss es reichen".
9.19 Simulationsparameter werden nicht gespeichert
Weder die Monte-Carlo-Annahmen noch die Tornado-Bandbreiten werden persistiert; beide leben nur im geöffneten Dialog. Das ist ein bewusster Entscheid (kein Datenmodell für Annahmen, keine Migration), hat aber den Preis, dass eine Analyse bei jedem Öffnen neu parametrisiert werden muss. Bei der Monte-Carlo-Simulation über mehrere Szenarien fällt das stärker ins Gewicht als zuvor, weil dort mehr Eingaben zusammenkommen.
9.20 Transparenz legt auch die Vereinfachungen offen
Die vollständige Offenlegung der Rechenwege macht sichtbar, was das Modell nicht kann: die nach der Pensionierung nicht indexierte AHV-Rente (9.11), den pauschalen Netto-Brutto-Faktor 1.12 (4.4.5), die nicht indexierten Spar- und Bezugsraten (9.6) und die fehlende Steuerberechnung (9.14).
Das ist der eigentliche Wert der Offenlegung und kein Nebeneffekt. Entscheidend ist aber, dass die
Erklärtexte die Grenze mitnennen, statt sie zu übergehen – dafür ist das Feld note je
Rechenschritt da. Ein fachkundiger Nutzer, der eine Vereinfachung selbst entdeckt, nachdem ihm
volle Transparenz zugesagt wurde, zieht den härteren Schluss.
9.21 Wasserfall: die Zuordnung ist eine Interpretation
Welcher Posten in welchen Wasserfall gehört, ist eine fachliche Entscheidung und nicht aus den Zahlen ableitbar. Die hier getroffene – Umbuchungen nur im Cash-Wasserfall, echte Zu- und Abgänge nur im Vermögens-Wasserfall (4.14.2) – ist begründet, aber nicht die einzig denkbare. Wer die Sparrate als „gebundenes Geld" verstanden wissen will, würde sie anders einordnen.
Konkret uneindeutig sind zwei Fälle:
- Amortisation senkt Cash und Hypothek. Im Vermögens-Wasserfall taucht sie nicht auf, obwohl sie das Eigenkapital erhöht – die Erhöhung ist bereits im unveränderten Vermögenssaldo enthalten.
- Verrentung erscheint als Vermögensabgang, obwohl der Gegenwert als Rentenstrom weiterlebt. Der Rentenstrom ist im Modell aber kein Bilanzposten, sondern Einkommen; er taucht in den Folgejahren über die Quote wieder auf.
Der Restposten (residual) ist die Kontrollgrösse dafür, dass die gewählte Zerlegung wenigstens
vollständig ist – nicht dafür, dass sie die einzig sinnvolle ist.
9.22 Verkaufspreis und Verkehrswert bleiben unabhängig
Seit Version 0.12 wird der Verkaufspreis mit dem modellierten Verkehrswert vorbelegt und die Abweichung ausgewiesen (3.5.8). Die beiden Grössen bleiben aber entkoppelt – das Tool erzwingt keine Konsistenz.
Das ist bewusst so: Ein Verkauf unter dem Verkehrswert ist ein realer Fall (Notverkauf, Verkauf an Nachkommen, Liebhaberobjekt ohne Markt). Eine Zwangskopplung würde diese Fälle unmöglich machen. Der Preis dafür ist, dass eine unplausible Kombination weiterhin eingebbar bleibt – neu aber nicht mehr unbemerkt.
Eine Folge bleibt bestehen und ist nicht offensichtlich: Wird die Immobilie vor Planende
verkauft, hat die angenommene Wertsteigerung keinen Einfluss mehr auf das Endvermögen. Der
Erlös folgt allein dem erfassten Verkaufspreis. Im Tornado führt das zu einem Nullbalken, der
seit 0.12 erklärt wird (4.13.5); in der
Vermögensbrücke erscheint stattdessen die Differenz Verkaufspreis − Verkehrswert als eigener
Posten.
9.23 Sammel-Dialoge: Teilzustand bei Abbruch
Die Bestandsaufnahme schreibt am Ende eine Sequenz bestehender API-Aufrufe (Element anlegen → Stammdaten → Phasenwerte, je Element). Bricht die Sequenz mittendrin ab (Netzfehler), existiert ein Teil-Plan. Der ist normal weiterbearbeitbar und der Fehlerhinweis sagt das auch – aber es gibt kein automatisches Rollback. Das wäre nur mit Backend-Unterstützung (Transaktion über mehrere Requests oder Batch-Endpunkt) sauber lösbar und ist bewusst nicht gebaut: Der seltene Fehlerfall rechtfertigt keinen neuen Endpunkt.
9.24 Tour: Spotlight ohne Engine
Die Tour hebt ihr Ziel als Spotlight hervor: Der Rest der Ansicht wird abgedunkelt, das Ziel
bleibt hell und trägt einen kräftig pulsierenden Rahmen. Der abgedunkelte Bereich entsteht aus
vier fixed-Flächen (oben/unten/links/rechts der Bounding-Box des Ziels), die ein «Loch» am
Ziel frei lassen. Das ist bewusst ohne Positionierungs-Engine umgesetzt und funktioniert –
anders als der zunächst probierte box-shadow-Trick (0.29) – unabhängig von z-index und
overflow: Der Schatten liess sonst sticky Matrix-Köpfe hell durchscheinen und wurde im
Matrix-Scrollbereich abgeschnitten. Die vier Flächen folgen dem Ziel per
requestAnimationFrame (sanftes Scrollen, Resize). Die Karte springt auf die dem Ziel
gegenüberliegende Bildschirmhälfte (oben/unten), damit sie es nie verdeckt, und lässt sich
jederzeit überspringen. Ziele, die es im aktuellen Plan nicht gibt oder die unsichtbar sind
(z. B. das linke Menü auf schmalen Screens), werden übersprungen.
9.25 Die Quote ist kein fester Betrag
Die Spar-/Verzehrquote verändert sich über die Phasenjahre: Das Einkommen wächst mit der Lohnerhöhung, die real erfassten Ausgaben mit der Inflation. Bei 0 % Lohnerhöhung und 1.5 % Inflation sinkt eine Quote von 20'000 über fünf Jahre auf rund 15'100 – ohne dass der Nutzer etwas geändert hätte.
annualContribution und die übrigen Raten sind dagegen flache Jahresbeträge
(9.6). Wer die Quote des ersten Jahres
als Rate verteilt, erzeugt sich damit in den späteren Jahren eine Liquiditätslücke.
Der Verteil-Dialog begegnet dem auf drei Arten, statt es zu verstecken:
- Er zeigt Quote erstes Jahr, letztes Jahr und absolut über die Phase.
- Er warnt ausdrücklich, wenn die Quote über die Phase sinkt.
- Die Live-Vorschau rechnet den ganzen Plan neu und meldet eine entstehende Liquiditätslücke sofort, statt sie erst nach dem Speichern sichtbar zu machen.
Bewusst nicht umgesetzt ist eine automatische Deckelung: Es gibt legitime Gründe, mehr zu sparen als die laufende Quote hergibt (etwa wenn ein Cash-Polster aus der Vorphase abgebaut werden soll). Das Werkzeug informiert, es bevormundet nicht.
9.26 Fall 2 ist der Nullpunkt – nicht 50 %
Naiv erwartet man, dass eine Planung ihren eigenen Endbetrag in 50 % der Fälle erreicht, sobald man um genau die geplanten Werte würfelt. Das ist falsch, und der Fehler ist gross genug, um ein Urteil zu verderben.
Der Grund ist der Volatilitäts-Drag (4.12.5): Der deterministische Pfad kapitalisiert das arithmetische Mittel, der Median einer Simulation folgt aber näher dem geometrischen. Gemessen an einem Testplan (800k Startkapital, 30 Jahre, geplante Rendite 5.2 %, deterministisches Endvermögen 3'457'554):
| Streuung σ | Median-Endvermögen | P(Plan-Endbetrag erreicht) |
|---|---|---|
| 0 % | 3'457'554 (= deterministisch) | – |
| 3 % | 3'432'399 | ~48 % |
| 6 % | 3'322'529 | ~45 % |
| 15 % | 2'612'222 | 37 % |
| 25 % | 1'451'659 | 27 % |
Eine Planung mit hoher Aktienquote landet also bei 27–37 %, obwohl an ihr nichts falsch ist. Ein fixer 50-%-Schnitt würde sie als «zu optimistisch» brandmarken.
Deshalb ist der Nullpunkt für das Urteil Fall 2 – dieselbe Schwelle, dieselbe Streuung, nur der Mittelpunkt aus den eigenen Planannahmen statt aus der Historie (4.12.7). Der Vol-Drag steckt dann in beiden Zahlen und kürzt sich weg; übrig bleibt genau die Frage, die interessiert: Ist meine Renditeannahme optimistischer als die Vergangenheit? Das Toleranzband von ± 5 Prozentpunkten fängt Simulationsrauschen ab.
Fall 2 selbst wird nur klein als Referenz gezeigt: als Zahl ist er wenig aussagekräftig (er misst im Wesentlichen die eigene Streuung), als Vergleichsanker dagegen unverzichtbar. Der Hinweis, dass die Simulation Risiko um deine Annahmen misst und nicht deren Richtigkeit (9.15), bleibt bestehen.
9.27 Warum die Regler Standardbereiche haben – und der Tornado nicht
Zwei Kapitel dieser Spezifikation scheinen sich zu widersprechen: 9.18 begründet, warum die Sensitivitätsanalyse bewusst keine Default-Bandbreiten anbietet, während die Live-Simulation (4.15) für jeden Regler einen vorbelegten Bereich mitbringt. Das ist kein Versehen.
Beim Tornado bestimmt die Bandbreite das Ergebnis. Die Balkenlänge ist die Spannweite zwischen dem tiefen und dem hohen Wert – wer «Rendite ±3 pp» gegen «Ausgaben ±5 %» stellt, erzeugt eine Rangfolge, die er selbst vorgegeben hat. Ein Default wäre dort eine frei erfundene Aussage: Das Werkzeug würde behaupten, ein Treiber sei wichtiger als ein anderer, obwohl der Unterschied nur aus den voreingestellten Bereichen stammt. Deshalb ist die Bandbreite dort Pflichteingabe ohne Vorschlag.
Ein Regler vergleicht nichts. Er zeigt genau einen Zustand: «bei dieser Rendite kommt dieses Endvermögen heraus». Der Bereich bestimmt nur, wie weit sich der Schieber bewegen lässt – er verändert das angezeigte Ergebnis an keiner Stelle. Ein Standardbereich erfindet hier also keine Aussage; er macht den Regler überhaupt erst bedienbar, denn ohne Ober- und Untergrenze gibt es keinen Schieber.
Die Bereiche sind trotzdem an beiden Enden editierbar (Häkchen «Bereiche anpassen»), und neben jedem Regler steht sein Neutralpunkt – der Wert, bei dem der Plan unverändert bleibt. Bei allen Verschiebungs-Treibern ist das 0, bei der Inflation der Planwert selbst, weil sie als einzige absolut und nicht als Differenz eingegeben wird.
9.28 Was die Versionierung nicht leistet
Das Zeitfenster ist eine Konvention, keine Wahrheit. Zehn Minuten sind gesetzt, weil FPT keinen Speichern-Knopf hat und der Nutzer den Schnitt sonst nie selbst zieht (3.8.1). Wer nach acht Minuten Pause weiterarbeitet, landet in derselben Version; wer nach zwölf Minuten eine Kleinigkeit ändert, bekommt eine neue. Beides ist gelegentlich nicht das, was man gemeint hätte. Ein Speichern-Knopf wäre die exaktere Lösung, würde aber die Bedienung des ganzen Werkzeugs umkrempeln.
Eine Nebenversion hält den Stand am ENDE der Sitzung fest, nicht jeden Zwischenschritt darin. Wer innerhalb einer Sitzung etwas ändert und wieder zurücknimmt, findet den Zwischenstand nirgends. Die Historie ist eine Folge von Arbeitsständen, kein Undo.
Es gibt keinen Versionsvergleich. Zwei Stände lassen sich nur nacheinander ansehen, nicht nebeneinander stellen. Der bestehende Diff (3.2.6) vergleicht Szenarien gegen ihr Eltern-Szenario, nicht Versionen gegeneinander – technisch wäre beides verwandt, aber es ist bewusst nicht Teil dieser Stufe.
Die Historie wird nie beschnitten. Jede Version hält den vollständigen Zustand als JSON (Grössenordnung 40 KB bei sechs Phasen und zwölf Elementen). Bei intensiver Nutzung wächst das linear; eine Aufräumregel (etwa: Nebenversionen älter als ein Jahr verdichten, Haupt- versionen behalten) gibt es noch nicht. Bei den heutigen Datenmengen ist das unkritisch.
«Wer» unterscheidet heute nichts. Ein Plan gehört genau einem Benutzer, es gibt keine Freigabe und keine Rollen – die Spalte zeigt also immer denselben Namen. Sie ist Vorbereitung auf den Finanzberater, und die Logik trennt Benutzer bereits sauber (Änderungen verschiedener Benutzer werden nie in einer Version zusammengefasst).
Der Plan selbst ist nicht versioniert, nur seine Szenarien. Wird ein Szenario gelöscht, verschwindet seine Historie mit ihm (Cascade). Das ist gewollt: Eine Historie ohne das Objekt, das sie beschreibt, wäre nicht wiederherstellbar.
9.29 Acht Zahlen je Zelle – und wie wir sie vermeiden
Mit den effektiven Werten (3.9) bekommt die Matrix eine zweite Achse. Naiv kombiniert ergibt das je Zelle: nominal und real, Plan und Ist, Phasenbeginn und Phasenende – acht Zahlen. Das ist keine Tabelle mehr, das ist ein Zahlenfeld.
Zwei Entscheide halten es lesbar, und sie fallen an den zwei Orten verschieden aus.
In der Matrix: Wert und Abweichung statt zweier Rohwerte. Es gibt nur «Plan» oder «Effektiv», kein «beide». Im Ist-Modus steht der Ist-Wert und daneben klein die Differenz zum Plan, grün oder rot. Das beantwortet auch die bessere Frage: nicht «wie lauteten die zwei Zahlen», sondern «wie weit bin ich weg». Nominal/real bleibt dort bei drei Möglichkeiten – es sind Zahlen in einer Zelle, keine Linien in einem Bild.
Nur die Abweichung trägt Farbe. Würde man die Beträge selbst einfärben, entstünde ein Ampelteppich, in dem die eigentliche Aussage untergeht.
In den Grafiken: nominal/real wird zur Einfachauswahl. Der Vermögensverlauf zeichnete bisher je Serie zwei Linien (nominal durchgezogen, real gestrichelt). Mit Plan/Ist wären es vier, bei zwei Szenarien acht. Das Stilbudget geht deshalb an die wichtigere Unterscheidung: Plan gestrichelt, Ist durchgezogen – genau die «Plan-Linie vs. Ist-Linie», die die Roadmap verlangt. Wer real sehen will, schaltet um, statt eine zweite Linie dazuzubekommen.
Nicht jede Grafik verträgt beides. «Plan und Ist gleichzeitig» gibt es nur beim Vermögensverlauf. Die Vermögensaufteilung zeigt schon Beginn und Ende je Phase als gestapelte Balken – Plan und Ist daneben vervierfachte sie. Und die Grafik «Einkommen vs. Ausgaben» lebt vom Band zwischen zwei Linien; ein zweites Paar darüber macht genau diese Aussage unkenntlich. Beide zeigen deshalb nur die gewählte Grundlage.
Serienobergrenze vier. Szenario mal Version mal Datenquelle wächst schnell; darüber hinaus hilft keine Farbpalette mehr.
9.30 Warum der Haushalt am Plan hängt
Bis V6 trug jedes Szenario sein eigenes Grundprofil: Haushaltsform, Personen (Name, Alter, Pensionsalter) und Startjahr. Das war zu grosszügig. Zwei Szenarien desselben Plans konnten so verschiedene Startjahre oder Haushaltsformen tragen – und niemand hätte es bemerkt, bis eine Auswertung Unsinn ergab: Derselbe erfasste Ist-Satz für 2031 wäre je Szenario auf einem anderen Planjahr gelandet.
V7 zieht die Trennlinie neu. Was den Haushalt beschreibt, gehört an den Plan; was eine Planungsvariante ausmacht, ans Szenario:
| Am Plan (für alle Szenarien) | Am Szenario (variantenspezifisch) |
|---|---|
| Haushaltsform | Pensionsalter je Person |
| Personen: Name, Alter | Inflationsannahme |
| Planstartjahr | Cash-Anfangswert |
| Phasen, Elemente, Werte |
Das Pensionsalter bleibt bewusst unten – es ist der Kern jedes Früh- oder Spätpensionierungs-Szenarios. Wollte man Name oder Alter einer Person unterschiedlich planen, wäre das kein Szenario mehr, sondern ein anderer Plan.
Der Rechenkern merkt nichts davon. toPlanInput() fügt die beiden Ebenen wieder zu einem
unveränderten PlanInput zusammen; computePlan und die 43 Golden Tests bleiben unberührt. Die
Verlagerung ist eine Frage der Datenhaltung, nicht der Berechnung.
Preis der Klarheit: Der Profil-Dialog muss jetzt je Feld anzeigen, ob es plan-weit gilt oder nur das Szenario betrifft – sonst änderte man beim Bearbeiten eines Nebenszenarios unbemerkt den ganzen Plan. Diese Beschriftung ist der sichtbare Teil der Entscheidung.
10. Glossar
| Begriff | Bedeutung im FPT |
|---|---|
| Plan | Selbsttragende Planungseinheit: Grundprofil + Phasenkette + Elemente |
| Szenario | Die berechenbare Einheit (seit V6): trägt Grundprofil, Phasenkette und Elemente. Jeder Plan hat genau ein Basisszenario; weitere entstehen als vollständige Kopie eines beliebigen Szenarios und hängen als Baum darunter |
| Logisches Element | Dasselbe finanzielle Element über Szenariogrenzen hinweg, erkannt über die Herkunfts-Kette sourceElementId – Grundlage der einmaligen Parametereingabe im Szenario-Vergleich (4.12.6) |
| Spannweite (Tornado) | Differenz zwischen dem Ergebnis beim tiefen und beim hohen Wert eines Treibers; bestimmt Balkenlänge und Rangfolge |
| Umbuchung | Bewegung, die Geld zwischen Cash und einem Vermögenswert verschiebt, ohne das Vermögen zu verändern (Sparrate, Amortisation, Zusatzinvestition). Erscheint nur im Cash-Wasserfall |
| Vermögens-/Cash-Brücke | Zerlegung der Vermögens- bzw. Cash-Veränderung einer Phase in ihre Summanden (WealthBridge / CashBridge) |
| Restposten | Differenz zwischen gerechnetem Endwert und der Summe der Brücken-Summanden; reine Rundung, Kontrollgrösse für die Vollständigkeit |
| Rechenweg (Trace) | Protokoll der Rechenschritte mit Formel, eingesetzten Zahlen, Ergebnis und geltender Vereinfachung; entsteht innerhalb von computePlan |
| Grundprofil | Haushaltsform, Personen (Alter, Pensionsalter, Name), Inflationsannahme |
| Lebensphase | Zeitabschnitt mit fester Dauer; darf keine Pensionierung überspannen |
| Phasentyp | ERWERB / PENSION / MIXED; abgeleitet, nie gespeichert |
| Finanzielles Element | Plan-weite Entität einer der 8 Kategorien, über alle Phasen identisch |
| Übergang | Grenze zwischen zwei Phasen; Ort der einmaligen Entscheide |
| Einmalige Sonderein-/ausgabe | Ereignis am Übergang (Erbschaft, Poolbau), das direkt aufs Cash wirkt und nicht in die Quote eingeht |
| Pensions-Übergang | Übergang, bei dem der Besitzer des Elements pensioniert wird |
| Carry / Fortschreibung | Live-Übertragung des Endwerts einer Phase in die nächste |
| Cash | Systemseitiges Ausgleichskonto; darf negativ werden (Liquiditätslücke) |
| Quote | Einkommen − nominale Ausgaben eines Jahres; negativ = Verzehr |
| Geplante Sparrate | 3a-Beiträge + Sparbeiträge + Amortisationen + Tilgungen |
| Geplante Verzehrrate | Summe der Bezugsraten aus Sonstigem Vermögen |
| Kapitalzufluss | Verkaufserlöse + PK-/3a-Bezüge aus dem Übergang in die Phase |
| Kapitalinvestition | Zusatzinvestitionen + Sofort-Tilgungen |
| Nominal | Betrag in Franken des jeweiligen Jahres |
| Real | Kaufkraftbereinigt auf den Planbeginn (nominal / Deflator) |
| Deflator | Kumulierte Inflation seit Planbeginn |
| Ausfalljahr | Jahr ohne AHV-Beiträge; kürzt die Rente um 1/44 |
| mdJE | Massgebendes durchschnittliches Jahreseinkommen – Mittel der Beitragsjahre, bestimmt die Rentenhöhe |
| R0 | Mindestbetrag der vollen AHV-Monatsrente (1'260); alle Schwellen sind Vielfache davon |
| Wendepunkt | mdJE = 36 × R0 = 45'360; dort wechselt die Rentenformel von Teil 1 auf Teil 2 |
| Plafonierung | Deckelung der Ehepaar-AHV auf 150 % der Einzel-Maximalrente |
| Umwandlungssatz | Prozentsatz zur Verrentung des PK-Kapitals |
| Ruin(alter) | Alter von Person A, in dem das Gesamtvermögen erstmals unter 0 fällt |
| Nachlass | Endvermögen der letzten Phase (nominal) |
Ende der Spezifikation v0.16