# Changelog - Pacifinance All notable changes to the project are documented in this file. The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and the project adheres to [Semantic Versioning](https://semver.org/) (`MAJOR.MINOR.PATCH`; pre-1.0.0, `MINOR` bumps can still include breaking changes). > **A note on the version numbers below `[0.10.0]`:** entries from `[0.9.0] - 2024 (Early)` through `[0.9.9] - 2026-04-17` were written narratively, grouping features into a `0.9.x` sequence that was never actually reflected in `package.json` (which really went `0.1.0` → `0.5.1` → `0.9.0` → `0.9.9`, confirmed from `git log -- package.json`, and only became `0.9.9` in February 2026 — a year after this file's own `[0.9.3] - 2024-10` entry). They're kept as-is below for the feature-level detail, which is accurate and worth having, but treat their version *numbers* as approximate rather than git-tag-accurate. Starting with `[0.10.0]`, every entry here corresponds to one real `package.json` bump and one git tag — see "Versioning policy" at the bottom. --- ## [Unreleased] ### Changed - The "+" menu icons are now color-coded (red outflow, green income, blue balance, and distinct colors for imports, recurring and shared expenses, reusing the app's existing palette) so each action is recognizable at a glance. - One "+" to add anything. The quick-add menu is now grouped (Add / Balance / Import / Manage) and opens every add flow on top of the current page: quick outflow/income entry (with a "More details" link that carries what you typed into the full form), the full outflow/income/balance forms with multi-insert, CSV and investment imports, recurring and shared expenses. Every flow has its own link (`?add=outflow|income|balance|import|...`), so dashboard shortcuts, onboarding and push notifications open the right form directly, and the phone's back button closes it. - The "Insert data" page became **Transactions** (`/transactions`): review and edit your outflows, incomes and monthly balance; adding happens from the "+" (or the page's own "Add" button, which opens the same form). Old `/insert-values` links redirect, keeping their `?section=`. ### Fixed - The "What's New" red dot never went away: closing the panel only marked the first 5 updates as seen out of a 3-month list that usually holds more, and closing it by clicking the icon again marked nothing. Closing it in any way now marks every recent update as seen. - Toast notifications now appear above open dialogs instead of behind them. - Links without a language prefix now keep their query string when redirected (e.g. notification links). - The quick-add button stays bottom-right on desktop instead of rendering inside the sidebar. - The monthly spending-limit alert no longer fires on every expense once the month is over the limit: it appears when the limit is first crossed, then only every further 5% of the limit (e.g. every 100 on a 2000 limit), and a "Don't warn me again this month" button silences it until next month. It is now a warning (not an error), translated in every language, uses the selected display currency, only counts expenses dated in the current month, respects a disabled limit (it used to keep alerting on the stored default amount) and also works from quick add. - Choosing a payment type (e.g. "single payment") is no longer silently overridden while typing the note, and the note-based guess from history now requires the recurring type to be the most common one for that merchant, instead of flipping to "periodic payment" because of a single similar past expense. - CSV import no longer drops Trade Republic buy/sell trades: they are real cash leaving (or entering) the account, so a buy is now imported as an outflow in the Investment category (counted in outflows, never in expenses) and a sale as investment proceeds kept out of income statistics. The mapping step says how many trades were found and points to "Import Investments" for recording quantity and price. - The quick-add "+" button (manual entry, balance update, CSV import, investment import) was only reachable on desktop from the Dashboard, while on mobile it's available on every page from the bottom bar. It's now on every page on desktop too, and is the single entry point for adding anything: recurring transactions and shared expenses moved into its menu, replacing the separate "More tools" bar on the insert page. - The redesigned Comparison page's percentile gauge crashed the whole page at render time (a styled-components `keyframes` object was interpolated into a plain inline `style` string instead of an actual styled-components template, which styled-components deliberately throws on) - visible in the browser console as `Uncaught Error ... errors.md#12`, reachable the moment benchmark consent was on and the gauge had a value to animate. Added a render smoke test for `Comparison` so this class of runtime-only bug (invisible to lint/types/pure-function tests) fails a test run again. - Comparison page polish from live-testing on a deployed preview: - The percentile gauge is now a full ring instead of a semicircle dome, and its number/label are centered with plain flexbox instead of a hand-tuned `top: 54%` guess - the old shape made pixel-perfect centering inherently fiddly; a full circle's visual center trivially matches its bounding box. - The gauge caption and the three headline chips (net worth/income/ frugality percentiles) didn't explain what the percentage actually meant. The caption now states it explicitly ("higher than X% of people with a similar profile"), and a new line under the chips clarifies that higher is always better, including for the frugality (outflow) one. - The info icon next to "Your comparison group" rendered black in dark mode (no theme-aware color was set, so it fell back to the browser default instead of inheriting one) - same class of bug as any other icon with no explicit color, now fixed here explicitly since no centralized icon-color helper exists in this codebase. - "Spending by Category" only ever showed the user's own amounts - added the same "vs. your comparison group" line every other accordion section already has, per category. - The gauge caption (the sentence explaining what the percentile means) was missing `margin: 0 auto` on its `max-width`-constrained box, so it hugged the left edge of the card instead of centering under the now-circular gauge above it. - Insert Data's "outflows"/"income" secondary tools row (CSV/Excel, Ricorrenti, Spese condivise) stayed capped at an old 1000px width on desktop while the page around it (`ContentWrapper`/`SectionCard`) was widened to 1400px in a previous change, leaving a wide dead strip to its right instead of right-aligning against the same edge as the form card beneath it. - Consistency and correctness pass across the CSV import wizard and the Aggiungi entrate/uscite flow, prompted by live-testing a real Trade Republic export: - Standardized every dropdown that wasn't already using the app's own `CategoryPicker` (categories, with sub-category support) or `ThemedSelect` (everything else) - column-mapping and default-category pickers in the CSV import wizard, several selects in Settings/Goals & Limits/Liquidity Accounts/Investment panels, and 6 profile-field selects in the sidebar that had no dark-mode styling at all. Category *filters* (Outflows/ Incomes/insert-values) now also list the user's own custom sub-categories, nested under their parent via ``, not just the official tags. - CSV import duplicate detection no longer widens its match window just because two transactions share a broad official category (e.g. "Alimentari") - that's true of dozens of unrelated purchases, so on its own it was flagging genuinely unrelated transactions days apart (even across a month boundary) as possible duplicates. Widening the window now requires either a shared merchant token in the notes or a match on the user's own specific custom sub-category. - A Trade Republic "saveback" cashback reward that Trade Republic invests directly (no cash ever hits the account) was being imported as plain income, inflating income statistics. It's now tagged as an investment purpose and excluded from income statistics, with a badge in the import review explaining why. Actually crediting the reward to the holding it funded is a bigger, separate change - tracked in `todo.md`. - The outflow "Tipo di movimento" (purpose) and "Tipologia" (payment type) fields could previously be combined in ways that don't make sense (e.g. "Piano di accumulo" on a plain expense). Tipologia's options now depend on the selected purpose, and switching purpose resets an option that's no longer valid instead of silently keeping it. - Replaced the cramped inline edit grid in Outflows/Incomes card view - and, in a follow-up, the equally cramped inline row edit in table view too - with a single shared centered modal (reusing the same modal primitives `CategoryPicker`'s create-category dialog already uses), at every screen size, instead of one popup for mobile and a different inline layout for desktop. - Outflows/Incomes table view on mobile no longer pins the category column left and the edit/delete column right - that combination left only a narrow sliver of the table actually scrollable, showing one value at a time and making the table hard to read. The whole row now scrolls together as one unit, like a normal mobile table. - Shared expenses are now grouped by month (collapsible, newest first) with a filter panel (note search, status, date range), instead of one long flat list. - Follow-up polish from live-testing the modal and mobile scroll fixes above: - The shared `FilterRow`/`FilterInlineRow` components (used by every Outflows/Incomes/Shared-Expenses filter panel) weren't receiving the `theme` prop they need to color their own note/date ``s and any nested `` instead of the grouped/indented MUI picker (`renderBalanceSourceMenuItems`) used everywhere else a balance source is picked - swapped to the shared component for visual consistency. - Fixed Umami analytics reporting zero traffic in production: the `/stats/:match*` proxy rewrite in `vercel.json` pointed at `https://eu.umami.is`, which 301-redirects to `https://cloud.umami.is` (the CSP `connect-src` already allow-listed `cloud.umami.is`, not `eu.umami.is` - a mismatch present since this proxy was first added). Loading the tracker script through a cross-origin redirect gets it blocked by `script-src 'self'`, so `window.umami` never initialized and every pageview/event silently no-opped. Pointed the rewrite at `https://cloud.umami.is` to match. - Fixed Umami analytics still reporting zero traffic after the fix above: the `/stats/:match*` reverse proxy only ever covers the `script.js` download - Umami Cloud's hosted tracker script has its actual event-collection host (`https://gateway.umami.is`) hardcoded at build time, so every `/api/send` call bypasses the proxy entirely and goes straight there regardless of what path the script itself was loaded from. That domain wasn't in the CSP `connect-src` allowlist (which listed the now-confirmed-unused `cloud.umami.is` instead), so the browser blocked every single event - confirmed via a real browser's console CSP violations, which is also what revealed the actual endpoint. Swapped `connect-src`'s `cloud.umami.is` for `gateway.umami.is`. - Fixed a site-wide styling regression from the Tailwind v4 migration: Tailwind v4 wraps its output in native CSS cascade layers, and per spec any unlayered rule always wins over a layered one regardless of specificity - so the global reset in `src/index.css` (previously plain, unlayered CSS) was silently overriding every Tailwind spacing utility (`p-8`, `mb-16`, `gap-5`...) across the whole app, breaking the landing page, footer pages (pricing/FAQ/sitemap), and the profile menu. Fixed by wrapping that reset in `@layer base`, the same layer Tailwind's own reset uses. ### Security - Added rate limiting (existing Redis-backed `checkAndConsumeRateLimit`) to the private-session middleware, the cron secret-check middleware, registration, login, and several password-verification/destructive user routes that had none - CodeQL had flagged 9 of these; login itself wasn't flagged but had the same gap and got the same fix as password recovery. - Fixed a tainted-format-string issue in the rate limiter's own error logging (`server/src/libs/rateLimiter.ts`). - Added CSRF protection: a same-origin check (`server/src/libs/csrfProtection.ts`) on all state-changing requests, comparing the browser's `Origin`/`Referer` against the request's own host - no configuration needed, so it's correct for both the hosted deployment and any self-hosted domain automatically. Complements the existing `sameSite: "lax"` on the auth cookies. - Fixed `sanitizeInput`'s HTML-tag strip (`server/src/routes/common.ts`, shared by ~40+ input fields) to loop until stable instead of a single pass, so a nested tag like `<