Ctrl K
ComunidadGuías…

CHANGELOG.md

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
# 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 `<optgroup>`, 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 `<input>`s and any
      nested `<select>` - rendering as plain unthemed white boxes with
      unreadable white-on-white text in dark mode. Fixed everywhere this
      pattern was used.
    - The transaction-edit modal's category/typology/amount/note/date/
      purpose fields still used the old inline-edit styling (a permanent
      blue-tinted border, meant to flag "you're editing this" in a dense
      list row) - visually inconsistent now that they live in a modal, which
      already signals that. Switched them to the same neutral field style
      used by the rest of the app's modals and forms.
    - Removed the redundant "Annulla modifica" button from the edit modal's
      footer - the header's ✕ already closes/cancels it.
    - `insert-values`' content area was capped at 1100px, leaving large
      unused margins on wide screens and forcing the transaction table to
      horizontally scroll even when the window had room to spare. Widened it
      to 1400px (matching the width already used on Comparison/Knowledge)
      and let the table fill its container instead of capping at 1200px on
      its own.

### Added
- Recurring transaction templates (subscriptions, rent, salary) can now pick
  a source/destination account and sub-account, the same grouped picker used
  everywhere else a balance source is chosen. When the daily cron fires a
  template, the generated transaction now records that source and - a
  genuine behavior change - automatically moves the amount in/out of the
  selected account/sub-account (liquidity account, investment holding, or
  the plain macro balance field), the same way a manually-entered
  transaction already does, since no one is there to confirm it when the
  cron runs unattended. Templates left without a source keep behaving
  exactly as before (ledger entry only, no balance movement).
- `docs/DEPENDENCIES.md`: a reviewable, per-package justification for every
  direct dependency, a periodic re-audit process, and the reasoning behind
  deliberately deferring the Vite 8 and TypeScript 7 upgrades for now.
  Prompted by a full `depcheck` audit (zero genuinely unused runtime
  dependencies found) and by noticing four separate icon libraries in active
  use with nobody having decided that on purpose - now tracked as a real
  cleanup target in `todo.md` instead of silently accumulating further.
- `.github/workflows/publish-docker.yml`: publishes prebuilt multi-arch
  (`linux/amd64`, `linux/arm64`) `web`/`api` images to GHCR on every tagged
  release, so self-hosting no longer requires a local build - see "Prebuilt
  images" under "Docker self-hosting" in `README.md`. Also pushes the same
  images to Docker Hub once `DOCKERHUB_USERNAME`/`DOCKERHUB_TOKEN` repo
  secrets exist (skips itself otherwise, so this stays optional and never
  breaks the GHCR publish). Ships without Turnstile/Umami/Web Push keys
  baked in (those are Vite build-time values); self-hosters who want their
  own still use `docker compose up --build`.
- README: a top-of-file quick-links row (Website/Live Demo/Pricing/FAQ/
  Roadmap/Contributing), a "Contributions Welcome" badge linking to
  `CONTRIBUTING.md`, and a short "who it's for" bullet list replacing part
  of the opening paragraph, to make the pitch scannable in a few seconds
  instead of requiring a full paragraph read.
- Outflow/income table and card views: a "Payment method" column showing
  which account/balance a transaction was paid from or credited to, editable
  in place. Changing it now correctly moves the amount out of the old
  account/balance and into the new one (previously, changing the source
  together with the amount could revert the wrong account).
- CSV import wizard: when a bank/provider export is auto-detected (Revolut,
  N26, Trade Republic, PayPal), the wizard now auto-selects the matching
  liquidity sub-account if one is already linked to it, or falls back to the
  right generic balance macrocategory (e.g. "Bank") instead of leaving the
  payment source unset. Liquidity accounts can be manually linked to a
  provider from the account management panel, and get linked automatically
  the first time one is created during an import.
- Fixed-denomination liquidity accounts (e.g. electronic meal vouchers issued
  in fixed units like €8): a liquidity account can now be given a
  denomination and a fallback account from the account management panel.
  Spending an amount that isn't an exact multiple of the denomination (or
  that exceeds the account's balance) now automatically splits the payment
  across the voucher account and its fallback, shown as one row (e.g.
  "Edenred (8,00€) + Banca (3,50€)") — applied automatically for imported
  CSV rows, and available when editing a transaction's payment method
  in-place.

### Changed
- Comparison page, fully rethought: replaced the dense always-visible stat
  grids and the dead motivational-popup/legacy-rankings-tab code with a
  narrative "financial mirror" layout - a single percentile gauge and
  plain-language headline insight up front, everything else (cashflow,
  savings rate, asset allocation, spending by category, behavior) tucked
  behind a progressive-disclosure accordion so the page shows one clear
  thing at a time instead of everything at once. Every number still comes
  from the same anonymous, privacy-gated cohort data as before (same
  `MIN_COHORT` threshold, same consent flow) - this is a presentation
  rewrite, not a data change. New: a "compare by country" view that isolates
  geography as the only cohort factor, and a "Region & city" placeholder
  that's honest about not collecting that data yet while laying the
  groundwork (see `todo.md`) for a future clickable map and location/job
  change simulator.
