Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

214 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Effetto Composto

La forza dell'interesse composto al servizio della tua indipendenza finanziaria

Next.js React TypeScript Tailwind SQLite License: CC BY-NC 4.0

effettocomposto.it

Versione corrente: v1.10.34


Simulatore mutuo, calcolatore FIRE, tracker patrimonio, carriera, stipendio netto, budget e molto altro. Tutto in italiano. Tutto gratuito. Tutto open-source.


📸 Scopri l'App in Azione

Dashboard Situazione Generale
Dashboard Patrimonio
Simulatore FIRE Monte Carlo
Simulatore Mutuo Immobiliare
Calcolo Stipendio Netto
Mutui Market Confronto

Panoramica

Effetto Composto e' una webapp completa per la gestione della finanza personale, pensata per chi vuole raggiungere l'indipendenza finanziaria (FIRE). Unisce in un unico strumento tutto quello che serve per simulare, pianificare e monitorare il proprio percorso verso la liberta' finanziaria.

Installabile come app su smartphone (PWA), funziona anche offline e i calcoli pesanti girano su Web Worker per non bloccare l'interfaccia.


Funzionalita'

Simulazione e Calcolo

Strumento Descrizione
Simulatore Mutuo Calcolo rata, ammortamento francese, confronto fino a 3 mutui side-by-side, analisi DTI (rata/reddito), analisi redditivita' acquisto vs affitto
Calcolatore FIRE 10.000 simulazioni Monte Carlo via Web Worker, proiezione patrimonio, probabilita' di successo per anno target
Interesse Composto Simulazione crescita capitale con versamenti periodici e reinvestimento
Calcolatore Inflazione Impatto dell'inflazione sul potere d'acquisto nel tempo
Calcolatore Finanziamento Rata di prestiti personali con ammortamento alla francese, anticipo, costo totale del credito e analisi DTI sul reddito
Viewer Movimenti Directa Importa il CSV dei movimenti da Directa Trading: dashboard con KPI, grafici cumulativi, flussi mensili, breakdown per strumento, riepilogo annuale e tabella filtrata (solo visualizzazione, nessun dato salvato)
Advisor Acquisti Analisi dell'impatto di un acquisto sul percorso FIRE con grafici comparativi
Lordo -> Netto Calcolo dinamico stipendio netto con IRPEF, INPS, addizionali, bonus, cronologia scenari salvati e richiamo rapido delle simulazioni

Monitoraggio e Gestione

Strumento Descrizione
Tracker Patrimonio Snapshot giornalieri del patrimonio netto, gestione immobili, liquidita' e titoli separati, portafoglio con dividendi, prestiti attivi, proiezione futura e storico esportabile
Budget Mensile Spese per categoria, import CSV estratto conto (Fineco, Intesa, formato generico)
Obiettivi di Risparmio Target personalizzati con tracking progressi e deadline
Tracker Abbonamenti Costi ricorrenti con riepilogo mensile e annuale
Strategia Debiti Confronto metodo snowball vs avalanche per l'estinzione dei debiti
Carriera Timeline retributiva e strumenti per stimare l'evoluzione del reddito nel tempo dentro la stessa area del dashboard

Riepilogo e Alert

Strumento Descrizione
Dashboard Riepilogo KPI principali, asset allocation, overview completa
Alert Automatici Notifiche su rapporto DTI, fondo emergenza insufficiente, deviazioni dal percorso FIRE
Export CSV Esportazione dati patrimonio e ammortamento in CSV (UTF-8 con BOM)

Tech Stack

