diff --git a/SPEZIFIKATION.md b/SPEZIFIKATION.md index b2aec87..a47b2ba 100644 --- a/SPEZIFIKATION.md +++ b/SPEZIFIKATION.md @@ -4,10 +4,10 @@ | | | |---|---| | **Dokument** | Funktionale und Technische Spezifikation FPT | -| **Version** | 0.29 | +| **Version** | 0.30 | | **Datum** | 2026-07-24 | | **Status** | Lebendes Dokument | -| **Codestand** | Arbeitsstand nach `52c48a9` inkl. Modul-Review 2 (Feinschliff) (Branch `main`) | +| **Codestand** | Arbeitsstand nach `efcc04c` inkl. Tour-Korrekturen (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` | @@ -17,6 +17,7 @@ | Version | Datum | Autor | Änderung | |---|---|---|---| +| 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. | @@ -452,8 +453,9 @@ Wertschriften, Wohneigentum und Schulden lassen sich gemeinsam **oder** je Perso werden nur die **heutigen Bestandswerte** erfasst (Guthaben, Kaufpreis, Hypothek, Restschuld) – die laufenden Jahresbeträge folgen in Schritt 5. Zusätzlich fragt das Wohneigentum die **Wertsteigerung** und den **Zins-in-Ausgaben-Schalter** ab (damit Hypothekarzinsen nicht -doppelt zählen), und die Säule 3a einen Schalter **«Selbstständig ohne PK (grosse Säule 3a)»**, -der die Beitrags-Obergrenze anhebt (siehe 4.11 / Feld `selfEmployed3a`). +doppelt zählen). Der Schalter **«Selbstständig ohne PK (grosse Säule 3a)»** der Säule 3a steht +in **Schritt 5** (direkt bei der 3a-Einzahlung, denn er betrifft deren Obergrenze) und hebt die +Beitrags-Obergrenze an (siehe 4.11 / Feld `selfEmployed3a`). **Schritt 5 (Sparen & Verteilen)** bringt das Kernmodell des Tools zum Anfassen: Aus `Nettoeinkommen − Ausgaben` entsteht die **Sparquote**; der Nutzer verteilt sie auf Säule 3a, @@ -4230,18 +4232,17 @@ gebaut: Der seltene Fehlerfall rechtfertigt keinen neuen Endpunkt. ## 9.24 Tour: Spotlight ohne Engine -Seit 0.29 hebt die Tour ihr Ziel als **Spotlight** hervor: Der Rest der Ansicht wird -abgedunkelt, das Ziel bleibt hell und trägt einen kräftig pulsierenden Rahmen. Der Trick ist -bewusst **ohne Positionierungs-Engine und ohne separates Overlay** umgesetzt – ein einziger -CSS-Kastenschatten mit riesigem Radius (`box-shadow: 0 0 0 9999px …`) dunkelt alles **um** das -Element herum ab, ohne es selbst zu berühren (keine Transparenz-Probleme). Die Karte springt auf -die dem Ziel **gegenüberliegende** Bildschirmhälfte (oben/unten), damit sie es nie verdeckt, und -lässt sich jederzeit **überspringen**. - -Grenze: Liegt das Ziel in einem Container mit `overflow: hidden` (z. B. der Matrix-Scrollbereich), -wird der Abdunkel-Schatten an dessen Rand geklippt – der Spotlight wirkt dann nur innerhalb dieses -Bereichs. Für die heutigen Ziele ist das akzeptabel; eine echte Coach-Mark-Bibliothek mit -Viewport-genauem Cutout wäre der nächste Schritt, wenn die Tour sich bewährt. +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 diff --git a/src/app/globals.css b/src/app/globals.css index 6770e28..e19ec92 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -207,23 +207,20 @@ body { } /* Hervorhebung des aktuellen Tour-Ziels. */ -/* Spotlight: Der riesige (9999px) erste Schatten dunkelt ALLES um das Element herum ab, das - Element selbst bleibt hell (der Schatten liegt ausserhalb). Der zweite Schatten ist der - pulsierende Akzent-Rahmen. So braucht es kein separates Overlay, und es gibt keine - Transparenz-Probleme. */ -@keyframes tour-pulse-kf { +/* Tour-Spotlight: Der abgedunkelte Bereich entsteht aus vier fixed-Flächen UM das Ziel herum + (in Tour.tsx aus der Bounding-Box berechnet) -- das funktioniert unabhängig von z-index und + overflow. Hier nur der pulsierende Rahmen, der als eigene fixed-Fläche über dem Loch liegt. */ +@keyframes tour-ring-kf { 0%, 100% { - box-shadow: 0 0 0 9999px rgba(0, 0, 0, 0.55), 0 0 0 3px color-mix(in srgb, var(--accent) 85%, transparent); + box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent) 85%, transparent), 0 0 14px 2px color-mix(in srgb, var(--accent) 40%, transparent); } 50% { - box-shadow: 0 0 0 9999px rgba(0, 0, 0, 0.55), 0 0 0 9px color-mix(in srgb, var(--accent) 45%, transparent); + box-shadow: 0 0 0 7px color-mix(in srgb, var(--accent) 50%, transparent), 0 0 22px 6px color-mix(in srgb, var(--accent) 25%, transparent); } } -.tour-highlight { - position: relative; - z-index: 40; - border-radius: 0.75rem; - animation: tour-pulse-kf 1.4s ease-in-out infinite; +.tour-ring { + border: 2px solid var(--accent); + animation: tour-ring-kf 1.4s ease-in-out infinite; } /* ------------------------------------------------------------------------- diff --git a/src/components/AppShell.tsx b/src/components/AppShell.tsx index 2859290..11a91de 100644 --- a/src/components/AppShell.tsx +++ b/src/components/AppShell.tsx @@ -378,6 +378,8 @@ function AppShellInner({ username }: { username: string }) { {plans.map((p) => { const navHere = planNav?.planId === p.id; + // Tour-Ziel «menu»: die Menü-Gruppe des gerade offenen Plans. + const isActivePlan = p.scenarios.some((s) => s.id === selectedScenarioId); const subItem = (tab: "dashboard" | "scenarios" | "actuals" | "analyses" | "reports", label: string, Icon: typeof FolderKanban) => ( + +

{step.text}

+
+ +
+
+ {steps.map((_, i) => ( + + ))} +
+
+ {index > 0 && ( + + )} + {index < steps.length - 1 ? ( + + ) : ( + + )}
- -

{step.text}

-
- -
-
- {steps.map((_, i) => ( - - ))} -
-
- {index > 0 && ( - - )} - {index < steps.length - 1 ? ( - - ) : ( - - )} -
-
-
- + ); }