- Custom comparison cohorts (the factor customizer and the new "compare by
  country" view) now relax automatically instead of just reporting "not
  enough data": when the exact combination of factors you picked doesn't
  reach the privacy threshold, the server progressively drops household,
  then life stage, then career and retries - geography is never dropped
  automatically, since cost of living dominates nominal financial
  differences more than any other single factor. The comparison always says
  plainly which factors it actually ended up using when it had to broaden.
  The main percentile comparison also now shows a running "X of 20 people
  so far" count instead of a bare "not available yet" while the platform is
  still small, so it's clear the wait is about community size, not something
  broken.
- The public demo account ("Try Demo") now shows the redesigned Comparison
  page fully populated instead of behind its opt-in wall or in an empty
  state: demo data ships with benchmark consent already granted and realistic
  cohort/population numbers, and the demo now mocks the behaviour-benchmark
  and custom-cohort endpoints (factor customizer, "compare by country")
  instead of leaving them to hit a real backend that doesn't exist in demo
  mode - including a demo-only simulation of the new automatic factor
  relaxation, so a visitor can see that in action too.
- Migrated Tailwind CSS from v3 to v4: switched from the PostCSS plugin to
  the official `@tailwindcss/vite` plugin (removes `postcss.config.js`
  entirely), dropped `autoprefixer`/`postcss` (built into v4), and kept the
  existing `tailwind.config.js` working via the new `@config` directive in
  `src/index.css` rather than hand-translating custom theme tokens to CSS
  `@theme` syntax (safer - zero risk of a typo silently dropping a utility
  class used across the app).

### Fixed
- Fixed the Quick Add popup's "Conto (opzionale)" field only ever showing the
  11 macro asset categories: it already fetched and merged in liquidity
  accounts and investment holdings as selectable sub-account entries, but
  rendered them all as a flat, unstyled native `<select>` 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 `<<script>script>` can no longer survive as
  `<script>` after sanitization.
- Fixed an origin-spoofing bug in `TradingViewWidget`'s `postMessage`
  listener: it checked `event.origin.includes('tradingview.com')`, which
  also matches attacker-controlled origins like `evil.com/tradingview.com`;
  now requires an exact hostname/subdomain match.
- Added a guard against prototype-pollution in `scripts/translateLocales.js`'s
  path-based deep-set helper (dev-only script, real exploitability was near
  nil, but cheap to close).
- Added explicit, minimal `permissions:` blocks to the two GitHub Actions
  workflows that were missing one.
- Resolved the `npm audit`-fixable dependency vulnerabilities (`npm audit
  fix`, lockfile-only, no `package.json` range changes). The remaining
  stragglers are all rooted in `@vercel/node`'s own dependency tree or need
  compatibility testing beyond a version bump - tracked in `todo.md`.
- Closed two more of those stragglers via `package.json` `overrides`:
  `js-yaml` to `^4.3.1` (fixes the quadratic-CPU-DoS CVEs in `!!omap`/merge-key
  resolution; pulled in transitively by `eslint` and `@vercel/node`'s bundled
  Python-runtime detector, both dev/build-time only) and `uuid` to `^11.1.1`
  (fixes the missing-buffer-bounds-check CVE; pulled in by `exceljs`, a real
  runtime dependency - verified its only call site uses the no-`buf` `uuidv4()`
  form that was never actually affected). `undici`/`path-to-regexp`/`smol-toml`/
  `ajv`/`@vercel/*` remain open: they're hard-pinned inside `@vercel/node`'s own
  tree, dev/build-time only (never reachable via the deployed app), and the
  only path that resolves them (`npm audit fix --force`) wants to downgrade
  `@vercel/node` to 4.0.0 untested - see `todo.md`.
- Resolved every Supabase Security/Performance Advisor warning
  (`supabase/migrations/harden-function-search-path-and-extension-schema.sql`,
  `optimize-rls-policies-and-add-fk-indexes.sql`, applied to `schema.sql` in
  its final-state form too): pinned `search_path` on the 5 SQL functions the
  advisor flagged as mutable (`function_search_path_mutable`); revoked
  `anon`/`authenticated` EXECUTE on `rls_auto_enable()` - a `SECURITY
  DEFINER` event-trigger handler that was reachable via `POST
  /rest/v1/rpc/rls_auto_enable` despite only ever needing to run as the
  trigger itself; moved the `pg_net` extension out of the `public` schema
  (best-effort - wrapped in exception handling since some pg_net versions
  aren't relocatable, see the migration's comment); rewrote all 26 RLS
  policies that called `auth.uid()` unwrapped in `USING`/`WITH CHECK` so it
  evaluates once per query instead of once per row
  (`auth_rls_initplan`); added the 41 missing covering indexes on foreign
  key columns the advisor listed (`unindexed_foreign_keys`). One advisor
  item is out of SQL's reach: "Leaked Password Protection Disabled" is a
  Supabase Auth dashboard toggle (Authentication -> Policies), not a
  database object - needs enabling manually per project, hosted and
  self-hosted alike.

### Added
- Deployment-mode detection: `DEPLOYMENT_MODE` env var (`server/src/libs/deploymentMode.ts`,
  fails safe to self-hosted), `GET /api/config`, and a new `DeploymentContext`
  (`src/contexts/DeploymentContext.tsx`) that lets the frontend tell a
  self-hosted instance apart from the official pacifinance.com deployment
  without ever calling the backend in mock/demo mode. First consumer: the
  Comparison page's benchmark opt-in card now shows honest self-hosted-
  instance copy (this instance's own users only; cross-instance community
  comparison is planned, not live yet) instead of the hosted-community
  wording, via new `optInTitleSelfHosted`/`optInDescriptionSelfHosted` i18n
  keys in all six locales.
- `server/src/services/communityStatsContribution.ts`: typed, pure envelope
  builder matching the shape already specified in
  `docs/COMMUNITY_STATS_PROTOCOL.md`, for future cross-instance community
  benchmarking. Structure only — no bucket-coarsening, pseudonym generation,
  signing, or network transport yet; see the file's own comments and
  `docs/PRODUCT_VISION.md` §9 for what's still open.
- `scripts/self-host-local.sh`: one command for a fully local self-host with
  no cloud account — clones Supabase's own official self-hosting stack on
  demand (never vendored into this repo, so it can't drift from their actual
  setup), generates its secrets, bootstraps `supabase/schema.sql` against it,
  wires this repo's `.env` to it, then builds and starts Pacifinance. Works
  because the backend already only talks to Supabase's API (including its
  Auth admin API), so it runs unmodified against a self-hosted instance.
  `docker-compose.yml`'s `api` service now maps `host.docker.internal` on
  Linux too, so it can reach that locally self-hosted stack the same way
  Docker Desktop already lets it on Mac/Windows. README documents both this
  script and the equivalent manual steps.