Frontend       Next.js 16 (App Router) + React 19 + TypeScript
Styling        Tailwind CSS 4 + Shadcn/UI (tema new-york)
Database       Prisma ORM + SQLite
Grafici        Recharts
Animazioni     Framer Motion
Performance    Web Workers (Monte Carlo), React.memo, lazy loading
PWA            Service Worker (cache-first statico, network-first API)
Testing        Vitest + GitHub Actions CI
Deploy         Docker + Traefik (HTTPS automatico via Let's Encrypt)

Versioning

  • Fonte di verita' - il numero versione del software vive in package.json (version) ed e' la base sia del repo sia del frontend
  • Regola di release - ogni deploy applicativo su VPS deve includere bump versione + nuova voce nel changelog con lo stesso numero
  • Bump rapido - per i prossimi rilasci puoi usare npm run release:patch, npm run release:minor oppure npm run release:major
  • CI prima del deploy - non deployare commit con badge GitHub rosso: il workflow .github/workflows/ci.yml deve restare in grado di fare npm ci, prisma generate, test e build, con DATABASE_URL=file:./ci-test.db disponibile gia' a livello job. In questo repo il controllo TypeScript affidabile di release e' quello eseguito dentro next build
  • UI discreta - la versione corrente viene mostrata in piccolo sotto il brand nell'header, in grigio tenue, cosi' resta sempre verificabile senza sporcare la dashboard

Changelog

v1.11.0 - 10 giugno 2026 (nuova tab "Lazy Portfolios" — esplora, confronta e simula portafogli passivi storici)

  • Nuova tab "Lazy Portfolios" — 16 portafogli passivi celebri (Golden Butterfly, All Weather, Permanent, Buffett, Bogleheads, Couch Potato, Talmud, Swensen, Ivy, Coffee House, Dedalo Four, ecc.) con allocazione, ETF reali (USD + EUR UCITS armonizzati), statistiche storiche su 3 periodi (1985-2020, 2000-2020, 2010-2020) in tre valute (USD, USD→EUR, EUR puro).
  • Simulatore Monte Carlo interattivo — proiezione decumulo/SWR con 2000 run, tasso di successo, SWR ottimale al 95%, grafico percentili (10°, 50°, 90°), parametri personalizzabili (capitale, prelievo, orizzonte, inflazione, entrate extra).
  • Confronto rischio di cambio — evidenza educativa USD vs USD→EUR vs EUR per mostrare l'impatto reale del rischio valutario per investitori europei.
  • Dati ETF completi — ticker, ISIN, TER, peso e ruolo per ogni costituente, con toggle EUR/USD.
  • Test di integrita' dati — test Vitest su allocazioni, pesi ETF e validita' statistiche.
  • Nessuna modifica al database — nessuna migrazione Prisma necessaria.

v1.10.34 - 31 maggio 2026 (UX - rifinitura grafici custom fuori da Recharts)

  • Problema iniziale - l'audit post-release ha confermato che tutti i grafici Recharts usavano il sistema condiviso, ma alcune visualizzazioni custom erano ancora rimaste con colori locali: asset allocation nel Riepilogo, asset allocation nel report stampabile e confronto strategie nella rendita immobiliare.
  • Soluzione applicata - portate anche queste visualizzazioni su CHART_COLORS, con barre piu' moderne, legenda compatta, totale lordo visibile nel Riepilogo e stack di allocazione nel report export.
  • Impatto utente - l'esperienza grafica ora resta coerente anche dove non viene usato Recharts: stessi significati cromatici per immobili, investimenti, crypto, liquidita', rendimento positivo e scenari immobiliari.
  • Verifica - eseguiti lint, test, build e QA browser mirata prima del nuovo deploy.

v1.10.33 - 31 maggio 2026 (UX - sistema grafico condiviso e restyling coerente di tutti i grafici)

  • Problema iniziale - i grafici dell'app rispondevano a domande finanziarie diverse, ma usavano stili non sempre coerenti: palette locali, assi densi, tooltip disomogenei e alcuni colori poco legati al significato economico della serie. Questo rendeva piu' faticoso confrontare patrimonio, debiti, FIRE, budget, inflazione, mutui, Directa, acquisti e performance.
  • Soluzione applicata - introdotto src/components/ui/chart-style.tsx, un layer condiviso per palette semantica, griglie leggere, assi compatti, tooltip moderni, cursori, legende e raggi delle barre. Tutti i grafici Recharts principali sono stati aggiornati per usare token coerenti, formattazione compatta in euro e colori legati alla logica della metrica (crescita, debito, interessi, target, rendimento, rischio, spese, capitale, liquidita').
  • Impatto utente - i grafici sono piu' leggibili su desktop e mobile, hanno meno rumore visivo, tooltip piu' chiari e una gerarchia cromatica piu' prevedibile. La heatmap dei rendimenti mensili e' stata resa piu' accessibile con contenitore dedicato e istruzione anche da tastiera.
  • Verifica - eseguiti lint, test unitari, controllo migrazioni, build Next.js e QA browser su desktop/mobile per confermare assenza di overflow orizzontale e rendering corretto dei grafici.

v1.10.32 - 2 maggio 2026 (UX — nuova KPI "Composizione del Saldo Finale" nel Calcolatore Interesse Composto: scompone il saldo nelle due sorgenti additive — capitale iniziale capitalizzato vs contributi mensili capitalizzati)

  • Problema iniziale — il Calcolatore Interesse Composto (src/components/compound-interest-calculator.tsx) racconta gia' molto bene il "quanto" e il "quando" del piano di accumulo (saldo nominale, saldo reale, guadagno reale, punto di svolta interessi-vs-versamenti, multiplicatore del capitale, tempo di raddoppio nominale e reale, rendita FIRE, costo del ritardo, TAEG-equivalent), ma mancava una metrica che rispondesse a una domanda strutturale di un piano di accumulo: "il mio saldo finale lo sta facendo soprattutto il capitale di partenza o il ritmo dei contributi?". La card % da interessi composti quantifica gli interessi totali sul saldo, ma sovrappone in un'unica voce il composto del lump iniziale e quello dei versamenti — due dinamiche con interpretazioni finanziarie diverse (un giovane senza patrimonio costruisce ricchezza con il "ritmo", un patrimonializzato con il "punto di partenza"). Senza questa scomposizione l'utente non aveva uno strumento per capire se il suo piano sta sfruttando di piu' la "leva" del lump sum (orizzonti corti / capitale iniziale grande) o l'effetto cumulativo del PAC (orizzonti lunghi / contributi consistenti), e quindi non poteva decidere consapevolmente se valesse la pena spostare un €1.000 dal mensile al lump iniziale (o viceversa).
  • Soluzione applicata — nuovo modulo puro decomposeFinalBalance in src/lib/finance/compound-interest.ts che decompone il saldo nominale finale nelle DUE traiettorie additive che la ricorrenza dell'interesse composto produce: fromInitial = initialCapital * (1+m)^N (lump sum capitalizzato per l'intero orizzonte, indipendente dai contributi) e fromContributions = monthlyContribution * ((1+m)^N - 1) / m (rendita posticipata mensile, indipendente dal lump sum). La decomposizione e' esatta perche' la ricorrenza balance = balance * (1+m) + contribution e' lineare nelle due sorgenti, quindi le due componenti sommano esattamente al saldo finale (verificato con tolleranza float a 4 cifre decimali sui parametri canonici 10k iniziale + 300/mese + 7% + 20 anni). La forma chiusa usa Math.expm1(N * Math.log1p(m)) invece di Math.pow(1+m, N) - 1 per non perdere precisione su tassi piccoli / orizzonti corti, e degenera correttamente a monthlyContribution * N quando il tasso mensile e' zero (no compound da estrarre). Esposte due quote initialShare e contributionsShare in [0, 1] che sommano a 1 quando il totale e' positivo.
  • Robustezza tecnica — sanitizzazione completa degli input (NaN/Infinity → 0 coerente con simulateCompoundInterest), guard espliciti su fromContributions < 0 e !Number.isFinite(fromContributions), gestione esplicita dei tre edge case finanziari: (1) monthlyContribution = 0 → 100% lump iniziale, (2) initialCapital = 0 → 100% contributi, (3) years = 0 → solo lump (no PAC fatto). Quando entrambi sono zero le quote restano a 0 (no NaN da divisione). La card UI si nasconde silenziosamente quando il saldo finale e' zero (caso degenere) per non mostrare "0% / 0%" privo di significato.
  • Coverage di test — 14 nuovi test in compound-interest.test.ts certificano la decomposizione:
    1. La somma fromInitial + fromContributions coincide con simulateCompoundInterest.finalBalance (proprieta' fondamentale: la decomposizione e' esatta perche' la ricorrenza e' lineare).
    2. Le quote sommano esattamente a 1 (no arrotondamenti spuri nella visualizzazione).
    3. Forma chiusa fromInitial = initialCapital * (1+m)^N verificata indipendentemente.
    4. Forma chiusa fromContributions = C * ((1+m)^N - 1) / m verificata indipendentemente.
    5. monthlyContribution = 0contributionsShare = 0, initialShare = 1.
    6. initialCapital = 0initialShare = 0, contributionsShare = 1.
    7. annualRatePct = 0 → degenerazione lineare: fromInitial = initial, fromContributions = C * N.
    8. years = 0 → tutto lump iniziale, no PAC.
    9. Orizzonti lunghi (30 anni @ 7%) → la quota PAC supera la quota lump (effetto del compound sui contributi).
    10. Orizzonti corti con grande lump (100k + 100/mese + 5 anni) → l'iniziale domina con initialShare > 90%.
    11. Entrambi a zero → tutte le quote a 0 (no esplosioni numeriche).
    12. Input non finiti sanificati a 0.
    13. Linearita' in initialCapital: 10× input → 10× fromInitial, fromContributions invariante.
    14. Linearita' in monthlyContribution: 5× input → 5× fromContributions, fromInitial invariante. Tutti i 598 test esistenti restano verdi: la coverage totale sale a 612/612 passati.
  • Impatto utente — il Calcolatore Interesse Composto mostra ora una nuova card cyan/blu Composizione del Saldo Finale con:
    • barra di progresso bicolore che visualizza il rapporto fra le due quote (cyan = capitale iniziale, blu = contributi);
    • due tile percentuali con il valore assoluto in € sotto ognuna ("Da capitale iniziale: 33% / €52.150" e "Da contributi mensili: 67% / €106.030");
    • tooltip educativo che spiega la matematica e collega le due dinamiche al profilo dell'utente (PAC dominante per orizzonti lunghi / lump iniziale piccolo, lump dominante per orizzonti corti / capitale grande);
    • icona PieChart di lucide-react per coerenza visiva con le altre KPI strutturali (Multiplicatore del Capitale, Tempo di Raddoppio, Rendita FIRE). La card si integra naturalmente nella colonna laterale fra "Multiplicatore del Capitale" e "Valore Reale", chiudendo il messaggio strutturale del calcolatore: dopo aver visto "quanto vale ogni €" (multiplicatore) e "quanto vale a fine piano" (saldo nominale + reale), l'utente ora vede anche da dove proviene quel saldo. E' la KPI piu' utile per decisioni "come distribuisco il mio risparmio fra lump sum iniziale e PAC mensile?", una domanda che fino ad oggi richiedeva di simulare manualmente due piani separati.

v1.10.31 - 2 maggio 2026 (BUG FIX — il "ritmo storico" degli Obiettivi di Risparmio non conta piu' il saldo iniziale come "savings", correzione di una sovrastima sistematica del completamento)

  • Falla logica scovata — il modulo src/lib/finance/savings-goals-completion.ts (e i suoi consumatori getEstimatedDate / getDeadlinePacing in src/components/savings-goals.tsx) calcolava il ritmo di risparmio storico di ciascun obiettivo come currentAmount / Math.max(1, mesi_trascorsi). La formula trattava l'INTERO saldo corrente come se fosse stato accumulato durante l'orizzonte del goal, contando il capitale di partenza (cioe' il valore iniziale che il goal aveva al momento della creazione) come "savings storici" — un errore di apples-to-oranges con conseguenze numeriche pesanti:
    • Goal creato OGGI con €5.000 gia' presenti → monthsElapsed clampato a 1 → pace fittizia di €5.000/mese, completamento "stimato" a 1 mese.
    • Goal creato 6 mesi fa con €5.000 di partenza, oggi €6.000 (cioe' €1.000 risparmiati in 6 mesi = €167/mese reali) → la formula riportava €6.000/6 = €1.000/mese, una sovrastima 6× che anticipava il completamento di anni. Il bug era inoltre cementato da un test esplicito (clamps elapsed months to a minimum of 1) che asseriva il comportamento sbagliato come "fix" della divisione per zero, garantendo regressione ad ogni refactor.
  • Soluzione applicata — nuovo campo initialAmount su SavingsGoal (Prisma + migrazione SQL 20260502072700_add_savings_goal_initial_amount) che persiste il saldo iniziale al momento della creazione del goal. Sulla POST /api/goals il campo viene impostato automaticamente a currentAmount (il valore digitato dall'utente in fase di creazione), mentre la PUT lascia il campo immutabile (l'identita' "saldo di partenza" non deve mai essere riscritta a posteriori). Il calcolo della pace storica diventa (currentAmount - initialAmount) / mesi_trascorsi con tre invarianti aggiuntive:
    1. Soglia minima di significativita': goal con meno di 1 mese intero di storia non contribuiscono al ritmo aggregato (MIN_MONTHS_FOR_PACE). Prima del fix il clamp Math.max(1, monthsElapsed) mascherava da "media mensile" cio' che invece era "saldo iniziale spalmato su 1 mese fittizio". Adesso e' un "dati insufficienti" onesto.
    2. Pace non negativa: se currentAmount <= initialAmount (utente non ha ancora risparmiato o ha prelevato dal goal), la pace di quel goal e' 0 — non un numero negativo che falserebbe la somma aggregata.
    3. Backward compatibility: i goal storici creati prima della migrazione hanno initialAmount = 0 per default, quindi la formula degenera al comportamento legacy current / mesi. Cosi' il fix non rompe la pace dei goal precedenti, mentre i goal creati post-fix calcolano la pace correttamente.
  • Robustezza tecnica — sanitizzazione dei nuovi input (Math.max(0, sanitize(initialAmount ?? 0))) per evitare propagazione di NaN/Infinity, validazione di createdAt con Number.isFinite(getTime()) su getEstimatedDate (eliminato il rischio di format(InvalidDate) che lanciava RangeError con createdAt corrotto), guard espliciti su setMonth(NaN). Estratto un helper computeHistoricalMonthly(goal, now) condiviso fra getDeadlinePacing (status on-track / stretch / behind) e getEstimatedDate (data prevista) per evitare drift fra le due implementazioni della stessa formula.
  • Coverage di test — quattro nuovi test in savings-goals-completion.test.ts certificano il fix:
    1. non produce pace per goal con meno di 1 mese di storico (BUG FIX) — sostituisce il vecchio test buggy: oggi un goal nuovo restituisce aggregateMonthlyPace = 0 e monthsToCompletion = null invece della pace fittizia.
    2. non conta il saldo iniziale come savings (BUG FIX core) — verifica numericamente il caso canonico €5k → €6k in 6 mesi: pace ~€166.67/mese, completamento in 24 mesi (vs. 4 mesi del bug).
    3. currentAmount <= initialAmount produce pace zero — copre il caso di prelievo dal goal.
    4. backward compatibility: initialAmount mancante (legacy) — garantisce che goal senza il nuovo campo continuino a comportarsi come prima. Tutti i test esistenti (594) restano verdi: la coverage totale sale a 598/598 passati.
  • Impatto utente — l'Obiettivi di Risparmio mostra ora date di completamento e badge di pacing (in linea / da accelerare / in ritardo / scaduto) basati sul ritmo REALE di risparmio dell'utente, non su una pace gonfiata dal capitale di partenza. La metrica "Tempo stimato al completamento di TUTTI i goal" passa da una proiezione ottimisticamente fuorviante a una stima onesta, allineata al messaggio FIRE del prodotto: il composto si misura su quello che versi, non su quello che gia' avevi.

v1.10.30 - 1 maggio 2026 (UX — nuovo chip "Tempo di Raddoppio Reale" nel Calcolatore Inflazione, dualita' "carrot" della Regola del 72 sul rendimento reale)

  • Problema iniziale — il Calcolatore Inflazione (src/components/inflation-calculator.tsx + src/lib/finance/inflation.ts) costruiva tutto il messaggio temporale sulla scala dell'EROSIONE: il chip Tempo di Dimezzamento (purchasingPowerHalvingYears, formula esatta ln(2)/ln(1+i)) raccontava in quanti anni 1 € di cash si svaluta a 0,50 € di potere d'acquisto. Era pero' una metrica monodirezionale: la "stick" mostrava il danno di non investire, ma non c'era una "carrot" duale che rispondesse alla domanda speculare — "in quanti anni il MIO investimento raddoppia il potere d'acquisto al netto dell'inflazione?". L'app si chiama Effetto Composto, ma sul calcolatore piu' didattico per spiegare l'effetto inflazione mancava proprio il numero che esprime quando il compounding "vince": il tempo di raddoppio REALE, calcolato sul rendimento di Fisher e non sul nominale gonfiato. Senza questa metrica il messaggio si fermava al pessimismo ("perdi meta' del valore in N anni") senza il counterpoint costruttivo ("ma se investi al X% reale, raddoppi in Y anni"). Era un buco didattico tipico per uno strumento FIRE-oriented dove la motivazione "carrot" e' tanto necessaria quanto la "stick" — pattern gia' adottato dalla Vantaggio Reale dell'Investire (v1.10.23), Costo Opportunita' a 30 Anni nel Tracker Abbonamenti (v1.10.5) e Costo Opportunita' del Risparmio a 30 Anni nel Budget Tracker (v1.10.18), tutte costruite sull'idea "ecco quanto guadagni se fai la cosa giusta" e non solo "ecco quanto perdi se non la fai". La Regola del 72 (approssimata 72 / r%) e' la versione mainstream di questa idea, ma la sua applicazione al rendimento NOMINALE produce una falsa rassicurazione (72 / 7% = 10 anni di raddoppio non tiene conto dell'inflazione che intanto erode lo stesso capitale): serviva la versione esatta sul rendimento REALE per dare un numero onesto.

  • Cosa e' stato modificato — aggiunto un nuovo campo realValueDoublingYears: number | null al risultato di projectInflation (modulo puro src/lib/finance/inflation.ts), calcolato con la formula esatta ln(2) / ln(1 + realReturn) dove realReturn = (1+nominal)/(1+inflation) - 1 e' il rendimento di Fisher gia' usato dal modulo per coerenza con FIRE dashboard e advisor. Quando realReturn <= 0 (cioe' il rendimento nominale non batte l'inflazione) il valore e' null esplicito, perche' in quel caso il valore reale dell'investimento NON raddoppia mai — coerente col pattern di purchasingPowerHalvingYears che torna null per inflazione <= 0. Sul lato UI il nuovo chip Tempo di Raddoppio Reale viene renderizzato accanto al Tempo di Dimezzamento esistente (palette emerald/teal-50 con icona Sparkles da lucide-react per evocare il tema "compounding magico", coerente col verde-emerald gia' usato per Valore Reale Investito e Vantaggio Reale dell'Investire) e mostra il tempo in formato compatto italiano (intero >= 10 anni, una cifra decimale sotto — stessa logica narrativa del chip di dimezzamento, cosi' i due numeri sono confrontabili a colpo d'occhio). Il tooltip educativo esplicita che la metrica e' calcolata sul rendimento REALE e non nominale per evitare la falsa rassicurazione della Regola del 72 applicata al netto dell'inflazione. La griglia esistente sm:grid-cols-6 viene esteso con un sm:col-span-3 per il nuovo chip che si dispone naturalmente sotto/accanto agli altri tre chip secondari (erosione totale, dimezzamento, erosione mensile), mantenendo il layout reattivo gia' rodato.

  • Implementazione tecnica — modifica chirurgica a due file: src/lib/finance/inflation.ts (aggiunto campo all'interfaccia InflationProjectionResult con docstring esplicativa, calcolo finale prima del return con guard Number.isFinite su realReturnDecimal e check > 0, esposto nel return literal coerente con gli altri campi) e src/components/inflation-calculator.tsx (importato Sparkles da lucide-react gia' usato altrove nel progetto -- zero nuove dipendenze, esteso il destructuring per estrarre realValueDoublingYears, aggiunto doublingLabel come computed locale con la stessa logica di halvingLabel per coerenza visiva, esteso il condizionale del wrapper grid a (lostValue > 0 || halvingLabel || doublingLabel), inserito il nuovo chip alla fine del blocco). 9 nuovi test in src/lib/finance/inflation.test.ts coprono: rendimento reale del 5% (7% nominale, 2% inflazione) -> raddoppio in ~14.5 anni, inflazione zero -> raddoppio = nominal halving (~10.24 anni al 7%), rendimento nominale = inflazione -> null (no raddoppio), rendimento reale negativo -> null, rendimento nominale = 0 con inflazione positiva -> null, coerenza algebrica (1+real)^doubling = 2, indipendenza da amount/years (invariante della formula), rendimenti reali alti (10% reale -> ~7.27 anni), input non finiti -> null o valore finito positivo (no NaN spuri). Suite totale: 594/594 verde (9 nuovi su 585), eslint pulito, zero errori TypeScript sui file modificati. Bumped a v1.10.30 in package.json.

  • Perche' migliora l'esperienza utente — completa il quadro narrativo del Calcolatore Inflazione aggiungendo la sola metrica temporale che mancava: la versione "carrot" della halving del cash. Ora l'utente vede in un colpo d'occhio la dualita' fondamentale dell'app: "il tuo cash si dimezza in 35 anni; il tuo investimento raddoppia in 14 anni — ecco cos'e' l'Effetto Composto". E' il messaggio comportamentale piu' potente per un'app FIRE-oriented perche' usa due numeri della stessa unita' di misura (anni) per quantificare l'asimmetria fra non-azione e azione. Il chip e' didatticamente piu' onesto della Regola del 72 mainstream perche' e' calcolato sul rendimento REALE (Fisher esatto, gia' fonte di verita' del progetto) e non sul nominale: un investitore con 7% nominale e 2% inflazione vede ~14.5 anni di raddoppio reale (il vero compounding) invece della falsa rassicurazione ~10 anni della Regola del 72 sul nominale. Il chip e' condizionale: con rendimento reale <= 0 sparisce — coerente con il pattern delle KPI condizionali del resto dell'app (niente promesse di raddoppio quando il compounding sta perdendo contro l'inflazione, allineato con Vantaggio Reale dell'Investire v1.10.23 che pure si nasconde nello stesso scenario). La centralizzazione del calcolo nel modulo puro inflation.ts (UNICA fonte di verita' del progetto per le proiezioni di inflazione) abilita futuri usi della stessa metrica in altri componenti — Header KPIs, dashboard Riepilogo, sezione FIRE — senza duplicare la formula esatta su rendimento reale.

v1.10.29 - 1 maggio 2026 (refactor & code quality — helper centralizzati formatEuroSigned / formatPercentSigned + deduplica del pattern >= 0 ? "+" : "")

  • Problema iniziale — il pattern (value >= 0 ? "+" : "") + formatEuro(value) (e la sua variante percentuale (value >= 0 ? "+" : "") + value.toFixed(1) + "%") era duplicato in 14+ posizioni del codice (compound-interest-calculator.tsx, patrimonio-dashboard.tsx, mortgage-simulator/profitability-analysis.tsx, performance-dashboard.tsx, directa-movements-viewer.tsx, app/report/export/page.tsx) per anteporre un "+" esplicito ai valori non negativi nei delta, cashflow e variazioni di performance. Tre file (performance-dashboard, report/export, api/ai/performance-analysis) avevano addirittura reinventato un helper locale formatPct / fmtPct con la stessa logica ma firma e fallback leggermente diversi (em-dash vs vs N/A, decimali 1 vs 2). directa-movements-viewer aveva un fmtEuroSigned locale. Il risultato era una violazione di DRY classica con tre conseguenze: (1) inconsistenza UX su valori non finiti — alcuni call site mostravano +NaN% (zero hardening) mentre altri mostravano em-dash, perche' il pattern inline value.toFixed(1) non ha alcun guard su Number.isFinite(); (2) costo di manutenzione moltiplicato — qualsiasi futura modifica alla convenzione del segno (es. niente "+" sullo zero, decimali variabili, spazio non breaking dopo "+") avrebbe richiesto 14+ edit sparsi; (3) carico cognitivo sul lettore — ogni call site mescolava logica di sanitizzazione + formattazione + segno in una sola JSX expression illeggibile ({m.totalCashFlow >= 0 ? "+" : ""}{formatEuro(m.totalCashFlow)}).

  • Implementazione tecnica — aggiunti due helper puri in src/lib/format.ts: formatEuroSigned(value: number): string e formatPercentSigned(value: number, decimals?: number): string. Entrambi rispettano il contratto canonico del modulo: ritornano l'em-dash () per input non finiti (NaN/±Infinity), riusano formatEuro / formatPercent come motore di base (zero duplicazione della logica di formattazione locale italiana), antepongono + ai valori non negativi (zero incluso, coerente col pattern preesistente) e NON raddoppiano il minus per i negativi (formatEuro(-5000) gia' contiene il minus tramite il locale). 10 nuovi test in src/lib/format.test.ts coprono: prefisso + su positivi/zero, no doppio segno sui negativi, em-dash su NaN/Infinity/-Infinity, decimali custom su formatPercentSigned (2 decimali, 0 decimali con arrotondamento), regressione algebrica formatEuroSigned(v) === formatEuro(v) per v < 0. Suite totale: 585/585 verde (10 nuovi test su 575). Refactor di 14 call sites in 6 file: compound-interest-calculator.tsx:336 (Guadagno Reale), patrimonio-dashboard.tsx:921+988 (Cashflow Atteso, due viste), mortgage-simulator/profitability-analysis.tsx:44+84 (Cashflow Mensile, Guadagno Cash Cumulata), performance-dashboard.tsx:57+283 (helper formatPct locale ora delega a formatPercentSigned, KPI Contributi netti), directa-movements-viewer.tsx (5 KPI percentuali: Rendimento semplice, MWR, pctReturn, annualizedReturn, MWR per ticker, contribPct + il wrapper locale fmtEuroSigned ora delega), app/report/export/page.tsx (helper fmtPct locale ora delega). L'unico call site lasciato intenzionalmente con la logica inline e' src/app/api/ai/performance-analysis/route.ts:48: usa "N/A" invece di em-dash come fallback perche' e' un prompt LLM e l'em-dash e' meno parseabile dal modello (semantica diversa, mantenere).

  • Perche' migliora la manutenibilita' dell'app — converte una violazione DRY pluriennale in un'API pura unica (UNICA fonte di verita' per "formato monetario/percentuale con segno esplicito"), abbassando il costo marginale di ogni futura modifica ai formatter da O(n) call sites a O(1) helper. La consistenza UX migliora immediatamente su tutti i 14 punti della UI: ora un NaN propagato in qualsiasi cashflow/delta produce em-dash invece di +NaN% o +€NaN, allineato col pattern gia' adottato da formatEuro / formatPercent / formatEuroCompact (v1.10.x serie di hardening su NaN propagation: fire-projection, coast-fire, inflation, sale-tax, pension-fund). Il refactor tocca componenti su cui altre release recenti hanno aggiunto KPI nuove (v1.10.28 Guadagno Reale, v1.10.13 Riepilogo Portafoglio Debiti, v1.10.5 Costo Opportunita'): future KPI con segno esplicito useranno automaticamente il helper canonico, prevenendo la rinascita del pattern inline. Test coverage estesa al modulo format di base (era a 17 test, ora 27) consolida la base di sicurezza per future varianti (es. un eventuale formatEuroSignedCompact per gli header mobile in scenari KPI delta dove lo spazio orizzontale e' limitato). Bumped a v1.10.29 in package.json. Marginal gain dell'1% sulla code quality e sulla consistenza UX in un'area attraversata trasversalmente da quasi tutte le dashboard finanziarie del progetto.

v1.10.28 - 1 maggio 2026 (bugfix finanziario — Guadagno Reale del Calcolatore Interesse Composto: fine al confronto reale-vs-nominale)

  • Bug scovato nel Calcolatore Interesse Composto - la KPI "Guadagno Reale" calcolava realGain = realFinalBalance - sim.totalDeposited, mescolando un valore in euro REALI (saldo finale deflazionato all'inflazione cumulata) con un valore in euro NOMINALI (somma grezza di capitale iniziale + tutti i contributi mensili). Il confronto apples-to-oranges sotto-stimava sistematicamente il guadagno reale ogni volta che era presente un PAC: un €100 versato in anno 10 veniva contato a potere d'acquisto piu' alto del suo reale valore di oggi (1/1.02^10 ≈ €82). Caso canonico: 10.000€ iniziali + 100€/mese, 2% rendimento, 2% inflazione, 20 anni. Il rendimento reale (Fisher) e' esattamente 0%, quindi il guadagno reale dovrebbe essere ~0€. Il calcolatore segnalava invece -4.125€ di "perdita reale", scoraggiando un piano di accumulo che in realta' preserva perfettamente il potere d'acquisto
  • Nuova computeInflationAdjustedTotals() in lib/finance/compound-interest.ts - funzione pura che deflaziona ciascun contributo mensile al tasso di inflazione per il numero di mesi trascorsi: realTotalDeposited = initialCapital + Σ_m contributo / (1 + inflazione)^(m/12). Cosi' la sottrazione realFinalBalance - realTotalDeposited ha entrambi i lati in euro di OGGI ed e' coerente. Sanitizzazione robusta degli input (NaN/Infinity/negativi → 0, deflazione totale ≤ -100% → fattore neutro 1 per evitare division-by-zero)
  • Nuovi test di regressione - aggiunti 8 test a compound-interest.test.ts che bloccano il ritorno del bug: realGain ~ 0 quando rendimento nominale = inflazione, realGain > 0 con rendimento reale positivo, realGain < 0 con inflazione che divora il rendimento, sanity-check con inflazione zero (real == nominal), verifica indipendente dell'algoritmo, gestione di input degenere. Tutti i 576 test della suite passano

v1.10.27 - 30 aprile 2026 (UX — chip "Libero dal debito" con data di liberazione su Snowball e Avalanche)

  • Problema iniziale — la Strategia Estinzione Debiti (src/components/debt-strategy.tsx) mostrava nelle card Snowball e Avalanche la durata del piano in due forme entrambe astratte: Math.ceil(months/12) anni come numero principale e months mesi come sottotitolo. La conversione "{months} mesi -> data calendario reale" e' lasciata implicita all'utente, che deve mentalmente partire da oggi e contare in avanti — esercizio non banale per orizzonti tipici dell'estinzione (60-180 mesi). La metrica "tempo" e' la leva motivazionale piu' forte di un piano debiti — ancor piu' degli interessi assoluti — e ridurla ad un numero di mesi le toglie corporeita': "84 mesi" e' un'astrazione, "aprile 2033" e' un appuntamento. Il pattern era gia' applicato altrove (KPI "Tempo Stimato al Completamento Totale" v1.10.25 negli Obiettivi di Risparmio mostra entro lug 2027 come data; il singolo goal mostra Stima: nov 2027 come chip): l'asimmetria con la Strategia Estinzione Debiti era un buco di coerenza UX evidente, e proprio sul componente dove l'ancoraggio temporale ha il valore comportamentale piu' alto perche' l'utente sta pianificando sacrifici concreti (extra mensile sopra le rate minime) per arrivare a quella data
  • Cosa e' stato modificato — aggiunto un chip Libero dal debito {mese} {anno} sotto la coppia di KPI (Tempo Totale, Interessi Pagati) di entrambe le card di strategia. Palette coordinata col branding di ogni card (blue-100/blue-700 per Snowball, orange-100/orange-700 per Avalanche), icona CalendarCheck2 da lucide-react per evocare il "traguardo segnato sul calendario", testo formattato come MMMM yyyy localizzato in italiano via date-fns/locale (aprile 2033 invece di April 2033) con text-transform: capitalize per la maiuscola iniziale del mese coerente con lo stile del resto dell'app. Tooltip esplicativo che esplicita le ipotesi del calcolo (rate minime + extra mensile sotto la strategia scelta), cosi' l'utente capisce immediatamente che la data e' deterministica condizionata agli input correnti, non una proiezione probabilistica. Il chip e' condizionale: nessun render quando months non e' finito, e' <= 0 (debito nullo o gia' estinto) o >= 600 mesi — la soglia magica di simulatePayoff (cap a 50 anni) che segnala matematicamente che il budget non basta a chiudere i debiti. In quel caso una "data di liberazione" sarebbe falsa rassicurazione (estinzione tecnicamente impossibile con gli input correnti); la card mantiene comunque le KPI esistenti, ma non promette un giorno che non arrivera' mai
  • Implementazione tecnica — modifica chirurgica al solo src/components/debt-strategy.tsx: tre nuovi import (addMonths e format da date-fns, it da date-fns/locale, CalendarCheck2 da lucide-react — tutte gia' usate altrove nel progetto, zero nuove dipendenze), una costante esplicita MAX_PAYOFF_MONTHS = 600 agganciata al cap interno di simulatePayoff con commento che spiega perche' e' la soglia di "insolvenza" (cosi' la UI non duplica un magic number senza contesto), e una helper pura locale formatDebtFreedomDate(months, now?) che ritorna la stringa formattata o null. La helper accetta now come parametro opzionale (default new Date()) per testabilita' deterministica futura, applica Math.ceil ai mesi (coerente col Math.ceil(months/12) esistente che non sottostima mai la durata), e usa addMonths di date-fns invece di setMonth raw cosi' la transizione fra anni e' robusta (capodanno, anni bisestili, edge case di setMonth su date con giorno 31). Inserito come IIFE ({(() => { ... })()}) inline nel JSX cosi' si evita di pollute l'oggetto result con un campo derivato che dipende da Date.now() (non puro, non memo-friendly): la logica vive dove serve. Suite test completa verde (567/567), ESLint pulito, zero errori TypeScript sul file modificato. Bumped a v1.10.27 in package.json
  • Perche' migliora l'esperienza utente — converte un'unita' astratta (mesi) in un'ancora narrativa concreta (mese e anno reali), aumentando di un ordine di grandezza la salience cognitiva del traguardo. La differenza fra "84 mesi" e "aprile 2033" non e' solo semantica: la seconda forma evoca eventi della propria vita ("mio figlio avra' 9 anni", "saro' al sesto anno in azienda", "tre anni dopo il mutuo") — ed e' proprio questa proiezione autobiografica che mantiene la motivazione sull'extra mensile. Il chip aggiunge anche una micro-decisione comparativa visiva fra Snowball e Avalanche: quando i due metodi differiscono per qualche mese l'utente vede direttamente la differenza in mesi del calendario (agosto 2032 vs giugno 2032) senza dover sottrarre. Coerente col pattern v1.10.25 della "data stimata" sui Goal: stesso linguaggio visivo, stessa palette MMMM yyyy italiana, stesso comportamento condizionale in caso di proiezione non significativa
  • Manutenibilita' — modifica isolata a un singolo componente (~40 righe di logica visualizzazione + helper di 5 righe). Nessuna modifica al modulo puro src/lib/finance/debt-strategy.ts o a debt-portfolio.ts: la data e' una pura derivata di un valore gia' calcolato, nessun rischio di drift fra moduli. Il pattern IIFE per il chip condizionale e' gia' usato in altri componenti del progetto e mantiene il computed locale alla zona JSX dove serve. Soglia MAX_PAYOFF_MONTHS centralizzata nel componente come singolo punto di manutenzione: se in futuro simulatePayoff dovesse alzare il cap, basta sincronizzare un valore. Suite test invariata e tutta verde — la helper non richiede test dedicati perche' non c'e' matematica nuova (solo addMonths + format di date-fns, gia' coperti dal vendor)

v1.10.26 - 30 aprile 2026 (UX — "Punto di Svolta" visibile direttamente sul grafico nel Calcolatore Interesse Composto)

  • Problema iniziale — il Calcolatore Interesse Composto (src/components/compound-interest-calculator.tsx) calcolava gia' il crossoverYear, cioe' il primo anno in cui gli interessi maturati superano il capitale versato (l'effetto compounding "prende il sopravvento"), e lo mostrava in due punti: la card laterale "Punto di Svolta" e una riga evidenziata in ambra nella tabella sotto al grafico. Mancava pero' il marcatore visivo sul grafico stesso, che e' l'elemento dominante della pagina: l'utente doveva fare un salto cognitivo "leggi Anno X dal pannello → cerca il punto sull'asse X" per collegare i tre artefatti
  • Cosa e' stato modificato — aggiunta una ReferenceLine di Recharts sul grafico AreaChart ancorata a Anno {crossoverYear} con linea tratteggiata ambra coerente con il colore gia' usato per il punto di svolta nelle altre due viste, label Punto di Svolta · Anno N posizionata in alto a destra e ifOverflow="extendDomain" per i casi limite. La linea appare solo quando crossoverYear !== null (con orizzonti corti o rendimenti bassi puo' non essere raggiunto e in quel caso continuiamo a mostrare ). Nessuna nuova dipendenza, nessun nuovo calcolo: riusa il valore gia' presente nel useMemo esistente
  • Perche' migliora l'esperienza utente — unifica il racconto narrativo del calcolatore: card laterale (numero), grafico (linea verticale ambra) e tabella (riga evidenziata) ora dicono la stessa cosa nello stesso colore, e l'utente vede immediatamente sul grafico quando l'area viola degli "Interessi" supera quella blu del "Versato". E' il momento didatticamente piu' importante della curva ed era l'unico spazio dove non veniva esplicitato visivamente
  • Manutenibilita' — modifica chirurgica (~17 righe) sul singolo componente. ReferenceLine e' gia' usata in 5 altri grafici dell'app (fire-dashboard, net-worth-projection, underwater-drawdown-chart, budget-trend-chart, advisor/fire-impact-chart) quindi il pattern e' consolidato. Suite test invariata e tutta verde

v1.10.25 - 30 aprile 2026 (UX — nuova KPI "Tempo Stimato al Completamento Totale" negli Obiettivi di Risparmio)

  • Problema iniziale - il componente Obiettivi di Risparmio (src/components/savings-goals.tsx) gia' mostrava 4 KPI aggregate nel pannello "Progresso Totale": Da risparmiare (capitale ancora da raccogliere), Ritmo richiesto (somma delle rate mensili necessarie per i goal con scadenza attiva), Ritmo attuale (somma del ritmo storico mensile sugli stessi goal con scadenza, con margine +/- rispetto al richiesto, KPI introdotta in v1.10.9) e Completati (count). Tutto il framework era pero' costruito sulle DEADLINE: ogni metrica forward-looking — ritmo richiesto, ritmo attuale, margine — era calcolata SOLO sui goal con deadline impostata e con monthsLeft > 0. Mancava una proiezione "se mantengo questo passo, quando finisco TUTTO" indipendente dalle scadenze: per un utente con goal senza deadline (es. fondo emergenza, viaggio aperto, investimento "ad libitum") l'app non rispondeva alla domanda piu' naturale — "in che tempo arrivero' a completare l'intera roadmap di risparmio?". Il singolo goal aveva gia' una stima getEstimatedDate() mostrata come chip ("Stima: nov 2027") quando senza deadline, ma a livello aggregato la roadmap era opaca: l'utente con 5 goal attivi doveva mentalmente sommare cinque chip e fare media pesata per capire "quando saro' libero da questa lista". Questo era un buco didattico tipico per uno strumento FIRE-oriented dove il valore aggiunto e' proprio dare scadenze realistiche, non solo numeri istantanei
  • Cosa e' stato modificato - aggiunto un nuovo callout Tempo Stimato al Completamento Totale nel pannello "Progresso Totale", posizionato sotto la griglia delle 4 KPI esistenti (sequenza didattica naturale: prima i numeri istantanei "quanto manca / a che ritmo vai", poi la proiezione narrativa "quando arrivi a completare tutto"). Palette emerald-50/teal con icona Hourglass da lucide-react per evocare il tema "tempo che scorre verso un traguardo", coerente col verde-emerald usato per Completati (4° KPI) e col branding generale del componente (header bg-emerald-50, accenti emerald). Mostra: il tempo in formato compatto italiano (5 mesi, 1 anno, 2 anni e 3 mesi — pluralizzazione corretta con mese/mesi e anno/anni, soppressione automatica dei 0 mesi residui quando il totale e' multiplo di 12), la data stimata corrispondente come sottotitolo (entro lug 2026, formato MMM yyyy localizzato in italiano via date-fns/locale), e una riga esplicativa che chiarisce il calcolo (al ritmo storico di €X/mese su N obiettivi attivi (mancano €Y)). Tooltip educativo sull'header che esplicita formula e indipendenza dalle deadline. Il callout si attiva solo quando monthsToCompletion !== null && estimatedCompletionDate !== null: con nessun goal attivo, ritmo aggregato zero (utente che ha appena creato i goal e non ha ancora risparmiato nulla), o tutti i goal completati il callout sparisce — coerente col pattern delle KPI condizionali del resto dell'app, niente card vuote o "scadenze infinite" che distrarrebbero
  • Implementazione tecnica - creato nuovo modulo puro src/lib/finance/savings-goals-completion.ts con la funzione computeSavingsGoalsCompletion(goals, now?) che ritorna SavingsGoalsCompletionResult (activeGoals: number, totalRemaining: number, aggregateMonthlyPace: number, monthsToCompletion: number | null, estimatedCompletionDate: Date | null). Algoritmo: per ogni goal sanifica targetAmount e currentAmount con Math.max(0, sanitize(x)) (clamp NaN/Infinity a 0, negativi a 0 — pattern canonico del progetto gia' adottato in subscription-fire-capital.ts v1.10.22, excess-liquidity.ts v1.10.15, savings-opportunity.ts v1.10.18 e sale-tax.ts v1.10.19), salta goal con target <= 0 o gia' completati (current >= target), accumula totalRemaining = sum(target - current) e aggregateMonthlyPace = sum(current / max(1, monthsElapsedFromCreatedAt)) (clamp a 1 mese per evitare divisione per zero su goal appena creati, stesso pattern usato dall'esistente getEstimatedDate() e getDeadlinePacing()). Quando activeGoals === 0 o aggregateMonthlyPace <= 0 o totalRemaining <= 0 ritorna monthsToCompletion: null esplicito (proiezione indefinita), altrimenti monthsToCompletion = ceil(totalRemaining / aggregateMonthlyPace) (arrotondamento per eccesso perche' "0.3 mesi" non ha valore operativo) e estimatedCompletionDate = now + monthsToCompletion mesi. Parametro now opzionale per testabilita' deterministica (default new Date()). createdAt invalido (stringa non parseable) viene gestito tramite check Number.isFinite(date.getTime()): il goal e' contato come attivo e contribuisce al totalRemaining, ma non al aggregateMonthlyPace — coerente col principio "input degenere non corrompe le metriche aggregate". Suite test src/lib/finance/savings-goals-completion.test.ts con 11 casi: nessun goal -> null, ignora goal completati, ignora target <= 0 (zero e negativo), single goal canonico (4 mesi fa, 400/1000 -> 100/mese, manca 600 -> 6 mesi), aggregazione multi-goal (somma remaining + somma pace -> ceil), pace zero -> null, clamp a 1 mese di elapsed, sanitizzazione NaN/Infinity senza propagare (verifica Number.isFinite su tutti i campi numerici), estimatedCompletionDate === now + monthsToCompletion mesi (regressione algebrica), createdAt invalido non contribuisce al pace, scenario misto completati+attivi (solo attivi pesano sul calcolo). Suite totale: 567/567 verde (11 nuovi su 556), eslint pulito, zero errori TypeScript sui file modificati. Componente: import della funzione + Hourglass da lucide-react, integrazione nel useMemo esistente (un solo recompute, dipendenza [goals] invariata), helper formatMonthsCompact() locale per la pluralizzazione italiana del tempo (testato implicitamente nel render condizionale)
  • Perche' migliora l'app finanziariamente - completa il quadro narrativo del componente Obiettivi di Risparmio aggiungendo la sola dimensione che mancava: la SCADENZA OPERATIVA dell'intera roadmap. Ora l'utente vede in un colpo d'occhio "quanto manca + a che ritmo vai + quando finisci": e' la triade canonica del FIRE planning ("how much / how fast / when") applicata ai goal di breve-medio termine. La proiezione e' indipendente dalle deadline impostate sui singoli goal, quindi funziona anche per chi usa lo strumento in modalita' "open ended" (fondo emergenza, viaggi, investimenti) — la maggioranza degli scenari reali secondo il pattern d'uso osservabile. Il messaggio comportamentale e' costruttivo ("entro lug 2027 sarai libero da questa lista") e non punitivo ("sei in ritardo del X%"): allineato alla Vantaggio Reale dell'Investire v1.10.23 e ai Costo Opportunita' v1.10.5/v1.10.18 nel framework "carrot non solo stick". La centralizzazione del calcolo in un modulo puro testato (savings-goals-completion.ts) abilita futuri usi della stessa proiezione in altri componenti — Header KPIs aggregate, sezione Riepilogo, alert "stai per completare un obiettivo" — senza duplicare logica matematica fragile

v1.10.24 - 30 aprile 2026 (bugfix robustezza Liquidazione Fondo Pensione: NaN propagation su exitTaxRate / accessAge / lifeExpectancy + deduplica logica nel Monte Carlo Worker)

  • Problema scovato - il modulo puro src/lib/finance/pension-fund.ts (usato dalla fire-dashboard.tsx per la proiezione FIRE deterministica e — tramite logica DUPLICATA inline — dal monte-carlo.worker.ts per i 10k run di stress test) sanitizzava SOLO pensionCap, lasciando passare qualunque valore non finito su exitTaxRate, accessAge, lifeExpectancy o exitMode. Le conseguenze osservabili erano due, entrambe critiche: (1) Math.max(0, NaN) === NaN (e specularmente Math.min(100, NaN) === NaN, Math.max(1, NaN) === NaN) — proprieta' del runtime JS che la maggior parte dei dev dimentica — quindi un singolo pensionFundExitTaxRate mancante in preferences (campo svuotato dall'utente, deserializzazione di una stringa, migrazione legacy non normalizzata) produceva taxRate = NaN, net = pensionCap * (1 - NaN/100) = NaN, monthlyAnnuity = NaN / NaN = NaN. Il NaN veniva poi sommato a tempCap nel ciclo della proiezione FIRE (tempCap += liq.cashLump) e a runCap in tutti i 10k run del Monte Carlo, trasformando sia la stima "anni al FIRE" sia la "probabilita' di successo" in NaN — metrica utente illeggibile o, peggio, silenziosamente sbagliata se cast a 0; (2) la stessa identica logica era duplicata inline nel monte-carlo.worker.ts con la stessa fragilita', un classico DRY violation che rendeva impossibile fixare il bug in un solo posto
  • Causa radice - il pattern di sanitizzazione canonico gia' adottato in fire-projection.ts (vedi commento "BUG FIX (NaN propagation)"), coast-fire.ts ("regressione #coast-fire-nan-propagation"), inflation.ts e sale-tax.ts (v1.10.19, "regressione #sale-tax-nan-propagation") non era stato esteso a pension-fund.ts. La suite test esistente copriva pensionCap: NaN ma NON gli altri tre input numerici, lasciando il bug invisibile fino a quando un utente reale non avesse svuotato il campo pensionFundExitTaxRate nel form delle preferenze. Il Math.max(0, x) originale clampa correttamente i valori negativi finiti ma fallisce silenziosamente su NaN — esattamente il pattern gia' fixato in tutti gli altri moduli del modulo finance
  • Fix applicato - introdotti tre helper di sanitizzazione coerenti col pattern del progetto: sanitizeFinite(value, fallback = 0) (NaN/Infinity/undefined -> fallback), sanitizeNonNegative(value, fallback = 0) (NaN/Infinity/undefined -> fallback, negativi -> 0) usato per pensionCap, e clampPercent(value) (NaN -> 0, < 0 -> 0, > 100 -> 100, +Infinity -> 100, -Infinity -> 0) usato per exitTaxRate. La sanitizzazione avviene una sola volta all'inizio della funzione, PRIMA di qualunque calcolo, garantendo che ogni campo del risultato (cashLump, monthlyAnnuity, netCapital) resti finito anche con tutti gli input NaN. Inoltre accessAge e lifeExpectancy passano per sanitizeFinite PRIMA della sottrazione: con entrambi NaN, (0 - 0) * 12 = 0, Math.max(1, 0) = 1 — divisore finito >= 1 garantito. Deduplicato monte-carlo.worker.ts rimuovendo la liquidazione inline e importando direttamente liquidatePensionFund dal modulo sanificato: stesso comportamento per il caso felice (verificato nei test esistenti), zero divergenze possibili in futuro fra proiezione deterministica e Monte Carlo
  • Test di regressione - estesa la suite src/lib/finance/pension-fund.test.ts con 6 nuovi casi mirati al pattern NaN-propagation: NaN exitTaxRate cae a 0% senza propagare NaN (verifica Number.isFinite su tutti i campi del risultato + valore atteso netCapital === pensionCap), undefined exitTaxRate cae a 0% (gestione del default mancante, scenario reale di campo non popolato), NaN accessAge non corrompe annuityMonths (sanificazione a 0 -> lifeExpectancy * 12 mesi totali, divisore finito), NaN lifeExpectancy non corrompe annuityMonths (sanificazione a 0 -> divisore clampato a 1 mese, scenario degenere ma matematicamente sano), tutti gli input non finiti insieme (Infinity exit tax + NaN accessAge + NaN lifeExpectancy) non producono mai NaN — invariante "dirty input safety" che blinda il modulo per sempre, +Infinity exit tax -> 100% (clamp a max fiscale, scenario speculare al 12.5% titoli di stato gia' coperto). Suite totale: 556/556 verde (6 nuovi su 550), eslint pulito, zero errori TypeScript sui file modificati
  • Perche' migliora l'app finanziariamente - elimina la falla piu' insidiosa rimasta nel modulo finance: un NaN propagato silenziosamente attraverso 10k run Monte Carlo NON produce un crash visibile, produce una "probabilita' di successo: NaN" che React puo' renderizzare come "—" o, peggio, un grafico vuoto senza spiegazione. Per un utente che sta valutando se andare in pensione anticipata, una proiezione FIRE corrotta dal NaN e' peggio di un errore esplicito perche' non c'e' segnale che dica "ricomponi le tue preferenze". La centralizzazione in liquidatePensionFund come UNICA fonte di verita' del progetto (eliminando il duplicato in monte-carlo.worker.ts) garantisce inoltre che future modifiche alla matematica della liquidazione (cambio aliquote 2027, nuova modalita' di uscita, formule di rendita aggiornate) si propaghino automaticamente sia alla proiezione deterministica sia al Monte Carlo — impossibile divergere. Chiude la stessa famiglia di regressioni gia' fixata in fire-projection.ts, coast-fire.ts, inflation.ts, sale-tax.ts (v1.10.19), portando a 5 il numero di moduli con sanitizzazione canonica esplicita

v1.10.23 - 29 aprile 2026 (UX — nuova KPI "Vantaggio Reale dell'Investire" nel Calcolatore Inflazione)

  • Problema iniziale - il Calcolatore Inflazione (src/components/inflation-calculator.tsx + src/lib/finance/inflation.ts) mostrava 4 KPI principali (Potere d'Acquisto Finale, Capitale Equivalente Futuro, Se Investito Nominale, Valore Reale Investito) e diversi chip secondari (Erosione inflazionistica, Tempo di Dimezzamento, Erosione Mensile Media, Risparmio Mensile Necessario). Tutto il messaggio era pero' costruito sulla scala dell'EROSIONE: quanto perdi se tieni i soldi fermi, quanto ti serve per stare in pari, quanto inflazione brucia ogni mese. Mancava la prospettiva DUALE — quanto preservi IN PIU' di potere d'acquisto investendo invece di tenere il cash. La differenza fra Valore Reale Investito (236k in scenario 100k @ 7% nominale, 2.5% inflazione, 20 anni) e Potere d'Acquisto Finale (61k nello stesso scenario) c'era visivamente, ma il delta di 175k — cioe' il vantaggio reale assoluto dell'investire — l'utente doveva calcolarselo a mente. Per un'app FIRE-oriented questo era un buco didattico: lo strumento misurava bene la perdita potenziale (motivazione "stick", paura) ma non il guadagno potenziale dell'azione corretta (motivazione "carrot", speranza). Era simmetrico al pattern gia' adottato nel Budget Tracker (Costo Opportunita' del Risparmio a 30 Anni, v1.10.18) e nel Tracker Abbonamenti (Costo Opportunita' a 30 Anni, v1.10.5), dove il messaggio comportamentale e' "ecco quanto guadagni se fai la cosa giusta" e non solo "ecco quanto perdi se non la fai"
  • Cosa e' stato modificato - aggiunto un nuovo chip Vantaggio Reale dell'Investire nel Calcolatore Inflazione, posizionato fra il blocco Risparmio Mensile Necessario e il grafico Evoluzione nel Tempo (sequenza didattica naturale: prima il piano operativo "quanto devi versare", poi il bonus narrativo "ecco cosa ottieni in piu'", poi la visualizzazione storica). Palette emerald/teal-gradient con icona ShieldCheck da lucide-react per evocare il tema "protezione del potere d'acquisto", coerente col verde-emerald gia' usato per Valore Reale Investito (4° KPI principale) e per il pattern di "vincita finanziaria" attraverso l'app. Mostra: il vantaggio reale in euro odierni in evidenza (+€175.131 di potere d'acquisto preservato), la quota in percentuale del capitale iniziale (+175% sul capitale iniziale) come metrica scalabile invariante, e una riga esplicativa che chiarisce il calcolo (rispetto a tenere €100.000 fermi per 20 anni: e' quanto investire al 7% nominale ti restituisce in euro odierni in piu' rispetto al cash eroso dall'inflazione). Tooltip educativo sull'header che esplicita la formula (finalReal - finalPurchasingPower) e il suo significato comunicativo. Il chip si attiva solo quando realInvestmentAdvantage > 0: con rendimento nominale = 0, anni = 0, capitale = 0 o input non finiti il chip sparisce — coerente col pattern delle KPI condizionali del resto dell'app, niente card vuote o "vantaggi nulli" che distrarrebbero
  • Implementazione tecnica - estesa la InflationProjectionResult di src/lib/finance/inflation.ts con due nuovi campi: realInvestmentAdvantage: number (delta in euro odierni fra valore reale investito e potere d'acquisto del cash, clampato a 0 per evitare numeri scoraggianti quando il rendimento nominale e' nullo o negativo) e realInvestmentAdvantagePct: number (la stessa quantita' espressa come percentuale del capitale iniziale, invariante sulla scala — utile per confrontare scenari con capitali diversi). Formula realInvestmentAdvantage = max(0, finalReal - finalPurchasingPower) e realInvestmentAdvantagePct = amount > 0 ? advantage / amount * 100 : 0. Sanitizzazione preventiva ereditata dal pattern gia' presente nel modulo (sanitize() su tutti gli input, gia' adottato per monthlySavingsToPreservePurchasingPower v1.10.2 e averageMonthlyPurchasingPowerLoss v1.10.14). Suite test estesa src/lib/finance/inflation.test.ts con 9 nuovi casi nel describe("realInvestmentAdvantage"): regressione del valore canonico (100k @ 2.5% / 7% / 20 anni → ~175k, range 150-200k per assorbire arrotondamenti), invariante "rendimento nominale = inflazione → vantaggio = lostValue" (con real return = 0, finalReal ~ amount, advantage = lostValue, perche' investire restituisce esattamente il capitale iniziale in reale e quel "non perdere" si trasforma in vantaggio rispetto al cash che invece perde), invariante "inflazione zero → vantaggio = guadagno nominale" (senza inflazione finalPurchasingPower = amount e finalReal = finalNominal, quindi advantage = nominal gain), edge case "rendimento nominale = 0 → vantaggio = 0" (con tasso nominale nullo il valore reale dell'investimento eguaglia il potere d'acquisto del cash, no benefit), clamp con scenari real return negativo, edge case amount = 0 e years = 0, regressione algebrica realInvestmentAdvantagePct = advantage / amount * 100, linearita' nel capitale (vantaggio assoluto scala 10x con capitale 10x, vantaggio percentuale invariante), sanitizzazione NaN/Infinity senza propagare. Suite totale: 550/550 verde (9 nuovi su 541). Componente: import dell'icona ShieldCheck da lucide-react, destrutturazione dei due nuovi campi dal projection, blocco JSX condizionale fra Risparmio Mensile Necessario e il grafico. Lint pulito, zero errori TypeScript sui file modificati
  • Perche' migliora l'esperienza utente - chiude il ponte logico fra "ecco quanto perdi tenendo cash" e "ecco quanto guadagni investendo", esattamente come Costo Opportunita' ha fatto per gli abbonamenti (v1.10.5) e per il budget (v1.10.18): tre strumenti diversi (calcolatore inflazione / abbonamenti / budget) parlano ora la stessa lingua duale "perdita evitata + guadagno potenziale". Il messaggio "+€175.131 di potere d'acquisto preservato investendo invece di tenere cash per 20 anni" e' visceralmente piu' motivante del solo "perdi €39k tenendo cash" perche' lavora sulla scala mentale del PATRIMONIO (il valore di un'azione, non il costo dell'inazione). La percentuale invariante (+175% sul capitale iniziale) e' particolarmente potente perche' permette al lettore di pensare in termini scalabili: "investire raddoppia con vantaggio il mio capitale in potere d'acquisto, indipendentemente dal numero che ho oggi". E' simmetrico ma opposto a Erosione Mensile Media (v1.10.14, "se tieni cash perdi X €/mese") e a Capitale Equivalente Futuro ("ti serviranno X € per comprare lo stesso paniere fra N anni"): le tre metriche raccontano la stessa storia da angoli complementari (perdita marginale mensile / capitale necessario per stare in pari / guadagno realizzato investendo). Il chip sparisce quando il vantaggio e' nullo invece di mostrare "+€0" — la psicologia del nudge e' importante: non spingiamo chi non ottiene benefit, valorizziamo chi sta facendo bene, in linea col pattern dell'app. Marginal gain dell'1% nel calcolatore che dovrebbe essere il principale strumento di consapevolezza inflazionistica dell'app, ora bilanciato fra messaggio di perdita (tutti gli altri KPI) e messaggio di guadagno (questo nuovo chip)

v1.10.22 - 29 aprile 2026 (UX — nuova KPI "Capitale FIRE Necessario" nel Tracker Abbonamenti)

  • Problema iniziale - il SubscriptionTracker (src/components/subscription-tracker.tsx) mostrava 4 KPI (Costo Mensile, Costo Annuale, Costo Opportunita' a 30 Anni, Abbonamento piu' costoso) ma NON traduceva mai la spesa ricorrente nel suo "patrimonio FIRE-equivalente": ovvero quanto capitale dovresti avere INVESTITO perche', prelevandone il 3.25% reale all'anno (il SWR di default dell'app, allineato col DEFAULT_FIRE_WITHDRAWAL_RATE di fire-metrics.ts), genera ogni anno una rendita pari al costo annuale degli abbonamenti, teoricamente a tempo indefinito. E' la "regola del 25x/30.77x" applicata ai costi ricorrenti, ed e' la prospettiva DUALE rispetto al Costo Opportunita' a 30 Anni esistente: l'opportunita' misura "cosa accumuleresti se investissi quella cifra mensile" (montante futuro), il capitale FIRE misura "cosa ti serve gia' avere per pagarli per sempre senza lavorare" (stock perpetuo). Erano due lati della stessa medaglia FIRE, ma il secondo mancava — un'app che si chiama "Effetto Composto" e ha al cuore l'indipendenza finanziaria non aveva il numero che lega DIRETTAMENTE i costi ricorrenti al patrimonio richiesto per affrancarsene
  • Cosa e' stato modificato - aggiunta una nuova card Capitale FIRE Necessario nel SubscriptionTracker, posizionata fra Costo Opportunita' a 30 Anni e Abbonamento piu' costoso (sequenza didattica naturale: prima il costo del flusso → poi cosa accumuleresti investendolo → poi quanto stock ti serve per coprirlo a vita → infine il taglio piu' efficace). Palette orange/amber-gradient con icona Flame da lucide-react per evocare il tema FIRE, distinta visivamente dal verde-emerald del Costo Opportunita' (verde = guadagno potenziale futuro) e dall'amber del Abbonamento piu' costoso (amber = warning/azione concreta). Mostra: il capitale FIRE necessario in evidenza (cifra principale), il sottotitolo per coprire €X/anno al 3.25% SWR, e l'esplicitazione della "regola del Nx" (regola del 30.8x sul costo annuale) che rende trasparente il moltiplicatore implicito. Tooltip educativo che chiarisce: il SWR e' prudente vs il classico 4% Trinity, la card e' la prospettiva DUALE al Costo Opportunita', il significato "patrimonio richiesto per affrancarti dal lavoro su questa spesa". La card si attiva solo quando totals.monthly > 0 && fireCapital.requiredFireCapital > 0: con zero abbonamenti o spese non finite la card sparisce, coerente col pattern delle KPI condizionali del pannello
  • Implementazione tecnica - estratta la matematica in un nuovo modulo puro src/lib/finance/subscription-fire-capital.ts con la funzione computeSubscriptionFireCapital({ monthlyAmount, swrPct? }) che ritorna { swrPct, annualCost, requiredFireCapital, capitalMultiplier }. Formula requiredFireCapital = annualCost / (swrPct / 100) (rendita perpetua a tasso costante: capitale × tasso = rendita annua → capitale = rendita / tasso). Sanitizzazione preventiva su tutti gli input numerici (NaN/Infinity → fallback default 3.25%, negativi clampati a 0 — coerente col pattern gia' adottato in subscription-opportunity.ts, savings-opportunity.ts, inflation.ts). Caso degenere swrPct <= 0 ritorna requiredFireCapital = 0 (una rendita perpetua richiede un tasso positivo per essere matematicamente definita; con SWR ≤ 0 il calcolo e' indefinito e mostrare "Infinity" o un numero astronomico sarebbe disinformativo). Riusa la costante canonica DEFAULT_FIRE_WITHDRAWAL_RATE di fire-metrics.ts come default del SWR (esportata appositamente — non era esportata prima): UNICA fonte di verita' del progetto per il SWR, garantisce coerenza con la Rendita Mensile FIRE del Calcolatore Interesse Composto, con FIRE Number del Riepilogo, e con tutta la macchina FIRE dell'app. Suite test dedicata src/lib/finance/subscription-fire-capital.test.ts con 10 nuovi casi: default canonici (3.25% applicato quando omesso), monthlyAmount = 0 → tutti zero, regola del 25x classica al 4% SWR (€100/mese → €30k), regola del ~30.77x al 3.25% SWR (€50/mese → €18.46k), linearita' nel costo (raddoppiando la spesa raddoppia il capitale, proprieta' della rendita perpetua, tolleranza 4 cifre), monotonia inversa nel SWR (SWR piu' alto → capitale richiesto minore), swrPct <= 0 → capitale zero (caso degenere), sanitizzazione NaN/Infinity senza propagare al risultato, monthlyAmount negativo clampato, invariante annualCost = monthly × 12. Suite totale: 540/540 verde (10 nuovi su 530). Componente: import della nuova funzione + costante + icona Flame da lucide-react, calcolo di fireCapital dentro un nuovo useMemo con dipendenza [totals.monthly] (zero re-render aggiuntivi se la spesa totale non cambia), JSX della card condizionale fra Costo Opportunita' e Abbonamento piu' costoso. Lint pulito, zero errori TypeScript sui file modificati
  • Perche' migliora l'esperienza utente - chiude il cerchio fra il Tracker Abbonamenti e il tema FIRE dell'intera app, esattamente come Costo Opportunita' ha fatto sul lato del flusso futuro: ora il pannello parla due lingue FIRE complementari (quanto accumuleresti / quanto ti serve avere). Il messaggio "ti servono €36.923 di capitale per pagare per sempre i tuoi €100/mese di abbonamenti" e' visceralmente piu' comunicativo del solo "spendi €1.200/anno" perche' lavora sulla scala mentale del PATRIMONIO (l'utente FIRE pensa in termini di "quanto patrimonio mi serve per smettere di lavorare"), non solo del flusso. La regola del Nx esposta esplicitamente (30.8x sul costo annuale) rende trasparente la matematica e crea un nudge educativo: ogni €10/mese di abbonamento richiede ~€3.700 di capitale FIRE — un numero che, sommato sui 5-6 abbonamenti tipici di un italiano, giustifica decisioni di taglio aggressive. La palette orange/amber con icona Flame mantiene la coerenza visuale con la Rendita Mensile FIRE del Calcolatore Interesse Composto: stesso semantismo "FIRE-target", stessa fonte di verita' del SWR (3.25%). Marginal gain dell'1% nel pannello che, per un utente FIRE-oriented, lega la spesa ricorrente piu' frequentemente sottovalutata (gli abbonamenti) al patrimonio richiesto per affrancarsene — il numero che davvero spinge al taglio del superfluo

v1.10.21 - 28 aprile 2026 (UX — nuova KPI "Costo Mensile Interessi" nel Riepilogo Portafoglio Debiti)

  • Problema iniziale - la card Riepilogo Portafoglio Debiti (src/components/debt-strategy.tsx + src/lib/finance/debt-portfolio.ts, introdotta in v1.10.13) mostrava 3 KPI — Saldo Totale, Tasso Medio Ponderato e Rate Minime Totali — ma NON traduceva mai l'esposizione attuale nel suo "tassametro" mensile, ovvero quanti euro il portafoglio brucia in soli interessi nel mese corrente a saldo invariato. La conseguenza didattica era doppia: (1) il Tasso Medio Ponderato e' una percentuale astratta che molti risparmiatori italiani fanno fatica a tradurre mentalmente in un costo concreto in euro/mese (con €17k di debito al 9.6% ponderato, dire "9.6% annuo" e' molto meno comunicativo che dire "€136/mese di soli interessi"), e (2) mancava un numero direttamente confrontabile con le altre voci di spesa fisse dell'utente — proprio nel pannello che dovrebbe spingere all'estinzione anticipata, il costo del debito restava invisibile sull'asse temporale che le persone usano davvero (il mese). Era un buco simmetrico al "latte factor" del Tracker Abbonamenti (v1.10.5) ma sul lato debito: lo strumento misurava il futuro (snowball/avalanche, mesi risparmiati) e il presente in percentuali (tasso medio), ma non il presente in euro/mese
  • Cosa e' stato modificato - aggiunta una quarta KPI Costo Mensile Interessi nella card Riepilogo Portafoglio Debiti con palette rose/rossa coerente col semantismo "perdita ricorrente" gia' usato altrove nell'app (saldo debito, interessi pagati nel confronto snowball/avalanche). Mostra: il valore principale in euro/mese (cifra in evidenza), il sottotitolo annualizzato (X €/anno solo di interessi, perche' su orizzonte annuale il costo si percepisce ancora piu' violentemente — €136/mese diventano €1.640/anno) e un fallback testuale "nessun interesse a saldo invariato" quando tutti i tassi sono 0 (debito promo o tasso zero) per mantenere la card sempre presente quando ci sono debiti attivi. Tooltip educativo che chiarisce sia il calcolo (somma di saldo × tasso / 12 per ogni debito attivo) sia il messaggio comportamentale chiave: "una rata mensile che resta sotto questa soglia non riduce il capitale, copre solo gli interessi" — e' la traduzione concreta del concetto di amortization che molti scoprono solo dopo anni di pagamenti che sembrano non scalfire il saldo. Il layout della card e' passato da sm:grid-cols-3 a sm:grid-cols-2 lg:grid-cols-4 per accomodare la quarta KPI senza compressione su mobile (su sm due righe da 2 colonne, su lg una riga unica da 4 colonne). Icona Banknote da lucide-react per evocare "denaro che esce", visivamente distinto dalle altre 3 card ma palette coerente con il Saldo Totale (entrambi in rose-rosso, perche' entrambi rappresentano la stessa "ferita finanziaria" da prospettive diverse: lo stock e il flusso)
  • Implementazione tecnica - estesa la DebtPortfolioSummary con il campo monthlyInterestCost: number calcolato come Σ (balance * rate / 100 / 12) su tutti i debiti con saldo > 0. La sanitizzazione e il clamping sono ereditati dalla logica gia' presente nel modulo debt-portfolio.ts: i debiti con saldo <= 0 sono ignorati (gia' estinti o input degeneri), i tassi negativi sono clampati a 0 (no "interessi negativi" che sarebbero un regalo, non una rata), gli input non finiti (NaN/Infinity) vengono normalizzati a 0 senza propagare al risultato. Il calcolo e' O(n) su una sola passata, condivide il loop esistente (zero costo computazionale aggiuntivo). Suite test estesa src/lib/finance/debt-portfolio.test.ts con 6 nuovi casi nel describe("computeDebtPortfolioSummary - monthlyInterestCost"): regressione del calcolo canonico (€5k @ 18% + €12k @ 5.5% = €130/mese, aggancio numerico col valore atteso), 0 per lista vuota, 0 quando tutti i tassi sono 0 (debiti promo), debiti gia' estinti ignorati, tassi negativi clampati senza propagare, sanitizzazione NaN/Infinity, e CRITICO l'invariante algebrica monthlyInterestCost === totalBalance * weightedAverageRate / 100 / 12 (le due formule item-by-item e aggregata devono coincidere per ogni distribuzione di saldi/tassi positivi — questo test blinda la coerenza fra le due metriche del riepilogo per sempre). Aggiornato anche il test "lista vuota" e il test "singolo debito" per includere il nuovo campo (regressione: il singolo debito da 1000 @ 12% deve produrre 10 €/mese di interessi). Suite totale: 530/530 verde (6 nuovi su 524 + 1 esistente esteso). Componente: import dell'icona Banknote da lucide-react, aggiunta della quarta card nel grid del riepilogo, aggiornamento del tooltip dell'header del riepilogo per riflettere la quarta metrica. Lint pulito, zero errori TypeScript sui file modificati
  • Perche' migliora l'esperienza utente - chiude il ponte logico fra "ho un debito al 9.6%" e "sto bruciando €136/mese di soli interessi", che e' il numero che davvero spinge al comportamento corretto (estinzione anticipata, surroga, consolidamento). E' lo stesso pattern che il Tracker Abbonamenti applica al lato spese (Costo Mensile/Costo Annuale) e che la card Liquidita' Eccessiva del Riepilogo applica al lato cash inattivo: tradurre in euro/mese il costo di una scelta finanziaria. Il messaggio annualizzato (X €/anno solo di interessi) e' particolarmente potente per debiti grandi: chi ha un mutuo da €200k al 4% scopre di pagare €8.000/anno di soli interessi — un dato che la rata mensile mescolata con la quota capitale tiene nascosto. Combinato col Tasso Medio Ponderato (per valutare un consolidamento) e col Rate Minime Totali (per il flusso minimo), il riepilogo diventa adesso uno strumento decisionale completo per il debito attuale, complementare al confronto snowball/avalanche che invece guarda al futuro. La card sparisce solo quando non ci sono debiti attivi (coerente col pattern delle KPI condizionali del resto dell'app: niente card vuote o numeri a zero che distrarrebbero da quelle utili). Marginal gain dell'1% nel pannello che orienta la decisione finanziaria piu' costosa nel breve periodo (estinguere o no un debito esistente)

v1.10.20 - 28 aprile 2026 (UX — nuova KPI "Multiplicatore del Capitale" nel Calcolatore Interesse Composto)

  • Problema iniziale - il Calcolatore Interesse Composto (src/components/compound-interest-calculator.tsx) mostrava 10 KPI (Capitale Finale, Totale Versato, Interessi Guadagnati, % da interessi composti, Valore Reale, Punto di Svolta, Guadagno Reale, Tempo di Raddoppio nominale+reale, Rendita Mensile FIRE, Costo del Ritardo) ma NON traduceva mai il piano nel suo "rendimento moltiplicativo" — il rapporto finalBalance / totalDeposited, ovvero "ogni € versato → €X". E' la metrica FIRE-friendly piu' visceralmente comunicativa per descrivere l'effetto composto, e l'unica che ha senso in un calcolatore intitolato "Interesse Composto": piu' del % di interessi maturati ("60% da composto" e' una quota astratta del finale) e piu' del saldo aggregato ("€500k a fine piano" e' un numero senza scala), il numero "ogni € → €Y" rende immediato cogliere quanto il tempo + il rendimento moltiplichino il capitale versato. Il buco era anche piu' evidente sul lato REALE: nessuna KPI rispondeva a "quanto vale, in potere d'acquisto odierno, ogni € che ho versato a fine piano?" — un numero che con rendimento reale negativo puo' essere < 1 e diventa un nudge potentissimo a spostare la liquidita' dai depositi infruttiferi agli investimenti
  • Cosa e' stato modificato - aggiunta una nuova card Multiplicatore del Capitale subito sotto la riga "% da interessi composti" (posizione semanticamente coerente perche' le due card descrivono la stessa relazione da prospettive complementari: la prima la QUOTA del finale che e' compound, la seconda QUANTE VOLTE il versato e' stato moltiplicato — sono matematicamente legate da nominalMultiplier = 1 / (1 - %compound) e un test esplicito blinda l'invariante). Mostra il valore principale "Ogni 1€ → €X" in viola/fuchsia (palette nuova non in conflitto con le altre card), il sottotitolo descrittivo "nominali a fine piano (N anni @ R%)", e — quando realMultiplier !== null — il valore reale "€Y in euro odierni" colorato in verde quando >= 1 (potere d'acquisto preservato/aumentato) e in rosa quando < 1 (potere d'acquisto perso, con etichetta esplicita "potere d'acquisto perso"). Tooltip educativo che chiarisce le tre cose importanti: (1) il moltiplicatore nominale e' "quello che vedi a saldo", (2) il moltiplicatore reale e' "la misura piu' onesta dell'effetto composto perche' tiene conto del fatto che i € futuri valgono meno di quelli odierni", (3) un valore reale < 1 vuol dire che, nonostante la crescita nominale, hai perso potere d'acquisto. La card si attiva solo quando nominalMultiplier !== null (i.e. totalDeposited > 0): se l'utente non ha versato nulla la card sparisce, coerente col pattern delle KPI condizionali del calcolatore
  • Implementazione tecnica - estratta la matematica in un nuovo modulo puro src/lib/finance/capital-multiplier.ts con la funzione computeCapitalMultiplier({ finalBalance, realFinalBalance, totalDeposited }) che ritorna { nominalMultiplier, realMultiplier, inflationDrag } con null in tutti i campi quando totalDeposited <= 0 (no divisione per zero, no Infinity propagato all'UI). Sanitizzazione preventiva su tutti gli input numerici (NaN/Infinity normalizzati a 0, negativi clampati a 0 — coerente col pattern gia' adottato in fire-projection.ts, inflation.ts, compound-interest.ts, excess-liquidity.ts). Il campo inflationDrag = nominalMultiplier - realMultiplier e' esposto per uso futuro (es. evidenziare visivamente la "tassa silenziosa" dell'inflazione) anche se per ora la UI mostra solo i due moltiplicatori principali. Suite test dedicata src/lib/finance/capital-multiplier.test.ts con 14 nuovi casi: calcolo base (3x nominale + 2.4x reale), totalDeposited = 0 -> tutti null, totalDeposited negativo -> null (clamp + null), finalBalance = totalDeposited -> moltiplicatore 1 (rendimento zero), regressione realMultiplier < 1 con rendimento reale negativo (caso piu' importante per il messaggio comportamentale), invariante algebrica inflationDrag = nominal - real, scaling 2x, invarianza di scala (10k:30k = 100k:300k), input NaN/Infinity sanitizzati, totalDeposited = NaN -> null, realFinalBalance negativo clampato a 0, finalBalance = 0 -> moltiplicatori = 0 non null (il caso "perdita totale" deve restare visibile, non sparire), scenario tipico FIRE realistico (10k + 300/mese per 30 anni @ 7% / 2.5% inflazione: nominale 3-3.5x, reale 1.4-1.7x), e CRITICO la coerenza algebrica con la card "% da interessi composti" (nominalMultiplier = 1 / (1 - pctCompound)) — questo test blinda che le due card non possano divergere semanticamente in nessun scenario futuro. Suite totale: 523/523 verde (14 nuovi su 509). Componente: import della nuova funzione + icona Layers da lucide-react, calcolo di multiplier dentro la useMemo esistente (zero re-render aggiuntivi, dipendenze invariate), aggiunta nominalMultiplier e realMultiplier al return della useMemo, JSX della card condizionale appena sotto la card "% da interessi composti". Lint pulito, zero errori TypeScript sui file modificati
  • Perche' migliora l'esperienza utente - chiude il buco didattico piu' evidente del calcolatore che e' il "biglietto da visita" dell'intera app (il prodotto si chiama letteralmente "Effetto Composto"): adesso l'utente vede in tempo reale quante volte i suoi versamenti si moltiplicano, sia in nominale che in potere d'acquisto odierno. Il messaggio "ogni 1€ → €3.20" e' incomparabilmente piu' visceralmente comunicativo del "60% del finale e' compound" perche' lavora su una scala mentalmente comprensibile: 1€ in 1€ e' qualcosa che ogni utente sa visualizzare, una percentuale del finale e' un'astrazione. La distinzione nominale/reale e' poi il vero valore aggiunto: con scenario tipico FIRE (7%/2.5% per 30 anni) il moltiplicatore reale e' ~1.5x mentre quello nominale e' ~3.2x — la differenza non e' marginale, e mostrarla esplicitamente educa l'utente al fatto che meta' della "crescita nominale" e' regalo dell'inflazione e NON aumento di potere d'acquisto. Quando il moltiplicatore reale scende sotto 1 (rendimento < inflazione, scenario tipico dei depositi a vista) il messaggio diventa un nudge chirurgico: "il tuo capitale, in potere d'acquisto odierno, vale MENO di quanto hai versato". E' il numero che traduce in una sola cifra "stai facendo bene" o "stai perdendo soldi" — coerente con la filosofia dell'app di tradurre i concetti FIRE in scelte quotidiane. Marginal gain dell'1% nel pannello che, in un'app FIRE-oriented, e' il piu' importante per orientare la decisione di lungo periodo (investire vs tenere fermo)

v1.10.19 - 28 aprile 2026 (bugfix robustezza Calcolo Fiscale Vendita: NaN propagation + aliquota fuori range + tassa negativa)

  • Problema scovato - il modulo puro src/lib/finance/sale-tax.ts (usato dalla SaleTaxModal del Patrimonio e dagli AI tools src/lib/ai/{tools,server-tools}.ts per simulare la tassa sulla vendita di azioni/ETF/cripto/titoli di stato) NON sanitizzava nessun input numerico ed era l'UNICO file in src/lib/finance/*.ts privo di test (sale-tax.test.ts non esisteva). Le conseguenze osservabili erano tre: (1) un singolo shares o currentPrice non finito (campo svuotato della modale, payload AI corrotto, parse error a monte) produceva Math.max(0, NaN) = NaN, propagando €NaN su quattro KPI affiancati della modale (Ricavo lordo, Costo base, Tassa, Netto incassato) e lasciando l'utente senza alcuna indicazione sulla tassa dovuta; (2) un taxRatePct aberrante (es. 200%) faceva si' che taxAmount = taxableGain * 2, producendo netProceeds = grossProceeds - 2 * taxableGain negativo — risultato matematicamente impossibile per una vendita (peggiore aliquota fiscale reale: 100%, ovvero esproprio totale del guadagno; mai oltre); (3) accumulatedLosses negative (errore di parsing o input AI sporco) saltavano il branch if (accumulatedLosses > 0) ma poi venivano restituite tale-e-quale come remainingLoss, propagando il valore "sporco" alla simulazione delle vendite future
  • Causa radice - il pattern di sanitizzazione canonico gia' adottato in fire-projection.ts (vedi commento "BUG FIX (NaN propagation)"), coast-fire.ts ("regressione #coast-fire-nan-propagation") e inflation.ts non era stato esteso a sale-tax.ts. Il Math.max(0, x) nei calcoli di grossProceeds/costBasis clampa correttamente i valori negativi finiti ma fallisce silenziosamente su NaN (Math.max(0, NaN) === NaN per design del runtime JS). Allo stesso modo, l'unico clamp esistente sulla tax rate era sul DEFAULT (taxRatePct = IT_CAPITAL_GAIN_TAX * 100 se non passata), non sul VALUE (chiunque passasse 200, -10 o Infinity passava intatto)
  • Fix applicato - introdotti due helper di sanitizzazione coerenti col pattern del progetto: sanitizeNonNegative(value, fallback = 0) (NaN/Infinity/undefined -> fallback, negativi -> 0) usato per shares, currentPrice, averageCost, accumulatedLosses, e clampTaxRatePct(value) (NaN/Infinity/undefined -> default 26%, < 0 -> 0, > 100 -> 100, in range -> identita') usato per taxRatePct. La sanitizzazione avviene una sola volta all'inizio della funzione, PRIMA di qualunque calcolo, garantendo che ogni campo del risultato (grossProceeds, costBasis, capitalGain, taxableGain, taxAmount, taxRatePct, compensatedLoss, remainingLoss, netProceeds, effectiveTaxRate) resti finito anche con tutti gli input NaN. Aggiunto inoltre un fix di semantica fiscale: il branch else originale (loss) eseguiva remainingLoss = accumulatedLosses + |capitalGain| anche per capitalGain === 0 (break-even), incrementando inutilmente il bagaglio minusvalenze di +0. Ora la condizione e' else if (capitalGain < 0), idempotente sul break-even
  • Test di regressione - nuova suite src/lib/finance/sale-tax.test.ts con 21 casi distribuiti in 5 sezioni: calcolo base (4 casi: profitto 26%, perdita azzera tassa, aliquota 12.5% titoli stato, break-even non aggiunge minusvalenza), compensazione minusvalenze (3 casi: parziale, totale, somma loss), sanitizzazione input - regressione #sale-tax-nan-propagation (7 casi: shares NaN, currentPrice Infinity, averageCost NaN, shares/prezzo negativi, accumulatedLosses negative/NaN, tutti NaN insieme — con helper expectAllFinite che blinda 10 campi di output), clamping aliquota - regressione #sale-tax-rate-out-of-bounds (4 casi: 200% -> 100%, -5% -> 0%, Infinity -> default 26%, 0% legittima rispettata), invarianti su netProceeds (3 casi: netProceeds >= costBasis con aliquota 100%, netProceeds = grossProceeds in perdita, effectiveTaxRate <= taxRatePct nominale). Suite globale 509/509 verde (21 nuovi su 488), eslint pulito, zero regressioni sui 42 file di test esistenti
  • Perche' migliora l'app finanziariamente - elimina due falle che producevano numeri matematicamente impossibili nella UI fiscale dell'utente (€NaN diffuso, netProceeds negativo, effectiveTaxRate > 100%) — proprio dove la fiducia nei calcoli e' piu' importante perche' l'utente sta valutando se VENDERE realmente un titolo. La sanitizzazione e' coerente col pattern del resto del progetto e protegge anche gli AI tools (src/lib/ai/tools.ts e server-tools.ts) da payload non finiti generati da catene LLM imperfette. Il test "netProceeds >= costBasis quando il gain e' positivo" cattura una proprieta' fondamentale della fiscalita' italiana che prima non era enforced: nessun utente puo' incassare meno del costo originario investendo, anche con aliquota piena. La logica e' ora pura, riusabile e coperta da test, in linea con la convenzione del progetto (vedi fire-projection.ts, coast-fire.ts, inflation.ts, excess-liquidity.ts)

v1.10.18 - 27 aprile 2026 (UX — nuova KPI "Costo Opportunita' del Risparmio a 30 Anni" nel Budget Tracker)

  • Problema iniziale - il BudgetTracker (src/components/budget-tracker.tsx + src/components/budget/budget-kpi-cards.tsx) mostrava quattro KPI principali — Entrate, Spese, Tasso Risparmio, Categorie Oltre — ma NON traduceva mai il surplus mensile (entrate - uscite) nel suo equivalente capitale futuro. Per un'app FIRE-oriented questo e' un buco didattico evidente: il Tracker Abbonamenti dal v1.10.5 ha una card Costo Opportunita' a 30 Anni che fa esattamente quel ponte (latte factor positivo: cifra mensile -> capitale composto), ma il pannello che dovrebbe essere il PIU' importante per orientare il comportamento di risparmio quotidiano non lo aveva. L'utente vedeva "risparmio €350/mese, 28% del reddito" e si fermava li': mancava il numero che traduce quel ritmo nel "perche' lo sto facendo" — "se mantieni questo ritmo, in 30 anni hai €X di capitale FIRE in potere d'acquisto odierno". Senza quel ponte il Budget Tracker era uno strumento di consapevolezza retrospettiva, non un nudge prospettico
  • Cosa e' stato modificato - aggiunta una nuova card Costo Opportunita' del Risparmio a 30 Anni nel componente BudgetKpiCards, posizionata SOTTO la griglia delle 4 KPI principali (full-width), con palette emerald/teal-gradient identica a quella della card analoga del Tracker Abbonamenti per coerenza visuale del pattern "latte factor" attraverso l'app. Mostra: il valore composto reale (cifra principale, in evidenza), l'input mensile usato + il rendimento + l'orizzonte (sottotitolo descrittivo), e — quando compoundGain > 0 — la quota di soli interessi composti e il totale versato (riga educativa che chiarisce "quanta parte e' regalo del compound"). Tooltip esplicativo sull'header chiarisce assunzioni (4% reale, 30 anni, capitalizzazione mensile, valore in potere d'acquisto odierno) e il messaggio comportamentale ("ponte fra il budget mensile e l'indipendenza finanziaria"). La card si attiva solo quando hasData && monthlySavings > 0: quando l'utente non ha ancora importato transazioni, quando spende quanto guadagna, o quando spende piu' di quanto guadagna, la card sparisce (no card vuote ne' "perdite ipotetiche", coerente col pattern delle KPI condizionali del resto dell'app). Icona Sparkles per evocare il "potenziale latente" del risparmio, stesso vocabolario visivo di Liquidita' Eccessiva (v1.10.15) e Costo Opportunita' abbonamenti (v1.10.5)
  • Implementazione tecnica - estratta la matematica in un nuovo modulo puro src/lib/finance/savings-opportunity.ts con la funzione computeSavingsOpportunity({ monthlySavings, years?, realReturnPct? }) che ritorna null quando il risparmio mensile e' <=0 o non finito (NaN/Infinity sanitizzati a null senza propagare al componente), altrimenti { monthlySavings, annualSavings, years, realReturnPct, totalContributed, futureValueReal, compoundGain }. La matematica del compounding e' DELEGATA al modulo gia' esistente subscription-opportunity.ts (UNICA fonte di verita' del progetto per il future value di una rendita posticipata mensile a rendimento reale: stessa formula chiusa PMT * ((1+m)^N - 1) / m con m = (1+r)^(1/12) - 1, stessa sanitizzazione, stessa gestione del caso degenere m ~ 0): il nuovo modulo applica solo la SEMANTICA budget-friendly (saldo <= 0 -> null) per non duplicare la matematica del compound — se domani cambia il rendimento di default o l'algoritmo, basta toccare un solo file. Le costanti DEFAULT_SAVINGS_OPPORTUNITY_REAL_RETURN_PCT = 4 e DEFAULT_SAVINGS_OPPORTUNITY_HORIZON_YEARS = 30 sono ri-esportate dal modulo subscription per garantire la coerenza dei default nel tempo (un test esplicito blinda l'invariante). Suite test dedicata src/lib/finance/savings-opportunity.test.ts con 15 nuovi casi: ritorno null per saldo zero/negativo/NaN/Infinity (4 casi), default canonici applicati, coerenza dei default con subscription-opportunity (regression guard), annualSavings = 12 * monthlySavings, regressione del FV canonico (€500/mese @ 4% per 30 anni in range [330k-360k], aggancio numerico col compound engine), monotonia del FV in realReturnPct e in years, linearita' esatta nel PMT (raddoppiando il risparmio raddoppia il FV, proprieta' della rendita posticipata, tolleranza 6 cifre), rendimento 0% -> FV = totale versato (no composto), compoundGain > 0 con rendimento positivo, gestione di years frazionari (.floor) e negativi (clampati a 0), realReturnPct = Infinity non propaga al risultato, monthlySavings = €0.01 non rompe il calcolo. Suite totale: 488/488 verde (15 nuovi su 473). Componente: import della nuova funzione + Sparkles da lucide-react + InfoTooltip, useMemo su savings + hasData per non ricalcolare ad ogni re-render (zero re-render aggiuntivi rispetto al prima), wrapping della struttura esistente in un <div className="space-y-4"> per la nuova griglia + card. Lint pulito, zero errori TypeScript sui file modificati
  • Perche' migliora l'esperienza utente - chiude il ponte logico fra "guarda quanto risparmi adesso" e "ecco perche' lo stai facendo", esattamente come la card Costo Opportunita' ha fatto per gli abbonamenti dal v1.10.5: due strumenti diversi (taglia il superfluo / massimizza il surplus) parlano ora la stessa lingua finanziaria. Il messaggio e' simmetrico ma opposto a quello della card Liquidita' Eccessiva del Riepilogo (v1.10.15, "se non investi quel cash, perdi X"): qui dice "se mantieni il ritmo e investi il surplus, accumuli Y". Entrambe ancorano l'utente al COMPOUND, che e' il claim del prodotto. Il valore in potere d'acquisto odierno e' deliberatamente confrontabile con il FIRE Number mostrato altrove nell'app: un utente che vede "in 30 anni accumuli €345k" puo' immediatamente confrontarlo con "il mio target FIRE e' €1.2M" e capire se sta procedendo a sufficienza, o se deve aumentare il tasso di risparmio. La card sparisce quando il saldo e' negativo invece di mostrare un numero rosso "fallisci": la psicologia del nudge e' importante — non spingiamo chi e' gia' in difficolta', valorizziamo chi sta facendo bene, in linea con il pattern dell'app. Marginal gain dell'1% nel pannello che orienta la decisione finanziaria piu' frequente (cosa spendere ogni mese)

v1.10.17 - 27 aprile 2026 (UX — nuova metrica "Rendimento Annuo Effettivo" (TAEG-equivalent) nel Calcolatore Interesse Composto)

  • Problema iniziale - il Calcolatore Interesse Composto (src/components/compound-interest-calculator.tsx) chiede in input il "Rendimento Annuo (%)" trattandolo come tasso annuo nominale (TAN), ma la simulazione sottostante in src/lib/finance/compound-interest.ts capitalizza MENSILMENTE (monthlyRate = annualRatePct / 100 / 12 applicato 12 volte all'anno con balance = balance * (1 + monthlyRate) + monthlyContribution). La conseguenza matematica e' che il rendimento annuo realmente percepito a saldo NON e' il TAN, bensi' il TAEG-equivalente (1 + TAN/12)^12 - 1, leggermente superiore: a 7% nominale la crescita annua effettiva e' ~7.23%, a 10% e' ~10.47%, a 12% e' ~12.68%. L'utente non aveva pero' nessuna indicazione visiva di questa distinzione: vedeva il TAN nello slider e nel testo, vedeva un saldo finale leggermente piu' alto di quanto la formula Capitale × (1 + TAN)^anni farebbe stimare a mente, e non aveva il ponte concettuale per riconciliare i due numeri. E' lo stesso pattern delle controversie TAN/TAEG sui prestiti (loan-calculator.tsx ha sempre distinto i due da quando esiste): rendere esplicito il rendimento effettivo e' didatticamente fondamentale, soprattutto in un'app FIRE-oriented che insegna la matematica del compound
  • Cosa e' stato modificato - aggiunta una piccola annotazione testuale subito sotto lo slider "Rendimento Annuo (%)" che mostra in tempo reale il TAEG-equivalente derivato dal TAN: formato 7.23% annuo effettivo (cap. mensile, +0.23 pp), con la cifra principale in viola coerente con la palette dello slider. La differenza in punti percentuali (compoundingSpreadPct = TAEG - TAN) e' esplicita: rende immediato vedere quanto "regala" il compounding infrannuale rispetto a quello annuale. Tooltip educativo aggiunto al label dello slider: "Tasso annuo nominale (TAN). La simulazione capitalizza mensilmente, quindi il rendimento realmente percepito ogni anno e' il TAEG-equivalente: (1 + TAN/12)^12 - 1, leggermente sopra il TAN per via dell'interesse sull'interesse infrannuale". L'annotazione si attiva solo quando compoundingSpreadPct > 0: con TAN = 0 sparisce per non aggiungere rumore (non c'e' composto da estrarre), coerente col pattern delle altre KPI condizionali del calcolatore
  • Implementazione tecnica - estesa l'API del modulo puro src/lib/finance/compound-interest.ts con una nuova funzione esportata effectiveAnnualRatePct(annualRatePct) che ritorna il TAEG-equivalente in percentuale. Implementazione: (1 + TAN/100/12)^12 - 1) * 100, con guard per TAN = 0 (ritorna 0 senza calcolare l'esponenziale, evita micro-errori floating-point) e per monthlyFactor <= 0 (TAN <= -1200%, degenerazione del composto: ritorna -100%). Sanitizzazione preventiva su input non finiti (NaN/Infinity normalizzati a 0, coerente con il resto del modulo). JSDoc esplicita la coerenza con la convenzione di simulazione e fornisce esempi numerici (TAN 7% -> TAEG 7.229%). Suite test estesa src/lib/finance/compound-interest.test.ts con 8 nuovi casi (describe('effectiveAnnualRatePct')): TAN 0% -> TAEG 0%, TAN 7% -> TAEG ~7.229% (verifica con la formula chiusa), CRITICO: coerenza con la simulazione (un capitale di 10k senza contributi a TAN 7% per 1 anno produce esattamente il TAEG come yield realizzato — questa e' la definizione operativa del TAEG ed e' il test che lega il modulo all'invariante di simulazione), monotonia crescente in TAN positivo, lo spread TAEG-TAN cresce col TAN (effetto compounding piu' forte a tassi alti), TAN negativo produce TAEG negativo ma piu' lieve (perdita attenuata dal composto, esempio -5% -> -4.887%), input non finiti -> 0, edge case TAN <= -1200% -> -100%. Suite totale 473/473 verde (8 nuovi su 465). Componente: import della nuova funzione, calcolo di effectiveAnnualPct e compoundingSpreadPct dentro la useMemo esistente (zero re-render aggiuntivi, dipendenze invariate), aggiornamento del JSX dello slider con l'annotazione condizionale. Lint pulito, zero errori TypeScript sui file modificati
  • Perche' migliora l'esperienza utente - chiude un buco didattico evidente del pannello che e' il "biglietto da visita" dell'intera app (il calcolatore e' letteralmente "Interesse Composto", il nome del prodotto): adesso l'utente vede in tempo reale che il "vero" rendimento dell'investimento simulato e' leggermente piu' alto del TAN inserito, e capisce perche'. La distinzione TAN/TAEG e' uno dei concetti finanziari piu' fraintesi anche da risparmiatori esperti (le banche italiane lo sfruttano per pubblicizzare "TAN agevolato" tacendo sul TAEG, e il decreto del Codice del Consumo impone proprio per questo l'esposizione obbligatoria del TAEG sui prestiti): mostrarlo qui — in un calcolatore esplicitamente educativo — abitua l'utente a cercare sempre il rendimento EFFETTIVO quando valuta un investimento o un prodotto bancario. Lo spread (+0.23 pp a 7%) sembra piccolo ma su orizzonti FIRE di 30-40 anni capitalizza in differenze a quattro cifre (su €100k iniziali a 7% per 30 anni la differenza fra 100k × 1.07^30 e la simulazione mensile e' ~€26k). Marginal gain dell'1% nel pannello di onboarding finanziario per eccellenza

v1.10.16 - 27 aprile 2026 (bugfix matematico Calcolatore Interesse Composto: il "Costo del Ritardo" sovrastimava la perdita includendo la crescita del capitale iniziale)

  • Problema scovato - la card Costo del Ritardo (1 anno) introdotta in v1.10.7 nel Calcolatore Interesse Composto (src/components/compound-interest-calculator.tsx) calcolava il delta come balance(year=N) - balance(year=N-1), cioe' la crescita dell'ULTIMO anno della stessa traiettoria. Algebricamente quel delta e' pero' la somma di DUE termini: i contributi mancati capitalizzati (cio' che si VUOLE misurare) PIU' la crescita del lump iniziale nell'ultimo anno (che e' identica nei due scenari "iniziare oggi" vs "iniziare fra 12 mesi", perche' il capitale iniziale capitalizza in entrambi i casi: l'utente non lo preleva, semplicemente non aggiunge contributi). Conseguenza: con initial = €10k, contribution = €300/mese, rate = 7%, years = 20 la card mostrava ~€16.800 di "costo del ritardo" quando il valore corretto e' ~€14.020 — sovrastima del ~20% sul valore principale e ~26% sul compound loss derivato (delayCostNominal - missedContributions). Il bias scala linearmente col capitale iniziale: per chi ha €100k da parte e simula 30 anni l'errore diventa decine di migliaia di euro, distorcendo proprio il messaggio comportamentale che la card vuole comunicare ("inizia presto perche' il compound mancato non si recupera")
  • Causa radice - la formula buggy si appoggiava a una falsa equivalenza ("ritardare di 12 mesi a parita' di orizzonte equivale a fermarsi un anno prima") che e' VERA solo se il capitale iniziale e' zero. La nota nel codice originale ("un anno in meno di accumulo + capitalizzazione") perdeva il fatto che la "capitalizzazione del lump" succede in entrambi gli scenari indistintamente, quindi NON e' un costo del ritardo. Solo i contributi mancati (e la loro futura capitalizzazione) sono attribuibili alla scelta di "iniziare in ritardo"
  • Fix applicato - estratta la matematica in un nuovo modulo puro src/lib/finance/compound-interest.ts con due funzioni: simulateCompoundInterest({ initialCapital, monthlyContribution, annualRatePct, years }) per il piano principale (saldi annuali, crossover compound, totali versato/interessi) e computeDelayCost({ ..., delayMonths = 12 }) che simula ESPLICITAMENTE lo scenario "delay" (i primi delayMonths mesi solo il lump capitalizza, dal mese delayMonths + 1 partono i contributi). Il nominalCost e' ora la differenza fra finalBalanceWithoutDelay e finalBalanceWithDelay — semantica identica a quella che la card vuole comunicare. Forma chiusa equivalente: delayCost = monthlyContribution * (1+m)^(N-d) * ((1+m)^d - 1) / m per m = annualRatePct/100/12, N = years*12, d = delayMonths. Il compoundLoss derivato resta nominalCost - missedContributions (con missedContributions = monthlyContribution * delayMonths), ora coerente sia matematicamente che semanticamente. Sanitizzazione preventiva su tutti gli input numerici (NaN/Infinity vengono normalizzati a 0 senza propagarsi). Il componente JSX e' stato aggiornato per usare le nuove funzioni e ridurre la useMemo da ~80 righe di logica inline a ~30 righe di orchestrazione
  • Test di regressione - nuova suite src/lib/finance/compound-interest.test.ts con 19 casi: anno 0 ha saldo = capitale iniziale, totalDeposited cresce monotonamente, verifica indipendente con la forma chiusa della rendita posticipata, crossover compound, edge case years frazionari, sanitizzazione NaN/Infinity. La regressione critica REGRESSION: il costo del ritardo NON include la crescita del capitale iniziale confronta il valore prodotto col risultato della formula chiusa esatta (~€14.020 per il caso canonico) e dimostra esplicitamente che la versione legacy balance(N) - balance(N-1) produrrebbe ~€16.760, gap di ~€2.700 riconducibile al solo termine spurio initial * [(1+r)^N - (1+r)^(N-12)]. Test aggiuntivi: indipendenza dal capitale iniziale (proprieta' chiave del fix — il bug originale dipendeva dal lump), linearita' nei contributi, scaling esponenziale del compound loss con l'orizzonte, edge case delayMonths >= totalMonths. Suite globale 465/465 verde (19 nuovi), eslint pulito, zero regressioni sui 41 file di test esistenti
  • Perche' migliora l'app finanziariamente - rende il numero in evidenza nella card un valore VERAMENTE attribuibile alla decisione di ritardare, eliminando un termine spurio che gonfiava il messaggio del 20-30% e premiava la procrastinazione facendo apparire piu' grande il costo di un comportamento gia' costoso (paradossalmente meno credibile per l'utente avveduto, che notando l'incoerenza con calcolatori esterni avrebbe perso fiducia nello strumento). Restituisce anche al compound loss il suo significato originale di "regalo del compound che inizia presto", un nudge comportamentale chirurgico ma corretto. La logica e' ora pura, riusabile e coperta da test, in linea con la convenzione del progetto (vedi fire-projection.ts, inflation.ts, excess-liquidity.ts)

v1.10.15 - 26 aprile 2026 (UX — nuovo alert "Liquidita' Eccessiva" nel Riepilogo)

  • Problema iniziale - il pannello FinancialAlerts (src/components/financial-alerts.tsx) trattava il fondo emergenza con tre tier asimmetrici: < 3 mesi -> danger ("insufficiente"), 3-6 mesi -> warning ("sotto soglia"), >= 12 mesi -> success ("eccellente"). La fascia "eccellente" pero' non aveva tetto: chi teneva 24, 36 o anche 60 mesi di spese in liquidita' improduttiva riceveva un complimento ("Ottima sicurezza finanziaria") quando in realta' era nel pieno della trappola comportamentale piu' diffusa fra i risparmiatori italiani — la "psicosi del materasso" che parcheggia capitali ingenti in conti correnti per "sicurezza" mentre l'inflazione li erode silenziosamente e l'effetto composto NON lavora per anni. Il pannello FIRE-oriented dell'app non aveva nessun nudge per intercettare questo errore costoso, anzi lo PREMIAVA con feedback positivo
  • Cosa e' stato modificato - aggiunto un quarto tier nella scala dell'emergency fund: >= 18 mesi -> warning "Liquidita' eccessiva". Il messaggio mostra (a) la copertura attuale in mesi, (b) il fondo emergenza in euro, (c) la cifra raccomandata (3-6 mesi delle spese mensili effettive dell'utente, calcolata in modo dinamico), (d) il SURPLUS rispetto alla soglia raccomandata di 6 mesi, (e) il VALORE COMPOSTO REALE che quel surplus avrebbe se investito al 4% reale per 30 anni in potere d'acquisto odierno, (f) la quota di soli interessi composti, (g) un nudge esplicito a "mobilizzarne una parte verso strumenti d'investimento". La fascia 12-18 mesi mantiene il messaggio "eccellente" originale (sicurezza ancora ragionevole), la fascia 6-12 resta neutra (no alert per non aggiungere rumore). Severity warning (non danger) perche' avere troppa liquidita' non e' un'emergenza — e' un costo opportunita'. Icona Sparkles per evocare il "potenziale latente" del capitale, coerente col linguaggio gia' usato dalla card "Costo Opportunita'" del Tracker Abbonamenti
  • Implementazione tecnica - estratta la matematica in un nuovo modulo puro src/lib/finance/excess-liquidity.ts (zero side effect, zero dipendenze) con la funzione computeExcessLiquidityImpact({ emergencyFund, monthlyExpenses, recommendedMonths?, triggerMonths?, years?, realReturnPct? }) che ritorna null quando non c'e' eccesso significativo (sotto la soglia trigger, spese a 0, fondo a 0) altrimenti { months, excess, futureValueReal, compoundGain, years, realReturnPct, recommendedMonths }. Costanti esportate RECOMMENDED_EMERGENCY_MONTHS = 6 (coerente con la soglia di "sicurezza" gia' usata in useFinancialAlerts), EXCESS_LIQUIDITY_TRIGGER_MONTHS = 18 (deliberatamente conservativa: lascia un buffer di 12 mesi sopra il "eccellente" prima di scattare), EXCESS_LIQUIDITY_DEFAULT_REAL_RETURN_PCT = 4 (allineato con subscription-opportunity.ts e fire-years.ts per non mostrare assunzioni divergenti) e EXCESS_LIQUIDITY_DEFAULT_HORIZON_YEARS = 30 (orizzonte FIRE canonico). Modello matematico: montante composto su SINGOLO capitale (excess * (1+r)^years), NON rendita posticipata — sono casi finanziariamente diversi e il commento nel codice lo esplicita per evitare errori futuri di refactor. Sanitizzazione preventiva su tutti gli input numerici (NaN/Infinity vengono ignorati senza propagarsi a futureValueReal), guard su rendimento reale 0 o negativo che cade su futureValueReal = excess per non mostrare "perdite ipotetiche" (l'utente sta cercando il guadagno potenziale, non lo scenario peggiore). Suite test dedicata excess-liquidity.test.ts con 15 casi: input invalidi, soglia trigger esatta a 18 mesi, default canonici, regressione esplicita della formula del montante (24k @ 4% reale x 30 anni ~ 77.835), override di tutti i parametri, rendimento 0/negativo, orizzonte 0, monotonia su excess e su orizzonte, sanitizzazione NaN/Infinity, calcolo mesi di copertura, regressione delle costanti. Suite totale: 446/446 verde (15 nuovi). Lint pulito, TypeScript senza errori sui file modificati
  • Perche' migliora l'esperienza utente - chiude un buco concettuale evidente nel pannello Riepilogo: prima il fondo emergenza era trattato come variabile a "monotona crescente" (piu' = meglio), ora la curva ha un MASSIMO vero. La trappola psicologica del cash eccessivo e' uno dei comportamenti piu' costosi nel lungo termine, soprattutto in Italia dove la cultura del risparmio in conto corrente resiste anche dopo decenni di tassi reali negativi: tradurre il surplus in un singolo numero ("varrebbe X in 30 anni") e' il modo piu' efficace per smuovere chi sta facendo questo errore, perche' rende visibile cio' che oggi e' invisibile (il "costo del non agire"). La soglia conservativa di 18 mesi evita falsi positivi (chi ha appena ricevuto un'eredita' o sta accantonando per un acquisto futuro non viene infastidito subito), e la calibratura "raccomandato 6 mesi" non "raccomandato 3 mesi" applicata al calcolo del surplus mantiene il consiglio prudente — non spinge a svuotare il salvadanaio, suggerisce di mobilizzarne SOLO la parte chiaramente eccessiva. Marginal gain dell'1% nel pannello che, in un'app FIRE-oriented, e' il piu' importante per disinnescare l'inerzia "sicurezza vs crescita"

v1.10.14 - 26 aprile 2026 (UX — nuova KPI "Erosione Mensile Media" nel Calcolatore Inflazione)

  • Problema iniziale - il Calcolatore Inflazione (src/components/inflation-calculator.tsx) traduceva l'impatto dell'inflazione in due metriche aggregate: la perdita totale in euro su tutto l'orizzonte (Erosione inflazionistica: "perdi €17.965 in 10 anni") e il Tempo di Dimezzamento. Manca pero' la scala temporale che l'utente percepisce davvero ogni giorno: il MESE. Una perdita di €17.965 spalmata su 10 anni e' €150 al mese — un costo che ogni utente sa istintivamente confrontare con bollette, abbonamenti e spesa al supermercato. Senza quel ponte mensile l'inflazione resta un'astrazione lontana ("succedera' fra 10 anni") invece di un'urgenza presente ("mi sta costando quanto Netflix + Spotify ogni mese, qui ed ora"). E' lo stesso pattern Pareto/visceralita' applicato dalla card "Costo del Ritardo" nel Calcolatore Interesse Composto e dalla "Costo Opportunita'" nel Tracker Abbonamenti: tradurre numeri grandi e lontani in unita' di misura quotidiane abbassa la barriera all'azione
  • Cosa e' stato modificato - aggiunta una nuova card "Erosione Mensile Media" nella riga delle KPI secondarie del calcolatore (subito sotto le quattro card principali, accanto a "Erosione inflazionistica" e "Tempo di Dimezzamento"). Mostra un singolo numero — la perdita media in euro al mese — accompagnato da etichetta "/mese" per ribadire la scala temporale e da un sottotitolo che chiarisce la natura "media" del valore. Palette arancio per differenziarla dal rosa "Tempo di Dimezzamento" (rosa = orizzonte temporale di erosione totale; arancio = costo periodico ricorrente, stessa scala dei consumi). Tooltip esplicativo che dichiara la formula e invita esplicitamente al confronto con le spese correnti dell'utente. La card si attiva solo quando averageMonthlyPurchasingPowerLoss > 0: con inflazione 0 o deflazione (rendimento reale del cash positivo o nullo) sparisce per non aggiungere rumore, coerentemente con la "Erosione inflazionistica" gia' nascosta in quegli scenari
  • Implementazione tecnica - estesa l'API del modulo puro src/lib/finance/inflation.ts con un nuovo campo averageMonthlyPurchasingPowerLoss: number nel tipo InflationProjectionResult, calcolato come Math.round(lostValue / (years * 12)) con guard espliciti contro years <= 0 e lostValue <= 0 (entrambi cadono su 0, evitando divisioni per zero e propagazione di NaN/Infinity). La metrica e' deliberatamente lineare-media e NON il valore esatto del singolo mese (che decrescerebbe nel tempo applicato su una base via via ridotta): la documentazione JSDoc dichiara esplicitamente questa scelta e la motiva come "metrica di comunicazione" allineata al pattern della card "Risparmio Mensile Necessario" (che usa anch'essa una rendita media per dare un singolo numero confrontabile). Suite test dedicata aggiunta a src/lib/finance/inflation.test.ts con 7 nuovi casi: calcolo nominale (100k @ 2% per 10 anni -> ~€150/mese, verificato con sanity check numerico), inflazione zero -> 0, capitale zero -> 0 (no NaN da divisione), orizzonte zero -> 0, deflazione (inflazione negativa) -> 0 (lostValue gia' clampato a 0 a monte), scala lineare con il capitale (10x amount = ~10x erosione mensile, tolleranza per arrotondamento), input non finiti -> 0 finito (sanitizzazione coerente con il resto del modulo). Suite totale: 431/431 verde (7 nuovi su 424). Componente: nuovo destructuring di averageMonthlyPurchasingPowerLoss dal projection, import di CalendarMinus da lucide-react, modifica del grid layout della riga KPI secondaria da sm:grid-cols-5 (3+2) a sm:grid-cols-6 (3+2+1) per ospitare la nuova card senza compressioni. Lint pulito, TypeScript corretto sui file modificati
  • Perche' migliora l'esperienza utente - colma l'ultimo "gap di scala temporale" del calcolatore: l'utente aveva il totale ("€17.965 in 10 anni"), il punto di non-ritorno ("dimezzamento in 35 anni"), il versamento correttivo ("X €/mese di risparmio per recuperare") ma non aveva la fotografia diretta del COSTO MENSILE attuale di tenere il capitale fermo. Quel singolo numero in euro/mese e' il piu' azionabile di tutto il pannello: e' direttamente confrontabile con qualunque voce del Budget Mensile o del Tracker Abbonamenti dell'app, chiudendo idealmente il loop "vedi il costo nascosto del cash -> apri il pannello investimenti -> agisci". E' anche il modo piu' efficace per persuadere chi tiene grandi liquidita' "per sicurezza" senza investirle: dire "perdi €18k in 10 anni" non scuote, dire "ti sta costando €150 al mese, ogni mese" si'. La scelta di tenere la card mancante nello scenario di inflazione zero/negativa preserva la pulizia visiva: la KPI compare solo quando aggiunge informazione, mai come segnaposto vuoto. Marginal gain dell'1% nel pannello che, in un'app FIRE-oriented, e' il piu' importante per disinnescare l'avversione all'investimento

v1.10.13 - 26 aprile 2026 (UX — nuova card "Riepilogo Portafoglio Debiti" nella Strategia Estinzione Debiti)

  • Problema iniziale - il pannello src/components/debt-strategy.tsx mostrava una riga input per ciascun debito e i risultati a livello di strategia (Snowball, Avalanche, Confronto Visuale, Impatto del Tuo Extra), ma NON esisteva alcuna metrica a livello di portafoglio: l'utente non vedeva da nessuna parte il saldo totale che doveva, il tasso medio ponderato che stava effettivamente pagando, ne' la somma delle rate minime mensili. Per chi traccia 3+ debiti (carta, prestito auto, finanziamento elettrodomestico, ...) questi tre numeri sono il punto di partenza obbligatorio di qualunque ragionamento: senza saldo totale non sai quanto ti manca alla liberta' debito-zero, senza tasso medio ponderato non puoi confrontare con il TAN di un consolidamento/surroga (la media aritmetica e' fuorviante: due debiti 1k @ 18% e 9k @ 5% NON pesano "11.5%" come direbbe la media, ma 6.3% ponderato), senza rate minime totali non sai qual e' il flusso di cassa minimo di sopravvivenza che il portafoglio richiede ogni mese. La card Impatto del Tuo Extra parla di mesi/interessi risparmiati, ma manca la fotografia statica dell'esposizione attuale
  • Cosa e' stato modificato - aggiunta una nuova card "Riepilogo Portafoglio Debiti" subito sotto la card di input (sopra le card Snowball/Avalanche), con tre KPI affiancati: Saldo Totale (somma dei saldi residui, evidenziato in rosa-rosso come la palette "debito" dell'app, con conteggio "N debiti attivi" sotto), Tasso Medio Ponderato (in ambra/arancio per evocare attenzione, con tooltip che invita esplicitamente al confronto con un'eventuale surroga) e Rate Minime Totali (il flusso minimo mensile, in tono neutro). La card si attiva non appena esiste almeno un debito con saldo > 0 — anche debiti con tasso o rata minima ancora a 0 vengono inclusi nel totale balance e nel conteggio (tasso 0 contribuisce 0 al ponderato, min payment 0 contribuisce 0 alla somma): cosi' la card resta utile mentre l'utente sta ancora compilando, senza scomparire al primo input parziale come fanno invece le card di simulazione (che richiedono balance > 0 && rate > 0 && minPayment > 0)
  • Implementazione tecnica - estratta la matematica in un modulo puro nuovo src/lib/finance/debt-portfolio.ts con la funzione computeDebtPortfolioSummary(debts) che ritorna { activeCount, totalBalance, weightedAverageRate, totalMinPayment }. Riusa il tipo Debt da debt-strategy.ts (UNICA fonte di verita' del modello dati debito, no duplicazione di interfaccia). Sanitizzazione preventiva su tutti i campi numerici (NaN/Infinity vengono ignorati senza propagarsi: bilanci non finiti diventano 0 e fanno scartare il debito, tassi NaN o negativi vengono clampati a 0 nel calcolo ponderato, min payment NaN/negativi diventano 0). Il tasso ponderato e' Σ(balanceᵢ × rateᵢ) / Σ(balanceᵢ), formula corretta per il "TAN medio del portafoglio"; ritorna 0 (non NaN) quando non ci sono debiti attivi. Suite test dedicata src/lib/finance/debt-portfolio.test.ts con 7 casi: lista vuota, singolo debito, ponderato vs aritmetico (caso didattico 1k@18% + 9k@5% = 6.3% ponderato vs 11.5% aritmetico, dimostra perche' l'aritmetica sarebbe fuorviante), debiti a saldo <= 0 ignorati, tassi negativi clampati a 0, sanitizzazione completa NaN/Infinity, somma esatta delle rate minime. Suite totale 424/424 verde (7 nuovi). Componente: nuovo useMemo su debts (non su validDebts filtrati come results), import di Wallet/Percent/CalendarClock da lucide-react, formatPercent da @/lib/format. Card con tooltip su ogni KPI per autoesplicarsi senza occupare spazio verticale extra
  • Perche' migliora l'esperienza utente - chiude un buco evidente nel pannello "Strategia Estinzione Debiti": prima mancava l'inquadramento attuale, ora la sequenza visiva e' input -> fotografia attuale del portafoglio -> due strategie (Snowball/Avalanche) -> confronto -> impatto dell'extra. Il tasso medio ponderato in particolare e' il numero che apre la decisione operativa piu' importante per chi ha piu' debiti: "vale la pena consolidare in un unico prestito?" — basta confrontare il valore mostrato con il TAN di un'offerta di consolidamento/surroga per avere la risposta. La metrica e' didatticamente diversa dalla media aritmetica a cui un utente potrebbe pensare istintivamente (e il test caso 1k@18%+9k@5% lo dimostra in modo eclatante), e averla calcolata correttamente nell'app evita errori di confronto. Le tre KPI insieme rispondono in 5 secondi a tre domande che l'utente si fa appena apre il pannello: quanto devo, a che tasso medio, quanto mi costa al minimo ogni mese — tre fondamenta su cui poi puoi ragionare sulle strategie di estinzione

v1.10.12 - 26 aprile 2026 (UX — nuova KPI "Abbonamento piu' costoso" nel Tracker Abbonamenti)

  • Problema iniziale - il Tracker Abbonamenti (src/components/subscription-tracker.tsx) mostrava aggregati globali (Costo Mensile, Costo Annuale, Costo Opportunita' a 30 anni) ma NON evidenziava il singolo abbonamento piu' costoso. Per chi ha 6+ voci tracciate (Netflix, Spotify, Disney+, palestra, assicurazione auto, iCloud...) la lista diventa lunga e l'occhio si perde: l'utente vede "spendo €X/mese" ma non capisce su quale leva agire per primo. Mancava il classico insight Pareto ("l'80% del costo viene dal 20% degli abbonamenti"), che e' il modo piu' efficace per trasformare la consapevolezza in azione concreta — tagliare l'unico abbonamento dominante invece di micro-tagliare quelli piu' piccoli
  • Cosa e' stato modificato - aggiunta una nuova card KPI "Abbonamento piu' costoso" subito sotto la card Costo Opportunita' esistente. Mostra: nome del top spender, costo mensile normalizzato (annuali divisi per 12), quota percentuale sul totale mensile, costo annuo equivalente, e il valore composto reale a 30 anni se quella stessa cifra fosse investita al 4% reale (stesso default e stesso modulo della card aggregata). La card si attiva solo quando ci sono >= 2 abbonamenti (con uno solo coincide con la card aggregata, sarebbe ridondante). Palette ambra/arancio per differenziarla visivamente dal verde della "Costo Opportunita'" (verde = "se investissi, accumuleresti"; ambra = "leva di attenzione, taglia per primo")
  • Implementazione tecnica - estratta la matematica in un nuovo modulo puro src/lib/finance/subscription-top-spender.ts (zero side effect, una sola dipendenza interna su subscription-opportunity.ts) con la funzione computeTopSubscriptionImpact(subscriptions, options?) che ritorna null su lista vuota / tutti gli importi <= 0, altrimenti { name, monthlyNormalized, annualNormalized, percentOfTotal, futureValueReal, horizonYears, realReturnPct }. Sanitizzazione preventiva su amount non finiti (NaN/Infinity vengono ignorati senza propagarsi a monthlyNormalized o percentOfTotal), normalizzazione frequenza annuale -> mensile (/12) per consentire il confronto tra abbonamenti eterogenei, riuso di computeSubscriptionOpportunityCost per il valore composto (UNICA fonte di verita' della matematica di compounding nel pannello, prima inline-duplicata). Suite test dedicata (subscription-top-spender.test.ts) con 12 casi: lista vuota, importi <= 0, normalizzazione frequenza annuale che batte mensile, somma percentuali = 100%, default canonici, override years/realReturnPct, monotonia future value, sanitizzazione NaN/Infinity, lista con un solo elemento (100%), stabilita' su pareggio (primo trovato vince), nome vuoto supportato. Suite totale: 417/417 verde (12 nuovi)
  • Perche' migliora l'esperienza utente - traduce un pattern lungo e disperso ("guarda la lista e individua manualmente il piu' costoso") in un singolo numero operativo ("X/mese, Y% del totale, +Z€ in 30 anni se lo tagli"). E' il pattern Pareto al servizio del comportamento finanziario: invece di chiedere all'utente uno sforzo cognitivo di confronto, l'app gli mostra DOVE agire e QUANTO vale agire. La normalizzazione automatica annuale->mensile rende il confronto giusto (un abbonamento da 600€/anno e' MOLTO piu' caro di uno da 30€/mese, ma la lista non lo grida); il valore composto al 4% reale per 30 anni traduce il taglio da "risparmio anno per anno" a "capitale FIRE in piu'", chiudendo lo stesso loop motivazionale della card aggregata ma su un'azione precisa e finita. Cosi' viene rispettato il claim "Effetto Composto" anche su decisioni piccole: tagliare l'abbonamento sbagliato all'inizio del percorso vale piu' di tagliarlo a meta'

v1.10.11 - 26 aprile 2026 (bugfix matematico Coast FIRE: propagazione NaN/Infinity nel motore computeFireTargetForRetirementAge)

  • Bug scovato - in src/lib/finance/coast-fire.ts la funzione computeFireTargetForRetirementAge (riusata da computeCoastFireScenarios, buildDynamicFireTargetSchedule, buildPassiveIncomeBreakdown per i tre scenari Bear/Base/Bull) clampava la SWR con il pattern Math.max(0.1, withdrawalRatePct) / 100 e accettava qualunque numero per monthlyExpenses, monthlyPublicPension, monthlyRealEstateIncome, currentAge, retirementAge, publicPensionAge, lifeExpectancy, nominalReturnPct e inflationPct senza alcuna sanitizzazione. In JavaScript Math.max(0.1, NaN) === NaN (e non 0.1) e Math.max(0.1, Infinity) === Infinity: bastava un singolo input non finito — un campo form svuotato dall'utente, una preferenza deserializzata come stringa vuota, una migrazione che lasciava null per la SWR — perché swr, baseFireTarget, pensionPV, passivePV e quindi fireTargetNet diventassero NaN, propagando il veleno in TUTTI e tre gli scenari di mercato e nel coastFireTarget scontato a oggi
  • Effetto sulla dashboard Coast FIRE - la regressione produceva due classi di sintomi silenziosi opposti ed entrambi rovinosi: (a) con withdrawalRatePct = NaN, monthlyExpenses = NaN o currentAge = NaN la dashboard mostrava €NaN su Coast FIRE Target, Target FIRE Netto, Pensione PV, Rendite PV e sui tre scenari, con i grafici delle proiezioni dinamiche spezzati; (b) con withdrawalRatePct = +Infinity (raro ma possibile con migrazioni che convertono male campi numerici da stringa) swr = Infinity / 100 = Infinity, baseFireTarget = annualExpenses / Infinity = 0 e fireTargetNet = Math.max(0, 0 - pensionPV - passivePV) = 0: l'app riportava silenziosamente "obiettivo Coast FIRE già raggiunto" anche quando l'utente era a metà del percorso. Stesso difetto colpiva Math.pow(1 + realReturn, yearsToRetire) con yearsToRetire = NaN (currentAge non finito) e la formula chiusa della rendita posticipata in presentValueOfAnnuity con tasso reale <= -100% (Math.pow di base non positiva con esponente non intero produce NaN)
  • Soluzione - aggiunte tre helper pure (sanitize, sanitizeNonNegative, sanitizeWithdrawalRatePct) in cima al modulo e una sanitizzazione esplicita di TUTTI gli input numerici prima di partecipare a qualunque formula. La SWR viene ora clampata importando MIN_WITHDRAWAL_RATE_PCT da fire-projection.ts (UNICA fonte di verità del progetto, prima inline-duplicata come literal 0.1), evitando la divergenza che aveva permesso al bug di sopravvivere all'hardening v1.10.8 di projectFire. presentValueOfAnnuity ora ha un guard esplicito su 1 + realReturn <= 0 (degenerazione su rendita non scontata, prima produceva NaN tramite Math.pow(non positivo, non intero)) e ritorna 0 invece di propagare risultati non finiti. yearsToGrow, computeFireTargetForRetirementAge, computeCoastFireScenarios e buildPassiveIncomeBreakdown ora normalizzano ogni input numerico, e il discountFactor in Math.pow(1 + realReturn, yearsToRetire) cade su 1 quando la base non è positiva (anziché propagare NaN nel coastFireTarget di tutti e tre gli scenari)
  • Copertura test completa (8 nuovi test, suite 405/405 verde) - aggiunto blocco di regressione in src/lib/finance/coast-fire.test.ts con: NaN su withdrawalRatePct (verifica che target/coastFireTarget restino finiti), +Infinity su withdrawalRatePct (verifica che NON appaia il falso "FIRE già raggiunto"), NaN su monthlyExpenses, monthlyPublicPension, nominalReturnPct, currentAge, scenario "tutti i campi NaN" che verifica output completamente finito (degraded ma stabile su tutte le metriche dei tre scenari), buildDynamicFireTargetSchedule con SWR non finita. Tutti i 405 test (37 file) passano, eslint pulito, TypeScript senza errori
  • Perché rende l'app finanziariamente più robusta - il motore Coast FIRE è il cuore della pianificazione "smetto di contribuire ma lascio lavorare il compounding fino alla pensione", il pattern decisionale più importante per chi è già oltre i 30 anni di carriera. Un singolo NaN proveniente da un input form svuotato — scenario perfettamente normale per un utente che cancella la SWR per riscriverla — basta a invalidare tutti e tre gli scenari Bear/Base/Bull e a rendere illeggibili le card del tab. La sanitizzazione preventiva all'ingresso garantisce che il motore degradi sempre verso un output finito e ragionevole, allineandosi al pattern già adottato da fire-projection.ts (v1.10.8), fire-sensitivity.ts (v1.8.1) e monte-carlo-helpers.ts (v1.10.4). Particolarmente critico il fix di +Infinity che produceva il falso positivo "obiettivo raggiunto": era il sintomo più pericoloso perché, a differenza di €NaN, non era visibilmente errato e poteva indurre l'utente a decisioni finanziarie sbagliate (smettere di contribuire credendo di aver finito). Chiude la stessa classe di bug "NaN propagation" sull'ultima delle helper FIRE centrali ancora vulnerabile

v1.10.10 - 25 aprile 2026 (UX — nuovo alert "Tempo stimato al FIRE" basato sul tasso di risparmio)

  • Problema iniziale - il pannello FinancialAlerts (src/components/financial-alerts.tsx) mostrava feedback FIRE solo a fireProgress >= 75% (alert fire-close/fire-reached) e calcolava la progress bar contro il fireTarget configurato dall'utente nelle preferenze. Tradotto: chiunque fosse all'inizio del percorso (la stragrande maggioranza degli utenti, sotto il 75%) non aveva NESSUN insight FIRE concreto nel Riepilogo. La domanda piu' importante per chi inizia ("a questo ritmo, in quanti anni arrivero' al FIRE?") rimaneva senza risposta nel dashboard, costringendo l'utente ad aprire il tab FIRE Monte Carlo, configurare le simulazioni e leggere la distribuzione di probabilita' — overhead cognitivo elevato per un singolo numero
  • Cosa e' stato modificato - aggiunto un nuovo alert fire-years-estimate che mostra "Tempo stimato al FIRE: ~N anni" usando reddito, spese e risparmio gia' presenti in FinancialData. Il messaggio cita il tasso di risparmio dell'utente, il rendimento reale assunto, l'SWR e il target in euro odierni, e chiude con un nudge ("aumenta il tasso di risparmio per accorciarli"). Severity dinamica: success per <=15 anni, warning oltre 15 anni o quando il piano supera i 100 anni. L'alert si attiva SOLO quando monthlyIncome, monthlyExpenses e monthlySavings > 0 e quando l'alert "FIRE raggiunto" non e' gia' attivo (no doppione informativo)
  • Implementazione tecnica - estratta la matematica in un nuovo modulo puro src/lib/finance/fire-years.ts (zero side effect, zero dipendenze) con la funzione estimateYearsToFire e tre costanti esposte (FIRE_YEARS_DEFAULT_REAL_RETURN_PCT = 4, FIRE_YEARS_DEFAULT_SWR_PCT = 3.25, FIRE_YEARS_MAX = 100) — i default sono allineati con subscription-opportunity.ts (real return) e fire-metrics.ts (SWR), evitando assunzioni divergenti fra schermate dello stesso prodotto. Modello in EURO REALI: target = annualExpenses / SWR, equazione di accumulo target = netWorth*(1+r)^n + annualSavings*((1+r)^n-1)/r risolta in forma chiusa n = ln(X) / ln(1+r) con X = (target + annualSavings/r) / (netWorth + annualSavings/r). Caduta lineare per r ≈ 0 (no NaN), guard su patrimonio gia' a target (alreadyFire = true), capping a 100 anni. Suite test dedicata (fire-years.test.ts) con 13 casi: input invalidi, default, monotonia (savings rate ↑ → anni ↓; netWorth ↑ → anni ↓), riproduzione del caso di riferimento Networthify (50% savings @ 5% real ≈ 17 anni), 10% savings @ 5% real ≈ 51 anni, edge case r=0 lineare puro
  • Perche' migliora l'esperienza utente - traduce un concetto astratto ("tasso di risparmio", "rendimento composto") in un singolo numero operativo ("~17 anni") che fa scattare la motivazione comportamentale tipica del messaggio di MMM "The Shockingly Simple Math Behind Early Retirement": il tasso di risparmio e' la leva piu' potente sul tempo al FIRE, molto piu' del rendimento. L'alert rende esplicita questa relazione direttamente nel Riepilogo, senza bisogno di aprire calcolatori dedicati. La citazione esplicita degli assunti (real return, SWR) nel messaggio mantiene l'utente informato e gli permette di tarare le aspettative; il nudge finale ("aumenta il tasso di risparmio per accorciarli") chiude il loop motivazionale
  • Manutenibilita' - intervento additivo: nuovo modulo puro testato + ~40 righe di alert nel componente esistente, nessuna modifica alle altre regole di alert, nessuna nuova dipendenza, una sola nuova icona (Hourglass di lucide-react, gia' usata altrove). Suite test 397/397 verde (13 nuovi casi sul motore), eslint pulito, TypeScript senza errori sui file toccati. La logica e' isolata in un file con un singolo export pubblico, riusabile in futuro da altri tab (es. Advisor Acquisti, Calcolatore FIRE) se servira' lo stesso insight altrove

v1.10.9 - 25 aprile 2026 (UX — nuova KPI "Ritmo attuale" e margine mensile negli Obiettivi di Risparmio)

  • Problema iniziale - il pannello di riepilogo del tab Obiettivi di Risparmio (src/components/savings-goals.tsx) mostrava gia' tre KPI utili (Da risparmiare, Ritmo richiesto, Completati) ma lasciava all'utente il salto mentale piu' importante: "il mio ritmo di risparmio storico e' davvero sufficiente a rispettare le scadenze che mi sono dato?". Il Ritmo richiesto rispondeva alla domanda "quanto serve al mese" ma non c'era nessun confronto con il ritmo attuale aggregato, costringendo l'utente a fare il calcolo a mente goal per goal usando solo l'indicatore Ritmo attuale per-card (visibile, ma non sommato). In pratica una persona con 4-5 obiettivi attivi non aveva un termometro complessivo: o tutti i suoi goal mostravano badge In linea (e si sentiva tranquillo) o trovava un mix di stati senza capire l'entita' aggregata del gap
  • Cosa e' stato modificato - aggiunta una quarta card Ritmo attuale al pannello di riepilogo, posizionata fra Ritmo richiesto e Completati, che mostra (a) la somma del ritmo storico mensile sugli obiettivi che alimentano Ritmo richiesto (deadline attiva, mesi residui > 0) cosi' il confronto e' apples-to-apples, e (b) un sub-label +€X vs richiesto (verde) o -€X vs richiesto (ambra) che quantifica il margine o il deficit aggregato. La card cambia colore di sfondo in funzione del segno del margine (emerald-50 per margine positivo, amber-50 per deficit, neutro quando non ci sono goal con deadline attiva), riusando lo stesso linguaggio cromatico gia' usato dai badge di pacing per-goal (In linea verde, In ritardo ambra/rosso). Il layout della griglia e' passato da sm:grid-cols-3 a sm:grid-cols-2 lg:grid-cols-4: su mobile resta una sola colonna, su tablet diventa 2x2, su desktop tutte e 4 le card affiancate. Scheletro caricamento aggiornato a 4 placeholder coerenti
  • Implementazione tecnica - estesa la useMemo esistente (zero passate aggiuntive sui goal) con historicalMonthlySum, sommato solo sugli stessi goal che concorrono a monthlySum (uso del pacing.historicalMonthly gia' calcolato da getDeadlinePacing, clampato con Math.max(0, ...) per robustezza). Calcolato fuori dalla useMemo un monthlyMargin = totalHistoricalMonthly - totalRequiredMonthly con null quando totalRequiredMonthly === 0 (impedisce di mostrare "+€0 vs richiesto" quando il confronto e' indefinito perche' nessun goal ha scadenza attiva). Formattazione coerente con il resto del codebase: +${formatEuro(margin)} per positivi e -${formatEuro(Math.abs(margin))} per negativi (stesso pattern usato in loan-calculator.tsx, snapshot-history-table.tsx, coast-fire-scenarios.tsx). Tooltip della card spiega in linguaggio naturale cosa significa il delta (Stai gia' superando il ritmo richiesto di €X/mese... vs Al ritmo attuale risparmi €X/mese in meno di quanto serve...)
  • Perche' migliora l'esperienza utente - trasforma il pannello da "termometro per-goal" in "termometro di portafoglio". Chi sta gestendo piu' obiettivi contemporaneamente (caso comune: fondo emergenza + caparra casa + viaggio) ottiene a colpo d'occhio la risposta operativa "il mio sforzo aggregato e' allineato con le scadenze che mi sono dato?". E' il classico nudge comportamentale che mancava: un margine positivo conferma il piano e libera capacita' di pianificazione (es. spostare l'extra su un nuovo goal), un deficit aggregato grida invece la quota mensile mancante in un'unica cifra, evitando che gli utenti sottovalutino la somma di piccoli ritardi distribuiti. La distinzione cromatica emerald/amber e' immediata e non richiede di leggere il numero per capire lo stato
  • Manutenibilita' - intervento chirurgico (~60 righe netto fra JSX della card e calcolo del margine), zero nuove dipendenze, una sola nuova icona (Activity di lucide-react, gia' presente in altri tab), nessun nuovo tipo condiviso, zero modifiche alla logica di pacing per-goal (riusa getDeadlinePacing e i suoi 4 stati gia' coperti dai test). Suite test 384/384 verde, eslint pulito, build TypeScript senza errori sul file modificato

v1.10.8 - 25 aprile 2026 (bugfix matematico FIRE: propagazione NaN nel motore projectFire)

  • Bug scovato - in src/lib/finance/fire-projection.ts la funzione projectFire (motore deterministico riusato dal tab FIRE, dall'Advisor e dai confronti "con / senza acquisto") usava i pattern Math.max(0, monthlyExpensesAtFire) * 12, Math.max(0.1, withdrawalRatePct) / 100 e Math.max(0, startingCapital - oneTimeOutflow) per "clampare" gli input. In JavaScript Math.max(0, NaN) === NaN (e non 0): bastava un singolo input non finito — un campo form svuotato dall'utente, una preferenza corrotta letta dal DB, una divisione per zero a monte — perché fireTarget, swr e initialCapital diventassero NaN, propagando il veleno nell'intero loop mensile e in TUTTI i punti del chart
  • Effetto sulla dashboard - la regressione produceva una catena di sintomi silenziosi: (a) header "€NaN" su Capitale a target FIRE, (b) chart spezzato con linea target invisibile, (c) monthsToFire bloccato a -1 (perché capital >= NaN è sempre false) e quindi banner "FIRE non raggiungibile entro l'orizzonte" anche con parametri sani, (d) advisor che riportava delayMonths = 0 (entrambe le proiezioni "non raggiungono FIRE", fireDelayMonths ritorna 0). Lo stesso difetto colpiva l'orizzonte temporale (maxYears), l'età corrente, l'età di pensione, i mesi di costi ricorrenti/ongoing e i delta dei plannedCapitalDeltaByMonth / plannedNetCashflowDeltaByMonth
  • Soluzione - aggiunte due helper pure (sanitizeFinite, sanitizeNonNegative) all'inizio del modulo e una sanitizzazione esplicita di TUTTI i 13 input numerici prima del loop di simulazione. Il SWR è ora clampato con la stessa soglia MIN_WITHDRAWAL_RATE_PCT = 0.1 già usata da fire-sensitivity.ts (UNICA fonte di verità del progetto), Number.isFinite discrimina sia NaN sia ±Infinity, e il guard if (!Number.isFinite(capital) || capital < 0) capital = 0 blocca anche eventuali underflow numerici nel ricorso capital * (1 + monthlyRate) + cashflow + plannedDelta. Sanitizzati anche i delta del plannedCapitalDeltaByMonth/plannedNetCashflowDeltaByMonth (un singolo valore corrotto in array passati dall'Advisor non corrompe più le mensilità successive)
  • Copertura test completa (9 nuovi test, suite 384/384 verde) - aggiunto blocco describe('projectFire — sanitizzazione input non finiti (regressione NaN)') in src/lib/finance/fire-projection.test.ts con: NaN su withdrawalRatePct / monthlyExpensesAtFire (in scenario già pensionato) / startingCapital / monthlySavings, Infinity su oneTimeOutflow, scenario "tutti i campi NaN" che verifica output completamente finito (degraded ma stabile), plannedCapitalDeltaByMonth con NaN/Infinity intercalati, regressione esplicita per withdrawalRatePct = 0 clampato a 0.1% e per valori negativi. Tutti i 384 test (37 file) passano, eslint pulito
  • Perché rende l'app finanziariamente più robusta - il motore projectFire è il cuore di QUATTRO superfici critiche (tab FIRE, Advisor "con/senza acquisto", proiezioni di confronto pre/post-mutuo, KPI di Riepilogo). Un singolo NaN proveniente da un input form svuotato — scenario perfettamente normale per un utente che cancella il campo per riscrivere — basta a invalidare l'intera dashboard, e il fallimento non è gridato (errore visibile) ma silenzioso (numero "€NaN", banner "non raggiungibile"). La sanitizzazione preventiva all'ingresso garantisce che il motore degradi sempre verso un output finito e ragionevole, allineandosi al pattern già adottato da fire-sensitivity.ts (v1.8.1) e monte-carlo-helpers.ts (v1.10.4) e chiudendo la stessa classe di bug sull'ultima delle helper finanziarie centrali

v1.10.7 - 25 aprile 2026 (UX — nuova KPI "Costo del Ritardo (1 anno)" nel Calcolatore Interesse Composto)

  • Problema iniziale - il Calcolatore Interesse Composto (src/components/compound-interest-calculator.tsx) era gia' ricco di KPI (Capitale Finale, Punto di Svolta, Tempo di Raddoppio, Rendita Mensile FIRE, Guadagno Reale) ma mancava la traduzione in euro del messaggio comportamentale piu' importante della finanza personale: "iniziare oggi vs iniziare fra un anno non e' solo 12 versamenti in meno, e' un anno di compounding che non lavora MAI piu' per te". L'utente poteva intuirlo solo abbassando manualmente lo slider degli anni, ma senza vedere il differenziale isolato e senza distinguere quanto della perdita fosse dovuto ai contributi saltati e quanto al compound mancato
  • Cosa e' stato modificato - aggiunta una nuova card Costo del Ritardo (1 anno) (gradient rose/amber per evocare urgenza senza essere allarmista) che appare solo per orizzonti >= 2 anni (con un solo anno il confronto degenererebbe sul capitale iniziale). La card mostra come cifra principale -€X (la perdita totale a fine piano se l'utente avesse iniziato 12 mesi piu' tardi a parita' di orizzonte finale) e, sotto, un breakdown didattico che separa i due contributi: €Y di solo compound mancato · €Z di versamenti saltati. Il compound mancato e' quasi sempre la quota piu' pesante e cresce esponenzialmente con l'orizzonte, esattamente il messaggio che il calcolatore vuole far passare
  • Implementazione tecnica - estesa la useMemo di result per catturare balanceMinusOne (saldo a fine years - 1) durante lo stesso loop di simulazione gia' esistente, evitando una seconda passata. Il delayCostNominal e' calcolato come balance - balanceMinusOne clampato a zero per robustezza, delayMissedContributions come monthlyContribution * 12 (i 12 versamenti che NON saresti riuscito a fare nei 12 mesi di ritardo), delayCompoundLoss come differenza fra i due. Edge case years <= 1 gestito esplicitamente settando balanceMinusOne = initialCapital e ritornando null (la card non viene renderizzata). Zero nuove dipendenze, zero modifiche alla logica core di simulazione, zero impatto sul rendering del grafico o della tabella
  • Perche' migliora l'esperienza utente - trasforma una verita' contro-intuitiva ("12 mesi di ritardo costano molto piu' di 12 versamenti") in un numero in euro che l'utente puo' confrontare con il proprio reddito mensile. E' il classico nudge comportamentale che mancava: chi sta valutando se "iniziare a gennaio prossimo invece di subito" vede istantaneamente che, su un piano da 30 anni con €300/mese al 7%, perdere il primo anno costa ~€16-20k a fine corsa (di cui solo €3.6k sono i contributi saltati: il resto e' compound che non potra' MAI essere recuperato neppure raddoppiando i versamenti dopo). Il breakdown compound vs versamenti e' didatticamente piu' potente del semplice totale perche' isola la quota irrecuperabile e fa vedere "in diretta" perche' iniziare presto e' la scelta piu' redditizia che si possa fare
  • Manutenibilita' - intervento chirurgico (~30 righe di JSX + 15 righe di calcolo nello stesso useMemo), una sola nuova icona (Hourglass riusata dal pattern esistente di Inflation Calculator). Suite 375/375 verde, eslint pulito, nessun nuovo tipo condiviso, nessuna estrazione di helper (la logica e' un differenziale di una simulazione gia' presente, non meritava un modulo a parte)

v1.10.6 - 25 aprile 2026 (UX — nuova card "Impatto del Tuo Extra" nella Strategia Estinzione Debiti)

  • Problema iniziale - il tab Strategia Estinzione Debiti (src/components/debt-strategy.tsx) chiedeva all'utente di inserire un Budget Extra Mensile per Estinzione ma non gli rispondeva mai alla domanda piu' importante: "questi €X extra al mese mi stanno davvero ripagando lo sforzo?". Il pannello mostrava il confronto Snowball vs Avalanche e il delta interessi tra i due metodi, ma mancava il confronto piu' motivazionale: scenario "solo rate minime" vs scenario "con il tuo extra". L'utente vedeva quanto pagava (rata + extra) ma non quanta liberta' e quanti interessi guadagnava in cambio
  • Cosa e' stato modificato - aggiunta una nuova card Impatto del Tuo Extra (gradient emerald/teal coerente con il pattern delle altre card "valore aggiunto" nell'app) che appare solo quando l'utente sta effettivamente versando un extra (extraMonthly > 0) e produce un risparmio misurabile. La card mostra tre KPI affiancati: (1) Mesi Risparmiati con conversione automatica in anni quando supera i 12 mesi, (2) Interessi Risparmiati in euro confrontati con totale extra versati, (3) Resa per €1 Extra come ratio interessi-evitati / euro-extra-versati che trasforma lo sforzo in un coefficiente di leva immediatamente confrontabile
  • Implementazione tecnica - estesa la useMemo di results con una terza simulazione avalancheBaseline che gira simulatePayoff(validDebts, "avalanche", 0) (zero extra) per ottenere lo scenario di riferimento "solo rate minime". Il nuovo useMemo extraImpact calcola la differenza in mesi e interessi vs questa baseline, riusando la stessa funzione gia' coperta da 14 test in debt-strategy.test.ts senza introdurre nuova logica finanziaria. Card renderizzata dopo il grafico di confronto con guard monthsSaved > 0 || interestSaved > 0.5 per evitare card vuote quando il rendimento marginale e' nullo
  • Perche' migliora l'esperienza utente - trasforma un input astratto ("metto 200€ extra al mese") in un piano d'azione misurabile ("pagando 200€/mese in piu' risparmi 4.200€ di interessi e 18 mesi di vita con i debiti"). E' il classico anchor motivazionale che manca nei tool di pianificazione debiti: chi sta gia' facendo lo sforzo riceve conferma immediata del valore prodotto, e chi sta valutando se aumentare l'extra ha una metrica concreta su cui ragionare. La metrica Resa per €1 Extra e' particolarmente potente perche' funziona come un IRR semplificato: se il tuo debito ha un tasso medio del 10% effettivo, ogni euro di extra ne "risparmia" circa 0.20-0.40 in interessi nel lungo periodo, rendendo il pagamento extra finanziariamente equivalente a un investimento risk-free al tasso del debito stesso
  • Manutenibilita' - intervento chirurgico (~50 righe di JSX + 12 righe di calcolo), zero modifiche alla logica core di simulatePayoff, zero nuovi tipi condivisi, zero nuove dipendenze. Suite test 375/375 verde, eslint pulito

v1.10.5 - 25 aprile 2026 (UX — nuova KPI "Costo Opportunita' a 30 anni" nel Tracker Abbonamenti)

  • Dal mero costo al "latte factor" composto (Tracker Abbonamenti) - il tab mostrava gia' il costo mensile e annuale aggregato degli abbonamenti, ma lasciava all'utente il salto mentale "ok, ma in 30 anni quanto mi costa davvero questa abitudine?". La nuova card emerald/teal sotto le due card rosa risponde direttamente: applica al totale mensile aggregato un investimento al 4% reale annuo (rendimento Fisher tipico, ~7% nominale - ~3% inflazione) per 30 anni con capitalizzazione mensile, e mostra quanto capitale - in potere d'acquisto odierno - avresti accumulato investendo invece di pagare. La cifra principale e' affiancata da un breakdown secondario "di cui X di soli interessi composti / speso in abbonamenti: Y" che spiega visivamente la magia del compounding senza richiedere all'utente alcun calcolo
  • Brand alignment FIRE-friendly - questa metrica chiude il cerchio fra il tracker abbonamenti (sezione "monitoraggio spese") e il claim centrale dell'app "Effetto Composto / Indipendenza Finanziaria". L'utente che traccia €60/mese di Netflix+Spotify+iCloud ora vede subito che - investiti al 4% reale per 30 anni - sono €41k in euro odierni, di cui €19k di soli interessi composti. E' il classico "latte factor" tradotto nel piano REALE (al netto dell'inflazione), confrontabile direttamente con i target FIRE/Coast FIRE che l'utente vede negli altri tab
  • Helper pura riusabile + test completi (11 nuovi test) - estratta utility computeSubscriptionOpportunityCost in src/lib/finance/subscription-opportunity.ts con costanti esportate (DEFAULT_SUBSCRIPTION_OPPORTUNITY_REAL_RETURN_PCT = 4, DEFAULT_SUBSCRIPTION_OPPORTUNITY_HORIZON_YEARS = 30) per consentire futuri consumer (es. budget tracker, advisor). Formula chiusa della rendita posticipata FV = PMT * ((1+m)^N - 1) / m con m = (1+r)^(1/12) - 1 (conversione esatta del tasso annuo a mensile, non il naive r/12), degenerazione lineare per rendimento 0%, sanitizzazione di NaN/Infinity, clamp negativi a zero. Suite test copre default canonici, monotonicita' rispetto a rendimento e orizzonte, edge case (zero, negativi, NaN, Infinity), troncamento intero degli anni e regressione del valore di riferimento (€50/mese × 30 anni @ 4% reale ~ €34.5k). Tutti i 375 test della suite (36 file) passano
  • Perche' migliora l'esperienza utente - chi tiene gli abbonamenti sotto controllo non lo fa per ossessione del centesimo, lo fa perche' sospetta che si stia "mangiando" qualcosa di piu' grande. Adesso quel "qualcosa" ha un numero, in euro odierni, sulla stessa pagina, senza dover aprire il Calcolatore Interesse Composto e rifare i conti. Stessa pagina, stessi input, una cifra in piu' che trasforma il tracker spese in uno strumento decisionale: serve davvero questo abbonamento se in 30 anni mi costa €X di patrimonio FIRE futuro?

v1.10.4 - 25 aprile 2026 (bugfix matematico Monte Carlo: capitale che cambiava segno con high volatility)

  • Bug scovato - in monte-carlo.worker.ts il rendimento annuo era campionato da una normale N(meanReale, stdDev) e applicato come runCap = runCap * (1 + randomYearReturn). La coda sinistra della normale puo' produrre valori < -1 (perdita > 100%): con lo slider della volatilita' fino al 30% (vedi fire-settings-panel.tsx) e rendimento reale tipico al 4%, la probabilita' P(Z < -3.47) ≈ 2.6e-4 si traduce in ~150 eventi attesi su 60 anni × 10.000 simulazioni. In quei casi il prodotto FACEVA FLIPPARE IL SEGNO al capitale (es. €1M → -€500k), valore matematicamente impossibile in un mercato reale (un asset puo' essere azzerato, non diventare negativo). Lo stesso bug colpiva anche il fondo pensione (runPensionCap) gestito con campionamento separato
  • Effetto sulle proiezioni FIRE - il segno negativo intermedio inquinava i calcoli successivi nel ramo retired (sottrazione di spese da capitale gia' negativo), nell'allocazione del risparmio dei redditi passivi pre-pensione, nella liquidazione del fondo pensione e negli eventi pianificati, prima che il clamp if (runCap < 0) runCap = 0 di fine anno tampasse il danno. Su 10k simulazioni a 60 anni con volatilita' alta significava distorcere i percentili p10/p50/p90 mostrati a video (specialmente la coda pessimista) e il successRate che il widget Monte Carlo riporta come "Probabilita' di sopravvivenza" — i valori spuriamente bassi della p10 davano consigli FIRE indebitamente prudenti
  • Soluzione - estratta una helper pura applyMonteCarloAnnualReturn(capital, annualReturnDecimal) in src/lib/finance/monte-carlo-helpers.ts che (a) clampa il rendimento a >= -1 (perdita massima reale del 100%), (b) sanitizza input NaN/Infinity senza propagarli nella griglia dei percentili e (c) garantisce capitale risultante >= 0 come ulteriore difesa. Il worker ora invoca questa helper sia per il portafoglio principale sia per il pot del fondo pensione, con identica semantica
  • Copertura test completa (16 nuovi test) - aggiunto monte-carlo-helpers.test.ts con: rendimenti positivi/negativi standard senza falso clamp, regressione esplicita per randomYearReturn = -1.5 / -2 / -10 (che prima invertivano il segno), simulazione di 10.000 estrazioni Box-Muller con stdDev=30% per verificare che NESSUN draw produca capitale negativo, gestione di NaN/Infinity su rendimento e capitale, e invarianti finanziarie (capitale finale sempre >= 0, monotonicita' del rendimento, flag wasReturnClamped che scatta solo sotto -1). Tutti i 364 test della suite (35 file) passano
  • Perche' rende l'app finanziariamente piu' robusta - le proiezioni Monte Carlo sono il core del tab FIRE e influenzano scelte di vita reali (quanti soldi mettere via, a che eta' smettere di lavorare). Garantire che le simulazioni rispettino vincoli fisici basilari ("il mercato non rende negativo il tuo capitale") elimina una classe di errori silenziosi nei percentili pessimisti, restituendo all'utente curve p10/p50/p90 coerenti con la realta' anche negli scenari ad alta volatilita'

v1.10.3 - 24 aprile 2026 (UX — nuova KPI "Rendita Mensile FIRE" nel Calcolatore Interesse Composto)

  • Dal capitale finale alla rendita di vita (Calcolatore Interesse Composto) - il tab mostrava gia' quanto il capitale vale nominalmente e realmente a fine orizzonte, ma lasciava all'utente il salto mentale "ok, ma questi euro quanto produrrebbero al mese se volessi vivere di rendita?". La nuova card arancio/ambra applica il Safe Withdrawal Rate prudente del 3.25% (lo stesso DEFAULT_FIRE_WITHDRAWAL_RATE gia' usato in FIRE dashboard, advisor e AI tools) al valore REALE del capitale finale e risponde direttamente: "con questi accantonamenti al {annualRate}% reale, fra {years} anni avresti una rendita mensile teorica di €X in potere d'acquisto odierno, teoricamente a tempo indefinito". Quando il valore nominale differisce sensibilmente da quello reale, viene mostrato anche il numero nominale come riga secondaria (utile per chi ragiona sul saldo che vedra' sul conto fra N anni)
  • Ponte concettuale Interesse Composto → FIRE - la KPI chiude il cerchio tra le due metafore centrali dell'app ("Effetto Composto" e "Indipendenza Finanziaria"). L'utente che sperimenta con capitale iniziale, contributo mensile e durata vede immediatamente l'impatto sulla propria rendita futura, senza dover aprire il tab FIRE e ricostruire gli input. Stessa pagina, stessi input, una cifra in piu' che trasforma un calcolatore teorico in un motore di pianificazione di lungo periodo
  • SWR coerente con il resto del progetto - il valore 3.25% e' lo stesso costante usata da fire-metrics.ts, server-tools.ts (AI) e fallback del calcolatore. Esplicitato via const DEFAULT_SWR_PCT con commento che rimanda alla fonte canonica; il tooltip spiega perche' e' piu' prudente del classico 4% Trinity. Guardie matematiche: Math.max(0, ...) su entrambi i valori per evitare rendite negative se input degeneri (capitale reale < 0 per iperinflazione estrema), e mostra la riga nominale solo quando c'e' divergenza >1€ rispetto al reale (evita rumore visivo per scenari a inflazione zero)
  • Perche' migliora l'esperienza utente - chi pianifica i contributi mensili per avere "abbastanza per vivere" non vuole sapere solo il saldo finale, vuole sapere quanta rendita passiva produrrebbe. E' il classico miglioramento dell'1% in stile "don't make me think": il numero finale non e' piu' astratto ma diventa concreto ("€2.100/mese in euro odierni") e confrontabile direttamente con le spese mensili attuali, rispondendo finalmente alla domanda "ma davvero mi basterebbe?"

v1.10.2 - 24 aprile 2026 (UX — nuova KPI "Risparmio Mensile Necessario" nel Calcolatore Inflazione)

  • Da insight a consiglio operativo (Calcolatore Inflazione) - il tab mostrava gia' il Capitale Equivalente Futuro (quanti euro nominali servono fra N anni per preservare l'attuale potere d'acquisto) e il Valore Nominale Investito (quanto produce il capitale iniziale al rendimento scelto), ma lasciava all'utente il compito di fare la sottrazione e trasformare il risultato in un piano di accumulo. La nuova card indigo/emerald integra la lettura chiudendo il cerchio: quando l'investimento del solo capitale iniziale non copre l'erosione inflazionistica (real return <= 0) dice esplicitamente quanti euro vanno versati OGNI MESE, al rendimento nominale scelto, per colmare il gap nell'orizzonte temporale indicato; quando invece il rendimento reale e' positivo conferma che il lump sum basta da solo con messaggio rassicurante ("Nessuno") in verde. La distinzione visiva tra i due stati e' immediata e comunica cosa fare (o non fare) a colpo d'occhio
  • Formula chiusa della rendita futura, non iterazioni - la nuova utility pura computeMonthlySavingsForGap in src/lib/finance/inflation.ts usa la formula in forma chiusa PMT = gap / [((1+m)^N - 1) / m] con m = rendimento_annuo/12 ed N = anni*12 (mensilizzazione standard dei piani PAC), con degenerazione lineare gap / N quando il rendimento nominale e' zero (evita divisione per zero nel limite) e guardie su input NaN/Infinity/years<=0 che restituiscono 0 invece di propagare valori non finiti in UI. Estesa l'interfaccia InflationProjectionResult con i campi purchasingPowerGap e monthlySavingsToPreservePurchasingPower, tipizzati e documentati per futuri consumer (es. advisor, report)
  • Copertura test completa - aggiunti 7 test di regressione a inflation.test.ts che coprono: rendimento reale positivo (nessuna rata), rendimento nominale = inflazione (gap ~ 0), rendimento reale negativo con verifica indipendente della formula della rendita, rendimento nominale zero (degenerazione lineare), orizzonte nullo, amount nullo e sanitizzazione input non finiti. Tutti i 348 test della suite (34 file) passano
  • Perche' migliora l'esperienza utente - chi usa il Calcolatore Inflazione per prendere decisioni reali (quanto mettere via per il figlio, per la pensione integrativa, per un acquisto futuro) ora ottiene la risposta che cercava davvero - "quanto devo mettere via al mese?" - invece di dover fare mentalmente l'inversa della rendita. E' un classico miglioramento dell'1% in stile "don't make me think": stessa pagina, stessi input, un numero in piu' che trasforma un calcolatore esplicativo in uno strumento di pianificazione azionabile

v1.10.1 - 24 aprile 2026 (UX — nuovo chip "Nuovo massimo storico" / "Sotto il picco" nel Riepilogo)

  • Nuova metrica "Current Drawdown" nel Riepilogo - accanto alle pillole CAGR e Max Drawdown il tab Riepilogo mostra ora un chip che indica la distanza percentuale del patrimonio corrente dal picco storico. Quando il patrimonio attuale coincide con il massimo mai registrato viene evidenziato un badge verde con icona trofeo ("Nuovo massimo storico"); quando siamo sotto il picco viene mostrata la distanza percentuale in ambra con tooltip che esplicita quanto manca in euro per tornare all'ATH. Complementa il Max Drawdown (che resta il peggior calo osservato) dando immediato feedback sullo stato ATTUALE: l'utente capisce a colpo d'occhio se sta segnando un nuovo massimo o se e' in fase di recupero da un drawdown
  • Refactoring computeHistoryStats - esteso il tipo HistoryStats con currentDrawdownPercent e isAtAllTimeHigh, calcolati nello stesso singolo passaggio usato per il max drawdown. Nessun costo computazionale aggiuntivo perche' riutilizza peakValue e il valore dell'ultimo snapshot, e nessun breaking change per gli altri consumer della libreria (l'unico consumer attuale e' overview-dashboard.tsx). Aggiunta tolleranza di 0.05% per la soglia ATH, cosi' da non far "ballare" il badge per rumore in virgola mobile quando due snapshot consecutivi sono numericamente identici
  • Nuovi test di regressione - aggiunti 3 test a history-stats.test.ts (ATH con serie in monotonico rialzo, current drawdown < 0 con ultimo valore sotto il picco, e caso di recupero parziale dove currentDrawdownPercent e maxDrawdownPercent divergono). Tutti i 341 test della suite passano

v1.10.0 - 24 aprile 2026 (carriera e timing nel calcolatore finanziamenti)

  • DTI prospettico collegato alla Carriera - il Calcolatore Finanziamento usa ora i dati salvati nella sezione Carriera per proiettare redditi futuri, aumenti annui e promozioni, stimando come cambia il rapporto rata/reddito nel tempo
  • Suggerimento operativo sul momento giusto - se una simulazione oggi supera la soglia del 33%, il pannello indica il gap mensile attuale e il primo anno in cui lo scenario potrebbe rientrare grazie alla crescita prevista del reddito
  • Specchietto annuale di sostenibilita - aggiunta una mini-tabella con reddito mensile atteso, DTI, rata massima sostenibile e margine/mancanza per gli anni chiave, cosi' l'utente puo' decidere se anticipare, ridurre o attendere
  • Motore condiviso e testato - introdotta la utility career-projection con normalizzazione robusta dei dati Carriera e test dedicati su parsing, aumenti annui, promozioni e selezione intestatario

v1.9.0 - 24 aprile 2026 (permuta e dettaglio salvataggi nel calcolatore finanziamenti)

  • Permuta dentro ogni finanziamento - ogni card del Calcolatore Finanziamento puo' ora attivare la voce Permuta, inserire il valore del bene dato in cambio e sommarlo automaticamente all'anticipo in denaro, riducendo in modo corretto il capitale effettivamente da finanziare
  • Nomi personalizzati per ogni card - ogni finanziamento simulato puo' avere un nome libero, utile sia nell'analisi generale sia quando si salva o si riapre uno scenario con piu prestiti contemporanei
  • Specchio scenari salvati piu dettagliato - il pannello dei salvataggi mostra ora per ogni scenario il dettaglio dei singoli finanziamenti con nome, durata, TAN, importo, anticipo, permuta e capitale richiesto, mantenendo anche la compatibilita' con gli scenari salvati prima di questa evoluzione

v1.8.2 - 24 aprile 2026 (UX — nuova KPI "Tempo di Raddoppio" nel Calcolatore Interesse Composto)

  • Nuova KPI educativa (Calcolatore Interesse Composto) - aggiunta card "Tempo di Raddoppio" con doppia lettura (nominale e reale) che mostra in quanti anni il capitale raddoppia per sola capitalizzazione degli interessi ai parametri scelti, in parallelo alla KPI "Tempo di Dimezzamento" gia' presente nel Calcolatore Inflazione. Crea simmetria concettuale tra le due leve opposte che determinano il potere d'acquisto nel tempo
  • Formula esatta, non Regola del 72 - il calcolo usa ln(2) / ln(1 + r) invece dell'approssimazione 72/r, piu' accurata soprattutto sui rendimenti bassi (es. al 2% la Regola del 72 stima 36 anni, la formula esatta 35 anni; al 15% 4.8 vs 5.0 anni). Coerente con la stessa formula gia' usata da projectInflation per il dimezzamento del potere d'acquisto
  • Lettura reale coerente con il resto dell'app - il tempo di raddoppio "reale" deriva il rendimento reale tramite computeRealReturn (Fisher esatto), la stessa funzione usata da FIRE dashboard, advisor e calcolatore inflazione. Mostra quando il rendimento reale e' <= 0 (il capitale non raddoppia mai per sola capitalizzazione in scenari con inflazione >= rendimento nominale)
  • Guardie sugli edge case - con rendimento nominale 0% o negativo la card mostra invece di Infinity o valori non finiti; formattazione compatta (un decimale sotto i 10 anni, intero oltre) coerente con quella di halvingLabel nel Calcolatore Inflazione

v1.8.1 - 23 aprile 2026 (hardening matematica: fire-sensitivity — guardie su SWR/rendimento reale)

  • Bugfix finanziario critico (Matrice di Sensitivita' FIRE) - src/lib/finance/fire-sensitivity.ts ora clampa withdrawalRatePct con la stessa soglia minima (0.1%) gia' usata da fire-projection.ts. Prima del fix, un utente che impostava SWR = 0 vedeva fireTarget = Infinity e l'intera matrice restituiva yearsToFire = null in ogni cella; con SWR < 0 il target diventava negativo e TUTTE le celle risultavano istantaneamente FIRE raggiunto (falso positivo pericoloso, soprattutto perche' la stessa funzione e' esposta agli AI tools di src/lib/ai/tools.ts e server-tools.ts)
  • Guardia contro NaN silenziosi - yearsToFire() ora gestisce esplicitamente il caso 1 + realReturn <= 0 (scenario iperinflattivo degenere: es. nominale 0% + inflazione >= 100%). Prima Math.pow(valore negativo o zero, 1/12) restituiva NaN che contaminava tutte le celle della griglia. Ora si cade su un fallback coerente con fire-projection.ts
  • Normalizzazione input - startingCapital, currentAge, monthlyExpensesBaseline, monthlySavingsBaseline e maxYears vengono sanificati (NaN/Infinity → fallback finito) prima di essere usati nei calcoli, evitando propagazione di valori non numerici nei risultati visualizzati nella UI sensitivity-matrix.tsx
  • Test di regressione - aggiunto fire-sensitivity.test.ts con 18 test (il modulo prima era completamente privo di copertura): coerenza dimensioni matrice, monotonicita' per riga/colonna, baseline cell, best/worst case, tutti gli scenari di input invalido (SWR 0/negativo/NaN, iperinflazione, NaN su input principali)

v1.8.0 - 23 aprile 2026 (workspace salvabile nel calcolatore finanziamenti)

  • Scenari salvati nel Calcolatore Finanziamento - ora ogni combinazione di card, intestatario e leva anti-DTI puo' essere salvata con un nome e ripresa in un secondo momento, cosi' l'utente puo' tornare sui propri ragionamenti senza ricostruire tutto da zero
  • Ripresa, modifica, aggiornamento e duplicazione - il pannello scenari permette di riaprire una simulazione salvata, modificarla, aggiornarla sullo stesso slot oppure salvarne una copia come nuovo scenario per confrontare varianti diverse
  • Persistenza coerente con account e backup - gli scenari vengono memorizzati nelle Preference come gli altri workspace personali dell'app, quindi seguono il login, il local fallback del browser e l'export/import dati utente

v1.7.0 - 23 aprile 2026 (simulazione multi-finanziamento nel calcolatore rate)

  • Simulazioni parallele nello stesso scenario - il Calcolatore Finanziamento permette ora di aggiungere piu' finanziamenti uno sotto l'altro con un bottone + dedicato, cosi' si possono confrontare e sommare piu' nuove rate nello stesso momento prima di prendere una decisione
  • Analisi generale cumulata sotto alle card - sotto i singoli blocchi viene costruita una vista aggregata con rata totale simulata, costo complessivo, interessi, grafico e piano di ammortamento cumulato di tutte le simulazioni attive, in modo da leggere l'impatto finale come farebbe una banca sul quadro complessivo
  • Integrazione con la leva anti-DTI gia' presente - la riduzione o estinzione di un debito esistente continua a funzionare, ma adesso viene applicata all'analisi complessiva di tutte le nuove simulazioni, cosi' il rapporto rata/reddito mostra uno scenario molto piu' realistico e operativo

v1.6.1 - 23 aprile 2026 (simulazione estinzione anticipata nel calcolatore finanziamenti)

  • Nuova leva anti-DTI nel Calcolatore Finanziamento - dentro Strumenti -> Calcolatore Rata Finanziamento e' stata aggiunta una sezione opzionale che permette di selezionare un prestito o mutuo gia' attivo, simulare un versamento extra o l'estinzione totale e vedere subito come cambia il rapporto rata/reddito prima di richiedere un nuovo finanziamento
  • Ricalcolo rata sul debito residuo in base al tipo di piano - il motore condiviso in src/lib/finance/loans.ts ora costruisce uno snapshot del prestito attivo e ricalcola la nuova rata mantenendo la durata residua con logica coerente al dato disponibile: ammortamento francese per mutui/prestiti standard, piano crescente per i debiti marcati come variabili/crescenti, approccio lineare per i casi a tasso zero e fallback proporzionale quando mancano abbastanza dati
  • Analisi DTI piu' operativa e guidata - il pannello mostra debito residuo stimato, nuova rata, risparmio mensile, totale rate prima/dopo e un pulsante rapido Portami al 33% per stimare quanto capitale versare sul prestito scelto per rientrare nella soglia bancaria
  • Qualita' della release - aggiunti test dedicati per snapshot del prestito, estinzione parziale, estinzione totale e ricalcolo su piano crescente; verifica locale completata con vitest, eslint e tsc --noEmit

v1.6.0 - 21 aprile 2026 (visualizzazione FIRE degli eventi futuri)

  • Nuovo tab "Eventi Futuri" nel FIRE - aggiunta una vista dedicata con grafico mensile moderno: barre verdi/rosse per impatto netto del mese, linea cumulata blu per l'effetto sul percorso FIRE e linea arancione per le rate future generate dagli eventi finanziati
  • Analisi avanzata piu' chiara - il grafico deterministico FIRE ora mostra anche l'impatto cumulato degli eventi pianificati e aggiunge KPI rapidi su eventi nel piano, impatto 12 mesi, peggior mese e picco rate future
  • Dettaglio operativo degli eventi - la nuova vista riepiloga i prossimi eventi con mese, tipo, importo, anticipo, rata e durata, cosi' l'utente capisce subito quando una spesa futura entra nel piano e quanto pesa nel tempo

v1.5.1 - 21 aprile 2026 (hotfix migration eventi futuri)

  • Fix produzione - aggiunta la migration Prisma versionata 20260421224500_add_planned_financial_events per creare la tabella PlannedFinancialEvent sul database del VPS. La release v1.5.0 aveva aggiornato correttamente schema e codice, ma mancava la migration da applicare con prisma migrate deploy, causando errore server in creazione/lettura degli eventi futuri in produzione
  • Impatto utente - la sezione Budgeting -> Eventi Futuri puo' ora salvare e leggere correttamente uscite future, entrate future e spese finanziate anche sul dominio pubblico

v1.5.0 - 21 aprile 2026 (nuovo calendario eventi finanziari futuri)

  • Nuovo layer unico di pianificazione finanziaria futura - introdotto il dominio PlannedFinancialEvent con supporto a uscite una tantum, uscite finanziate ed entrate una tantum con precisione YYYY-MM. Gli eventi vivono in Prisma, hanno validazione Zod dedicata, API CRUD su /api/planned-events, export/import dati utente e hook client condiviso per aggiornare tutta l'app senza logiche duplicate
  • Nuova sezione "Eventi Futuri" nel tab Budgeting - aggiunta una UI dedicata sopra gli obiettivi di risparmio con creazione/modifica/eliminazione, filtri rapidi, KPI sul prossimo evento, impatto netto 12 mesi e numero massimo di rate future concorrenti. La sezione e' pensata solo per la pianificazione prospettica: gli eventi entrano nei calcoli futuri, ma i dati reali continuano a essere inseriti manualmente nelle sezioni corrette
  • Motore finanziario condiviso per delta mensili - nuova utility src/lib/finance/planned-events.ts che espande ogni evento in impatto mensile su capitale, cashflow e servizio del debito, riusando la matematica dell'ammortamento gia' presente in loans.ts. Espone timeline mensile, summary a 12/24/36 mesi e simulazione del percorso di liquidita' per evitare implementazioni divergenti tra le dashboard
  • Integrazione con FIRE, Consulente, Patrimonio e Riepilogo - gli eventi pianificati entrano ora nella proiezione patrimonio, negli alert forward-looking, nella simulazione FIRE deterministica e Monte Carlo e nel Consulente Acquisti. Il Consulente usa il baseline gia' corretto dagli eventi futuri e aggiunge controlli prospettici su minLiquidityNext36m e peakDTINext36m, cosi' un nuovo acquisto viene valutato anche rispetto agli impegni che si sovrapporranno nei prossimi 36 mesi
  • Read-only summary nei tab operativi - nel tab Patrimonio e nel Riepilogo compaiono card sintetiche sugli eventi in arrivo, sull'impatto netto atteso e sui casi in cui la liquidita' futura rischia di andare sotto zero. Lo storico del patrimonio e i dati attuali non vengono alterati: l'effetto si applica solo alle proiezioni future, coerentemente con il fatto che gli eventi rappresentano pianificazione e non movimenti gia' avvenuti
  • Qualita' e regressioni - aggiunti test su validazione, motore eventi, integrazione FIRE e advisor helpers. Verifica completa locale superata con prisma generate, prisma db push, npm run lint, npm test e npm run build

v1.4.1 - 20 aprile 2026 (hardening UX — i formatter valuta/percentuale non mostrano piu' "€NaN" o "Infinity%")

  • Problema iniziale — i formatter condivisi formatEuro e formatPercent in src/lib/format.ts non proteggevano contro input non-finiti (NaN, Infinity, -Infinity): passando uno di questi valori la UI avrebbe stampato letteralmente "€NaN", "€Infinity" o "NaN%". Questi formatter sono usati in 533 punti in 54 file (tutte le dashboard finanziarie: Patrimonio, FIRE, Mutuo, Advisor, Budget, Obiettivi, Performance, Abbonamenti, ecc.), quindi un singolo calcolo a monte che divide per zero o propaga un campo mancante poteva deturpare qualsiasi KPI. La sorella formatEuroCompact era gia' stata protetta con un em-dash ("—") come placeholder ma le due funzioni principali erano rimaste indietro, lasciando una falla difensiva
  • Cosa e' stato modificato — (1) entrambi i formatter ora fanno un early return su !Number.isFinite(value) restituendo lo stesso placeholder em-dash ("\u2014") gia' adottato da formatEuroCompact, estratto in una costante condivisa NON_FINITE_PLACEHOLDER per coerenza tipografica in tutta l'app. (2) formatEuroCompact e' stato allineato a Number.isFinite (prima usava il globale isFinite, che fa coercion: isFinite("foo") puo' restituire true in alcuni edge case; Number.isFinite no). (3) Aggiunti 7 nuovi test di regressione in format.test.ts che coprono NaN, POSITIVE_INFINITY, NEGATIVE_INFINITY per entrambi i formatter piu' il caso formatPercent(NaN, 3) per verificare che il placeholder ignori correttamente il parametro decimals
  • Perche' migliora l'esperienza utente — e' una difesa in profondita' coerente con lo spirito del fix v1.3.5 (rata mutuo NaN/Infinity): anche se un bug futuro a monte dovesse aggirare i guard matematici, la UI degraderebbe in modo elegante a "—" invece di mostrare stringhe tecniche incomprensibili a un utente finale italiano non-dev. Inoltre l'em-dash e' gia' lo standard visivo dell'app per "valore mancante", quindi il lettore lo interpreta immediatamente senza confusione
  • Manutenibilita' — modifica chirurgica (9 righe di codice produttivo + 16 di test), zero impatto sul formato dei valori finiti esistenti, zero dipendenze nuove. Suite totale: 293/293 verdi (+7), eslint pulito

v1.4.0 - 20 aprile 2026 (nuovo comparatore dettagliato "Da auto termica a elettrica")

  • Nuovo strumento di nicchia dentro Strumenti - aggiunta la sottosezione discreta Vari calcolatori con il nuovo comparatore Da auto termica a elettrica, pensato per chi vuole capire se conviene vendere la propria auto termica e passare a una Tesla. La sezione resta volutamente fuori dal percorso principale della dashboard, ma e' ora disponibile on demand con lazy loading dedicato
  • Confronto auto davvero completo - il nuovo calcolatore considera in un unico flusso: valore attuale dell'auto posseduta, differenza tra vendita privata e permuta concessionario, costi di vendita dell'usato, prezzo Tesla, incentivi, spese di immatricolazione e messa su strada, wallbox/setup domestico, costi annui di assicurazione/manutenzione/bollo, consumi reali e quota di ricarica in casa vs fuori casa, valore residuo futuro di entrambe le auto e costo totale di possesso sull'orizzonte scelto
  • Modalita finanziamento integrata - oltre all'acquisto cash, il comparatore ora gestisce anche il caso con finanziamento Tesla: anticipo, tasso, durata e spese pratica vengono tradotti in capitale finanziato, rata mensile, interessi pagati entro l'orizzonte di analisi e debito residuo finale, mantenendo separati il cash da mettere oggi e il costo economico totale per evitare confronti fuorvianti
  • Modello economico piu' rigoroso e testato - estratta una nuova utility finanziaria dedicata in src/lib/finance/thermal-to-electric-car.ts con breakdown annuale, pareggio economico interpolato, sensibilita' della ricarica e supporto a crescita futura di benzina/elettricita/costi fissi. A supporto sono stati aggiunti test unitari dedicati in thermal-to-electric-car.test.ts che coprono consumi EV reali con perdite di ricarica, permuta vs vendita privata, costi iniziali e comportamento del finanziamento
  • UI guidata per la decisione - il comparatore mostra KPI immediati su ricavo netto dell'usato, costo netto del cambio, cash richiesto oggi, rate/interessi, rivendita futura, soglia km annui, scenario solo-casa vs solo-colonnina, grafico cumulato dei costi e tabella anno-per-anno con energia, costi annui, interessi e debito residuo. L'obiettivo e' trasformare una scelta complessa in un percorso leggibile e simulabile senza fogli Excel esterni

v1.3.5 - 20 aprile 2026 (bugfix finanziario — rata mutuo: NaN con tasso 0% e Infinity con durata 0)

  • Falla logica scovata — nel Simulatore Mutuo (src/components/mortgage-simulator/index.tsx), nel Consulente Acquisti (src/components/advisor-dashboard.tsx) e nell'export del piano di ammortamento (src/lib/export/csv.ts) la rata mensile era calcolata inline con la formula francese diretta R = P·i·(1+i)^n / ((1+i)^n − 1). Questa formula ha due singolarita' matematiche che NON erano protette: (1) con tasso = 0% il numeratore e il denominatore sono entrambi zero e il risultato e' 0/0 = NaN; (2) con durata = 0 anni il denominatore e' zero e il numeratore e' positivo, il risultato e' Infinity. Il campo HTML del tasso aveva min="0", quindi 0% era un input legittimo (prestiti familiari, promozioni auto a tasso zero): bastava digitarlo per vedere "€NaN" propagarsi in DTI, profittabilita', cashflow, costo opportunita' e confronto mutui in un colpo solo. La stessa formula, duplicata in 4 file, era andata fuori sincrono — solo il tab "Confronto" (mortgage-comparison.tsx) aveva i guard giusti
  • Soluzione applicata — estratta la logica in un'unica funzione pura calculateMortgagePayment in src/lib/finance/loans.ts, con tre guard espliciti: (a) annualRatePct = 0 ⇒ rata = loanAmount / n (ammortamento lineare, matematicamente corretto per un prestito a tasso zero); (b) loanAmount = 0 o numPayments = 0 ⇒ rata = 0 (no esplosione a Infinity); (c) input NaN, Infinity o negativi ⇒ normalizzati a 0 con Number.isFinite + Math.max(0, …). Aggiunta anche calculateMortgageRemainingDebt per il debito residuo con gli stessi guard (chiusa della French amortization D_k = P·((1+i)^n − (1+i)^k) / ((1+i)^n − 1)). I 4 call-site (Simulatore Mutuo, Confronto offerte, Consulente Acquisti, export CSV) ora importano l'unica implementazione condivisa, eliminando la duplicazione e la possibilita' di divergenze future
  • Perche' l'app e' piu' robusta — nessun utente puo' piu' far rendere "€NaN" o "€Infinity" alla dashboard semplicemente digitando un tasso legale sul campo input; la suite di test protegge i casi degeneri con 15 nuovi test di regressione in loans.test.ts che verificano valori di rata su un mutuo standard (160k @ 3.5% / 25a ≈ 800€/mese), il caso tasso=0% (1000€/mese lineari), durata=0, loanAmount=0, combinazioni di input invalidi (NaN, Infinity, negativi) e decrescenza monotona del debito residuo lungo il piano. Totale suite: 285/285 verdi (+15), eslint pulito

v1.3.4 - 20 aprile 2026 (UX — nuova KPI "Tempo di Dimezzamento" nel Calcolatore Inflazione)

  • Problema iniziale — il Calcolatore Inflazione (src/components/inflation-calculator.tsx) mostrava gia' potere d'acquisto finale, capitale equivalente futuro, valore nominale e reale dell'investimento, piu' una callout di erosione. Mancava pero' la domanda piu' intuitiva e didattica che gli utenti si pongono davanti a un'inflazione annuale ("a quel ritmo, fra quanti anni i miei soldi varranno la meta'?"): per saperlo bisognava mentalmente applicare la Regola del 72 sulla % inserita, operazione che il calcolatore dovrebbe fare al posto dell'utente
  • Cosa e' stato modificato — (1) nuovo campo purchasingPowerHalvingYears: number | null nel risultato di projectInflation (src/lib/finance/inflation.ts), calcolato con la formula esatta ln(2) / ln(1 + i) invece della Regola del 72 approssimata (tolleranza: a 7% la Regola del 72 restituisce 10.29 anni, la formula esatta 10.245 → differenza inferiore all'1% ma coerente col resto del codice finanziario gia' basato su formule esatte come Fisher). (2) Restituisce null per inflazione ≤ 0, in modo che il caso "deflazione o tasso zero" non generi un numero fuorviante (il potere d'acquisto in quei casi non si dimezza mai). (3) Nuova card UI compatta con icona Hourglass che mostra il valore formattato come intero sopra i 10 anni ("35 anni") o con una cifra decimale sotto ("7.3 anni") per distinguere inflazioni alte. La card e' affiancata alla callout di erosione esistente in un grid 3+2, ha colori rose/tonalita' critiche coerenti con il tema "inflazione = erosione", e un title HTML descrittivo per l'accessibilita'
  • Perche' migliora l'esperienza utente — trasforma una percentuale astratta in un orizzonte temporale tangibile: "inflazione al 5%" diventa "il tuo potere d'acquisto si dimezza in 14 anni", rendendo molto piu' viscerale l'urgenza di investire invece di tenere liquidita'. E' una metrica finanziaria nota (half-life del potere d'acquisto) ampiamente usata nella divulgazione economica, e completa il trittico narrativo gia' presente nel calcolatore (quanto perdi / quanto ti serve / quanto tempo impiega a dimezzarsi)
  • Manutenibilita' — aggiunti 6 test di regressione in inflation.test.ts che verificano valori noti (2% → ~35 anni, 7% → ~10 anni), coerenza con il modello ((1+i)^halving = 2), gestione di inflazione zero e deflazione (null), e indipendenza da amount e years. La nuova metrica e' pura funzione dell'inflazione, quindi il calcolo e' O(1) e non appesantisce il useMemo esistente. Suite completa: 270/270 verdi (+6), eslint pulito

v1.3.3 - 20 aprile 2026 (UX — KPI header piu' leggibili su mobile con delta compatto)

  • Problema iniziale — la barra KPI in testa all'app (src/components/header-kpis.tsx) mostrava sia il patrimonio netto sia la variazione vs snapshot precedente con formatEuro completo (es. €1.234.567 + +€125.000). Su mobile questo produceva bottoni molto larghi che andavano facilmente a capo e competevano visivamente con il valore principale; inoltre la direzione del trend usava netWorthChange >= 0, quindi anche una variazione nulla mostrava freccia verde verso l'alto, visivamente fuorviante
  • Cosa e' stato modificato — (1) nuova utility formatEuroCompact(value) in src/lib/format.ts che usa formato K/M sopra i 10.000 € (es. €125K, €1.2M) mantenendo la forma completa per importi minori per non perdere precisione sui centesimi. Include gestione segno, NaN/Infinity → em-dash e strip dello zero decimale finale. (2) HeaderKpisBar applica il compact solo al delta secondario (il patrimonio principale resta in formato completo per massima leggibilita'), introduce uno stato flat con icona Minus grigia e etichetta neutra quando la variazione e' tra -0.5 e +0.5 € (rumore di arrotondamento), e sposta il dato completo nel title HTML cosi' l'utente puo' comunque leggere il valore preciso in hover
  • Perche' migliora l'esperienza utente — su mobile la barra KPI smette di andare a capo per portafogli a 6-7 cifre, l'occhio coglie subito il valore principale senza essere distratto da un delta molto lungo, e il caso di variazione nulla non viene piu' falsamente colorato di verde con freccia up. L'accessibilita' aumenta grazie al titolo descrittivo sul bottone che include sia patrimonio sia variazione nel formato esteso
  • Manutenibilita'formatEuroCompact e' riutilizzabile negli altri 57 file che importano formatEuro ogni volta che serve una rappresentazione sintetica (es. tooltip grafici, card KPI affollate). Aggiunti 6 test unitari dedicati in src/lib/format.test.ts che coprono soglie K/M, segno negativo, strip dello zero decimale e valori non finiti. Suite completa vitest: 264/264 verdi; eslint pulito

v1.3.2 - 20 aprile 2026 (migration di produzione per il workspace Consulente)

  • Migration Prisma mancante aggiunta correttamente al repo - la release v1.3.0 aveva introdotto i campi advisorSavedScenarios e advisorReminders nel modello Preference, ma senza una migration versionata corrispondente. In locale il problema era rimasto nascosto perche' il database di sviluppo era stato aggiornato con prisma db push, mentre in produzione prisma migrate deploy non aveva nulla da applicare
  • Fix strutturale del processo di deploy - aggiunta la migration 20260420003500_add_advisor_workspace_preference_fields che allinea finalmente schema Prisma e database reale in produzione con due ALTER TABLE espliciti. In questo modo i deploy futuri non dipendono piu' da stato locale implicito o da DB gia' “sporchi” di sviluppo
  • Impatto diretto sul bug utente - senza queste colonne, il runtime server-side poteva fallire leggendo Preference e il refresh snapshot notturno andava in errore con P2022 (The column main.Preference.advisorSavedScenarios does not exist). La migration elimina questa classe di mismatch e rende coerenti login, preferenze, scheduler e workspace Advisor sul DB live
  • Allineamento release - bump versione a v1.3.2 per tenere tracciato anche il fix infrastrutturale del database, oltre all'hotfix cache/sessione della v1.3.1

v1.3.1 - 20 aprile 2026 (hotfix cache/sessione per dati utente e cashflow)

  • Fix robusto per dati utente stale o incoerenti dopo login/release - le route autenticate piu' sensibili (/api/auth/me, /api/preferences, /api/patrimonio) ora rispondono con header Cache-Control: private, no-store e Vary: Cookie, impedendo al browser o a layer intermedi di riutilizzare risposte utente vecchie per sessioni diverse
  • Service Worker corretto per non cacheare piu' API utente - public/sw.js e' stato aggiornato a fi-cache-v4 e ora bypassa completamente tutte le richieste /api/*. Questo elimina la possibilita' che preferenze, storico patrimonio o stato sessione vengano riletti da cache obsolete dopo deploy, refresh o problemi di rete, che era il candidato principale dietro il ritorno di etichette come Persona 1 / Persona 2 e cashflow incompleto
  • Fetch client critiche forzate no-store - AuthContext, usePreferences, Patrimonio, FIRE, Riepilogo, header KPI e Advisor ora richiedono i dati account-specific sempre con cache: "no-store" e credentials: "same-origin", cosi' i nomi reali delle persone, le spese e lo storico vengono letti dal backend live invece che da un eventuale cache locale stantia
  • Error handling piu' trasparente - nelle dashboard principali il fallimento del caricamento dati non resta piu' silenzioso dietro fallback fuorvianti: vengono loggati errori piu' precisi e l'utente riceve un feedback esplicito se i dati del proprio account non risultano caricabili
  • Dati di stefano verificati integri sul DB live - durante l'analisi del bug e' stato confermato che l'account stefano mantiene ancora preferenze corrette (Stefano, Simona), expensesList, prestiti e 13 snapshot patrimonio; il problema era quindi di lettura/cache/sessione e non di perdita database
  • Verifica release - hotfix validato con eslint, vitest e next build tutti verdi prima del deploy

v1.3.0 - 19 aprile 2026 (Consulente acquisti come workspace decisionale)

  • Consulente acquisti separato dai dati reali - il vecchio flusso Accetta Spesa e' stato rimosso: una simulazione non aggiorna piu' automaticamente patrimonio, rate reali o dashboard FIRE/Overview. Il tab Consulente torna a essere uno spazio decisionale, non un punto di registrazione contabile
  • Nuovo workspace decisionale nel Consulente - aggiunta un'area persistente dedicata con CTA chiare e gerarchia visiva piu' forte: Salva scenario, Crea piano di acquisto, Rivedi piu avanti, Confronta con alternative ed Esporta sintesi. Ogni scenario salva nome, prezzo, esborso/rata, score, TCO, impatto FIRE, data e nota operativa, cosi' l'utente puo' costruire una memoria delle decisioni senza sporcare il resto dell'app
  • Shortlist e promemoria intelligenti - il Consulente ora permette di marcare scenari come shortlist e di creare promemoria sia a data fissa sia condizionati al ritorno sopra una soglia di fondo emergenza. L'utente puo' quindi dire "rivaluta tra 3 mesi" oppure "rivedi quando torno sopra 6 mesi di cuscinetto" direttamente dal risultato della simulazione
  • Piano di acquisto collegato agli Obiettivi di Risparmio - l'azione Crea piano di acquisto genera un vero SavingsGoal con target, deadline e categoria coerenti allo scenario (es. anticipo per acquisto finanziato), trasformando il consulente da strumento di analisi a strumento di execution planning senza introdurre side effect patrimoniali
  • Nuova API backend dedicata al workspace Advisor - introdotta src/app/api/advisor-workspace/route.ts con validazioni strutturate e persistenza dedicata in Preference tramite i nuovi campi advisorSavedScenarios e advisorReminders. La release mantiene anche compatibilita' legacy: eventuali acceptedPurchases storici vengono letti solo come fallback dentro il nuovo workspace, senza piu' impattare FIRE e Overview
  • Pulizia inter-tab e coerenza dei numeri - rimossi i box "Acquisti Accettati" da FIRE e Riepilogo, cosi' i numeri mostrati fuori dal Consulente tornano a rappresentare solo dati realmente registrati dall'utente. A supporto del refactor sono stati aggiunti test unitari dedicati per scenario fingerprinting, import legacy, goal draft e report export; release verificata con prisma db push, eslint, vitest e next build

v1.2.3 - 19 aprile 2026 (UX — skeleton loader Obiettivi di Risparmio)

  • Stato di caricamento migliorato nel tab "Obiettivi di Risparmio" (src/components/savings-goals.tsx) — durante il fetch iniziale dei goal da API, il componente mostrava un semplice testo statico "Caricamento..." su sfondo tratteggiato; sostituito con un componente SavingsGoalsSkeleton dedicato che riproduce fedelmente la struttura reale della UI: card di riepilogo con barra di progresso e 3 mini-metriche, seguite da 3 card-goal con placeholder per icona categoria, nome, badge, importi e barra di avanzamento individuale
  • Coerenza visiva — il pattern skeleton è già presente in overview-dashboard.tsx (importa Skeleton da @/components/ui/skeleton); estenderlo agli obiettivi elimina l'incoerenza tra i tab e dà una percezione di velocità più elevata, riducendo il layout shift al completamento del fetch
  • Impatto zero su logica — nessuna modifica alle API call, agli hook o ai calcoli; la funzione SavingsGoalsSkeleton è definita a livello di modulo (non inline dentro il componente) per evitare ricreazioni ad ogni render

v1.2.2 - 19 aprile 2026 (fix finanziario: cliff di €65 nelle detrazioni IRPEF al confine 28k)

  • Bug nel calcolo delle detrazioni lavoro dipendente (src/lib/finance/irpef.tscalculateNetSalary) — il terzo scaglione di detrazioniLavoro (28.001–50.000€ di imponibile) usava il coefficiente base 1910 come punto di partenza: detrazioniLavoro = 1910 × (50.000 − imponibile) / 22.000. Tuttavia il secondo scaglione (15.001–28.000€) include una correzione +65 aggiornata alle direttive 2025/2026 e termina a 1975 (= 1910 + 65) sull'imponibile di esattamente 28.000€. La discrepanza tra i due scaglioni produceva un salto brusco di €65 nelle detrazioni appena sopra la soglia: guadagnare un euro in più a 28.001€ aumentava l'IRPEF netta di €65, riducendo il reddito netto di circa €64
  • Conseguenza concreta — il simulatore stipendi mostrava uno stipendio netto artificialmente gonfiato per redditi appena sotto 28.000€ e un brusco crollo appena sopra; le proiezioni FIRE e la pianificazione di risparmio erano falsate per chiunque si trovasse nella fascia 28.000–50.000€ di imponibile
  • Formula corretta — allineato il coefficiente del terzo scaglione da 1910 a 1975 (= 1910 + 65), in modo che la curva delle detrazioni sia continua al confine 28k: detrazioniLavoro = 1975 × (50.000 − imponibile) / 22.000. A 28.000€ il valore è ancora 1975 (invariato rispetto al secondo scaglione), a 50.000€ si azzera correttamente a 0; i contribuenti nella fascia 28k–50k ricevono detrazioni leggermente superiori (max +€65 a 28.001€, proporzionale a zero verso 50.000€)
  • Test di regressione — aggiunto test automatico REGRESSION: nessun cliff di detrazioni al confine 28k che verifica con due RAL ravvicinati (imponibile uno sotto e uno sopra 28.000€) che il reddito netto sia monotonicamente crescente, prevenendo il re-inserimento del cliff in future modifiche ai coefficienti IRPEF

v1.2.1 - 19 aprile 2026 (performance: eliminato JSON.parse ridondante nel hot path FIRE)

  • Hot path simulazione FIRE ottimizzato — la funzione getActiveRealEstatePassiveIncomeAtMonth chiamava JSON.parse(realEstateListStr) a ogni invocazione: nella simulazione deterministica (1.201 iterazioni × 2 chiamate) e nel pre-computo Monte Carlo (~800 chiamate) questo produceva oltre 2.400 parse ridondanti per ogni run, su una stringa identica per tutta la durata della simulazione. La lista immobili e' ora memorizzata con useMemo e aggiornata solo quando cambia realEstateListStr, eliminando il parsing ripetuto e rendendo l'intera simulazione piu' veloce
  • Tipo any rimosso — il parametro prop nel reduce era annotato any con eslint-disable; ora usa correttamente RealEstateProperty (gia' importato), migliorando la type safety e rimuovendo il commento di soppressione
  • Dead state rimosso — lo state const [, setLoading] era impostato ma il valore non veniva mai letto (la schermata di caricamento e' gestita da isLoadingUser): rimossi i tre statement useState, setLoading(true) e setLoading(false) inutili che causavano un re-render aggiuntivo inutile all'avvio del fetch

v1.2.0 - 19 aprile 2026 (AI Telegram piu' robusto + Consulente acquisti/FIRE ridisegnato)

  • Telegram piu' affidabile e leggibile lato utente - il bot ora renderizza una porzione molto piu' ampia del Markdown in HTML compatibile con Telegram (bold, inline code, link, heading e code block), spezza i messaggi senza superare i limiti effettivi dopo il rendering HTML, evita per quanto possibile di rompere i blocchi di codice a meta' e mantiene la tastiera inline solo sull'ultimo chunk. Se Telegram rifiuta il payload HTML per errori di parsing delle entity, il send torna automaticamente in plain text invece di perdere la risposta
  • Error handling Telegram/AI meno rumoroso e piu' onesto - gli errori transienti di Gemini/OpenRouter (429/5xx, overload, internal error, timeout transitori) vengono riconosciuti come temporanei: l'utente riceve un messaggio chiaro e non allarmistico, mentre il webhook non persiste un lastError fuorviante. Restano invece visibili e persistiti gli errori realmente correggibili dall'utente, come formati comando invalidi o allegati non validi
  • Provider Gemini con retry automatico sui transitori - il bridge server-side verso Gemini ritenta fino a 3 volte sui 429 e 5xx con backoff leggero, riducendo i falsi fallimenti durante i picchi del provider senza cambiare l'interfaccia del runtime AI
  • Prompt AI specializzato per Telegram - il system prompt ora differenzia chiaramente il canale web da quello Telegram: su mobile l'assistente deve usare risposte piu' compatte, niente tabelle Markdown superflue, e per le domande FIRE tipo "a che punto sono?" viene istruito a partire da simulate_fire_scenario e a restituire sempre un mini-quadro numerico ordinato e leggibile
  • Flusso /spesa piu' robusto - quando l'utente arma /spesa con una nota testuale e invia il file in un messaggio successivo, la nota viene mantenuta e reiniettata nel prompt del turno AI invece di andare persa. In piu' /ricategorizza valida davvero il mese in formato YYYY-MM con mesi 01-12, evitando input tipo 2026-13
  • Consulente acquisti molto piu' leggibile - il pannello risultati del tab Advisor e' stato riorganizzato attorno a una lettura guidata: verdetto in testa, KPI chiave subito visibili, sintesi separata da criticita' e approfondimenti, e metriche anonime meno fuorvianti per chi non e' loggato. Le analisi avanzate sono state spostate in tab dedicate (Decisione, FIRE, Formule, Costo) per evitare il muro di informazioni tutto in una volta
  • Blocco "Impatto sul tuo FIRE" completamente ridisegnato - la parte piu' importante del consulente acquisti e' diventata una vera decision card: hero visuale con ritardo FIRE in evidenza, confronto Senza acquisto vs Con acquisto, driver del ritardo (target, capitale tolto oggi, freno mensile), grafico molto piu' forte con tema dark dedicato, tooltip custom, marker sui punti di arrivo FIRE e fascia evidenziata che mostra il tempo perso tra i due scenari. L'obiettivo non e' solo "mostrare il grafico", ma far capire a colpo d'occhio quanto tempo di liberta' costa davvero l'acquisto
  • Copertura test estesa sulle regressioni di canale - aggiunti test dedicati per il rendering Telegram, il fallback da HTML a plain text, la persistenza della nota /spesa, la validazione di /ricategorizza, gli errori transienti Gemini nel webhook, le istruzioni canale-specifiche del prompt AI e il retry automatico del provider Gemini. Release verificata anche con eslint e next build

v1.1.4 - 19 aprile 2026 (fix finanziario critico: rimborso IRPEF pensione calcolato sulla base imponibile errata)

  • Bug nel motore Pension Optimizer (src/lib/finance/pension-optimizer.tscomputePersonBreakdown) — il calcolo del risparmio fiscale annuo derivante dalla deduzione pensionistica usava il RAL grezzo come base imponibile IRPEF: calculateIrpef(grossAnnualSalary) - calculateIrpef(grossAnnualSalary, deductibleAmount). La funzione calculateIrpef si aspetta pero' un reddito gia' depurato dei contributi INPS, non il lordo annuo: passarle il RAL diretto spostava artificialmente il reddito in uno scaglione IRPEF superiore. Per un RAL di 28.500-33.000€ (fascia molto comune) il codice applicava l'aliquota marginale del 33% invece del corretto 23%, gonfiando il rimborso fiscale stimato fino al 43% (es. 280€ invece di 230€ per 1.000€ di versamento, +166€/anno su 1.200€)
  • Formula corretta — introdotto DEFAULT_INPS_RATE = 0.0919 (aliquota media lavoratori dipendenti settore privato, aziende > 15 dipendenti) e ricalcolata la base imponibile come imponibileIrpef = RAL × (1 − 0.0919) prima di invocare calculateIrpef. L'approssimazione con aliquota fissa introduce uno scarto < 1% rispetto alla singola aliquota contrattuale specifica (range 5.84–9.49%), trascurabile rispetto all'errore precedente che spostava il reddito nello scaglione sbagliato
  • 2 nuovi test di regressione in pension-optimizer.test.ts: (1) RAL 28.500€ + 1.000€ di contributo → rimborso atteso 230€ al 23% puro (non 280€ al 33% come prima), (2) RAL 30.000€ + 1.200€ → rimborso 276€ al 23% (non 396€). Tutti i test esistenti aggiornati con helper expectedTaxRefund(ral, deductible) che rispecchia la formula corretta
  • Impatto finanziario — il fix elimina la sovrastima del risparmio fiscale pensionistico per i contribuenti con RAL tra 28.000€ e 33.000€, fascia in cui la differenza 23%/33% genera l'errore massimo. La proiezione FIRE ora incorpora un beneficio fiscale reale invece di uno gonfiato, migliorando l'accuratezza della simulazione a lungo termine

v1.1.3 - 18 aprile 2026 (UX — "Guadagno Reale" nel calcolatore Interesse Composto)

  • Nuova KPI "Guadagno Reale" nel calcolatore Interesse Composto — aggiunta una card dedicata che mostra la crescita effettiva del potere d'acquisto (realFinalBalance - totalDeposited), ovvero di quanto l'utente si e' davvero arricchito in termini reali rispetto a quanto ha versato. Il valore viene colorato in verde se positivo (capitale cresciuto al netto dell'inflazione) o in rosso se negativo (rendimento non sufficiente a compensare l'erosione inflattiva), con icona TrendingUp/TrendingDown coerente e sottotitolo che ricorda il totale versato di confronto. Include un InfoTooltip didattico che spiega il significato finanziario del dato
  • Perche' migliora l'esperienza — il simulatore mostrava gia' "Valore Reale" (saldo finale deflazionato) e "Totale Versato", ma l'utente doveva fare mentalmente la sottrazione per capire se i suoi soldi avevano davvero lavorato. In scenari realistici (rendimento basso + inflazione alta + orizzonte breve) il valore reale puo' risultare inferiore ai contributi, cioe' la strategia ha perso potere d'acquisto: renderlo esplicito come singolo numero colorato trasforma il calcolatore in uno strumento che smaschera l'illusione dei rendimenti nominali gonfiati, completando il trittico "Valore Reale / Punto di Svolta / Guadagno Reale" senza introdurre nuove API o parametri utente
  • Zero regressioni — il calcolo vive nello stesso useMemo gia' esistente (nessuna nuova dipendenza), lint pulito e suite di 228 test unitari confermata verde

v1.1.2 - 18 aprile 2026 (fix finanziario critico: PAC trimestrali/semestrali saltati silenziosamente)

  • Bug di scheduling nel motore PAC (src/lib/pac.tsmatchesPeriodicMonth) — la funzione che decide se una cadenza trimestrale o semestrale e' dovuta nel giorno corrente usava offset = currentMonth - anchorMonth e richiedeva offset >= 0. Conseguenza: ogni mese precedente all'anchor nello stesso anno solare veniva scartato, interrompendo le esecuzioni tra dicembre e l'anchor dell'anno successivo. Esempio concreto e pericoloso: un PAC trimestrale con anchor Novembre sarebbe dovuto scattare a Nov/Feb/Mag/Ago — invece il codice lo eseguiva solo in Novembre, saltando silenziosamente 3 esecuzioni su 4 ogni anno (il 75% dei versamenti). Analogamente, un PAC semestrale con anchor Agosto non scattava mai a Febbraio. Lo scheduler gira giornalmente (applyDuePacSchedules in src/lib/pac-executor.ts) e non lascia alcuna traccia quando isPacScheduleDue risponde false: il bug era dunque invisibile sia in UI che nel DB, erodendo nel tempo il piano di accumulo dell'utente
  • Formula corretta — sostituito il confronto lineare con un modulo ciclico positivo (offset = ((diff % intervalMonths) + intervalMonths) % intervalMonths), che riconosce correttamente le cadenze indipendentemente dall'anno solare. Aggiunti inoltre guardrail difensivi che scartano anchorMonth fuori range (0, 13, NaN) e intervalMonths <= 0, evitando che un record DB corrotto faccia partire esecuzioni casuali
  • 3 nuovi test di regressione in pac.test.ts: (1) cadenza trimestrale con anchor Novembre verifica Nov + Feb/Mag/Ago dell'anno successivo, (2) cadenza semestrale con anchor Agosto verifica Ago + Feb, (3) anchorMonth fuori range (0, 13) non attiva mai la schedule. Suite totale: 228 test passati
  • Impatto finanziario — il fix elimina la perdita silenziosa di versamenti PAC per tutti gli utenti che hanno configurato una cadenza trimestrale o semestrale con anchor nella seconda meta' dell'anno. Recupera fino al 75% delle esecuzioni mancate su piani quarterly con anchor Nov/Dic e il 50% su semestrali con anchor Jul-Dec, rendendo il motore di accumulo matematicamente corretto e conforme all'aspettativa esplicita della UI ("ogni 3 mesi a partire da…")

v1.1.1 - 18 aprile 2026 (UX — alert tasso di risparmio nel Riepilogo)

  • Nuovo alert "Tasso di risparmio" nel pannello FinancialAlerts del tab Riepilogo: sfrutta il netIncome gia' calcolato in Patrimonio (risparmio mensile al netto di spese, mutuo, affitti e subscription) e lo rapporta al reddito lordo familiare per esporre tre scenari concreti — cashflow negativo (danger), tasso di risparmio < 10% (warning) e tasso >= 50% da FIRE (success). Prima del cambio, l'utente vedeva solo il numero netIncome nella card "Risparmio Netto" senza un riferimento percentuale ne' un feedback qualitativo: ora il Riepilogo segnala esplicitamente quando il flusso di cassa sta erodendo il patrimonio, quando il risparmio e' troppo esile per costruire capitale, e quando si e' gia' a un ritmo da indipendenza finanziaria
  • Formattazione valutaria coerente negli alert su obiettivi di risparmio: sostituito €${x.toLocaleString('it-IT')} con formatEuro() da @/lib/format, cosi' i messaggi rispettano la stessa regola (simbolo unicode, zero decimali, separatore italiano) usata ovunque nell'app e restano allineati se il formatter cambiera' in futuro
  • Perche' migliora l'esperienza — il tasso di risparmio e' il KPI piu' predittivo del tempo di raggiungimento del FIRE, ma finora l'app lo mostrava solo come valore assoluto in euro. Renderlo percentuale con soglie chiare trasforma un dato osservativo in un nudge comportamentale, senza introdurre API esterne, senza nuovi componenti e senza toccare il flusso dati esistente (il valore monthlySavings era gia' disponibile in overview-dashboard, viene semplicemente passato al componente alert)
  • Zero regressioni — suite di 211 test unitari verde, lint pulito, nessun cambio ai tipi condivisi o allo schema Prisma

v1.1.0 - 18 aprile 2026 (import spese Telegram completo + Coast FIRE piu' realistico)

  • Import spese Telegram davvero operativo end-to-end - il bot personale ora gestisce il flusso /spesa con screenshot singoli, PDF e album multi-immagine (media_group_id) nello stesso thread AI, mantenendo il ciclo corretto "capisco -> ti mostro -> tu correggi -> confermi -> salvo". Gli allegati vengono scaricati da Telegram, passati direttamente al provider multimodale e trasformati in un batch di transazioni da confermare, senza fallback OCR e senza scritture silenziose sul database
  • Batch import budget revisionabile prima del salvataggio - aggiunti tool server-side per leggere il batch pending, correggerlo in linguaggio naturale prima della conferma, salvare regole merchant permanenti e rimuoverle. L'assistente puo' quindi modificare importi, categorie, date, righe da ignorare e note dopo un feedback discorsivo dell'utente, invece di costringere a rifare tutto da capo
  • Deduplica, audit e apprendimento merchant - le transazioni budget ora memorizzano merchantNormalized, movementType, importConfidence e importBatchId; sono stati introdotti BudgetImportBatch e BudgetMerchantRule per tenere audit degli import, rollback dell'ultimo batch, regole "merchant -> categoria" o "ignora sempre" e deduplica piu' robusta su descrizioni bancarie sporche, date vicine e importi uguali
  • Nuovi comandi Telegram per il budget - oltre a /spesa, il bot supporta /ultimespese, /annullaultimoimport, /categorie e /ricategorizza, cosi' l'utente puo' consultare gli ultimi movimenti, annullare l'ultimo import Telegram, vedere categorie/regole apprese e riapplicare le regole automatiche direttamente dalla chat
  • Backup/export budget portati a v3 - l'export/import dati utente include ora anche batch di import budget e regole merchant, mantenendo la storia degli import AI e la logica di categorizzazione tra device, backup e restore. La serializzazione resta compatibile con i dati esistenti ma aggiunge il nuovo livello audit
  • Coast FIRE piu' fedele alla vita reale - il motore FIRE ora riconosce le rendite immobiliari che partono prima del retirement, permette di decidere quanta rendita pre-FIRE reinvestire (percentuale o quota fissa), propaga questa scelta nella simulazione Monte Carlo e nel Coast FIRE target, e rende piu' trasparente la UI con varianti prudenziali, breakdown delle rendite e copy piu' accurata sul significato del Coast FIRE
  • Rendite immobiliari future trattate meglio - gli stream immobiliari mantengono la loro vera eta' di partenza invece di essere forzati al retirement, cosi' una casa che inizia a rendere a 42 anni pesa davvero nel percorso FIRE e non solo nel giorno del ritiro. I test di regressione coprono ora sia la partenza anticipata sia l'effetto della quota reinvestita sul Coast FIRE
  • Calcolatore finanziamento con DTI piu' leggibile - il tool prestiti mostra ora una barra DTI molto piu' chiara, con zone comfort/attenzione/critica, marker 33% e 40%, indicatore visivo sulla soglia e badge coerenti con il livello di rischio. L'obiettivo e' trasformare il DTI da numero secco a segnale leggibile anche a colpo d'occhio
  • GitHub Actions riportata su binari puliti - la CI non deve piu' andare in rosso per motivi cosmeticamente brutti ma evitabili: il workflow ora riceve DATABASE_URL a livello job anche per prisma generate, i test provider importano esplicitamente describe/it/expect da Vitest, il lockfile include anche i peer @emnapi/* richiesti dai pacchetti WASI opzionali su Linux, e il gate TypeScript di release passa dal next build reale invece di un tsc --noEmit standalone che in questo setup segnalava falsi positivi. Risultato: niente badge rosso su push validi e controllo allineato al deploy effettivo
  • Qualita' di release verificata - confermati verdi eslint, vitest, next build, sync Prisma locale e runtime Prisma rigenerato correttamente anche su Windows dopo il lock del DLL

v1.0.1 - 18 aprile 2026 (fix finanziario critico: projectFire ignorava l'esborso immediato nel flag alreadyFire)

  • Bug critico nel motore FIRE deterministico (src/lib/finance/fire-projection.ts) — il flag alreadyFire e il conseguente azzeramento di monthsToFire/yearsToFire erano calcolati con startingCapital >= fireTarget, ignorando completamente oneTimeOutflow. Quando un utente gia' FIRE simulava un acquisto che lo portava SOTTO la soglia (es. patrimonio 2M, casa 1.7M, target 600k → capitale effettivo 300k), il loop individuava correttamente i mesi necessari per tornare a FIRE ma poi li sovrascriveva a 0, dando all'Advisor la falsa certezza che "l'acquisto non ritarda il FIRE". Conseguenza: fireDelayMonths(baseline, withPurchase) ritornava 0 anche per esborsi molto significativi, silenziando il principale indicatore comparativo dell'Advisor
  • Formula corretta — introdotta la variabile derivata initialCapital = max(0, startingCapital - oneTimeOutflow) usata sia come punto di partenza della proiezione sia come criterio per alreadyFire. Il punto 0 del chartData riflette ora il capitale reale disponibile dopo l'esborso, e il flag indica correttamente se il piano e' gia' FIRE dopo l'acquisto
  • 4 nuovi test di regressione in fire-projection.test.ts: (1) esborso che porta sotto il target NON deve lasciare alreadyFire=true, (2) fireDelayMonths rileva il ritardo quando il baseline e' gia' FIRE, (3) esborso che non scende sotto il target mantiene alreadyFire=true, (4) esborso >= capitale iniziale non produce capitale negativo. Suite totale: 215 test passati
  • Impatto — il fix rende finanziariamente affidabile il confronto "con vs senza acquisto" per tutti gli utenti con patrimonio superiore al target FIRE, che rappresentano il segmento piu' esposto a decisioni di spesa ad alto impatto (prima casa, investimenti immobiliari, passaggi generazionali)

v1.0.0 - 18 aprile 2026 (AI unificata server-side + bot Telegram personale)

  • Release 1.0 ufficiale - Effetto Composto entra nella fase 1.0.0 con un'architettura AI stabile e pronta per l'uso reale: il motore conversazionale non vive piu' nel browser ma in un runtime server-side unico, riusato dalla tab AI web, dall'analisi performance e dal nuovo canale Telegram
  • Nuovo runtime AI server-side unificato - introdotti un contesto utente derivato privacy-first, prompt builder centrale, registry tool lato server e persistenza completa di thread, messaggi, allegati e memoria. Il browser non chiama piu' direttamente Gemini/OpenRouter per la chat principale: provider, modello e chiave vengono salvati sul server in forma cifrata e riusati in tutti i canali
  • Bot Telegram personale per ogni utente - ogni account puo' configurare il proprio token BotFather, registrare automaticamente il webhook e collegarsi tramite deep link /start <codice>. Il bot gira sullo stesso modello della tab AI, vede gli stessi dati utente e supporta /help, /new, /status, /unlink, piu' dialogo libero in linguaggio naturale
  • AI consulente + calcolatore professionista - Telegram e web condividono tutti i tool analitici principali della piattaforma: lettura patrimonio, budget, obiettivi, dividendi, preferenze e simulazioni di scenario. Aggiunti anche tool ad alto livello per simulazione mutuo e simulazione FIRE con override testuali, cosi' il bot puo' rispondere a domande tipo "se compro casa a 300k con mutuo 20 anni?" usando i dati reali del profilo
  • Azioni scrivibili con conferma esplicita - introdotto il lifecycle AssistantPendingAction: l'AI puo' preparare operazioni su budget, obiettivi e memoria, ma ogni scrittura resta in stato pending finche' l'utente non conferma da web o da Telegram. Questo evita side effect silenziosi e rende il bot davvero utilizzabile su dati personali sensibili
  • Cronologia unica multi-canale - i thread AI ora hanno metadato channel (web o telegram), badge dedicato nella sidebar e persistenza unificata. Una conversazione Telegram non e' piu' separata dal resto dell'assistente: entra nello stesso storico dell'utente con i messaggi e gli eventuali esiti delle conferme
  • Secret ripuliti dai payload client/LLM - aiApiKeyEnc, token Telegram e webhook secret non compaiono piu' nelle API browser e non entrano nel contesto inviato al modello. Le preferenze esposte al frontend vengono sanificate, e l'export utente AI usa un bundle derivato controllato
  • Analisi performance migrata sul backend - anche il dialog "Analizza con AI" del tab performance usa ora il motore server-side, mantenendo lo stesso provider/modello dell'assistente principale e togliendo un altro punto di contatto diretto browser-provider
  • Fondamenta deploy-ready per il live - aggiunta la migration 20260418153000_add_telegram_ai_runtime, introdotta APP_BASE_URL per webhook e deep link e aggiornato il percorso di deploy per gestire in modo sicuro la nuova major senza toccare i dati utente esistenti
  • Qualita' di release verificata - suite confermata verde con eslint, next build e 211 test passati

v0.4.1 - 18 aprile 2026 (Calcolatore finanziamento + DTI prestiti)

  • Nuovo Calcolatore Rata Finanziamento - la sezione Strumenti include ora un simulatore dedicato ai prestiti personali con ammortamento alla francese, pensato per auto, moto, arredamento e altri finanziamenti non immobiliari. Il tool consente di impostare importo, anticipo, TAN e durata fino a 10 anni e restituisce subito rata mensile, totale pagato, interessi complessivi e percentuale di costo del credito
  • Analisi DTI integrata con i prestiti gia' presenti in piattaforma - il calcolatore puo' attribuire il nuovo finanziamento a Persona 1, Persona 2 oppure Entrambi e combina automaticamente la nuova rata con le rate gia' censite nel patrimonio. In questo modo il rapporto rata/reddito viene valutato sul carico debitorio reale gia' sostenuto dall'utente, invece che su una simulazione isolata
  • Feedback di sostenibilita' piu' concreto - oltre al DTI percentuale, l'interfaccia evidenzia fascia verde/amber/rossa rispetto alle soglie bancarie, mostra quanta rata aggiuntiva resta sostenibile al 33% e rende piu' leggibile il piano di rimborso con riepilogo live, grafico annuale capitale/interessi/debito residuo e tabella dettagliata espandibile

v0.4.0 - 17 aprile 2026 (Fondo pensione strutturato + PAC automatici)

  • FIRE meno ottimistico per ritiri anticipati - il target FIRE usato da simulazione standard, stress test e Monte Carlo ora viene ricalcolato in base all'eta' effettiva di ritiro/soglia raggiunta, non solo sull'eta' pensionabile pianificata. Le pensioni pubbliche e le rendite future vengono quindi valorizzate solo quando partono davvero, evitando sottostime del capitale necessario per FIRE molto anticipati
  • Monte Carlo fino a fine vita utile - il success rate non si ferma piu' a 30 anni o poco oltre: l'orizzonte della simulazione arriva fino alla lifeExpectancy, cosi' un FIRE a 40-45 anni viene stressato su tutta la durata prevista del piano
  • Cashflow FIRE separato dal risparmio familiare - introdotto monthlyPacBudget: il campo FIRE diventa "PAC mensile extra al fondo pensione" e non viene piu' sovrascritto dal risparmio netto calcolato in Patrimonio. monthlySavings resta una metrica derivata del profilo familiare, mentre il budget investibile FIRE e' una preferenza dedicata
  • Fondo pensione per persona - la configurazione del fondo pensione e' ora distinta per Persona 1 e Persona 2, con RAL annua dedicata, contributo volontario percentuale o fisso, contributo datore percentuale o fisso, TFR automatico e rimborso IRPEF calcolato per persona con cap deducibile separato
  • Accrediti automatici del fondo pensione in Patrimonio - lo scheduler notturno applica il giorno 1 del mese gli accrediti dovuti di lavoratore, datore e TFR dentro Altri Asset, con ledger idempotente PensionFundAccrual e riepilogo di ultimo accredito, YTD e cumulato. Le modifiche manuali del saldo restano possibili e gli accrediti futuri si sommano al nuovo valore corrente
  • PAC automatici per singolo strumento - ogni ETF/azione in Patrimonio ha un pulsante PAC con modal dedicata per creare, modificare, pausare o eliminare regole. Sono supportate cadenze settimanali, mensili, trimestrali, semestrali e annuali; piu' giorni sullo stesso asset si modellano con piu' regole
  • Motore PAC notturno idempotente - lo scheduler controlla ogni notte le regole dovute, usa l'ultimo prezzo di chiusura disponibile dai provider gia' presenti, calcola quote frazionarie, aggiorna customStocksList/stocksSnapshotValue e registra ogni esecuzione come executed, skipped o failed
  • Export/import dati v2 - l'export utente include ora monthlyPacBudget, pensionConfig, regole PAC, esecuzioni PAC e accrual del fondo pensione. L'import v2 ripristina anche queste nuove entita' mantenendo compatibilita' con le chiavi legacy patrimonio e obiettivi
  • Milestone FIRE piu' trasparenti - Lean/Fat FIRE vengono esplicitati come moltiplicatori euristici del target base, non come calcoli autonomi o garanzie di sostenibilita'
  • Copertura test ampliata - aggiunti test su calcolo fondo pensione per persona, cap fiscale, TFR e scheduling PAC; suite aggiornata a 211 test passati

v0.3.1 - 17 aprile 2026 (UX — Interesse Composto piu' educativo: punto di svolta + valore reale)

  • Nuova metrica "Punto di Svolta" nel calcolatore Interesse Composto — il simulatore ora identifica ed evidenzia il primo anno in cui gli interessi maturati superano il totale dei contributi versati, rendendo concretamente visibile il momento in cui il capitale "lavora piu' di quanto venga alimentato". Il numero appare in una card KPI dedicata (icona Sparkles, accento amber) e la riga corrispondente nella tabella di ammortamento viene evidenziata con uno sfondo ambra e un'icona inline, cosi' l'utente puo' collegare a colpo d'occhio la metrica riassuntiva con il dettaglio anno per anno. Se l'orizzonte scelto e' troppo breve perche' l'incrocio avvenga (o il capitale iniziale parte gia' elevato rispetto ai contributi), la card mostra "non raggiunto in N anni" invece di un valore fuorviante
  • Nuovo slider Inflazione e KPI "Valore Reale" — aggiunto un controllo dedicato all'inflazione attesa (0-10%, default 2.5%, step 0.1%) al pannello parametri. Il capitale finale viene deflazionato con fattore (1 + i)^n e mostrato in una nuova card KPI (icona TrendingDown, accento rose) come potere d'acquisto odierno, con sottotitolo esplicito "al netto X% inflazione". Prima dell'intervento il calcolatore comunicava solo valori nominali: un utente che simulava 30 anni con rendimento 7% vedeva cifre finali impressionanti ma senza alcun riferimento al loro reale potere d'acquisto, sottovalutando l'effetto erosivo dell'inflazione sulle proiezioni di lungo periodo. Ora il cruscotto restituisce contemporaneamente il dato nominale (capitale finale) e quello reale (valore reale), riallineando le aspettative a quanto davvero si potra' comprare con quel capitale
  • Perche' migliora l'esperienza — queste due metriche trasformano il calcolatore da semplice simulatore numerico a strumento educativo: il "Punto di Svolta" materializza l'effetto compounding in un singolo numero memorizzabile ("dal dodicesimo anno il mio capitale produce piu' di quanto verso"), mentre il "Valore Reale" evita la trappola psicologica delle cifre nominali gonfiate dall'inflazione. Entrambi i KPI usano i pattern di UI gia' consolidati (card arrotondate, InfoTooltip per la spiegazione, palette consistente con il resto dell'app) senza introdurre dipendenze nuove
  • Compatibilita' totale — nessun cambio alle API o ai tipi condivisi, la suite di 196 test unitari resta verde, calcoli derivati centralizzati in un singolo useMemo con dipendenze corrette incluso inflationRate

v0.3.0 - 17 aprile 2026 (AI con allegati + FIRE coerente)

  • AI Advisor ora accetta immagini e PDF - la chat supporta allegati reali via picker o incolla, con anteprima nel composer, rendering dentro i messaggi, persistenza nei thread e recupero protetto via API. OpenRouter riceve content parts compatibili con immagini/file e Gemini usa inlineData, con limiti centralizzati su numero e peso dei file
  • Persistenza completa degli allegati AI - introdotto il modello Prisma AssistantAttachment e il salvataggio multipart su /api/ai/threads/[id]/messages, cosi' le conversazioni restano rileggibili anche dopo reload e un thread senza testo puo' usare il filename dell'allegato come titolo iniziale
  • FIRE piu' realistico sugli immobili - il valore degli immobili non entra piu' nel capitale FIRE: contano solo le rendite nette, anche future, ricavate da rentStartDate e convertite in stream passivi nel motore Coast FIRE. Header KPI, tab FIRE e Riepilogo usano ora la stessa computeFireMetricsFromSnapshot() per evitare discrepanze
  • Dashboard sincronizzate in tempo reale - cambi a patrimonio, preferenze FIRE o acquisti accettati propagano subito un evento client-side, cosi' overview, KPI in header e simulatore FIRE si aggiornano senza restare con dati stantii tra un tab e l'altro
  • Regressioni coperte lato FIRE - aggiunti test su rendite passive future, rendite negative e calcolo centralizzato delle metriche FIRE per bloccare ritorni di incoerenze sul target netto

v0.2.6 - 17 aprile 2026 (UX — validazione input numerici: vincolo min su tutti i campi finanziari)

  • Prevenzione valori negativi su tutti gli input numerici della piattaforma — aggiunti attributi HTML min="0" (per importi e percentuali) e min="1" (per durate in anni e notti) su circa 50 campi <Input type="number"> distribuiti in 10 componenti. Prima dell'intervento, l'utente poteva digitare valori negativi per importi mutuo, saldi debiti, tassi di interesse, canoni di affitto, costi IMU, quantita' BTC, contributi mensili e numerosi altri campi finanziari, producendo dati logicamente invalidi che si propagavano nei calcoli derivati (grafici, proiezioni FIRE, confronti mutui, strategie debito). Il browser ora impedisce la selezione di valori sotto la soglia tramite spinner e validazione nativa, fornendo un primo livello di difesa lato client senza modificare la logica applicativa esistente
  • Componenti aggiornati: inflation-calculator, subscription-tracker, debt-strategy, compound-interest-calculator, progressione-dashboard, patrimonio-dashboard, patrimonio/real-estate-section, rental-income, mortgage-simulator/mortgage-inputs, mortgage-simulator/mortgage-comparison

v0.2.5 - 17 aprile 2026 (fix critico — divisione per zero nei calcoli finanziari)

  • Bug finanziario critico risolto: divisione per zero in 4 moduli di calcolo — identificata e corretta una famiglia di vulnerabilita' matematiche che producevano NaN, Infinity o loop infiniti quando l'utente inseriva valori zero o di confine per parametri critici (durata mutuo, tasso di prelievo SWR, rata minima debiti). I valori corrotti si propagavano nei grafici, nei KPI e nelle proiezioni FIRE, rendendo l'intera dashboard finanziariamente inaffidabile
  • Confronto Mutui (mortgage-comparison.tsx) — con anni = 0 (campo svuotato dall'utente), numPayments = 0 causava divisione per zero nella formula della rata mensile francese (P * r * (1+r)^n) / ((1+r)^n - 1) e nel calcolo del debito residuo P * ((1+r)^n - (1+r)^k) / ((1+r)^n - 1), producendo Infinity nella rata, NaN nel grafico Debito Residuo nel Tempo e valori assurdi nel riepilogo confronto. Fix: guard numPayments > 0 prima di ogni divisione, con fallback a rata zero e debito residuo pari all'importo originale
  • Coast FIRE (coast-fire.ts) — con withdrawalRatePct = 0, il FIRE target lordo annualExpenses / (swr / 100) produceva Infinity, che si propagava nel Coast FIRE target, nelle tre varianti Bear/Base/Bull e nel gap "quanto ti manca". Fix: clamp SWR a minimo 0.1% (Math.max(0.1, withdrawalRatePct)), allineato alla stessa guardia gia' presente in fire-projection.ts ma mancante in coast-fire.ts
  • Dashboard FIRE (fire-dashboard.tsx) — stessa divisione per zero del Coast FIRE nel calcolo grossFireTarget = annualExpenses / (fireWithdrawalRate / 100). Fix: stessa guardia Math.max(0.1, ...) applicata anche qui per coerenza
  • Strategia Debiti (debt-strategy.ts) — con debiti a saldo zero/negativo o rata minima zero e nessun extra mensile, la simulazione snowball/avalanche entrava in un loop di 600 iterazioni senza progredire (nessun pagamento effettuato, interessi che si accumulano). Fix: filtro preventivo dei debiti con balance <= 0, clamp rate e minPayment a >= 0, early return quando il budget totale e' zero, e soglia di completamento abbassata da 0.01 a 0.005 per evitare residui fantasma
  • 18 nuovi unit testmortgage-comparison.test.ts (12 test: rata standard, anni=0, tasso=0, tasso=0+anni=0, anticipo>=prezzo, prezzo=0, anni negativi, debito residuo a t=0/t=meta'/t=fine, debito con numPayments=0, debito a tasso 0%), coast-fire.test.ts (+3 test: SWR=0, SWR negativo, spese=0), debt-strategy.test.ts (+3 test: debiti con saldo zero/negativo, budget zero senza loop infinito, tassi negativi). Suite totale: 190 test passati

v0.2.4 - 17 aprile 2026 (UX — skeleton loader Abbonamenti Ricorrenti)

  • Skeleton loader per il tracker abbonamenti — il componente SubscriptionTracker restituiva null durante il caricamento asincrono delle preferenze utente, lasciando un vuoto visivo nella pagina fino al completamento della fetch. Ora mostra uno skeleton strutturato che replica il layout reale del componente (titolo con icona, 3 righe abbonamento placeholder, e le due card riepilogo costo mensile/annuale), allineandosi al pattern gia' adottato nei tab FIRE, Riepilogo e nel fallback globale dei tab lazy-loaded. L'utente percepisce immediatamente il tipo di contenuto in arrivo invece di fissare un buco vuoto, migliorando la perceived loading performance specialmente su connessioni lente e dispositivi mobili

v0.2.3 - 17 aprile 2026 (fix FIRE in tempo reale)

  • Mappa FIRE coerente in tempo reale - il target FIRE, la timeline del viaggio, le milestone e le linee di riferimento ora si ricalcolano subito usando il capitale netto realmente richiesto dal piano, quindi modifiche a pensione pubblica, eta' pensionabile, eta' FIRE, rendita immobiliare e altri parametri aggiornano tutta la schermata in modo allineato
  • Auto-salvataggio piu' rapido e trasparente - le preferenze FIRE vengono salvate con debounce piu' corto e la UI mostra chiaramente quando ci sono modifiche in attesa oppure un salvataggio in corso
  • Persistenza completata per i parametri FIRE - monthlySavings e includeIlliquidInFire entrano nello schema validato, nel database e nel caricamento iniziale, evitando stati incoerenti tra impostazioni mostrate e dati realmente salvati
  • Test dedicati sulla logica Coast FIRE - aggiunta copertura per verificare che pensione pubblica ed eta' pensionabile riducano davvero il target netto e il capitale richiesto

v0.2.2 - 17 aprile 2026 (fix UI AI: solo risposta finale in italiano)

  • Niente piu' ragionamenti interni visibili in chat - il bridge Gemini/Gemma ora filtra i Part marcati come thought, cosi' l'utente vede solo la risposta finale dell'assistente invece di thought summaries o note interne del modello
  • Comportamento italiano rinforzato - il prompt operativo dell'AI Advisor esplicita che devono essere mostrati solo output finali rivolti all'utente, sempre in italiano, senza checklist o testo di lavoro
  • Test di regressione sul caso reale - aggiunta copertura per il caso in cui Gemini restituisce un thought in inglese seguito dalla risposta finale in italiano, in modo da evitare ricadute su future modifiche del provider

v0.2.1 - 17 aprile 2026 (fix AI Gemini)

  • Fix errore 400 Gemini su tool calling con enum numerici - le functionDeclarations inviate a Gemini contenevano enum numerici (per esempio mensilita e durata mutuo) che l'API rifiuta se non rappresentati come stringhe. Il bridge Gemini ora normalizza ricorsivamente gli schemi dei tool convertendo enum, type e default in formato compatibile solo per il provider Google, senza alterare il comportamento OpenRouter
  • Copertura di regressione dedicata - aggiunto un test su geminiSanitizeSchema() per bloccare il ritorno di questo bug su futuri tool con enum numerici

v0.2.0 — 16 aprile 2026 (versioning release)

  • Numero versione ufficiale nel repo - package.json diventa la fonte canonica della versione software e viene portato a v0.2.0, con script dedicati per incrementi patch, minor e major
  • Versione visibile anche nel frontend - aggiunta una micro-etichetta grigia sotto il logo nell'header principale, pensata per essere sempre disponibile ma visivamente discreta
  • Processo di rilascio reso esplicito - documentato nel repository che ogni deploy applicativo deve allineare numero versione e changelog, cosi' l'avanzamento del software resta sempre tracciabile

16 aprile 2026 (fix critico debiti — rollover rate minime + edge-case obiettivi e Monte Carlo)

  • Bug finanziario critico risolto: strategia Snowball/Avalanche senza rolloversimulatePayoff() in debt-strategy.tsx non ridirezionava le rate minime dei debiti estinti verso il debito prioritario successivo. Questo e' il meccanismo fondamentale di entrambe le strategie: quando un debito viene saldato, la sua rata minima diventa budget aggiuntivo per accelerare l'estinzione del debito successivo. Il codice originale inizializzava remaining = extraMonthly ad ogni iterazione, ignorando completamente le rate liberate. Con il fix, il budget mensile totale e' calcolato come somma(tutte le rate minime) + extraMonthly, le rate minime attive vengono scalate dal budget, e tutto il residuo (incluse le rate dei debiti gia' estinti) viene applicato al debito prioritario. Esempio concreto: con 2 debiti (Carta €5.000 min €100 al 18%, Auto €12.000 min €250 al 5.5%) e €200 extra, il vecchio codice produceva una simulazione piu' lenta perche' dopo aver estinto la Carta, i €100/mese della sua rata svanivano nel nulla invece di accelerare l'Auto
  • Modulo src/lib/finance/debt-strategy.ts estratto e testato — la logica di simulazione e' stata estratta dal componente UI in un modulo puro importabile e testabile, con 12 nuovi unit test che coprono: lista vuota, singolo debito, rollover a 2 e 3 debiti, ordinamento snowball/avalanche, avalanche < snowball in interessi, overpayment quando rata > saldo, tasso zero, safety cap 600 mesi, e extra = 0
  • Fix obiettivi scaduti: importo fuorviantegetDeadlinePacing() in savings-goals.tsx restituiva requiredMonthly: remaining (l'intero importo mancante) per obiettivi con scadenza superata, mostrando es. "€5.000/mese" come se fosse un contributo mensile quando in realta' era il totale residuo. Ora restituisce requiredMonthly: 0 e l'UI mostra correttamente l'importo mancante con label "ancora da risparmiare". Inoltre, historicalMonthly non viene piu' forzato a 0 per gli obiettivi scaduti: il ritmo storico di risparmio resta visibile come contesto utile
  • Fix indice percentili Monte Carlo — in monte-carlo.worker.ts, gli indici per p10/p50/p90 calcolati con Math.floor(runsCompleted * 0.9) potevano teoricamente accedere fuori dall'array per chunk molto piccoli. Aggiunto Math.min(..., runsCompleted - 1) come clamp di sicurezza
  • Suite test — da 156 a 168 test passati (+12 nuovi test su debt-strategy.test.ts)

16 aprile 2026 (AI persistente, Performance, Dividendi e Report)

  • AI Advisor con thread persistenti e memoria utente su database - la chat AI non vive piu' solo nello stato client: sono stati aggiunti thread salvati (AssistantThread + AssistantMessage) con sidebar conversazioni, rinomina/eliminazione, memoria persistente (AssistantMemory) con pin manuale e auto-estrazione dei fatti stabili dalle conversazioni. Il vecchio store session-memory e' stato rimosso e il system prompt ora combina profilo utente, snapshot dati e memoria storica.
  • Tool calling AI molto piu' ampio (23 strumenti) - l'assistente puo' interrogare patrimonio, storico net worth, allocazione asset, performance portafoglio, dividendi, budget, obiettivi, preferenze, portafoglio titoli, quote bond via ISIN, oltre a eseguire calcoli Coast FIRE, sensitivity matrix FIRE, Monte Carlo, ammortamento mutuo, stipendio netto, sale tax e piani di accumulo. In pratica l'AI passa da "chat con qualche tool" a vero copilota operativo sui dati dell'utente.
  • Nuovo tab Performance - aggiunta una dashboard dedicata con metriche ROI, CAGR, TWR, MWR/IRR, volatilita', Sharpe ratio, max drawdown, drawdown corrente e durata/recovery, piu' heatmap dei rendimenti mensili, grafico underwater e calendario dividendi. Inclusa anche una finestra di analisi AI focalizzata sulla performance del portafoglio.
  • Dividendi e bond entrano nel prodotto in modo strutturato - nuovo modello DividendRecord con API CRUD, endpoint statistiche, scraping storico dividendi da Borsa Italiana per ISIN, ritenuta italiana applicata automaticamente e conversione in EUR. In parallelo il recupero prezzi titoli supporta ora il fallback per obbligazioni italiane via MOT/Borsa Italiana e nel portafoglio compare una modal per simulare l'impatto fiscale di una vendita (26% azioni/ETF, 12.5% titoli di stato, con compensazione minusvalenze).
  • FIRE piu' ricco e piu' robusto - il dashboard FIRE ora include il pannello "Coast FIRE - scenari di mercato" e la matrice di sensitivita' spese/risparmio; inoltre e' stato corretto un bug potenziale di Rules of Hooks legato al worker Monte Carlo, spostando useRef e cleanup prima degli early-return.
  • Export report patrimoniale pronto per PDF/stampa - nel top bar appare il nuovo ExportReportModal, da cui l'utente sceglie quali sezioni includere (Patrimonio, Allocazione, Performance, FIRE, Mutuo/Debiti, Dividendi). Il report viene generato su /report/export, impaginato per stampa e pensato per essere salvato facilmente in PDF dal browser.
  • Refactor dati e compatibilita' mercati - user-data ora esporta anche i dividendi, i tassi FX coprono anche CHF/JPY/CAD/AUD oltre a USD/GBP, e la normalizzazione prezzi/dividendi in EUR e' stata centralizzata in normalizePriceToEur() per evitare conversioni duplicate e incoerenti tra route diverse.

16 aprile 2026 (UX — allocazione asset leggibile e accessibile nel Riepilogo)

  • Percentuali visibili nella legenda allocazione asset — la barra di asset allocation nel tab Riepilogo mostrava le percentuali di ogni categoria (Immobili, Liquidita' & ETF, Crypto, Altro) solo tramite attributo title sulle sezioni colorate della barra. Gli utenti su mobile e tablet non potevano in alcun modo visualizzare questi valori (il title richiede hover con il mouse). Ora ogni voce della legenda mostra il valore percentuale in grassetto accanto al nome della categoria, rendendo l'informazione immediatamente visibile su qualsiasi dispositivo senza necessita' di interazione
  • Accessibilita' screen reader — aggiunto role="img" e aria-label descrittivo alla barra di allocazione, cosi' gli screen reader leggono il breakdown completo ("Allocazione asset: Immobili 45.2%, Liquidita' & ETF 30.1%, ...") invece di ignorare silenziosamente un elemento puramente decorativo. Rimossi i title individuali dai segmenti (ora ridondanti con la legenda visibile e l'aria-label sul contenitore)
  • Pulizia dead code — rimossa la variabile sparkData (array di 30 punti per sparkline) che veniva calcolata dentro il useMemo principale ma mai restituita ne' renderizzata, eliminando un'allocazione inutile ad ogni ricalcolo delle metriche

16 aprile 2026 (fix critico FIRE — rendita immobiliare negativa + IRPEF + UI)

  • Bug finanziario critico risolto: rendita immobiliare negativa ignoratacalculatePropertyAnnualNetIncome() in src/lib/finance/real-estate.ts applicava Math.max(0, rent - totalCosts), azzerando silenziosamente la perdita di immobili in cui i costi superano l'affitto. Quando un utente aveva sia un immobile redditizio (+€9k) sia uno in perdita (-€2.5k), il sistema calcolava un reddito passivo totale di €9k invece del corretto €6.5k, sottostimando il target FIRE di decine di migliaia di euro (es. con SWR 4% e spese €30k/anno: target calcolato €525k invece del corretto €587.5k, errore di €62.5k / 12%). Il bug si propagava nel Monte Carlo, nel calcolo deterministico FIRE, nel Coast FIRE e nel consulente acquisti — ovunque venisse usata sumRealEstateAnnualNetIncome(). Fix: rimosso il floor, la rendita netta puo' ora essere negativa e viene correttamente compensata nella somma aggregata
  • IRPEF_BRACKETS costante disallineata — il secondo scaglione in src/lib/constants.ts dichiarava aliquota 0.35 (35%, scaglioni 2024) ma il motore di calcolo effettivo irpef.ts usava correttamente 0.33 (33%, scaglioni 2026). Aggiornata la costante e il commento a "IRPEF 2026" per prevenire errori in futuri refactor che la importassero al posto dei valori hardcoded
  • Fix NaN nel calcolatore interesse composto — quando capitale iniziale e contributo mensile erano entrambi 0, la percentuale "da interessi composti" calcolava 0/0 = NaN, mostrando "NaN%" nell'UI. Aggiunto guard: se il bilancio finale e' zero, mostra 0%
  • Pulizia dead code IRPEF detrazioniMath.max(690, 1955) in irpef.ts restituiva sempre 1955 (690 e' strettamente minore): semplificato a 1955 per chiarezza
  • Test aggiornati e nuovi — aggiornato il test real-estate.test.ts per verificare che la rendita negativa venga correttamente propagata (non piu' azzerata), aggiunto test di regressione che dimostra l'impatto sul reddito passivo aggregato con immobili misti, aggiornato constants.test.ts per aspettarsi 0.33 nel secondo scaglione. Suite totale: 156 test passati

16 aprile 2026 (UX — skeleton loader per caricamento tab e simulatore FIRE)

  • Skeleton loader globale per tutti i tab — il TabFallback usato come Suspense fallback per tutti i 9 tab lazy-loaded e' stato trasformato da un semplice spinner centrato in uno skeleton strutturato che mima il layout tipico di una dashboard: blocco hero con titolo/sottotitolo, griglia di 4 card metriche e area grafico. Cosi' l'utente percepisce immediatamente il tipo di contenuto in arrivo invece di fissare uno spinner anonimo, migliorando significativamente la perceived loading performance su ogni cambio tab (specialmente su connessioni lente o dispositivi meno potenti)
  • Skeleton dedicato per il simulatore FIRE — il tab FIRE (uno dei piu' pesanti: ~1150 righe, fetch preferenze + patrimonio + calcoli derivati) ora mostra uno skeleton strutturato durante il caricamento iniziale dei dati, con 5 card KPI placeholder, barra tab e area grafico. Prima il componente renderizzava immediatamente con tutti i valori a zero (€0 patrimonio, 0 anni al FIRE, obiettivo €0) causando un flash confuso di dati falsi prima che i valori reali apparissero — un pattern che poteva sembrare un bug. L'isLoadingUser flag gia' presente e' stato riutilizzato come gate per il rendering dello skeleton, senza aggiungere stato nuovo
  • Cleanup import — rimossa l'importazione inutilizzata di Loader2 da page.tsx (l'icona spinner non serve piu' nel nuovo TabFallback basato su Skeleton)

16 aprile 2026 (AI Advisor v2 — profilo utente, tool calling, derived data, API key cifrata)

  • "Parlami di te" — profilo personale persistente iniettato in ogni prompt — nuovo pulsante in header al tab AI che apre un modal con textarea libera (max 8000 caratteri) dove l'utente racconta eta', lavoro, situazione familiare, obiettivi di vita, tolleranza al rischio, vincoli e preferenze d'investimento. Il testo viene salvato sul DB nel modello Preference.aiUserProfile (cross-device, sopravvive ai refresh) e iniettato in ogni system prompt come blocco dedicato --- PROFILO UTENTE --- separato dallo snapshot dati. Cosi' l'AI ha sempre il contesto su CHI sei prima di guardare i numeri. Status bar mostra "Profilo: compilato (N car.)" / "vuoto — clicca per compilare" come scorciatoia
  • Tool calling — l'AI esegue calcoli precisi invece di stimare a parole — registry di 7 tool dichiarati in src/lib/ai/tools.ts e supportati su entrambi i provider (Gemini con functionDeclarations, OpenRouter con formato OpenAI tools/tool_calls). I tool: calculate_mortgage_amortization (rata francese + schema annuale), calculate_net_salary (RAL → netto IRPEF/INPS/cuneo 2025-2026 riusando lib/finance/irpef.ts), run_fire_monte_carlo (2000 traiettorie con percentili p10/p50/p90 + success rate vs target), get_stock_price (Yahoo + fallback Stooq via API esistente), get_bitcoin_price (Binance), get_mortgage_market_offers (top 8 offerte scrapate da MutuiSupermarket), get_net_worth_delta (variazione patrimonio tra due date arbitrarie). Loop multi-turn implementato in providers.ts con cap a 6 round-trip per evitare loop infiniti. Trace dei tool eseguiti visibile in UI: badge espandibile "N strumenti usati" sotto ogni messaggio assistant con args + risultato JSON, e durante l'esecuzione chip in tempo reale "Eseguo strumenti (N)..." con i nomi dei tool che stanno girando
  • Dati arricchiti: blocco derived aggregato lato server/api/user-data?ai=1 ora restituisce, oltre allo snapshot grezzo, un blocco derived con aggregati pronti pensati per il consumo LLM: netWorth.timeline (serie storica patrimonio con breakdown per asset class), netWorth.deltas (MoM, YoY, YTD, since first in % e assoluti), allocation corrente per Immobili/Azioni-ETF/BTC/Beni rifugio/Liquidita'/Pensione, budget con saving rate medio + ultimi 3 mesi + top 5 categorie, subscriptions totale mensile/annuale, goals con % progress e mensile-per-target, fire.quickCheck con net worth/target/gap/anni a retirement, market con valori correnti. L'AI usa direttamente questi numeri invece di scorrere a mano centinaia di snapshot, riducendo errori di calcolo e latency
  • API key opzionalmente cifrata sul server (AES-256-GCM) per sync cross-device — toggle "Ricorda su questo account" nel modal Impostazioni AI: se attivo, la API key viene cifrata con AES-256-GCM (src/lib/crypto.ts usando crypto.createCipheriv con IV random 12 byte + auth tag 16 byte, payload base64) usando una chiave master ENCRYPTION_KEY lato server, e salvata nei nuovi campi Preference.aiApiKeyEnc/aiProvider/aiModel/aiRememberKeys. Al login successivo (anche da un altro browser/dispositivo) useAiSettings idrata automaticamente localStorage dalla risposta del nuovo endpoint /api/ai-settings (GET/POST/DELETE protetti da auth + Zod). Default: toggle OFF, comportamento invariato (solo localStorage). Documentazione in DEPLOY.md: la ENCRYPTION_KEY va impostata UNA volta sola e mai cambiata (perdere la chiave master = tutte le API key salvate diventano illeggibili)
  • Trasparenza — descrizione modal Impostazioni AI riscritta — il modal ora spiega esplicitamente cifratura AES-256-GCM, link al repo open source per verifica indipendente, e che la chiave non viene mai loggata ne' usata per altro che restituirla all'utente al login. Nessuna garanzia matematica zero-knowledge (la chiave master e' sul server, decifrabile da chi ha accesso al codice + DB), ma trasparenza completa
  • Fix lint pre-esistente — overview-dashboard.tsx — rimossa la chiamata sincrona setLoading(false) dentro un useEffect (violazione di react-hooks/set-state-in-effect): il branch era inutile perche' il caso !user rendeva gia' <WelcomeOnboarding /> prima del check di loading. Ripuliti anche due eslint-disable ormai inutili in user-data/route.ts e fire-dashboard.tsx. Build ora lint-clean (0 errori, 0 warning)

16 aprile 2026 (AI Advisor — chatbot personalizzato con memoria di sessione)

  • Nuovo tab AI con chatbot integrato — aggiunta una nuova sezione "AI" (icona robot viola) accanto a "Strumenti" che permette all'utente di conversare con un assistente finanziario personale e ottenere consigli basati sui dati reali della piattaforma. Ad ogni messaggio il sistema inietta automaticamente nel system prompt l'intero snapshot dati dell'utente (lo stesso JSON che si otterrebbe cliccando "Esporta dati" dall'ingranaggio), così l'AI conosce patrimonio, obiettivi, preferenze FIRE, mutuo, budget e transazioni senza bisogno di spiegazioni manuali
  • Supporto dual-provider: Google Gemini e OpenRouter — l'utente può scegliere liberamente tra i due provider dalla nuova voce "Impostazioni AI" nel menu ingranaggio. Per ogni provider inserisce la propria API key (salvata solo nel browser in localStorage, mai inviata al nostro server) e sceglie il modello da usare. C'è un pulsante "Carica elenco" che interroga dinamicamente GET /v1beta/models (Gemini) o /api/v1/models (OpenRouter) per popolare il selettore con tutti i modelli disponibili per quella chiave, oppure si può digitare manualmente l'id del modello
  • Architettura privacy-first: le chat non passano dal server — le richieste al modello partono direttamente dal browser verso generativelanguage.googleapis.com o openrouter.ai. Il backend vede solo il GET /api/user-data iniziale (identico al pulsante Esporta dati già esistente) e nulla del traffico AI, API key inclusa. Un adapter unificato src/lib/ai/providers.ts astrae le differenze tra le due API (sistema systemInstruction su Gemini, role: system su OpenRouter)
  • Memoria conversazionale con persistenza cross-tab — ogni turno include lo storico completo dei messaggi, quindi l'AI capisce follow-up e riferimenti ("e per il secondo obiettivo?"). Lo stato vive in uno store a livello di modulo (src/lib/ai/session-memory.ts con pattern useSyncExternalStore) che sopravvive al mount/unmount del componente: cambiando tab e tornando su "AI" la conversazione è ancora lì. Al reload della pagina invece la chat si azzera — nessuna persistenza server-side, nessun impatto sul VPS
  • UX della chat — prompt di esempio cliccabili quando la chat è vuota ("Analizza il mio FIRE number", "Come dovrei ribilanciare il portafoglio?", ecc.), indicatore del provider/modello attivo e del contesto iniettato (in KB), pulsante "Aggiorna dati" per forzare il refresh dello snapshot dopo modifiche in altri tab, pulsante "Pulisci" per resettare la conversazione, invio con Enter e nuova riga con Shift+Enter, auto-scroll al nuovo messaggio
  • Export/Import dati completi — aggiunta tabella BudgetTransaction — prima del lavoro AI l'export utente (/api/user-data) si dimenticava delle transazioni del Budget Tracker (inclusi i CSV bancari importati). Ora l'export include anche budgetTransactions con tutti i campi (date, description, amount, category, categoryOverride, hash), l'import Zod le valida e le reinserisce preservando gli hash originali per deduplicare eventuali re-import incrementali da CSV. Toast di conferma import aggiornato con il conteggio delle transazioni importate

15 aprile 2026 (calcolatore inflazione — KPI "Capitale Equivalente Futuro" + DRY su Fisher)

  • Nuovo KPI "Capitale Equivalente Futuro" — il calcolatore inflazione mostra ora, accanto al "Potere d'Acquisto Finale" (quanto valgono in euro odierni i tuoi soldi tenuti fermi), anche la metrica inversa: quanti euro NOMINALI servono fra N anni per acquistare gli stessi beni che oggi compri con amount euro. Risponde alla domanda piu' pratica per un piano di accumulo — "se voglio comprare casa fra 15 anni a 200k €, di quanto deve crescere il mio target nominale per non essere eroso dall'inflazione?". La card sostituisce la vecchia "Valore Perso" (informazione duplicata rispetto al sotto-titolo della prima card) e l'erosione inflazionistica e' ora riassunta in una riga contestuale piu' leggibile sotto il pannello KPI
  • Indicatore "perdita reale" — quando il rendimento nominale e' inferiore all'inflazione (rendimento reale < 0) la card "Valore Reale Investito" mostra in rosso il tag "(perdita reale)" accanto alla percentuale. Cosi' chi simula scenari conservativi (es. conto deposito al 2% con inflazione al 3%) capisce subito che sta perdendo potere d'acquisto anche se il saldo nominale cresce
  • Nuovo modulo src/lib/finance/inflation.ts — la logica di proiezione e' stata estratta dal componente UI in una funzione pura projectInflation() che restituisce punti del grafico, valori finali, erosione, capitale equivalente e rendimento reale. Il modulo delega il calcolo del rendimento reale al singleton computeRealReturn() di fire-projection.ts, eliminando l'ULTIMA copia inline della formula di Fisher rimasta nel progetto (le altre tre erano gia' state consolidate nel commit precedente). La funzione e' robusta a input NaN/Infinity, anni frazionari (troncati), amount = 0, years = 0 e inflazione = -100% (scenario degenere)
  • 13 nuovi unit test in inflation.test.ts — coprono Fisher esatto vs sottrazione, simmetria fra capitale equivalente futuro e potere d'acquisto residuo, equivalenza fra valore reale e valore nominale scontato, scenari edge (amount/years zero, NaN, inflazione zero, rendimento nominale = inflazione) e regressione contro un futuro cleanup che reintroduca la formula inline. Suite totale: da 142 a 155 test passati

15 aprile 2026 (fix critico FIRE — liquidazione fondo pensione)

  • Bug fix matematico — fondo pensione mai liquidato — risolto un bug finanziariamente critico nel simulatore FIRE (deterministico, Monte Carlo e stress test "Lost Decade"). La logica originale usava una strict equality yAge === pensionFundAccessAge per decidere quando liquidare il fondo pensione complementare: se l'utente aveva gia' superato l'eta' di accesso (es. 65 anni con accesso a 62) OPPURE inseriva un'eta' non intera, il trigger non scattava MAI e il capitale del fondo pensione cresceva all'infinito senza mai essere convertito in rendita/capitale netto. Di conseguenza, la proiezione FIRE ignorava completamente il patrimonio del FP nelle spese di ritiro, sottostimando il successo del piano — in particolare per utenti gia' prossimi al pensionamento
  • Nuovo modulo src/lib/finance/pension-fund.ts — estratta la logica di liquidazione in due helper puri e testabili: liquidatePensionFund() (applica tassazione in uscita, calcola rendita mensile e quota lump-sum per modalita' annuity o hybrid, clamp tax rate a [0,100], gestione edge-case lifeExpectancy <= accessAge evitando divisione per zero) e shouldLiquidatePensionFund() (trigger idempotente con flag hasAccessed + confronto >=, robusto a eta' frazionarie e a currentAge > accessAge)
  • Suite di test regressione — 12 nuovi unit test in pension-fund.test.ts che coprono: modalita' annuity/hybrid, edge-case capitale zero/negativo/NaN, clamping aliquote fuori scala, protezione da divisione per zero, e i due scenari del bug originale (currentAge > accessAge e eta' frazionarie)

15 aprile 2026 (obiettivi — riepilogo con totali aggregati e ordinamento per urgenza)

  • Riepilogo obiettivi piu' ricco — la card "Progresso Totale" del tab Obiettivi di Risparmio ora mostra tre nuovi KPI aggregati in un terzetto di mini-card: "Da risparmiare" (quanto manca complessivamente al raggiungimento di tutti gli obiettivi), "Ritmo richiesto" (somma dei contributi mensili necessari per rispettare tutte le scadenze attive) e "Completati" (contatore X/Y di obiettivi gia' raggiunti). Prima l'utente aveva solo la somma corrente/target e la percentuale: ora ha anche la risposta immediata alla domanda piu' pratica — "quanto devo mettere da parte ogni mese in totale per stare in carreggiata?"
  • Ordinamento intelligente delle card obiettivo — le card sono ora ordinate per urgenza invece che per data di creazione: prima quelli scaduti, poi in ritardo, poi da accelerare, poi in linea, poi gli obiettivi senza scadenza e infine i completati. A parita' di stato il tie-breaker e' la scadenza piu' vicina, cosi' l'obiettivo piu' critico e' sempre in cima senza bisogno di scrollare. Migliora significativamente l'usabilita' per chi gestisce molti obiettivi in parallelo
  • Refactor interno con useMemo — l'estrazione di totali, conteggi, pacing e lista ordinata e' stata unificata in un singolo useMemo su goals, eliminando doppi calcoli di getDeadlinePacing (prima invocata sia in map che in derived) e nuova funzione getGoalSortPriority pura per assegnare la priorita' di ordinamento in base allo stato del pacing

15 aprile 2026 (FIRE — fix rendimento reale con equazione di Fisher esatta)

  • Bug finanziario critico risolto nel calcolo del rendimento reale — il motore FIRE (fire-projection.ts), il tab FIRE (fire-dashboard.tsx) e il consulente acquisti (advisor-dashboard.tsx) calcolavano il rendimento reale con la formula approssimata realReturn = (nominal − inflation) / 100. Per tassi tipici di un piano FIRE (7% nominale, 3% inflazione) questa scorciatoia dava 4.000% mentre l'equazione di Fisher esatta da' 3.883% — un errore relativo del ~3% che si compone nel tempo: su 30 anni portava a sovrastimare il capitale finale di circa il +3-4% e ad anticipare artificialmente il momento FIRE di quasi un anno. Peggio ancora, il calcolatore inflazione (inflation-calculator.tsx) usava gia' la formula corretta, quindi la stessa app mostrava rendimenti reali diversi a seconda del tab visitato
  • Unica fonte di verita' per il rendimento reale — introdotta la funzione pura computeRealReturn(nominalPct, inflationPct) in src/lib/finance/fire-projection.ts che applica Fisher esatto (1 + nominale) / (1 + inflazione) - 1 e include sanitizzazione degli input (NaN → 0) e guard contro scenari degeneri (inflazione ≤ -100%). Tutti i moduli finance ora usano questo helper, eliminando le tre copie della formula sbagliata e garantendo coerenza tra FIRE dashboard, advisor e calcolatore inflazione
  • Proiezione FIRE piu' robusta — in projectFire il monthlyRate e' ora protetto contro rendimenti reali ≤ -100% (prima avrebbe prodotto NaN nel ciclo di simulazione) e anche il coastFireTarget ha un fallback sicuro nello stesso scenario degenere
  • Regressione bloccata da 18 nuovi test — nuovo file fire-projection.test.ts con copertura completa di computeRealReturn (Fisher vs sottrazione, reale negativo, input NaN, inflazione iperbolica) e di projectFire (fireTarget corretto, scenari con/senza acquisto, output non negativi, alreadyFire, coerenza con il calcolatore inflazione). Suite totale: da 112 a 130 test passati

15 aprile 2026 (obiettivi — contributo mensile richiesto e pacing badge)

  • Contributo mensile necessario — ogni obiettivo di risparmio con scadenza ora mostra quanto devi mettere da parte ogni mese per arrivare al traguardo in tempo. Il calcolo e' semplice ma trasparente: (obiettivo − gia' risparmiato) / mesi rimanenti, cosi' invece di vedere solo "8 mesi rimanenti" capisci subito che servono ad esempio "1.000 €/mese per 8 mesi"
  • Badge di stato In linea / Da accelerare / In ritardo / Scaduto — confrontando il ritmo storico di accumulo (calcolato dal momento di creazione dell'obiettivo) con quello richiesto per rispettare la scadenza, la card mostra un badge colorato con icona: verde "In linea" se stai mantenendo il passo, ambra "Da accelerare" se sei oltre il 60% del target ma sotto il necessario, rosso "In ritardo" se sei sotto quella soglia, e "Scaduto" se la deadline e' passata senza raggiungere l'obiettivo. Tooltip esplicativo on-hover su ogni badge
  • Ritmo attuale visibile — sotto al contributo richiesto viene mostrato anche il ritmo effettivo di risparmio ("Ritmo attuale: X €/mese"), cosi' l'utente ha sia il target sia la realta' sott'occhio e capisce esattamente di quanto deve aumentare lo sforzo. Niente piu' obiettivi "impostati e dimenticati": il feedback sul pacing e' immediato e azionabile

15 aprile 2026 (FIRE — fix IMU + nuovi KPI storici nel Riepilogo)

  • Fix double-count IMU nella rendita passiva FIRE — nel tab FIRE, il calcolo della rendita passiva immobiliare sottraeva l'IMU due volte: una volta dentro calculateNetRentalYield (che restituisce gia' il rendimento netto di tutte le spese) e una seconda volta nel ciclo di somma in fire-dashboard.tsx. Conseguenza: gli utenti con immobili locati vedevano una rendita passiva sottostimata, e quindi un target FIRE piu' distante del reale. Bug corretto estraendo la logica in un nuovo modulo puro src/lib/finance/real-estate.ts con le funzioni calculateNetRentalIncome e sumNetRentalIncome, ora coperte da 118 righe di unit test (real-estate.test.ts) che verificano casi con/senza affitto, IMU gia' scorporata e scenari con immobili multipli
  • Refactor fire-dashboard.tsx — il componente orchestratore scende da ~920 a ~885 righe delegando il calcolo della rendita passiva al nuovo modulo; meno logica inline nel componente UI, piu' testabilita'
  • Nuovi KPI storici nel Riepilogo: CAGR e Max Drawdown — due card aggiuntive in cima al tab Riepilogo che analizzano la serie storica degli snapshot patrimoniali:
    • CAGR annualizzato (Compound Annual Growth Rate): tasso di crescita medio composto del patrimonio netto, calcolato sulla distanza temporale effettiva tra primo e ultimo snapshot. Si attiva quando c'e' almeno un anno di storia e mostra "N/A" con tooltip esplicativo negli altri casi
    • Max Drawdown: massima perdita percentuale da un picco storico a un minimo successivo, indicatore chiave di volatilita' e resilienza del portafoglio nei momenti peggiori (utile per capire se si e' psicologicamente pronti a reggere un bear market)
  • Modulo src/lib/finance/history-stats.ts — nuove funzioni pure calculateCAGR(snapshots) e calculateMaxDrawdown(snapshots) coperte da 105 righe di unit test (history-stats.test.ts). Isolate dal componente UI per poter essere riutilizzate altrove (es. grafici storici, report export)
  • Suite test — da 88 a 111 test passati (+23 nuovi test sui due moduli finance)

14 aprile 2026 (riepilogo — tooltip breakdown patrimonio netto)

  • Tooltip "cosa ha mosso il patrimonio" — passando col mouse sopra la cifra del Patrimonio Netto nel tab Riepilogo appare un pannello a scomparsa che spiega in dettaglio da dove arriva la variazione rispetto allo snapshot precedente. Il breakdown mostra il delta per ciascuna categoria (Liquidita' & ETF, Fondo Emergenza, Bitcoin, Beni Rifugio, Fondo Pensione, Debiti), con la percentuale di contributo al movimento totale, ordinato per impatto assoluto e con riga "Totale" finale che combacia con il +/-€ mostrato sotto. I debiti sono invertiti (una riduzione debiti conta come contributo positivo), cosi' l'utente capisce subito se il mese e' andato bene grazie a mercato, risparmio o deleveraging

14 aprile 2026 (consulente acquisti v2.1 — fix su dati reali)

  • Impatto patrimoniale su investibile — il peso dell'acquisto ora e' misurato sul "patrimonio investibile" (asset totali − immobili − debiti) invece che sul patrimonio netto totale. Per chi ha casa di proprieta', un'auto da 30k non "vale" il 2% dei 1.4M totali: vale il 6% dei 500k realmente mobilizzabili, ed e' questa la cifra che conta
  • Coast FIRE supportato — il grafico Impatto FIRE non mostra piu' "non calcolabile" quando il risparmio mensile e' 0: se hai capitale gia' accumulato, il motore proietta comunque la crescita dell'investito e l'erosione causata dall'acquisto, con banner "Modalita' Coast FIRE" esplicativo
  • Verdetto numerico — il verdetto finale ora cita i numeri concreti: "ti costa X totali in N anni, sposta il FIRE di circa M mesi, include Y di mancato rendimento". Niente piu' frasi generiche, solo impatto misurabile
  • Mesi di copertura basati sulle spese — l'emergency fund dopo l'acquisto e' calcolato su liquidita' residua / (spese + rate), non piu' sul reddito. Cap semantico "oltre 24 mesi (abbondante)" invece di stringhe tipo "255.7 mesi"
  • Pesatura su prestiti multipli — se hai gia' 2+ finanziamenti in corso, scatta un avviso cumulativo dedicato e il verdetto viene spinto verso "cautela" perche' gli impegni si sommano e la resilienza agli imprevisti si riduce su piu' fronti
  • Acquisti accettati in cima — la lista dei finanziamenti gia' accettati nel Consulente e' stata spostata in cima alla pagina (prima del form di simulazione), con totali impegno TCO e rate mensili aggregate. Cosi' vedi l'impegno cumulato prima di aggiungerne un altro
  • Ordine didattico delle sezioni — "Come abbiamo calcolato" ora precede i grafici di impatto: prima capisci da dove vengono i numeri, poi vedi le conseguenze (Impatto FIRE → Confronto Scenari → Sensitivita' → ammortamento)
  • Rimossa proiezione patrimoniale legacy — il vecchio chart "Con vs Senza acquisto" di PurchaseImpactChart e' stato eliminato: mostrava "differenza -€0" quando il risparmio era 0 ed era ridondante con il grafico Impatto FIRE piu' robusto
  • Default svalutazione auto 20% — bumpato da 15% per riflettere meglio la svalutazione reale di auto nuove (specie EV: 20-25%/anno)

14 aprile 2026 (consulente acquisti v2)

  • Consulente usa tutti i dati dell'utente — fix della sorgente dati (l'API ritorna history, non records, quindi prima lo snapshot era vuoto) e caricamento completo delle preferenze: spese mensili reali da expensesList (gestisce isAnnual), risparmio reale (reddito - spese - rate esistenti) invece del 20% hardcoded, rata totale dei prestiti gia' in corso calcolata con getInstallmentAmountForMonth, DTI pre-acquisto e post-acquisto (con rate esistenti incluse), parametri FIRE personali (anno di nascita, eta' pensione, spese attese, SWR, rendimento, inflazione). Il costo opportunita' ora usa il rendimento reale dell'utente (nominale - inflazione) invece del 7% fisso
  • Grafico Impatto FIRE — nuova sezione che sovrappone due curve di proiezione del capitale (con e senza acquisto) fino al raggiungimento del Target FIRE, con badge prominente dello spostamento in mesi/anni ("+2a 4m" / "-3 mesi"), quattro mini-KPI (FIRE senza, FIRE con, Target, Risparmio usato) e ReferenceLine sull'eta' di raggiungimento FIRE per entrambi gli scenari
  • Pannello "Come abbiamo calcolato" — sette blocchi accordion espandibili (rata, TCO, DTI, liquidita', costo opportunita', target FIRE, impatto patrimoniale) che mostrano per ogni numero: formula matematica, sostituzione dei valori reali dell'utente, risultato. Cosi' il Consulente smette di essere una black-box e diventa didattico
  • Confronto Scenari Cash / Finanziato / Non comprare — BarChart comparativo a tre colonne con patrimonio finale, liquidita' dopo l'esborso, interessi pagati e anni al FIRE in ciascuna strategia. Badge "Migliore" sullo scenario vincente e gap in euro per quelli perdenti, cosi' la decisione diventa evidente
  • Sensitivita' del finanziamento — ComposedChart con toggle Anticipo%/Durata anni che mostra come cambiano TCO, interessi totali e ritardo FIRE al variare di quel parametro. ReferenceDot verde evidenzia la scelta attuale dell'utente, con riassunto testuale sotto al grafico
  • Motore FIRE condiviso (src/lib/finance/fire-projection.ts) — nuova funzione pura projectFire(params) deterministica con modificatori di scenario (esborso una tantum, rata ricorrente, costi ongoing) + helper fireDelayMonths e formatDelay. Riutilizzabile da Consulente e potenzialmente dal tab FIRE, evita la duplicazione dei calcoli
  • Top bar Consulente ampliata — da 4 a 6 KPI: Patrimonio Netto, Liquidita', Reddito, Risparmio Reale, DTI Attuale e Debiti/Eta', con sottotitolo descrittivo su ogni card

13 aprile 2026 (sync abbonamenti → patrimonio)

  • Abbonamenti sincronizzati nel cashflow patrimonio — gli abbonamenti inseriti nel Tracker Abbonamenti Ricorrenti vengono ora inclusi automaticamente tra le spese automatiche della sezione Patrimonio (Passivita & Cashflow). Il totale mensile degli abbonamenti si somma a costi immobiliari e rate prestiti, aggiornando in tempo reale il Risparmio Netto Mensile, il Cashflow Atteso e tutti gli indicatori derivati (Indice di Sopravvivenza, proiezione FIRE). Nessuna duplicazione manuale necessaria: basta aggiungere un abbonamento nel tracker e il profilo finanziario si aggiorna da solo al prossimo caricamento

13 aprile 2026

  • Abbonamenti ricorrenti persistenti — il Tracker Abbonamenti ora salva gli abbonamenti nel database (subscriptionsList nelle preferenze utente) e li ricarica al login, invece di perderli al refresh della pagina. Il campo e' incluso anche nell'export/import dati JSON, cosi' gli abbonamenti vengono trasferiti tra account e dispositivi
  • File demo demo-marco-giulia.json — file JSON di esempio con dati completi di una coppia (profilo, spese, portafoglio ETF, Bitcoin, beni rifugio, fondo pensione, prestiti, parametri FIRE, simulatore mutuo, obiettivi di risparmio, categorie budget, abbonamenti e 7 snapshot storici). Pronto per essere importato con "Importa dati" dal menu ingranaggio

12 aprile 2026 (istruzioni import Directa)

  • Guida "Come scaricare il file da Directa" — il riquadro iniziale di Importa Movimenti Directa ora mostra i 5 passaggi per ottenere il CSV: accedi a Directa versione Libera, apri la sezione Movimenti, seleziona il periodo temporale dal calendario, clicca sull'icona del file Excel per scaricare il CSV, carica il file nell'app. Cosi' chi non conosce l'interfaccia Directa non deve cercare altrove

12 aprile 2026 (budget tracker v2)

  • Budget persistente con categorie personalizzate — il Budget Mensile non e' piu' un semplice viewer dell'ultimo CSV: categorie, limiti, colore, icona e parole chiave vengono salvati nel database (budgetCategoriesList nelle preferenze utente) e sopravvivono al refresh e al cambio dispositivo. Ogni utente configura le proprie categorie con il pannello dedicato (crea, rinomina, cambia limite, elimina, assegna parole chiave per l'auto-categorizzazione)
  • Transazioni salvate con dedupe — nuova tabella BudgetTransaction nel database per persistere le transazioni importate dai CSV bancari. Ogni transazione ha un hash univoco (data + descrizione + importo) che impedisce doppi inserimenti: re-importare lo stesso CSV non crea duplicati, importi storici di mesi diversi si accumulano nel tempo. Nuove API GET/POST/DELETE /api/budget/transactions, PATCH /api/budget/transactions/[id] e POST /api/budget/transactions/reapply
  • Auto-categorizzazione con keyword utente — le parole chiave definite dall'utente per ogni categoria hanno la precedenza sulle regole built-in del parser bank-csv. Al momento dell'import ogni transazione viene assegnata in base alle keyword (match case-insensitive, min 2 caratteri) e l'assegnazione puo' sempre essere corretta manualmente dal drawer transazioni. Nuovo endpoint reapply per ricategorizzare tutte le transazioni quando cambi le keyword, preservando gli override manuali
  • Modalita' visualizzazione multiple — switch tra "Mese corrente" (spesa del singolo mese), "Media mensile" (media su tutti i mesi con transazioni) e "Totale" (somma di tutte le transazioni importate). Persistente come preferenza utente in budgetSettings
  • KPI, grafici e alert — nuova intestazione con KPI cards (speso totale, limite, residuo, % utilizzo), grafico di confronto categorie (spesa vs limite con colori dinamici), grafico trend mensile sulle ultime 6 mensilita', alert per le transazioni non categorizzate ("Altro") con link diretto al drawer per assegnare la categoria corretta
  • Drawer transazioni — nuovo pannello laterale con elenco completo, filtri per categoria/periodo, possibilita' di cambiare categoria inline (con flag categoryOverride per proteggere la scelta dalla ricategorizzazione automatica), eliminare singole transazioni o ripulire tutto. L'intero Budget Tracker e' stato decomposto in 7 sotto-componenti memo (budget-header, budget-kpi-cards, uncategorized-alert, budget-categories-panel, budget-comparison-chart, budget-trend-chart, transaction-drawer) orchestrati da budget-tracker.tsx e dal nuovo hook useBudget che incapsula fetch, import, update e delete

12 aprile 2026 (fix variazioni ETF)

  • Fix variazioni ETF errate su ticker duplicati e holding nuove — nel dettaglio snapshot le holding con ticker duplicato (es. SUSW.MI 2130 e SUSW.MI 825, o lotti diversi dello stesso ISIN) mostravano variazioni assurde perche' il fallback per ticker associava la holding sbagliata nello snapshot passato. Allo stesso modo, holding nuove assenti nel passato apparivano con +€totale (+0.00%). Ora il matching e' strict: si usa id; se fallisce, il fallback per ticker si applica solo quando il ticker e' unico in entrambi gli snapshot (current e passato). Negli altri casi la variazione e' "n/d"

12 aprile 2026 (variazioni per ETF)

  • Variazioni 1g / 7g / 30g per singolo ETF — nel pannello espanso del Dettaglio Snapshot, ogni riga della sezione "Dettaglio ETF / Strumenti" ora mostra le variazioni (euro + percentuale, verde/rosso) rispetto a 1, 7 e 30 giorni prima per quel specifico titolo. Il matching avviene per id con fallback sul ticker, cosi' segue lo stesso holding anche se il record storico aveva prezzi diversi

12 aprile 2026 (dettaglio snapshot)

  • Liquidita ed ETF separati nello storico snapshot — la tabella Dettaglio Snapshot ora mostra due colonne distinte: "Liquidita" (conto, contante) ed "ETF / Strumenti" (titoli e strumenti finanziari), invece di sommarli in un'unica voce confusa
  • Ordinamento discendente degli snapshot — lo storico ora mostra sempre gli snapshot dal piu' recente al piu' lontano nel tempo, sia su desktop che su mobile
  • Tendina espandibile con dettaglio completo — ogni riga/card snapshot puo' essere espansa per mostrare un pannello con tutti i componenti del patrimonio a quella data: breakdown per immobile (da realEstateList), breakdown per ticker ETF (da customStocksList), liquidita, fondo emergenza, fondo pensione, beni rifugio, bitcoin (quantita x prezzo), debiti totali
  • Variazioni 1g / 7g / 30g per ogni componente — ogni card del pannello dettaglio mostra le variazioni rispetto a 1, 7 e 30 giorni prima per il singolo componente (non solo il totale): immobili, liquidita, ETF, bitcoin, fondo emergenza, fondo pensione, beni rifugio, debiti e patrimonio netto totale. Ogni badge mostra sia il delta in euro sia la percentuale, colorato verde se positivo / rosso se negativo, con icona trend e data di riferimento
  • Helper findPastSnapshot — trova lo snapshot piu' recente con data <= (target - N giorni), cosi' le variazioni funzionano anche con snapshot non giornalieri (prende il piu' vicino disponibile)

12 aprile 2026

  • Proiezione patrimonio con interesse composto — nuova terza modalita' di proiezione: oltre a "Risparmio Netto" e "Trend Storico", ora disponibile "Risparmio + Rendimento" che combina il risparmio mensile con il rendimento atteso configurato nella sezione FIRE (interesse composto con versamenti periodici)
  • Spiegazioni on-hover — tutti i testi esplicativi che occupavano spazio nell'interfaccia sono stati convertiti in tooltip: passando il mouse sull'icona info accanto a ogni parametro si legge come viene calcolato, senza ingombrare la vista. Interessa proiezione patrimonio, parametri FIRE (SWR, inflazione, rendimento, volatilita', fondo pensione, bollo, pensione INPS), simulatore mutuo (DTI, cashflow, costo opportunita'), profilo finanziario, strategia debiti e calcolatore interesse composto
  • Componente InfoTooltip riutilizzabile — nuovo componente UI basato su Radix Tooltip per mostrare spiegazioni contestuali al passaggio del mouse, con icona Info e stile coerente in tutta l'app
  • Fix risparmio netto in proiezione — la proiezione patrimonio nel Riepilogo ora usa il netIncome salvato dal Profilo Finanziario (single source of truth) invece di ricalcolarlo con una formula diversa che non teneva conto delle rate dinamiche dei prestiti, causando un valore piu' alto del reale

11 aprile 2026 (mobile patrimonio)

  • Card Patrimonio piu' chiare su smartphone - superfici meno trasparenti e meno "annerite" nelle sezioni Patrimonio, Asset, Cashflow e Storico; tab attivo della navigazione Patrimonio ora usa un accento blu invece del blocco scuro
  • Navigazione mobile piu' leggibile - tab principali, sottotab Asset e scorciatoie overview ottimizzati per viewport strette con testi meno troncati e scorrimento orizzontale dove serve
  • Grafici piu' leggibili da telefono - migliorati margini, tooltip, tick e legenda mobile dello storico Patrimonio; semplificata anche la simulazione portafoglio per lasciare piu' spazio al grafico
  • Storico e ribilanciamento mobile-friendly - la tabella snapshot ora mostra card compatte su smartphone e il ribilanciamento ha un layout dedicato mobile invece della griglia compressa da desktop
  • Form debiti e cashflow rifiniti - card debiti con layout verticale piu' comodo su schermi piccoli, filtri proprietario piu' flessibili e modal prestiti con campi a una colonna su mobile

11 aprile 2026 (brand refresh)

  • Logo ufficiale in UI - nuovo BrandLogo con crop dedicato del wordmark per eliminare gli spazi vuoti del PNG e mantenere proporzioni corrette; header principale, onboarding e pagina /login ora usano il marchio in modo coerente su desktop e mobile
  • Favicon e icone PWA - collegate le nuove risorse favicon in Next.js, manifest.json, site.webmanifest, favicon.ico e Service Worker, cosi' browser tab, shortcut mobile e installazione PWA usano lo stesso set di icone
  • Login piu' pulita - rimosso l'autofocus iniziale del campo username che faceva scorrere subito la viewport e poteva nascondere il logo nelle schermate piu' piccole

11 aprile 2026 (pomeriggio)

  • Logo e branding — nuovo logo grafico con componente BrandLogo riutilizzabile; header principale e pagina /login ora usano l'immagine al posto di icona+testo
  • Favicon e icone PWA — set completo di favicon (SVG, PNG 96x96, ICO) e icone PWA (192x192, 512x512, apple-touch-icon 180x180) nella cartella /favicon/; manifest.json e Service Worker aggiornati con le nuove risorse
  • Proiezione patrimonio con regressione lineare — la proiezione del patrimonio futuro usa ora la regressione ai minimi quadrati su tutti gli snapshot storici invece del semplice delta primo-ultimo punto, dando una stima piu' affidabile e meno sensibile agli outlier
  • Fix cashflow riepilogo — le spese annuali (assicurazioni, revisione, ecc.) vengono ora divise per 12 nel calcolo del cashflow mensile; gli affitti degli immobili vengono contati come entrata
  • Fix overflow testo navigazione Patrimonio — i bottoni di navigazione (Panoramica, Asset, Passivita & Cashflow, Storico) ora troncano correttamente il testo che eccedeva i bordi su schermi stretti

11 aprile 2026 (tarda notte)

  • Maialino cashflow familiare — il KPI con il maialino e la percentuale di risparmio nell'header ora naviga direttamente alla sezione Profilo Familiare nel tab Patrimonio (invece che al Budget), dove sono visibili Reddito Lordo Familiare, Totale Spese e Risparmio Mensile Netto; scroll automatico alla sezione
  • Fix calcolo tasso di risparmio — le spese annuali (assicurazioni, revisioni, ecc.) venivano sommate senza dividere per 12, gonfiando le uscite e mostrando una percentuale errata; ora il calcolo nell'header e' allineato con il Profilo Familiare
  • Colori tasso di risparmio — rosso sotto il 20%, arancione tra 20% e 35%, verde tra 36% e 50%, verdissimo con razzetto sopra il 50%

11 aprile 2026 (notte)

  • Snapshot giornalieri automatici — nuovo scheduler in background che ogni giorno a mezzanotte aggiorna gli snapshot di tutti gli utenti con prezzi BTC e stock/ETF freschi, cosi' lo storico patrimonio riflette l'andamento reale anche senza aprire l'app per settimane
  • Auto-save al caricamento — quando l'utente apre il tab Patrimonio, i prezzi BTC e stock vengono aggiornati e lo snapshot del giorno viene salvato automaticamente con i valori live
  • Pagina /login dedicata — nuova route /login con form pulito e centrato per accedere o registrarsi senza passare dal modal della dashboard; redirect automatico se gia' loggati
  • Endpoint cron snapshot — nuovo endpoint /api/cron/snapshots (protetto da CRON_SECRET) per forzare manualmente il refresh degli snapshot di tutti gli utenti

11 aprile 2026 (sera)

  • Fondo pensione complementare nel FIRE — il valore del fondo pensione non viene piu' contato come asset liquido nel calcolo FIRE; ora cresce come pot separato con contributi volontari (max 5.164 €), TFR automatico (~6.91% RAL) e contributo datore di lavoro (% o fisso, configurabile)
  • Modalita' uscita FP — l'utente sceglie tra "50% capitale + 50% rendita mensile" (max legale) oppure "100% rendita"; la rendita viene sottratta dalle spese dopo l'eta' di accesso
  • Eta' accesso RITA — campo separato dall'eta' pensione INPS per modellare l'accesso anticipato al fondo pensione (default 62 anni)
  • Tassazione uscita FP — slider 9%-15% per configurare l'aliquota in base all'anzianita' di partecipazione al fondo
  • Rimborso IRPEF — il risparmio fiscale sui versamenti volontari viene reinvestito automaticamente a luglio nella simulazione
  • Coerenza simulazioni — tutte e 3 le simulazioni (deterministica, Monte Carlo 10k runs, stress test Lost Decade) aggiornate con il modello a doppio pot
  • Persistenza — tutti i parametri del fondo pensione (ottimizzatore, RAL, contributi, RITA, tassazione, modalita' uscita, contributo datore) vengono ora salvati nel database e sopravvivono al refresh

11 aprile 2026

  • Controvalore manuale per ticker non trovati — se un ticker azione/ETF non viene trovato sui mercati, ora e' possibile cliccare sul "?" per inserire manualmente il controvalore in euro; il valore manuale ha la precedenza su prezzo*quote in tutti i calcoli (patrimonio, snapshot, totali). Toast informativo a scomparsa mostrato una sola volta per sessione.

10 aprile 2026 (notte)

  • Esporta / Importa dati — nuovo menu ingranaggio accanto al nome utente con possibilita' di esportare tutta la situazione finanziaria (preferenze, snapshot patrimonio, obiettivi) in un file JSON e reimportarla su un altro account o dispositivo

10 aprile 2026 (sera)

  • Proprietario per asset — ogni investimento (azioni/ETF), immobile, altro asset (Bitcoin, beni rifugio, TFR) e debito puo' ora essere assegnato a Persona 1 o Persona 2 con badge colorato e totale parziale per ciascuno
  • Filtro per persona — barra filtro "Tutti / Persona 1 / Persona 2" nelle sezioni Investimenti, Immobili e Passivita con subtotali dinamici
  • Altri asset per persona — Bitcoin, beni rifugio e fondo pensione hanno ora due campi affiancati (uno per persona) con totale combinato
  • Intestatario debiti — il modal di creazione/modifica prestito include il campo Intestatario; i debiti sono filtrabili per persona nella sezione Passivita

10 aprile 2026

  • Analisi rendimenti avanzata — ogni strumento nella classifica investimenti mostra ora il rendimento MWR (Money-Weighted Return / XIRR annualizzato), il rendimento semplice, il rendimento annualizzato, la durata dell'investimento e la contribuzione percentuale al P/L totale del portafoglio
  • XIRR di portafoglio — nuovo KPI che mostra il rendimento annualizzato complessivo di tutte le posizioni chiuse, ponderato per i flussi di cassa reali
  • Tutti gli strumenti visibili — la tabella dettaglio per strumento mostra ora tutti i ticker senza limiti, con riepilogo guadagni/perdite totali

9 aprile 2026

  • Viewer Movimenti Directa — nuovo strumento nella sezione Strumenti per importare il CSV scaricato da Directa Trading. Analisi completa con 8 KPI, grafico cumulativo (conferimenti, investito, dividendi), flussi mensili, distribuzione operazioni, riepilogo annuale, dettaglio per strumento e tabella movimenti con filtri per anno, ticker, categoria e ricerca testuale. Solo visualizzazione client-side, nessun dato salvato
  • Sezione "Strumenti" — la sezione Calcolatori e' stata rinominata Strumenti per raccogliere calcolatori finanziari e il nuovo viewer Directa
  • Patrimonio rinnovato — snapshot piu' ricchi con liquidita' distinta dal valore titoli, storico migliorato ed export CSV aggiornato
  • Sezione Carriera evoluta — simulazioni di crescita professionale e nuovo calcolatore lordo-netto integrato nella dashboard con IRPEF, INPS, detrazioni e addizionali
  • Cronologia stipendio — ogni simulazione dello stipendio netto puo' essere salvata, ricaricata o eliminata; persiste sull'account o sul dispositivo in modalita' guest

8 aprile 2026

  • Lancio Effetto Composto — prima release pubblica su effettocomposto.it con 8 sezioni: Riepilogo, Patrimonio, Carriera, Consulente Acquisti, Immobiliare, FIRE, Budget e Calcolatori
  • Simulatore Mutuo — calcolo rata con ammortamento francese, confronto fino a 3 mutui side-by-side, analisi DTI e analisi redditivita' acquisto vs affitto
  • Calcolatore FIRE — 10.000 simulazioni Monte Carlo via Web Worker con proiezione patrimonio e probabilita' di successo per anno target
  • Tracker Patrimonio — snapshot giornalieri del patrimonio netto, gestione immobili, portafoglio titoli con dividendi, prestiti attivi, proiezione futura e storico esportabile in CSV
  • Budget e Abbonamenti — spese per categoria con import CSV (Fineco, Intesa, generico), tracker abbonamenti ricorrenti, obiettivi di risparmio e strategia debiti (snowball vs avalanche)
  • Mutui Market — confronto offerte mutui in tempo reale da MutuiSupermarket.it con aggiornamento automatico giornaliero, filtri per tipo tasso e durata, ricalcolo rata personalizzato
  • Proiezione patrimonio — stima dell'evoluzione futura del patrimonio anche senza storico, basata su patrimonio attuale e risparmio mensile
  • PWA installabile — funziona come app su smartphone, supporto offline con Service Worker
  • Sicurezza — autenticazione JWT, password bcrypt 12 rounds, rate limiting, HTTPS con HSTS, validazione input con Zod, security headers completi
  • Dark mode e responsive — interfaccia ottimizzata per mobile e desktop con tema chiaro e scuro

Avvio Rapido

# 1. Clona il repository
git clone https://github.com/iSte94/EffettoComposto.git
cd EffettoComposto

# 2. Installa le dipendenze
npm install

# 3. Configura l'ambiente
cp .env.example .env

# 4. Inizializza il database
npx prisma generate
npx prisma db push

# 5. Avvia il dev server
npm run dev

Apri http://localhost:3000 nel browser.

Comandi

Comando Descrizione
npm run dev Dev server con hot reload
npm run build Build di produzione
npm run lint Linting con ESLint
npm test Esegui i test (una volta)
npm run test:watch Test in watch mode
npx prisma studio GUI per esplorare il database

Variabili d'Ambiente

Variabile Descrizione Obbligatoria
DATABASE_URL Path al database SQLite Si
JWT_SECRET Secret per i token di autenticazione JWT In produzione
NODE_ENV development o production No

Sicurezza e Privacy

I tuoi dati sono solo tuoi. Effetto Composto e' progettato con la privacy al centro:

  • Nessun tracking, nessuna analytics, nessun cookie di terze parti — zero dati inviati a servizi esterni
  • Database locale — tutti i dati finanziari restano nel database SQLite sul server, mai condivisi
  • Nessuna email richiesta — la registrazione usa solo username e password, niente dati personali
  • Codice open-source — chiunque puo' verificare esattamente cosa fa l'applicazione

Protezione dei dati

Misura Dettaglio
Password Hash con bcrypt (12 rounds, irreversibile). La password in chiaro non viene mai salvata
Autenticazione Token JWT firmati con HMAC-SHA256, durata 24h, cookie HttpOnly + Secure + SameSite=Strict
Brute-force Rate limiting: max 5 tentativi ogni 15 minuti per IP
Trasporto HTTPS obbligatorio con TLS 1.2+ (Let's Encrypt), HSTS preload attivo
Headers X-Content-Type-Options, X-Frame-Options, X-XSS-Protection, Referrer-Policy, Permissions-Policy
Validazione Ogni input API validato con Zod schema prima dell'elaborazione
API protette Tutte le route dati richiedono autenticazione JWT valida

Cosa NON facciamo

  • Non raccogliamo email, numeri di telefono o dati identificativi
  • Non vendiamo, condividiamo o analizziamo i dati degli utenti
  • Non usiamo Google Analytics, Facebook Pixel o altri tracker
  • Non inviamo i dati finanziari a servizi di terze parti
  • Le uniche chiamate esterne sono per i prezzi di mercato (Yahoo Finance, Binance) e non contengono dati utente

Contribuire

I contributi sono benvenuti! Apri una issue per segnalare bug o proporre nuove funzionalita', oppure invia una pull request.

  1. Forka il repository
  2. Crea un branch (git checkout -b feature/nuova-funzionalita)
  3. Committa le modifiche (git commit -m 'feat: aggiungi nuova funzionalita')
  4. Pusha il branch (git push origin feature/nuova-funzionalita)
  5. Apri una Pull Request

Licenza

Distribuito con licenza CC BY-NC 4.0. Libero per uso personale e non commerciale.


About

Strumenti gratuiti e open-source per l'indipendenza finanziaria. Simulatore mutuo, calcolatore FIRE, tracker patrimonio, budget. Nessun tracking, nessun dato condiviso, privacy-first. Tutto in italiano.

Topics

Resources

Stars

3 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages