summaryrefslogtreecommitdiff
path: root/app/README.md
diff options
context:
space:
mode:
authorMatthias Andreas Benkard <code@mail.matthias.benkard.de>2026-08-07 12:32:48 +0200
committerMatthias Andreas Benkard <code@mail.matthias.benkard.de>2026-08-22 19:18:02 +0200
commit8a2862590bb76d90597fe02432ac2e01c70d26a2 (patch)
tree9f07095112d7d090bcd1a109c23a6077c29da328 /app/README.md
Erste Version
Diffstat (limited to 'app/README.md')
-rw-r--r--app/README.md229
1 files changed, 229 insertions, 0 deletions
diff --git a/app/README.md b/app/README.md
new file mode 100644
index 0000000..9265909
--- /dev/null
+++ b/app/README.md
@@ -0,0 +1,229 @@
+# Altersvorsorge-Vergleich — Web-App
+
+Progressive Web-App zum Excel-Modell im übergeordneten Ordner. Derselbe Rechenkern, dieselben
+Ergebnisse, aber bedienbar von Menschen, die keine Tabellenkalkulation aufmachen wollen.
+
+| Datei | Rolle |
+|---|---|
+| `index.html` | **Die ganze App.** Rechenkern, Oberfläche, Diagramme, Texte — eine Datei, keine externen Abhängigkeiten. Läuft per Doppelklick. |
+| `manifest.json` | Macht die App installierbar (Name, Farben, Symbole). |
+| `service-worker.js` | Offline-Cache, `stale-while-revalidate`. |
+| `icon-*.png` | App-Symbole, 192 / 512 / 512-maskable. |
+
+---
+
+## 1. Weitergeben — drei Wege
+
+**a) Nur die Datei.** `index.html` verschicken. Sie ist vollständig autark: Doppelklick genügt,
+alles rechnet, Eingaben werden im Browser gespeichert. Was fehlt, ist nur die PWA-Hülle —
+kein Symbol auf dem Startbildschirm, kein Offline-Cache. Beides braucht es hier auch nicht,
+die Datei liegt ja schon auf dem Gerät. Die App sagt das dem Nutzer auch selbst.
+
+**b) Der ganze Ordner auf einem Webspace.** Irgendein HTTPS-Hosting genügt (GitHub Pages,
+Netlify, ein Unterverzeichnis auf der eigenen Domain). Dann ist sie installierbar und
+offlinefähig. Es gibt kein Backend, keine Datenbank, keine Build-Kette.
+
+```bash
+# lokal ausprobieren, mit Service Worker:
+cd app && python3 -m http.server 8000 # -> http://localhost:8000
+```
+
+Service Worker laufen auch auf `http://localhost` — als „secure context“ ist localhost
+ausdrücklich ausgenommen.
+
+**c) Als Link.** Der Knopf *„Link mit meinen Zahlen kopieren“* kodiert den kompletten
+Eingabestand base64url in den Fragment-Bezeichner. Fragmente werden vom Browser **nicht** an
+den Server geschickt (RFC 3986 § 3.5) — der Link enthält also persönliche Zahlen, überträgt
+sie aber an niemanden außer den Empfänger.
+
+## 2. Wo die Daten liegen
+
+Ausschließlich in `localStorage` unter dem Schlüssel `altersvorsorge.v1`, also im Browser des
+Nutzers. Kein Server, keine Analytik, keine Schriftarten von fremden Domains, keine externen
+Skripte. Die einzigen Netzwerkzugriffe der App sind die, die der Service Worker macht, um sich
+selbst zu aktualisieren.
+
+## 3. Aufbau der Datei
+
+`index.html` ist in elf nummerierte Teile gegliedert:
+
+| Teil | Inhalt |
+|---|---|
+| 1 | **Rechenkern** — Portierung von `build_bav.py`. Reine Funktionen, kein DOM |
+| 2 | Felddefinitionen: 77 Eingaben mit Bereich, Einheit, Erklärung, Fundstelle |
+| 3 | Zustand, `localStorage`, Kodierung für den Teilen-Link |
+| 4 | Formatierung (de-DE, Komma als Dezimaltrenner in beiden Richtungen) |
+| 5 | Kennzahlen |
+| 6 | Aufbau der Oberfläche |
+| 7 | Ergebniskarten mit Δ-Anzeige |
+| 7b | Klartext-Fazit, Beispielwert-Hinweis, mitlaufende Ergebnisleiste |
+| 8 | Diagramme, reines Canvas |
+| 9 | Rechenweg |
+| 10 | Render-Schleife |
+| 11 | Installation und Offline-Betrieb |
+
+Der Rechenkern in Teil 1 ist **die einzige Stelle, an der gerechnet wird.** Er ist zeichengenau
+aus `build_bav.py` übertragen; Änderungen an der Rechtslage gehören in beide Dateien.
+
+### Brutto und Netto sind gekoppelt
+
+Es gibt keinen Umschalter „ich gebe vor: Brutto / Netto“ mehr. Beide Felder zeigen dieselbe
+Größe, nur in verschiedener Währung:
+
+```
+N(B) = B − SV-Ersparnis_AN(B) − Steuerersparnis(B)
+```
+
+`N` ist auf `[0, Bruttogehalt]` stetig und wegen `dN/dB = 1 − Grenz-SV-Satz − Grenzsteuersatz > 0`
+streng monoton wachsend, also eine Bijektion auf `[0, N(Bruttogehalt)]`. Ein Umschalter wäre
+eine Auswahl zwischen einer Funktion und ihrer Umkehrfunktion — also keine.
+
+`beitrag` bleibt die einzige Quelle der Wahrheit; `netto_ziel` ist ein **abgeleitetes Feld** mit
+`get` (Vorwärtsrechnung, Jahr 0 der Ansparphase) und `set` (Bisektion, `bruttoVonNetto`).
+Beide werten dasselbe erste Jahr aus — Lohn- und Tarifindex sind dort 1 —, weshalb die
+Richtungen exakt invers sind. Gemessen über 12 Parameterkombinationen × 5 Stützstellen:
+
+| Richtung | größter relativer Fehler |
+|---|---|
+| B → N → B | 2,3 · 10⁻⁸ |
+| N → B → N | 2,1 · 10⁻⁸ |
+
+Das ist genau die Auflösung der Bisektion: 34 Halbierungen auf `[0, 250.000 €]` sind
+1,5 · 10⁻⁵ €/Jahr, also gut ein Millionstel Cent im Monat. Die Monotonie ist zusätzlich auf
+`1 … 1.500 €/Monat` in Ein-Euro-Schritten geprüft; die kleinste beobachtete Steigung ist
+`dN/dB = 0,545`, d. h. Steuer und Sozialabgaben tragen zusammen nie mehr als 45,5 % des
+Bruttobeitrags.
+
+Fällt der Nettoaufwand für das erste Jahr auf null — weil man dann bereits ausgeschieden ist —,
+existiert die Umkehrung nicht. Die App sagt das statt still nichts zu tun.
+
+## 4. Prüfung
+
+Der Kern ist gegen die Excel-Mappe geprüft, drei unabhängige Fälle, jeweils 22 Aggregate:
+
+| Testfall | größte relative Abweichung |
+|---|---|
+| Basisfall (40 J., 75.000 €, 300 €/Monat, GKV, Kapital, Ausscheiden mit 63) | 3,6 · 10⁻¹⁵ |
+| Splitting, KiSt 9 %, kinderlos, PKV, Rente, Fünftelregelung, Modus 2, Teilkapital | 1,3 · 10⁻¹⁴ |
+| GKV + lebenslange Rente, Zuschussmodus 0, KiSt 8 %, Teilkapital 30 % | 6,2 · 10⁻¹⁵ |
+
+Das sind wenige Dutzend ULP — akkumulierter Gleitkommafehler, nichts Systematisches.
+Die Referenzwerte stammen aus einem LibreOffice-Recalc der Mappe mit denselben Eingaben.
+
+> Die **Voreinstellungen** der App weichen bewusst vom Excel-Basisfall ab: „Ausscheiden aus dem
+> Job“ und „Teilzeit ab Alter“ stehen beide auf 67, also auf „durchgehend bis zur Rente“. Die
+> Testfälle setzen `ausscheid: 63, tzalter: 55` deshalb ausdrücklich — sonst prüfte man ein
+> anderes Szenario und merkte es nicht.
+
+Damit die beiden Kopien des Kerns nicht auseinanderlaufen, schneidet `extract.js` Teil 1 aus
+`index.html` heraus und erzeugt daraus das `model.js`, gegen das getestet wird. Der Testkern
+*kann* also gar nicht vom ausgelieferten abweichen.
+
+Zusätzlich prüfen zwei Testskripte (nicht Teil der Auslieferung, siehe Sitzungsprotokoll):
+
+- **DOM-Test:** 40 Zusicherungen über jsdom — Karten, Sichtbarkeitsregeln, Regler, Segment-
+ umschalter, Brutto-Netto-Kopplung in beide Richtungen, Fazittexte, mitlaufende Leiste,
+ Rundung, Tabs, Kennzahlwechsel, Referenz merken, Zurücksetzen, `localStorage`.
+ Die beiden seltenen Zweige des Fazits — „zulasten“ und „die Reihenfolge kehrt sich um“ —
+ sind eigens durch Parametersuche im Modell aufgespürt und dann im DOM nachgestellt worden;
+ ungetestete Textzweige sind sonst genau die, die beim Nutzer falsch dastehen.
+- **Canvas-Test, Stufe 1** (Endlichkeit): 120 Kombinationen aus 2 Breiten × 15 Randfällen ×
+ 4 Diagrammen. Jeder Zeichenaufruf wird auf endliche Koordinaten, gesetzte Farben und
+ Beschriftungen ohne `NaN` geprüft. Fand drei echte Fehler: Division durch null bei
+ Beitrag 0, entarteter Zeitraum bei Horizont = Rentenbeginn, und — der interessanteste —
+ nicht ganzzahlige Zwischenwerte in der Sensitivitätskurve über das Feld *Alter*, wodurch
+ `Z[6,2]` indiziert wurde.
+- **Canvas-Test, Stufe 2** (Sichtbarkeit): 208 Kombinationen aus 4 Breiten-/DPR-Paaren ×
+ 13 Szenarien × 4 Diagrammen. Prüft zusätzlich, dass jede Koordinate **innerhalb der Bitmap**
+ liegt. Diese Stufe entstand, nachdem Stufe 1 einen sichtbaren Fehler durchgelassen hatte:
+ `prep()` skalierte die Zeichenmatrix mit `devicePixelRatio`, setzte `canvas.height` aber in
+ logischen Pixeln — alles unterhalb von `h/dpr` wurde aus der Bitmap herausgezeichnet.
+ Sämtliche Koordinaten waren dabei endlich, also fehlerfrei nach Stufe 1. Lehre: bei Canvas
+ reicht „endlich“ nicht, es muss „im Bild“ geprüft werden.
+
+## 5. Was die App über das Excel hinaus kann
+
+- **Sensitivitätskurve** über den zuletzt bewegten Regler: 41 vollständige Modellläufe
+ (~0,5 ms je Lauf), mit markierten Schnittpunkten gegen das Privatdepot. Damit sind
+ Break-evens ablesbar statt ausgerechnet.
+- **Wasserfall der Werttreiber**, teleskopierend zerlegt, also exakt aufgehend. Reihenfolge-
+ abhängig, weil die Effekte nicht additiv separierbar sind — die App sagt das dazu.
+- **Warnung bei uneiniger Rangfolge.** Barwert und äquivalente Monatsleistung ranken
+ unterschiedlich, sobald der Wiederanlagezins nach Steuern deutlich über der Inflation liegt
+ (im Basisfall 5,54 % gegen 2,00 %). Genau daran hängt die Aussage
+ „PKV im Ruhestand dreht das Ergebnis“: nach Monatsleistung ja, nach Barwert nicht.
+- **Alle 45 Rechengrößen editierbar**, mit Fundstelle. Wer die Modellannahme
+ „Sonderausgaben-Höchstbetrag 2.340 €“ (Vertrauensgrad ~70 %) nicht teilt, ändert sie.
+- **Live-Notizen an den Reglern.** Der Arbeitgeberzuschuss meldet, ab welchem Prozentsatz er
+ sättigt; der Monatsbeitrag meldet das Überschreiten der 4-%- und der 8-%-Grenze der
+ Beitragsbemessungsgrenze RV. Beides sind Schwellen, an denen der Regler seine Wirkung
+ verliert oder die Rechtslage wechselt — ohne Hinweis dreht man ratlos weiter.
+- **Klartext-Fazit** in einem Satz: wer vorn liegt, um wie viel, und welcher Posten des
+ Wasserfalls den Abstand hauptsächlich trägt. Beschreibend formuliert, nicht empfehlend.
+ Angehängt ein Belastbarkeitstest über ±1 Prozentpunkt Bruttorendite, der ausdrücklich sagt,
+ wenn die Reihenfolge in dieser Spanne kippt — was sie überraschend oft tut.
+
+## 6. Bedienung auf dem Telefon
+
+Die Regler standen ursprünglich rund 1.000 px unter den Ergebniskarten. Wer auf einem 390-px-
+Gerät einen Regler schob, sah von der Wirkung **nichts** — genau das Gegenteil der Anforderung.
+Zwei Änderungen:
+
+1. **Reihenfolge:** Ergebnis → *Ihre Angaben* → Diagramme → Weitere Angaben → Rechenweg.
+ Die Diagramme sind Vertiefung, die Eingaben sind der Arbeitsplatz.
+2. **Mitlaufende Ergebnisleiste**, eingeblendet per `IntersectionObserver`, sobald die
+ Ergebniskarten den oberen Rand verlassen. Sie zeigt alle drei Werte, das Δ zum
+ Referenzstand, die Bezeichnung der Kennzahl und einen Sprung zurück nach oben.
+
+Dazu: `:focus-visible` ist jetzt definiert (vorher gab es gar keine sichtbare Tastaturführung
+auf den Segment-Umschaltern), und `prefers-reduced-motion` schaltet Übergänge ab.
+
+Der Rechenweg baut 98 Tabellenzeilen. Solange kein Abschnitt aufgeklappt ist, wird der
+Neuaufbau jetzt bis 300 ms nach der letzten Eingabe verschoben und beim Aufklappen nachgeholt.
+
+## 7. Eine Lesefalle in der Sensitivitätskurve
+
+Reglern wie *Jahresbrutto* oder *Monatsbeitrag* verändern nicht nur die Förderarchitektur,
+sondern auch den **Nettoaufwand** — und damit den Betrag, der in allen drei Optionen investiert
+wird. Die drei Kurven sind deshalb bei jedem einzelnen x fair vergleichbar, das Niveau zwischen
+verschiedenen x aber nicht. Die auffälligen Sprünge sind echt und sitzen an den
+Beitragsbemessungsgrenzen: oberhalb von 69.750 € spart die Entgeltumwandlung keine
+KV/PV-Beiträge mehr, oberhalb von 101.400 € auch keine RV/AV-Beiträge. Der Nettoaufwand
+springt dort um bis zu 15 %, die investierte Summe mit ihm. Die App blendet einen Hinweis mit
+der tatsächlichen Spanne ein, sobald der Effekt 2 % übersteigt.
+
+## 8. Zwei Befunde, die beim Portieren aufgefallen sind
+
+**Die Vorabpauschale ist im Basisfall ein Vorteil für das Privatdepot.** Über 35 Jahre fallen
+121.774 € Vorabpauschale an, davon werden aber nur 11.942 € tatsächlich versteuert — der Rest
+läuft in den Sparer-Pauschbetrag. Die *volle* Summe erhöht dagegen die Kostenbasis und spart
+bei der Entnahme rund 22.000 € Abgeltungsteuer. Setzt man den Sparer-Pauschbetrag auf null,
+dreht das Vorzeichen. Der Balken „Wegfall der Vorabpauschale“ im Wasserfall ist deshalb
+negativ; das ist kein Vorzeichenfehler.
+
+**Der Arbeitgeberzuschuss sättigt — und zwar früher, als man denkt.** Im Zuschussmodus „spitz“
+(§ 1a Abs. 1a BetrAVG wörtlich) wächst der Zuschuss nur bis zur tatsächlichen SV-Ersparnis des
+Arbeitgebers. Bei 75.000 € Gehalt und 300 €/Monat liegt das Entgelt bereits **über** der
+KV-Beitragsbemessungsgrenze von 69.750 €: der Arbeitgeber spart dort überhaupt keine
+KV/PV-Beiträge, sondern nur RV/AV auf 3.600 €, also 381,60 € im Jahr — das sind 10,6 % des
+umgewandelten Betrags. Der gesetzliche Mindestzuschuss von 15 % ist damit schon der gedeckelte
+Wert; **jede Erhöhung des Reglers ist wirkungslos.** Die App zeigt das jetzt direkt am Regler an.
+
+Der im README des Modells genannte Break-even von ~26,7 % ist folglich im spitzen Modus gar
+nicht erreichbar, sondern nur in den Modi „voller Beitrag“ bzw. „sv-freier Anteil“.
+Die Sensitivitätskurve zeigt die Sättigung als waagerechten Ast.
+
+---
+
+## 9. Bewusst nicht gebaut
+
+- **Onboarding-Assistent.** Elf Kernfelder tragen keinen Wizard; er verstellte nur den Blick
+ auf die Kopplung der Größen.
+- **Animierte Zahlenübergänge.** Sie lenken von genau dem ab, was man vergleichen soll.
+- **Aufgeräumte Rechengrößen.** Die 45 gesetzlichen Werte stehen bewusst hinten und bewusst
+ vollständig. Wer die Modellannahme mit 70 % Vertrauensgrad nicht teilt, soll sie ändern
+ können, ohne den Quelltext anzufassen.
+
+---
+
+*Keine Steuer- oder Anlageberatung. Ein Modell ist kein Vertrag.*