- `.env.example`: documented `VITE_UMAMI_WEBSITE_ID`, `VITE_UMAMI_SCRIPT_URL`,
  `GITHUB_TOKEN` and the `GITHUB_APP_*` trio, which the code already read
  but which weren't listed anywhere.
- Info page (`/info`): rewritten. Replaced the six UI-tutorial FAQ entries
  ("how do I add an income", "how do I change my password"...) with actual
  frequently-asked questions (privacy, data export, account deletion,
  pricing, self-hosting, AI use); added a "Where we're headed" section
  summarizing `docs/PRODUCT_VISION.md` (generic assets/complete net worth,
  scenario simulations, explainable financial health, optional/local-first
  AI) with links to the Roadmap and the full vision doc on GitHub, since
  that material previously wasn't surfaced anywhere in the app; replaced the
  generic "Continuous Support" feature card with an "Open Source" one and
  added a GitHub link next to the donation button; dropped the redundant
  "Our Commitment" section (four paragraphs restating things already said
  elsewhere on the page); fixed the hardcoded English "Support Pacifinance"
  button label. Also fixed the page's background, which had its own opaque
  gradient silently overriding the shared background every other page uses
  — Info was the one page in the app that visibly looked different.
- Dashboard: Income/Outflow, Balance Analysis, Financial Insights and Goal
  Tracker sections are now collapsible (Liquidity/Emergency Fund/Investments
  already were), with the open/closed state remembered per section across
  visits. Any section with no saved preference yet starts collapsed on
  mobile and expanded on desktop — the mobile dashboard previously forced
  everyone to scroll past every large section fully open on every visit.
  All six sections now share the exact same collapsed/expanded card chrome
  (`PortfolioSection`) instead of four different ad hoc treatments — Balance
  Analysis previously had no border at all when collapsed, and Financial
  Insights was noticeably wider than every other section.
- Account deletion confirmation now spells out, before you commit to it: what
  gets deleted, that it only actually happens after a 30-day grace period,
  that logging back in during those 30 days cancels it, and that any
  community prices you've verified stay visible to other users (previously
  a single generic sentence, with that detail only shown *after* confirming).
- `server/src/libs/userDataDomains.ts`: a single registry of every
  user-owned data domain, now driving the "Export Data" endpoint
  (`POST /api/user/alldata`), which previously only ever returned balances
  and transactions (truncated at that — even those were missing several
  columns) while silently omitting the other 20 domains (investments,
  custom categories, liquidity sub-accounts, goals, recurring transactions,
  shared expenses, notification preferences, roadmap votes...). Two new
  tests (`userDataDomains.test.ts`, `userDataCascadeGuard.test.ts`) fail if
  a future data domain is ever added to the database without being either
  registered for export or explicitly excluded with a reason, and if any
  user-owned table's foreign key isn't `ON DELETE CASCADE`. Documented as
  a standing rule in `AGENTS.md` (rule 12) and `CONTRIBUTING.md` (rule 12).
- Notification settings: a "Send test notification" button (shown once
  reminders are enabled) that triggers an immediate push through the same
  delivery path as real reminders, for the user to confirm push actually
  works on their device.
- `AI_POLICY.md`: AI-assisted contributions are welcome, but must be
  disclosed, reviewed, tested, and aligned with the project's vision before
  a PR — linked from `CONTRIBUTING.md` and a checklist item in the PR
  template.
- Demo account: investment holdings (stocks/ETF/crypto with real
  quantities, cost basis, transactions and dividends), liquidity
  sub-accounts, recurring transactions, a shared-expense receivable, and
  populated spending/savings limits with custom categories — previously
  most of these returned empty arrays or nulls in demo mode, so several
  shipped features (portfolio insights, balance reconciliation, recurring
  transactions, shared expenses, goals & limits, custom categories) had
  nothing to show a visitor trying the demo. Documented in `AGENTS.md`
  (rule 5) as a standing requirement: new demo-reachable features need
  real seed data, not just a structurally-correct empty state.
- Custom 404 page: unknown routes previously redirected silently to the
  homepage, giving no feedback that the URL was wrong. Now shows a real
  not-found page with a CTA back to the dashboard (if logged in) or the
  homepage, and is excluded from indexing.
- Sticky "Get Started" call-to-action on mobile landing page, so the
  primary CTA stays reachable without scrolling back to the hero on small
  screens.
- Security headers: Content-Security-Policy, X-Frame-Options,
  X-Content-Type-Options, Referrer-Policy, Permissions-Policy and
  Strict-Transport-Security, set via `vercel.json` for the app shell/static
  pages and via Express middleware for the JSON API.
- FAQ page: `FAQPage` JSON-LD structured data, so search engines can show
  the questions as rich results.
- `scripts/generateSitemap.js`: `public/sitemap.xml` is now generated at
  build time from the real public route list (`npm run prebuild`), instead
  of being a hand-maintained file that had drifted — it listed auth-gated
  routes a crawler could never reach (`/dashboard`, `/comparison`) while
  missing real public pages added since, and its `<lastmod>` was frozen at
  a single past date.
- Dark mode now persists across visits (`localStorage`) and, absent a saved
  choice, follows the OS `prefers-color-scheme` on first load instead of
  always defaulting to dark.
- Terms of Service and Cookie Policy pages are now fully translated in all
  six supported languages via the standard i18n system, matching how the
  Privacy Policy already worked — previously both fell back to English for
  four of the six languages (es/de/fr/pt-BR).

### Changed
- `.github/dependabot.yml`: minor/patch version-update PRs are now grouped
  into one PR per ecosystem per week (npm, GitHub Actions) instead of one
  PR per dependency — major bumps stay ungrouped since those are the ones
  actually worth a separate look before merging.
- Consolidated `CLAUDE.md`, `AGENTS.md` and `.github/copilot-instructions.md`
  into one source of truth (`AGENTS.md`); the other two now just import it.
  Dropped a stale "server/ is off-limits" line that contradicted `AGENTS.md`
  and no longer reflected how the project actually works.
- Roadmap page: the pre-filled bug-report title is always in English now
  (GitHub issues are triaged in English regardless of the reporter's UI
  language).
- Landing hero: softened the desktop background fade so it reads as a
  gradual blend instead of a hard edge partway across the screen; the art
  now stays visible (lightened via filter) in light mode too, instead of
  disappearing behind a plain dotted background.
- Landing footer: moved the "Support Pacifinance" button into the
  Community column, next to the text that's actually about supporting the
  project, instead of sitting under the logo.
- `todo.md`: reconciled the backlog against the actual code — checked off
  the onboarding checklist and roadmap voting (fully shipped but left
  unchecked), corrected push notifications back to in-progress (backend is
  scaffolded — VAPID, cron scheduler, reminder types — but not working
  end-to-end yet) and OFX/QIF support back to not-started (no code behind
  it despite being marked in-progress), added a real entry for 2FA
  (doesn't exist yet — this also fixed a false "completed" on the public
  roadmap, see below), and removed two stale/duplicate lines (a superseded
  per-bank-template entry, an old one-off bundle-size check for a feature
  shipped long ago).

### Added
- `docker-compose.yml`: added local `redis` + `redis-http` (Upstash's own
  official REST shim, `serverless-redis-http`) services, with `api`
  defaulting to them when `UPSTASH_REDIS_REST_URL`/`_TOKEN` are unset in
  `.env`. Redis was a hard, silent dependency for Docker self-hosting even
  though nothing provisioned it - registration hard-fails without a
  reachable Redis (the Turnstile anti-replay guard calls it
  unconditionally), so self-hosting via Docker was broken for registration
  specifically until now unless you separately owned a cloud Upstash
  account. `.env.example` documents both the local default and how to point
  at a real Upstash instance instead.

### Changed
- `docker-compose.yml`: the `api` service's host-side port is now
  configurable via `API_HOST_PORT` (`.env.example`, defaults to 3001)
  instead of hardcoded, since no single default port is safe from colliding
  with something else already running on a given self-hoster's machine.
- `Dockerfile`: both `npm ci` steps (`web` and `api` build stages) now use a
  BuildKit cache mount for npm's package cache, so rebuilds after the first
  one don't re-download the entire dependency tree from the registry every
  time `package.json`/`package-lock.json` haven't changed.

### Fixed
- `scripts/self-host-local.sh` decided whether to apply `supabase/schema.sql`
  based on whether `.selfhost-supabase/` already existed, not on whether the
  schema was actually present in the database - a first run interrupted
  partway through (a crashed Docker Desktop/WSL, for example) could leave a
  permanently empty database that every later run silently kept skipping.
  It now checks the database itself (`profiles` table presence) and
  restarts PostgREST after applying the schema, since PostgREST caches the
  schema at startup and wouldn't otherwise see new tables until restarted.
- Registration on a self-hosted instance using Cloudflare's public Turnstile
  test keys (the ones documented in `.env.example` for local testing) always
  failed with a hostname mismatch: Cloudflare's test keys report a fixed
  `hostname: "example.com"` in their verification response regardless of the
  real page origin, which the non-production `localhost`/`127.0.0.1`
  allowlist didn't account for. Sign-up and password recovery now also show
  a small note when a self-hosted instance is using a public test key,
  pointing at setting up a real Turnstile site before deploying for real.
- The Turnstile-test-key notice (and `.env.example`'s comment) wrongly
  implied every self-hoster eventually needs real Turnstile keys "before a
  real deployment." Not true if the instance stays local/private forever
  (personal use, never reachable from the public internet) - there's no
  bot-abuse surface to protect in that case, so the test keys are fine
  indefinitely. Real keys are only needed if registration is ever exposed
  publicly.
- The self-hosted Turnstile-test-key notice on Sign-up/Recovery was a quiet
  gray sentence *below* Cloudflare's own alarming red "test key" banner, so
  a self-hoster would see the scary part first and the explanation second.
  Moved it above the widget instead and styled it as a proper amber notice
  box (matching the page's existing info-box language, `theme.warningColor`)
  so it reads as "this is expected" before the red banner, not after.
- Sign-up error toasts (registration failure, Turnstile pending/error) were
  built as raw HTML template strings but rendered as plain text, so users
  saw literal `<div><strong>` markup instead of formatted text. Replaced
  with plain sentences, matching every other `showError` call in the app.
- `Dockerfile`/`docker-compose.yml`: the `web` image's build stage never
  received any `VITE_*` variables from `.env` (`.env` is deliberately
  excluded from the Docker build context so server secrets can't end up in
  an image layer, but that silently starved Vite of the public ones too -
  Turnstile site key, Umami website ID, web push key) - they'd compile in
  empty with no error. Passed through explicitly as Docker build args
  instead, sourced from `.env` via Compose's own interpolation.
- `Dockerfile`: the `api` image's start command
  (`node --import tsx/esm server/src/index.ts`) crash-looped on Node 22 with
  `ERR_REQUIRE_CYCLE_MODULE` - handing a `.ts` entry file directly to `node`
  while registering `tsx/esm` as a loader makes it load through both the CJS
  and ESM paths at once. Switched to running it through the `tsx` CLI
  instead, the same way `package.json`'s `dev:server` script already does.
- `docker/nginx.conf`, referenced by the Dockerfile's `web` build stage, was
  missing from the repo entirely — `docker compose build` failed immediately
  for any self-hoster following the README. Added it, serving the SPA with a
  static-file/`index.html` fallback and proxying `/api/` to the `api`
  container (the frontend calls `/api/...` with a relative path, so the two
  containers need nginx to bridge them).
- `.env.example`: clarified that `VITE_DEV_MODE`'s Turnstile bypass only works
  against the frontend-only mock/demo mode, not the real backend (Docker
  self-hosting included) — the real backend never had a dev bypass and still
  calls Cloudflare's siteverify with whatever token it's sent, so the old
  wording would have led a self-hoster straight into a confusing 401 on
  registration. Documented Cloudflare's own official test keys as the
  correct way to test registration against a real backend without owning a
  real Turnstile site.
- Analytics no longer falls back to pacifinance.com's own Umami website ID
  when `VITE_UMAMI_WEBSITE_ID` is unset — a self-hosted deployment that never
  set it was silently reporting its traffic into pacifinance.com's own
  dashboard instead of just staying off.
- `supabase/schema.sql` had silently drifted since 6 August: it still
  created a `public.expenses` table, but a same-day migration
  (`use-transactions-domain.sql`) had renamed it to `transactions` — a fresh
  self-hosted deploy applying `schema.sql` per README.md/CONTRIBUTING.md
  would get a database the server code couldn't actually use. Also missing
  entirely: `roadmap_votes`, `notification_preferences`, `push_subscriptions`,
  `instrument_historical_prices` (community prices) and several investment
  tables, none of which had ever made it in. Regenerated from a live
  `supabase db dump --linked --schema public` and re-added the one thing the
  dump can't export, the `rls_auto_enable` event trigger (a database-level
  object, created directly against the live project outside any migration,
  that auto-enables RLS on future tables). `AGENTS.md` (rule 11) now
  requires every schema-changing migration to update `schema.sql` in the
  same PR so this can't happen again. Also fixed
  `userDataCascadeGuard.test.ts`'s FK scanner, which only recognized the
  old hand-written `column uuid ... references auth.users(id)` style and
  had gone completely blind to `schema.sql` once it became a raw
  `pg_dump` (which always expresses foreign keys as separate `ALTER TABLE
  ... FOREIGN KEY` constraints) — it now recognizes both styles, so the
  guard is actually checking `schema.sql` again instead of silently passing
  on an empty match set.
- Gamification badges (Savings streaks, First Save, Big/Super Saver, Budget
  Master, Frugal Month, Spending Down) compared income against
  `outflowsArray`, which sums *every* outflow transaction including money
  moved into investments and transfers between the user's own accounts (see
  `buildMonthlyArrays`). A user who invested a large share of their income,
  or transferred money between accounts, could be told they "hadn't saved"
  that month even though they clearly had. Switched all of these to
  `expensesArray` (true discretionary/necessary spending only), with a
  fallback to `outflowsArray` for callers that haven't computed it.
- Public roadmap: the "Two-Factor Authentication" item showed as completed
  because `scripts/generateRoadmap.js`'s fallback text-match
  (`todoMatch: "2FA"`) landed on an unrelated, already-checked `todo.md`
  line about mandatory 2FA for GitHub org members — 2FA for user accounts
  doesn't exist yet. Added a real `todo.md` entry with a stable
  `<!-- roadmap:2fa -->` marker, which takes priority over the text-match
  fallback and resolves this correctly.
- `vitest.config.mjs` coverage `include` globs only matched `.js`/`.jsx`,
  silently excluding `src/utils`, `src/contexts`, `src/hooks` and
  `src/services` (100% TypeScript today) from coverage measurement — the
  60/60/50/60 thresholds were checking almost nothing. Fixed the globs and
  reset the thresholds to the real, now-measured baseline (55/47/48/55);
  wired `npm run test:coverage` into CI in place of the plain `npm test`
  step, so a regression below today's actual coverage now fails a PR.
- Landing hero: the desktop artwork was cropping off the tree and moon
  because it was center-positioned inside a panel narrower than the source
  image — repositioned so the full subject is visible instead.
- Account deletion could fail for an admin account that had ever
  approved/rejected another user's community price submission:
  `instrument_historical_prices.verified_by` referenced `auth.users(id)`
  without `ON DELETE CASCADE` (every other user reference in the schema
  has it), so deleting that admin's Supabase Auth user hit a foreign-key
  violation. Fixed via a migration setting it to `ON DELETE SET NULL`
  (it records who reviewed a submission, not who owns it — `submitted_by`,
  already cascading, is the real owner reference).
- Deleting an account used to cascade-delete every community price the user
  had ever submitted, including already-verified ones other users' portfolios
  actively rely on (`getVerifiedCommunityPricesForInstrument`) — a
  contributor closing their account shouldn't take that shared data down
  with them. `instrument_historical_prices.submitted_by` is now nullable and
  `ON DELETE SET NULL` instead of `CASCADE`; the application already treated
  it as nullable everywhere (provider-sourced rows already store `null`
  there), so this was a schema-only fix.
- Mobile header: the privacy toggle rendered in alarming red with a red-tinted
  background whenever privacy mode was on — every time, since it's a
  deliberate protective feature many users leave on by default, not an error
  state. Restyled to the brand accent color with a subtle pop animation on
  toggle instead.
- Notification settings: enabling reminders always blamed a failure on
  browser notification permission, even when the actual cause was
  something else (a subscribe/network failure), which was misleading when
  the browser had already granted permission. Now only a genuine
  permission denial shows that message; other failures show a distinct,
  actionable one.
- Desktop sidebar's and Settings page's privacy (eye) toggles were leftover,
  differently-styled buttons (plain dark/light square, no animation) instead
  of the same brand-accent button with the pop animation already used in the
  mobile header. All three now render the same shared `PrivacyToggleButton`.
- Settings page's privacy-toggle description said only "hide amounts in
  charts", understating what the mode actually does: it hides values
  everywhere in the app, and in charts it also randomizes percentages/values
  and desaturates colors to grayscale so the hidden data can't be guessed
  from shape or hue. Rewritten to say so, in all six languages (previously
  hardcoded it/en only).
- Removed a fully unreachable legacy "Settings" popup in `SidebarModals.tsx`
  (its trigger button no longer existed anywhere in the UI) together with
  the duplicate delete-account confirmation flow it was the only way to
  reach — account deletion has been fully served by the redesigned,
  itemized-consequences flow on the Settings page for a while, so this was
  a second, unreachable copy of the same feature. Also deleted two
  components this left orphaned with zero remaining callers
  (`SidebarMobile`, `PrivacyToggleModeButton`) and their now-dead styled
  helpers, and dropped a dozen imports in `Sidebar.tsx` and
  `SidebarModals.tsx` left over from before this code was split into its
  own file.

## [0.10.0] - 2026-08-11

### Added
- Landing page: dedicated **Features** section (dashboard, investments, multi-currency, import, recurring transactions, goals/limits, market prices, gamification), distinct from the existing "why Pacifinance" pillars, with a link into the roadmap for what's coming next.
- Roadmap page: "show more" per column instead of one long list, and a bug-report link on completed items (opens a pre-filled GitHub issue).

### Changed
- Roadmap page: modernized card/column styling; voting on already-completed items is now read-only (voting is for prioritizing what's next, not re-litigating what already shipped).
- Landing hero: the mobile background artwork is now a real full-bleed background (matching the desktop treatment) instead of a separate boxed image sitting above the text.

### Fixed
- `scripts/setVersion.js` was writing `src/data/appVersion.js`, but the file actually imported by the app is `appVersion.ts` — every past version bump silently created a stray, unused `.js` file instead of updating the real one. The script now targets the correct file.

## [0.9.9] (continued) - 2026-07 to 2026-08

`package.json` stayed at `0.9.9` through this entire window (see the note above the [0.10.0] entry, and "Versioning policy" — this gap is exactly what that process now prevents). Shipped in that time, grouped by month:

**2026-08** — Net worth trend charts, account recovery code (block code + word phrase, printable/regenerable), income & outflow explorer, balance and holdings reconciliation.

**2026-07** — Interactive investment charts (collapsible legends, asset detail), recurring transactions, quick add, goals & spending limits, duplicate-entry detection on manual input, shared expenses, live market prices for stocks/ETFs, adaptive portfolio insights, monthly investment target, community-verified historical prices.

**2026-04** (between this and the `[0.9.9] - 2026-04-17` entry below) — multi-insert and detailed outflow analysis, already covered there.

## [0.9.9] - 2026-04-17

### Added
- **Multiple insert**: insert multiple outflows, incomes, or balance entries in a single operation (MultiOutflowInsert, MultiIncomeInsert, MultiBalanceInsert)
- **Detailed outflow analysis**: in-depth analysis by category, payment methods, and recurring patterns with monthly comparisons and 12-month average
- **Real-time crypto prices**: Market Prices page with CoinGecko data and 7-day sparklines
- **PWA installation guide**: auto-detect device instructions (iOS, Android, Desktop) in Settings and Info
- **Inline transaction editing**: edit incomes and outflows directly in the table with delete+reinsert
- **Balance impact choice for past dates**: when you insert outflows or incomes with a date in a past month, a modal asks whether you also want to update that month's balance snapshot. The preference can be saved (Ask every time / No impact / Update month's balance) and changed from Settings. Hook `usePastDateBalancePref`, selector `getBalanceForMonth`, component `PastDateBalanceChoiceModal`.
- Mock CoinGecko data for the Market Prices page in dev mode
- Mock data aligned with i18n files for demo/dev/prod consistency
- Changelog file to track the project's history

### Fixed
- Hardcoded English categories in the detailed outflow analysis: now translatable in all languages
- Category colors and icons not aligned with the centralized file in recurring sections
- Category color i18n: robust fallback with reverse-lookup for translated tags
- More stable app: resolved post-registration loading issues and improved network error recovery
- Improved mobile experience: eliminated accidental zoom, optimized layout

### Changed
- Updated roadmap with recent features (multi-insert, detailed outflow analysis, i18n colors)
- Updated Copilot instructions: requirement to update the roadmap for every user-facing feature

---

## [0.9.8] - 2026-02

### Added
- **More stable and reliable app**: fixed post-registration loading, automatic network error recovery
- **Improved mobile experience**: eliminated accidental zoom/pinch, optimized layout, faster loading
- **Inline transaction editing**: edit directly in the table without leaving the list view
- **PWA installation guide**: step-by-step instructions with automatic device detection
- **Real-time crypto prices**: price monitoring with 7-day sparklines
- Tests for critical app states: 79 new tests for authentication, data loading, error recovery
- Tests: 401 axios interceptor, full auth flow, error retry

---

## [0.9.7] - 2025-02

### Added
- **CSV/Excel import**: multi-step wizard with automatic column mapping and fuzzy category recognition
- **Multi-currency support**: 19 currencies with automatic conversion (frankfurter.app API, 24h cache, fallback rates)
- **Public roadmap**: kanban page auto-generated from todo.md with filters and translations
- **Feedback**: link to GitHub Issues in Settings and Info page
- Undo/rollback of last import (saveLastImport + UI undo)
- Drag and drop file upload in the import wizard
- Edit notes from the Excel file before inserting transactions
- Import: fixed white dropdowns, fixed negative amounts, parseExcel padding
- Import: modal overlay, direct URL (?section=import), landing page card, SEO
- Multi-currency: Settings currency session-only (not persisted), ProfilePage currency persisted in the DB
- Multi-currency: preferredCurrency from DB (index → code via currency tags)
- Ranking: backend now sends the %, frontend adapted to use it directly

### Fixed
- Fix: blank page after registration (reset authentication state on logout)
- Fix: retry mechanism after API error (retryCounter to force re-fetch)
- Fix: language change from settings (double language prefix)
- Fix: dashboard loading stuck (error recovery + retry + timeout)
- Fix: achievements toast above BottomNavBar on mobile
- Fix: buggy achievements (verify real data, not just structure)

---

## [0.9.6] - 2025-01

### Added
- **Customizable dashboard**: drag-and-drop sections, compact view, toolbar
- **Gamification**: 44 badges across 10 categories with GamificationSection and IT/EN translations
- **Native mobile navigation**: BottomNavBar with React Portal, popup menu, active indicator
- **Goals and Spending Limits**: dedicated frontend monitoring (backend in development)
- **Anonymous comparison**: net worth/income/outflow rankings against similar users
- Client-side generated avatar (1400+ combinations, regenerable)
- In-app notifications/changelog (WhatsNewBanner) for updates and new features
- Dark/Light mode: animated transition on theme change
- Haptic feedback on nav bar taps (navigator.vibrate)
- Skeleton loading with shimmer animation
- Lazy loading: FinancialInsights, GoalTracker, GamificationSection
- Pie chart memoization with useMemo
- Dependency Injection architecture with ServiceContext (internal refactoring)
- SettingsPage redesign: compact layout, Account Preferences, fixed currency dropdown
- BuyMeACoffee widget: CSS for mobile positioning
- ScrollNavigationIndicator: bottom 74px on mobile

### Fixed
- Fixed horizontal overflow on mobile on insert pages
- Prevented accidental zoom/pinch on mobile (viewport + CSS touch-action)

---

## [0.9.5] - 2024-12

### Added
- **Multilingual support**: interface in Italian and English with automatic language detection
- **URL-based language routing**: all routes with language prefix (/it/, /en/)
- useLocalizedNavigate and LocalizedLink used everywhere
- Desktop sidebar: fixed Link → LocalizedLink for i18n routing
- DashboardPage: localized navigate() instead of window.location.href
- Sidebar translations: dashboard, more, goalsLimits

---

## [0.9.4] - 2024-11

### Added
- **Data export**: export in CSV, Excel, JSON, and PDF format

---

## [0.9.3] - 2024-10

### Added
- **Anonymous comparison**: compare net worth, income, and outflows with similar users in a fully anonymous way
- 401 axios interceptor (automatic logout on expired session)
- ProtectedRoute redirect to / (landing)
- Mobile dashboard UI/UX (2-per-row cards, metric cards column, reduced charts, spacing)
- Floating animations disabled on mobile
- SettingsPage: typo fix, toggleLanguage fix, useNavigate fix, section reordering

---

## [0.9.0] - 2024 (Early)

### Added
- Initial launch of Pacifinance
- Anonymous registration (password only, auto-generated userId)
- Multi-platform net worth tracking (bank, stocks, ETF, crypto, cash, etc.)
- Income and outflow categorization
- Dashboard with interactive charts (Recharts)
- PWA-ready with service worker
- Cloudflare Turnstile for bot protection
- Styled-components with dark/light theme support
- Loading screen with branding
- Landing page image performance optimization (LCP preload, fetchpriority)
- Tests: BottomNavBar (10), useScrollNavigation (10), SettingsPage order (2)
- Tests for removeLanguageFromPath edge cases (8 tests)
- Tests for DataImportWizard processRowDual (15 tests)

---

## Before this file existed

`git log -- package.json` shows `package.json`'s version field actually went `0.1.0` (2023-06-11, first commit) → `0.5.1` (2023-10-18) → `0.9.0` (2025-07-12, real infra cycle: dependency injection, Redis-based sessions/cache, CRA→Vite migration, the roadmap generation script, first Vitest suite) → `0.9.9` (2026-02-17). `v0.1.0`, `v0.5.1` and `v0.9.0` are tagged on those exact commits for the record, even though the entries above don't cover that period in detail.

---

## Versioning policy

- **Every commit that changes user-facing behavior** gets a line under `[Unreleased]` above, in the same PR/commit as the change — not reconstructed later from memory or from `git log`.
- **Cutting a release**: run `npm run version:bump:<patch|minor|major>` (updates `package.json`, `package-lock.json` and `src/data/appVersion.ts` together), move the `[Unreleased]` entries under a new `## [x.y.z] - YYYY-MM-DD` heading, commit, then tag:
  ```
  git tag -a vX.Y.Z -m "vX.Y.Z"
  git push origin vX.Y.Z
  ```
- Use `MAJOR` for breaking changes (self-hosters, API consumers), `MINOR` for new user-facing features, `PATCH` for fixes only.
- This is what the `[0.9.9] (continued)` entry above exists to prevent: six months of real feature work landed without a single version bump, so `package.json`'s `0.9.9` stopped meaning anything specific. From `[0.10.0]` on, a version number always identifies one exact commit.