Ctrl K
ComunidadGuías…

CHANGELOG.md

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

Archivo original ↗Preguntar a KiBot
# Changelog -- Ledger Sync

All notable changes to this project are documented here.

Format based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).

---

## 2.36.0 - 2026-11-09

Every lifecycle in the app with more than two states is now written down in [docs/STATE_MACHINES.md](docs/STATE_MACHINES.md): about 130 of them, from an audit of the whole repo, each with its states, the file that owns it and any known gap. The contradictions it found came from a few repeated shapes, so most fixes below are one shared piece used everywhere rather than many local patches. No migration, and the API shape is unchanged.

### Fixed

- **The "figures are out of date" banner after a successful recompute.** An import that emptied the latest month deleted that month's rollup, and with no later month nothing was restamped, so Data Health compared an old stamp with the new import and reported stale. Freshness now also counts the last successful publication; a recompute that really failed still reports stale.
- **A failed background refresh no longer wipes the page.** When a read that already had data failed to refresh (often right after an upload, a save or a recompute), about 30 pages and panels swapped everything, the period toolbar included, for an error screen. They now keep the figures and say "Could not refresh" with a Retry that refetches only what failed. The error screen is for a read with nothing to show.
- **A retry no longer shows "No transactions" or zeros.** The period pages (Dashboard, Overview, Spending, Income, Year in Review, Returns) stored a failed read as if it were the period's data, so the next Retry displayed it as empty figures while it ran. They now keep the last good period on screen until the new one arrives.
- **Delete and reset confirmations wait for the result.** The dialog used to close the moment you confirmed, before the delete or reset finished, so a failure only flashed by as a toast. It now shows that it is working, cannot be dismissed half way, and stays open with the reason if the action fails, so you can retry or cancel.
- **One message per failure.** Several saves and deletes showed two error toasts (their own and a generic one), and a failed budget edit showed its inline error plus a toast. Each failure now reports in one place.
- **A save made offline no longer spins forever.** Saves paused while the browser reported offline; they now fail with a network error, like page loads already did.
- **The AI assistant waits until it knows how it is paid for.** While your AI settings were loading, or after they failed to load, the chat button, the header's Ask AI, the weekly recap's Explain and the AI Canvas all assumed the app-funded model, so a personal-key user could spend the shared quota. They now wait, offer a retry on a failed load, and say what is missing when a personal setup has no provider or model.
- **Chat:** two quick sends no longer start two runs (the second could not be stopped), and a send with an incomplete personal setup now says nothing was sent instead of silently clearing your message.
- **A demo tab can no longer sign out a real one.** Entering the demo on reload wrote the demo session over a real sign-in another tab had stored, and that tab signed out on its next reload. The demo session is no longer stored where other tabs read it.
- **The stale banner comes back when it should.** Dismissing it hid every later warning for the rest of the visit; the dismissal now applies to that one warning and clears when the figures are current again. When the check itself cannot run (offline), it says so instead of implying the figures are old.
- **Sign-in:** after you choose Google or GitHub, the buttons stay disabled until the provider's page opens, so a second click can no longer start a second sign-in that breaks the first.
- **Currency switcher:** a failed exchange-rate fetch now says the rates could not be loaded, instead of claiming your currency has no rate.

### Added

- [docs/STATE_MACHINES.md](docs/STATE_MACHINES.md), with the conventions new code follows and the gaps held for a decision (recording why a commitment is paused, goal pause and cancel, AI funding after a key is removed).
- Shared pieces for those conventions: `pageStatus` and `RefreshFailedNotice` for loading, error and refresh-failed states, `markErrorReported` for one message per failure, and `useAiAvailability` for whether the assistant can run.

### Changed

- The Settings reset dialog now shows its own failure instead of a banner on the page, and an access token's failed revoke shows in the dialog instead of under the row.

## 2.35.1 - 2026-11-02

### Fixed

- **A page opened after a deploy no longer fails to load.** A tab, or the installed app's first launch, could still run the previous build after the new service worker had deleted its files, so the next page you opened asked for a file that no longer exists (a 404 on `OverviewPage-<hash>.js`). The app now reloads once to pick up the current build. If the page still fails within a minute, it shows the existing "Failed to load page" screen with a Reload button instead of reloading again.

## 2.35.0 - 2026-11-02

A whole-repo bug hunt: 524 raw findings, merged to 487 distinct ones, were each checked by three independent refuters. 386 held up, and 363 are fixed here; 21 wait on a decision. No new migration (two old revisions now refuse to downgrade, like the rest), and the API only gains optional fields.

### Fixed

- **Offline no longer reads as "no data".** A page opened without a connection showed its empty state ("No transactions yet", zero totals) because the request paused instead of failing. Requests now fail like any network error, pages show their retry state, and they reload by themselves when the connection returns.
- **Sessions survive a flaky start.** A network error or server error on the startup profile check or on a token refresh signed you out; now only a 401 or 403 does. Requests time out after 60 seconds (20 for a refresh) instead of hanging, and every list request works again on iOS 16.4 to 17.3, which lacks `AbortSignal.any`.
- **Uploads:** a UTF-8 CSV without a byte-order mark keeps its non-ASCII text (the rupee sign, accented names), an Excel workbook on the 1904 date system keeps its dates, a body over the hosted 4.5 MB limit gets a message naming it, and an anonymous upload is refused before its body is read.
- **Figures:** the assistant's cash-flow averages use complete months; Year in Review savings subtract classified realised losses; GST leaves out classified losses and future rows; Comparison keeps categories outside each period's top 8, names the real biggest mover and aligns whichever period is still running; the net-worth period change counts its first day; investment shares never pass 100%; mutual fund projections use the accounts you mapped and count redemptions; years to FIRE start from your current net worth.
- **Tax and AIS:** superseded (Inactive) AIS rows are skipped, detail rows take the payer from the summary level, a cut-short AIS download gets its own message, EPF rows show the taxable share the total uses, only taxed slabs are highlighted, and a rise in tax shows red.
- **Settings:** an excluded account stays in the picker so it can be un-excluded; Save sends only what changed, so a currency switched in the sidebar is no longer reverted; Apply rules waits until edits are saved; a cleared earning-start date and a cleared NPS amount now save; inputs are held to the server's limits.
- **Notifications and badges:** the bell shows a load error instead of "All caught up", the Bill Calendar badge counts only accepted expense bills on the IST day, an exceeded budget alert has its own dismissal, and ages of server timestamps are read as UTC.
- **Security and robustness:** CSV exports neutralise formula cells; `%` and `_` in a search match literally; two bank-alert regexes and three merchant regexes run in linear time (an alert padded with 800 newlines took about 3 seconds to parse); NUL characters and oversize values get a 422 instead of a 500; settings errors never echo a value, and an undeclared key in `backend/.env` no longer stops start-up; 429 responses carry `Retry-After` and the rate-limit headers.
- **Demo:** no row is dated after today, table sorting works, card bills cover trips and gadgets, commitments roll to their real due dates, and the net-worth chart starts from the opening balance.
- Many smaller fixes on Transactions, Budgets, Goals, Bill Calendar, Merchants, Foreign Assets, Review Queue, the AI Canvas and the chat.

### Added

- Transactions opens filtered from its link (`/transactions?type=Income&category=X`), so clicking an income source lands on its rows.
- Foreign Assets has a "Refresh market data" button.
- Optional response fields: `stale` on each `GET /api/rates/instruments` rate once its window ends, `latest_past_date` on `GET /api/analytics/v2/data-health`, and `planning_message` on a Money Manager import whose repeat rules and budgets were not saved.
- CI fails when the frontend and backend versions differ, before anything deploys.

### Changed

- `pnpm run setup` installs the backend's optional extras, so CAS import works on a local install.
- The Pages deploy logs what it saw on each poll, and a run whose backend has already moved to a newer version stops at once instead of waiting ten minutes.
- The CLI `import` and `import-file` commands take a required `--user-id`; any error other than an already-imported file exits 1.
- Assistant cost estimates use current Opus 4.5 and later and GPT-4.1 prices, and price the Bedrock Nova, Llama and DeepSeek models.
- `GET /health/db` is limited to 30 requests a minute.

## 2.34.1 - 2026-10-31

The working agreement for finance math is written down where every agent reads it. No app, API or schema change.

### Changed

- `docs/CALCULATIONS.md` "Changing a calculation" opens with the rule: any change to tax, TDS, bonus, RSU, salary or projection math starts with the formulas in plain text and one worked example, signed off before code, and salaried income actually received is never recomputed.
- `CLAUDE.md` points to `docs/CALCULATIONS.md` as the owner of every finance formula, with the same rule. Tax, TDS and RSU math had been re-derived in six sessions across three coding agents before this.
- `CLAUDE.md` goes from 54,280 to 11,718 characters (233 to 136 lines). Area detail moved verbatim, all 183 original non-empty lines kept, into six path-scoped rules under `.claude/rules/` that Claude Code loads only when a matching file is read or edited: `backend.md` (13,027 characters), `frontend.md` (13,049), `frontend-ui.md` (5,211), `shared-patterns.md` (6,156), `ai-chat.md` (7,284) and `deployment.md` (4,437). A one-word `claude -p` in the repo (Haiku 5.5, the maintainer's setup) now starts at 58,814 input tokens instead of 75,934. `.gitignore` tracks `.claude/rules/` and still ignores the rest of `.claude/`.

## 2.34.0 - 2026-10-28

The assistant now remembers how it got its earlier answers, the AI Canvas draws 16 kinds of chart from your data, calculations or outside figures (each labelled), and the owner's admin page covers users and the project. No migration.

### Fixed

- **Follow-ups no longer "correct" earlier answers.** The chat sent only the text of earlier replies, so on the next message the model saw figures with no look-up behind them, decided it had invented them and said a chart "was never drawn". Each answer's tool calls and results now stay in the history the model reads (hidden from the transcript), and a follow-up reuses them.
- **No more promises without action.** Both prompts require calling the tools in the same turn: no "let me fetch the data", no refusing while related data exists, and no apologising for an earlier answer unless a new result contradicts it.
- Canvas layout on wide screens: the chat column fills the height with the composer at its foot, the newest chart spans the canvas and older ones fill an auto-fit grid instead of leaving half the width empty; the range brush is gone from canvas line and area charts.

### Added

- **16 canvas chart types:** bar, stacked bar, ranking, line, area, stacked area, combo (bars with a line on an optional right axis), pie, donut, radar, treemap, sankey, slope, scatter, heatmap and waterfall, each validated before it is drawn (for example a pie holds at most 7 slices, a radar 0 to 100, a sankey no cycles, and a waterfall rejects a final total step because it adds Net itself).
- **`calculate`**, a canvas tool that runs SIP (with step-up), lump sum, loan EMI schedules, inflation, goal SIP and CAGR in code, so a projection never rests on the model's arithmetic (a 10,000 monthly SIP at 12% for 10 years gives 23,23,391; a 50 lakh loan at 8.5% for 20 years an EMI of 43,391).
- **Where the numbers came from**, on every chart: "Your data", "Calculated" or "Estimate, not your data" (outside figures such as inflation, only when asked), with the assumptions in "How this was made". Canvas amounts show whole rupees.
- Three read-only AI tools, 19 in all (20 over MCP): `list_tax_years` (income, deductions and tax paid per fiscal year), `get_net_worth_history` (snapshots plus a monthly book balance) and `list_investment_holdings` (CAS funds at the statement NAV and investment-account balances).
- **Admin page:** active accounts (a sign-in, import or AI call in 7 or 30 days), sign-ups per month, sign-in providers, an accounts table with masked emails (`abc***@domain`), imports per day, transactions by type, AI calls by model, the server's version, database and migration revision, feature adoption and table sizes. Still no account's transactions, amounts or full emails.

### Changed

- `aggregate_transactions` keeps a lone leftover series by name instead of an "Other" of one.
- Scatter labels are placed outliers first without overlapping, and axis ticks switch to whole rupees when the short form would repeat a label.

## 2.33.0 - 2026-10-28

The AI Canvas now charts almost anything you ask, shows its work while it runs, and lets you check every number. The owner gets an admin page of app-wide totals. No migration.

### Added

- **`aggregate_transactions`**, a 16th read-only AI tool (chat, canvas and MCP): sums or counts any slice of the ledger by month, quarter, fiscal year or year, split into up to six series by category, account or type, or ranks it by category, subcategory, account or type, filtered by type, categories, subcategories, accounts, note text and dates. Periods with no rows come back as zero, so a 36-month chart never skips a month; excluded accounts and classified capital-loss expenses are left out as everywhere else. Asked for "tax deductions per month for 36 months", the assistant used to say its tools only had a yearly total; now it charts the matching categories and labels the stand-in (rent as the HRA basis, for example).
- **AI Canvas, generating**: the next chart's place holds a chart-shaped placeholder with each step in words as it runs ("Finding your categories", "Totalling expenses by month for Rent, 2023-11-01 to 2026-10-28", "Drawing ..."), the elapsed seconds and Stop, which stops waiting (a call already sent still counts). A screen reader hears the start and the end of each run, and a failed request offers Try again.
- **AI Canvas, checking**: every chart has a Chart/Table switch with the exact values, a "How this was made" list of the look-ups behind it (kept with the chart), and one-tap follow-ups such as "as a line chart" or "grouped by quarter".
- **Admin page** (`/admin`, sidebar Owner section) for the accounts in `LEDGER_SYNC_ADMIN_EMAILS`: accounts, sign-ins, new accounts, live transactions, imports, AI calls and estimated Bedrock spend per day for 30 days, split by funding source. Counts and sums only: no account's rows, names or emails. `GET /api/admin/stats` answers 403 to everyone else, `/api/auth/me` reports `is_admin`, and admins have no app AI cap.

### Changed

- The AI Canvas sits beside its chat from `xl`: the conversation on the left with the composer at its foot, the charts on the right; phones keep the composer first.
- The assistant's prompts tell it to use `aggregate_transactions` for any breakdown the fixed tools lack, to chart the closest stored data rather than refuse, and to quote only numbers it drew.

## 2.32.0 - 2026-10-27

An AI Canvas that draws charts of your data on request, and a way for the server owner to lift the app-mode daily AI limit for chosen accounts. No migration.

### Added

- **AI Canvas** (`/ai-canvas`, Analytics in the sidebar and on More). Ask for a chart in plain words and the assistant fetches the numbers with its 15 read-only tools, then calls a new browser-side `render_chart` tool that draws a bar, ranking, line, area or pie chart with the in-house chart kit, followed by a one or two sentence takeaway. Arguments the chart cannot show (more than 60 labels or 4 series, a pie over 7 slices or with negative values, values that do not line up with the labels) come back to the model as an error it corrects. The 12 newest charts stay for the tab's session, per signed-in account. Phone first: the composer, then the charts; from `xl` the conversation sits beside them. Off in demo mode.
- `LEDGER_SYNC_AI_UNLIMITED_EMAILS` (JSON array): accounts whose stored, provider-verified email is listed have no app-mode daily call limit. `/api/ai/usage` reports their `limits.app_daily_messages` as 0, and the chat badge shows today's count instead of what is left. Token budgets set in Settings still apply.

### Changed

- `useChat` takes optional browser-side tools and a system-prompt note, so a page can extend the assistant without a backend tool. The floating assistant button is hidden on the AI Canvas, which has its own composer.

### Fixed

- Docs that still said no Vercel build had measured the CAS dependencies (CLAUDE.md, backend README, architecture). They now say what 2.31.0 found: the bundle measured 283.6 MB, so CAS import is the optional `cas` extra and the hosted `POST /api/upload/cas` answers 503, which the API reference now lists.

## 2.31.0 - 2026-10-27

The app no longer keeps a CPU core busy while it sits open, with every animation looking exactly as before. It also gains Safe to spend, a weekly recap and a watchlist on the Dashboard, a Review Queue, balance and bank-alert checks on Data Health, Tax Year 2026-27 with advance tax, AIS import and a Schedule FA worksheet for RSUs, CAS mutual fund import, Smart search, phone alerts and a read-only MCP server for Claude Code. App-mode chat moves to Claude Haiku 5.5. One migration adds 14 tables.

### Performance

Idle traces of 13-36 s per page on a 1920x1080 display that draws about 130-240 frames a second, live 2.30.2 against a local production build of this release, one tab open at a time. Busy time is per thread, and each row compares the two builds in the same session:

| Idle page | Renderer main thread | GPU process | Compositors |
| --- | --- | --- | --- |
| Landing page (2026-10-26) | 23.1% -> 9.3% | 60.7% -> 42.6% | 15.6% -> 11.2% |
| Cash Flow, Sankey (2026-10-26) | 83.8% -> 31.7% | 94.9% -> 37.8% | 52.1% -> 20.7% |
| Dashboard, any signed-in page (2026-10-27, final build) | 1.3% -> 1.0% | 25.2% -> 17.2% | 24.7% -> 18.1% |

The shares move with the machine's state and frame rate between sessions: a same-session Cash Flow pair on 2026-10-27 read 62.4% -> 57.8% on the main thread, the bead JavaScript gone but style recalculation per frame higher than on 2026-10-26. What holds in every run is the structure: the idle Cash Flow page makes no DOM writes, the landing page about 950 fewer a second, and the signed-in shell runs the same 10 CSS loops as before with no JavaScript per frame.

- The Cash Flow and landing-page beads drift on CSS animations instead of a Motion loop that ran JavaScript on every frame. On Cash Flow that loop also rewrote every bead's `d` path about 2,700 times a second; the page now makes no DOM writes while idle. Speed, spacing and phase are unchanged, and the beads still stop off screen, in Reduced motion and in print.
- The two scan beams moved out of the masked, translucent background field into their own layer and carry its fade and opacity themselves. Every beam frame used to redraw a full-viewport masked surface. Frozen frames of the old and new layouts match pixel for pixel (largest channel difference 1 of 255).
- Count-ups write their number straight into the text node instead of re-rendering the card on every frame, and a count that is already at its target no longer schedules frames.
- The route-pending and period-settling sweeps animate a full `transform`, which Motion hands to the compositor, instead of `x`, which ran on its JavaScript loop.
- The AI usage badge refreshes when a message settles instead of polling every 30 s, Settings polls usage only while the AI section is open, and the currency rate pill no longer re-renders the sidebar every minute.
- Number and date formatters are built once per locale and option set instead of on every call (25-45x cheaper per label).
- Skeleton bars pulse as one layer per group instead of one per bar (47 animated layers on the page skeleton become about 7).
- Disclosure panels measure their layout only when they open or close, not on every render.
- Chart wrappers memoise their screen-reader tables, which also rebuild when the display currency changes.
- The SheetJS chunk (about 490 KB) is cached the first time an upload uses it instead of being precached on every device; the precache drops from 2,744 KiB to 2,267 KiB.

### Added

Every figure below is a computation aid from your recorded data, not financial or tax advice, and the pages say so.

- **Dashboard**
  - Safe to spend: cash, bank and wallet balances less card dues and overdrafts, bills due before payday and a reserve for later bills the next paychecks cannot cover. It shows a per-day allowance, a spending pace bar and a low-balance warning from a 30-day per-account balance projection (`GET /api/safe-to-spend`, `GET /api/safe-to-spend/projection`).
  - Weekly recap of the last full Monday-to-Sunday week (IST): spending against the 4-week average, the top category movers, the book-value net-worth change and its biggest driver, commitments due in the next 7 days, open anomalies and days to payday (`GET /api/weekly-recap`). Explain sends only these figures to your configured AI assistant for a three-sentence summary, counted against the chat's daily quota.
  - Watchlist: pin up to 8 saved views from the Views menu on Transactions. Each tile shows this month so far against the same days of last month, with the last six complete months behind it. With nothing pinned it offers last month's top three spending categories as one-tap pins (`GET /api/saved-views/tiles`, `PUT`/`DELETE /api/saved-views/{view_id}/pin`, `PUT /api/saved-views/pins/order`, `POST /api/saved-views/pins/category`).
  - Safe to spend, the recap and the watchlist ignore the time range, so they sit above the period content and never dim while it settles.
- **Review Queue** (`/review`): one card per unreviewed income or expense row of the last 30 days, newest first. Swipe right or press K to keep the category; swipe left or press C to change it, with the category suggester's top two picks first; Undo with U. A change is stored per transaction in `category_overrides` and put back on every later spreadsheet or Money Manager import (`GET /api/review-queue`, `PUT`/`DELETE /api/review-queue/{transaction_id}`).
- **Category suggestions** in the spreadsheet upload review: a per-user naive Bayes over note words, account, amount size and weekday suggests up to two categories for rows with a placeholder category. Nothing is applied. It turns on at 30 categorized rows in 2 categories and holds back guesses under 50% (`POST /api/category-suggestions`, `GET /api/category-suggestions/status`).
- **Data Health**
  - Balance checks: record the balance your bank or card app shows on a day and the ledger's running balance for that day is compared with it, per account, with a month grid of rows, gaps and checks (`/api/balance-checks`).
  - Bank alerts: an iPhone Shortcut or any automation posts bank SMS or alert-email text to `POST /api/ingest/text` with a Bank alert inbox token. The amount, direction, date, last four digits, payee and bank are read; OTPs, promotions, collect requests and future or declined debits are refused, and the text itself is never stored. Data Health lists alerts no ledger row answers (same amount, date within 2 days, a fitting account), with Ignore and Restore. Alerts never become ledger rows.
- **Bill Calendar**
  - Credit card statement cycles from the Money Manager card days: each card's last and next statement, due date, billed and unbilled amounts, payment status and the cash left in the paying account after dues (`GET /api/card-cycles`), with each due date on the calendar and a Card statements panel.
  - The four advance tax instalment dates (15 Jun, 15 Sep, 15 Dec, 15 Mar) as markers that link to the estimate on Income Tax and stay out of every total.
- **Income Tax**
  - Tax Year 2026-27 under the Income-tax Act 2025. The slabs, standard deduction and rebate match FY 2025-26, so no estimate moves; years from 2026-27 read Tax Year, and deduction labels show the new section beside the old one, such as s.123 (old 80C).
  - Old-regime deductions are saved per tax year with Save and Clear instead of resetting on every visit, and each capped section shows how much of its limit is left (`/api/tax-deductions/{tax_year}`).
  - Advance tax planner: the year's tax less TDS and TCS split into the four cumulative instalments once it reaches INR 10,000, with ledger payments matched to each date and estimated deferment and default interest. A filing calendar lists the return, belated and revised deadlines.
  - AIS import (`/tax/ais`): the Annual Information Statement JSON is decrypted and read entirely in the browser with your PAN and date of birth, which are never sent, saved or logged. It shows TDS and TCS, interest, dividends, securities and mutual fund transactions, and compares the statement with the ledger both ways. Saving keeps only four totals per year (`/api/tax-ais`), which the Withholding to date card then shows beside its estimate.
  - Foreign Assets (`/tax/foreign-assets`) for RSUs of a foreign employer: a lot ledger per vest, sales and dividends with US tax withheld, Schedule FA rows per calendar year with initial, peak and closing values, CSV export, and a FAST-DS 2026 check (holdings on 31 Mar 2026, declarations by 31 Dec 2026).
  - RSU vests convert to rupees by the tax convention: the employer's perquisite figure, else the SBI TT buying rate you enter for the last day of the month before the vest, else the ECB rate labelled an approximation (`/api/rsu-tax`).
- **Investments**: import a CAS PDF (CAMS or KFintech, detailed or summary, NSDL or CDSL), read in memory on the server; neither the PDF nor its password is stored or logged. Holdings are valued at AMFI's latest NAV, with cost, gain and XIRR from the statement's own transactions (`POST /api/upload/cas`, `GET /api/mf-holdings`). MFCentral statements and re-printed copies are refused with a message naming the statement to request. CAS import needs the optional `cas` extra; the hosted Vercel backend runs without it for now (its ISIN database took the function past Vercel's 250 MB standard size), so there the upload answers 503 and saved holdings still show.
- **Smart search** on Transactions: an opt-in index embeds each transaction's note, category, subcategory and account (never amounts or dates) with Amazon Titan Text Embeddings V2, and the Smart search toggle answers the search box by meaning within the page filters. Indexing runs in rate-limited batches, re-embeds only changed rows, counts toward AI usage and budgets, and can be removed (`/api/semantic-search`).
- **FIRE Calculator**: a Monte Carlo tab simulates 1,000 to 10,000 lifetimes in a Web Worker from today's net worth split into equity, debt and cash pots, each with its own return and volatility, plus INR inflation. It reports the success rate with its sampling margin, the median at retirement and at the end age, failure ages and a percentile fan, with an optional equity fall at retirement as a stress test.
- **Budgets** (`/budgets`): this month's category budgets above the 50/30/20 rule, with inline limit and threshold edits, pause and resume, delete and Add budget (`PATCH`/`DELETE /api/analytics/v2/budgets/{budget_id}`).
- **Recurring**: tap an amount to edit it in place; an edited commitment shows an Edited chip and its detected amount, with Reset.
- **Anomaly Review** runs the four checks that were only in the type list (Unusual Category, Large Transfer, Possible Duplicate, Missing Recurring for confirmed commitments) and a new Fee or Interest check, follows the saved enabled-types and auto-dismiss settings again, and explains each check on its card.
- **Money Manager import** brings repeat rules (as scheduled payments) and monthly category budgets, linked by their Money Manager IDs in `external_refs` so later backups update the same rows and keep edits made here. The Upload page gains a Sync from Money Manager button for the iOS Shortcut.
- **Phone and browser alerts** (Web Push) in Settings > Notifications, on a Home Screen app on iOS 16.4 or later or any browser with push: bills due, budgets past their threshold and anomalies waiting for review, sent once a day by a Vercel cron, with counts and a deep link but never amounts. They stay off until the VAPID keys and `CRON_SECRET` are set.
- **MCP server** at `POST /mcp`: Claude Code can query the ledger through the assistant's read-only tools with a Claude MCP read access token (`claude mcp add --transport http ledger-sync <API>/mcp --header "Authorization: Bearer <token>"`). Calls run in a transaction that cannot write, 60 a minute and 1,000 an hour per token.

### Changed

- The Dashboard and Overview no longer download the whole ledger (`/api/transactions/all`, 3.4 MB of JSON for a 9,000-row synthetic ledger). Age of Money, Days of Buffering, the investment-transfer split and the health-score inputs come from `GET /api/dashboard-metrics/pulse` and `GET /api/dashboard-metrics/health`, about 1.7 KB together, whose numbers matched the browser calculators on all 2,491 compared figures. Cash Flow, Comparison, GST, Tax, Returns, Investment Analytics and SIP projection now fetch the ledger on their first visit instead of finding it cached by the Dashboard.
- App-mode chat defaults to Claude Haiku 5.5 (`us.anthropic.claude-haiku-5-5`, USD 0.11 / 0.55 per 1M tokens on the us. profile against 1.10 / 5.50 for Haiku 4.5). The Bedrock proxy turns Claude 5.x thinking off, or to low effort where it cannot be turned off, so tool rounds keep working. To go back, set `LEDGER_SYNC_AI_DEFAULT_BEDROCK_MODEL=us.anthropic.claude-haiku-4-5-20251001-v1:0` and redeploy. Cost estimates cover the Claude 5.x models, and the BYOK Bedrock list adds Haiku 5.5 and Sonnet 5.5.
- Net Worth projects by compounding the average monthly saving at the measured growth of yearly savings (needs 24 complete months, held within 15% a year) for the 5 years the chart draws, then holds it; milestone ETAs follow the same path. With no measurable growth it is the previous straight line, which stays on the chart beside it.
- Settings > Upload tokens is now Settings > Advanced > Access Tokens, with a purpose per token (Money Manager upload, Claude MCP read access, Bank alert inbox) and the five-token cap per purpose. Existing upload tokens keep working unchanged.
- The Budget Rule page and nav entry are now Budgets.
- Chart keyboard navigation follows the slider model: on a horizontal chart Up, Right and Page Up move forward; on a vertical chart Down and Page Down do.
- Applying rules to existing transactions skips rows with a Review Queue fix, as imports already do.
- Anomaly Review applies auto-dismiss, on by default, to existing accounts: a large expense that is an entry of a recurring pattern, and a category month explained by a bill on a longer-than-monthly schedule, are filed as dismissed with a note, so they leave the open list and the sidebar and notification counts. Turning the setting off brings them back on the next refresh.
- Anomaly Review lists open findings first, then by type, severity and month instead of by detection time, 50 cards at a time. Each type has its own cap per refresh, and reviewing a finding on a transaction silences only that type there.
- Money Manager import: a repeat rule or budget that cannot be stored no longer blocks the ledger import, transfer repeat rules are skipped, and monthly budgets follow this month's override.

### Fixed

- An amount you set on a detected recurring commitment survives the next analytics refresh; totals, sorting, the AI tools and the weekly recap use it. Un-confirming a bill clears it.
- Creating a budget for a category with a paused one resumes it, an active duplicate returns 409 and a zero or negative limit 422 (both were 500s), and a new budget counts this month's spending at once instead of reading zero until the next import.
- A full account reset now also clears RSU sales, dividends and vest exchange rates, bank alerts, balance checks, saved deductions, AIS totals, CAS holdings and the Money Manager links. A ledger-only reset keeps them, as before for other settings.

### Database

- One expand-only migration, `feature_domains_2026`, adds `balance_checks`, `bank_alerts`, `category_overrides`, `external_refs`, `foreign_asset_events`, `rsu_vest_fx`, `mf_holdings`, `mf_transactions`, `mf_nav_prices`, `push_subscriptions`, `saved_view_pins`, `tax_ais_summaries`, `tax_deductions` and `transaction_embeddings`. On PostgreSQL it first runs `CREATE EXTENSION IF NOT EXISTS vector`. No existing table changes. CI installs pgvector for the native PostgreSQL job.
- `anomalies.anomaly_type` gains `FEE_CHARGE`, stored as text, so no migration. Before rolling back to 2.30.x, run `DELETE FROM anomalies WHERE anomaly_type = 'FEE_CHARGE'`, or older code fails to load those rows.
- New dependencies: pgvector and numpy, plus casparser and pypdfium2 in the optional `cas` extra (already installed through pandas, now declared). Push messages are encrypted (RFC 8291) and signed (RFC 8292) with `cryptography` and PyJWT rather than pywebpush, whose `http-ece` dependency ships no wheel; the RFC 8291 test vector reproduces byte for byte.

## 2.30.2 - 2026-10-24

Every outdated dependency, CI action and package-manager pin moves to its newest release, which also fixes all 24 open Dependabot alerts and all 9 `pnpm audit` advisories. No app, API or schema change.

### Security

- virtualenv 21.13.0 -> 21.14.13 through a new `constraint-dependencies` floor of 21.14.9 (15 alerts, #125 to #132 and #134 to #140), and filelock 4.0.6 -> 4.2.4 (#133, #141 and #142). Both arrive only through pre-commit, a dev dependency.
- Frontend build and test tooling: zod 4.6.5 -> 4.6.7 (through eslint-plugin-react-hooks), undici 8.11.2 -> 8.12.3 (through jsdom), and fast-uri 4.2.1 -> 4.2.3 and serialize-javascript 7.1.2 -> 7.1.3 (both through workbox-build). This covers Dependabot #144 to #148 and #151, plus #143, #149 and #150, which GitHub auto-dismissed as dev-only. `pnpm audit` goes from 9 advisories (2 high, 4 moderate, 3 low) to 0. None of these packages reach the shipped app; `pnpm audit --prod` was already clean.
  - The `fast-uri` override pinned the now-vulnerable 4.2.1 and pins 4.2.3 instead. The `undici` and `serialize-javascript` overrides take the fixed versions as their floors, and the stale `minimumReleaseAgeExclude` entry for fast-uri 4.1.4 is removed.

### Changed

- The backend lock is refreshed, 37 packages in all (36 updated, 1 added): FastAPI 0.141.1 -> 0.162.0 with Starlette 1.9.0, Pydantic 2.14.1, pydantic-settings 2.16.1, pandas 3.1.0, SQLAlchemy 2.0.55, Alembic 1.20.1, cryptography 50.0.2, boto3 1.43.118, Ruff 0.17.2, mypy 2.4.0 and pre-commit 4.7.0.
  - FastAPI now depends on `opentelemetry-api`; nothing in the app configures telemetry. The FastAPI releases in between change form-field defaults, drop deprecated `**extra` arguments and tell an explicit JSON `null` body from an omitted one. The API takes JSON and raw bodies, not forms, and passes none of the removed arguments.
  - `uv.lock` is written by uv 0.13.5, the version CI already runs. Its lockfile revision moves from 3 to 5, which older uv versions still read.
- SQLAlchemy stays on 2.0.x. 2.1.6 still fails mypy with 27 errors, 21 of them `order_by`/`Select` arg-type, and 2.1 also makes autoflush unconditional, so it moves in a release of its own.
- Frontend: Motion 14.6.0, lucide-react 1.61.0, Vite 8.3.5, zustand 5.0.16 and vite-plugin-pwa 2.0.0, whose only breaking change is a Node 20.19 minimum. The production build still precaches 126 entries and writes an identical web manifest and `registerSW.js`. In all, 83 of the lock's 669 packages change version; besides these five, every one moves within its existing range. At the root, concurrently is 10.0.6 and the shell-quote override 1.12.0.
- pnpm 11.28.6 (from 11.28.3) in both `packageManager` fields.
- CI actions by commit SHA: setup-node 7.1.0, setup-uv 10.3.2 and pnpm/action-setup 6.1.0. The shared workflows move to their latest `main`, where only `python-ci.yml`, which this repo does not call, changed.

## 2.30.1 - 2026-10-24

The architecture and flow diagrams are redrawn with Orrery 0.12, which draws icon tiles at 64 px instead of 48 px. No app, API or schema change.

### Changed

- Nine figures in `docs/diagrams/` are re-rendered from their specs: light and dark SVGs, the OAuth sign-in sequence and each editable `.drawio.svg`. Money Manager import, statement import and release delivery draw no icon tiles and render byte for byte as before.
- Four specs take layout settings so the bigger tiles still read at the 880 px README width. Every node, edge and evidence entry stays as it was, and `orrery verify` finds all of them. Scores are `orrery review --width 880`, against the unchanged spec drawn with 64 px tiles:
  - Chart kit: CategoryTreemap, Page x/y charts and Treemap move up in their groups so the wrappers and pages meet the chart roots in order, CartesianChart children sit in the roots' column under CartesianChart (`align: cartesian`), and spacing is 0.5. Edge crossings drop from 9 to 4, the review passes (88.9 to 93.5) and the canvas is 1208 px wide instead of 1325 px.
  - Backend layers: compact spacing, 1092 px wide instead of 1148 px, node labels at 10.5 px; 93.9 to 94.4.
  - Shared finance rules: spacing 0.6, 1171 px wide instead of 1215 px; 93.8 to 95.3.
  - System overview: spacing 0.5, 1090 px wide instead of 1199 px, node labels at 10.5 px; 97.4 to 97.9.

## 2.30.0 - 2026-10-22

Money Manager backups import directly with stable transaction IDs, charts are drawn by an in-house animated SVG kit instead of Recharts, every architecture and flow diagram is redrawn from the code, and re-importing an older ledger with repeated identical rows no longer fails.

### Added

- Money Manager (Realbyte) `.mmbak` backups import from the Upload page. The browser gzips the backup and the server reads it, through an in-memory, query-only SQLite copy with size and time caps. A review shows what the import would add, update and remove before anything is saved.
  - Each transaction keeps its Money Manager ID (`transactions.source_uid`). Editing an amount, date, account, note or category in Money Manager updates the stored row in place under the same public ID, so tags and anomaly reviews stay attached. Rows imported earlier from a spreadsheet are adopted on the first backup, including rows whose note was edited since, when the match is unique.
  - The time of day of each transaction, each account's Money Manager group, card statement and payment days, and paying account are stored. An unclassified account takes the type its group implies; a type you chose is kept.
  - The same backup again reports "already up to date" instead of a conflict. A backup that would remove more than 5% of the ledger (at least 25 transactions) waits for a confirmed review.
  - Measured on a real backup (8,780 records, 4.96 MB, 1.32 MB gzipped): 7,427 transactions and transfers read in 0.12 s and imported in 4.5 s against an existing 7,161-row ledger. 7,154 existing IDs were kept.
- Upload tokens (Settings > Upload tokens) for automations such as an iOS Shortcut that uploads the newest backup whenever Money Manager closes. A token is shown once and stored only as a SHA-256 hash. It can upload backups and nothing else, cannot preview or lift the deletion guard, expires after a year and can be revoked at once. At most five are active. The Handbook has the Shortcut steps.
- Migration `money_manager_source_2026` adds nullable `transactions.source_system`, `source_uid`, `source_updated_at` and `occurred_at` with a unique per-user UID index, the account settings columns on `ledger_accounts`, and the `upload_tokens` table. It is additive with no backfill, so the backend and the migration can deploy in either order.
- An in-house SVG chart kit in `frontend/src/components/charts/`, built on React 19 and Motion 14, draws every chart in the app:
  - Charts animate once they scroll into view. Bars grow out of the zero line in a stagger, lines draw in left to right with their fills wiping in behind them, pie slices sweep open from 12 o'clock, radar series grow out of the centre, and Sankey ribbons sweep in with beads drifting along them.
  - Changing a filter or range moves the marks, axes and grid to the new data instead of replaying the entrance (a line whose point count changes wipes in again). Sankey drill-downs glide the nodes and ribbons both views share.
  - Tooltips glide between rows with the cursor and, on phones, pin to the top of the chart so a finger never covers them. x/y charts, pies, radars and the Sankey can be stepped through with the arrow keys. Home and End jump to the first or last point, and Page Up and Page Down move a tenth of an x/y chart at a time.
  - On bar, area and line charts with two or more series, the legend items are buttons that show or hide a series. The axis rescales and the remaining marks morph, the last visible series cannot be hidden, and the accessible table keeps every series.
- Expense Analysis has a squarified treemap of spending by category above the category list. Choosing a category zooms into its subcategories in one motion, and the breadcrumb, Escape or Backspace zooms back out. Arrow keys, Home and End move between tiles, and each tile's label shows as much as fits. A deep link to a category opens the map on it.
- Comparison has a slope chart of the top 10 expense categories: each line runs from period A to period B, with rank moves, and its color shows a rise or a fall. Overlapping labels spread apart with a hairline to their point. The focused category comes forward and is read out with both values, the change and the rank move.
- Year in Review's heatmap waves in week by week when it scrolls into view and retints in a left-to-right sweep when the year changes. On desktop its legend swatches highlight the days at one level, Enter or Space selects a day, Escape clears the selection, and the focused day is read out. Phones keep the month tiles.
  - The Reduced motion setting and printing draw the finished chart at once, and very long series skip the entrance.
- Architecture and flow diagrams drawn with Orrery from YAML specs in `docs/diagrams/`: system overview, backend layers, statement import, Money Manager import, analytics boundaries, shared finance rules, chart kit, session lifecycle, token refresh, OAuth sign-in (a sequence), AI chat and release delivery. Every node and edge cites the file and line it comes from, so `orrery verify` fails when the code moves on. Each figure has light and dark SVGs and an editable `.drawio.svg`.

### Changed

- The frontend ships 398 kB less JavaScript than 2.29.0 (95 kB, 11%, gzipped): the 469 kB Recharts chunk (128 kB gzipped) is gone, and the kit's shared chunks come to 40 kB (19 kB gzipped).
- Motion moves from 13.4 to 14.4, React and React DOM to 19.3.1, React Router to 8.5.0, TanStack Query to 5.104.5, TanStack Table to 9.3.1, lucide-react to 1.60.0 and react-dropzone to 20.1.4. Tooling: ESLint 10.13.0, typescript-eslint 8.72.0, Vitest 5.0.5, jsdom 30.1.3, PostCSS 8.5.31, @vitejs/plugin-react 6.1.2, globals 17.13.0 and @types/node 26.6.5.

### Removed

- Recharts, its `vendor-recharts` chunk and the adapters around it: `ChartContainer`, `ChartTooltip`, `ChartTooltipContent`, `chartBaseline`, and the axis, grid, reference-line and brush presets in `chartDefaults`.
- The Archify and Mermaid diagrams the Orrery figures replace (`docs/diagrams/*.json`, `docs/diagrams/build.mjs` and `docs/images/`).

### Fixed

- Re-importing a ledger whose rows predate source fingerprints no longer fails with "Legacy transactions have ambiguous source identities" just because it holds identical transactions (two same-day payments with the same amount, account and note). When every stored copy of a repeated row has the same category, subcategory and currency and none carries a tag or anomaly, the copies are paired with the file's rows in a fixed order, and any extra copy on either side is inserted or removed as usual. Copies that do differ still reject the whole file, and the message now names up to three of the conflicting groups (date, amount, account, how many in the file and how many stored).

### Security

- Transitive `follow-redirects` moves from 1.16.0 to 1.16.1 through the frontend override, fixing two high-severity header disclosures on redirects (CVE-2026-111755 and CVE-2026-111756, flagged by Trivy).

## 2.29.0 - 2026-10-21

A consistency and cleanup sweep. The backend and frontend now apply the same spending and averaging rules, 14 API routes that no client called are gone, CI tests the Python version production runs, and about 6,600 lines of dead or duplicated code are removed (net).

### Fixed

- The backend reports its real release on `/health`. `ledger_sync.__version__` stayed at 2.28.0 through 2.28.1 and 2.28.2, so the Pages deploy, which waits for the backend to report the frontend's version, timed out and neither frontend published.
- Pages, AI tools and demo now agree on spending rules (`core/metric_rules.py` and `lib/finance/metricRules.ts`):
  - Needs keywords ignore surrounding spaces in user entries, and a keyword is a whole word when no letter, mark or digit of any script touches it, so Devanagari keywords match on both sides.
  - Monthly averages leave out future-dated months and count an empty month between the first month and today as a zero month on the backend too.
  - The Financial Health score uses the user's Needs categories instead of a fixed list, and the shared investment perimeter.
  - Returns Analysis counts only mapped investment accounts.
  - The demo 50/30/20 split uses the shared Needs rule.
  - Every frontend "today" and "this month" (`getTodayKey`, `getCurrentMonth`, the partial-month helpers and the 21 places that built today from the browser clock) is the IST ledger day, as the backend does, so pages no longer disagree about the current month for viewers outside IST.
- Mutual Fund Projection no longer shifts a transaction into the previous month for viewers west of UTC.
- When the AI assistant uses all 6 tool rounds without answering, the chat now says so instead of leaving the last "Looking up your data..." status as the reply.
- Sign-in provider icons are hidden from screen readers (the button text names the provider), and the page skeleton announces "Loading" once instead of three times.
- `pnpm dlx` inside `frontend/` no longer crashes in the TypeScript 6 install hook, and the schema reference generator handles `FetchedValue` columns (the reference now names `recurring_user_amount_2026` as head).

### Removed

- API routes with no client: `/api/reports/monthly`, `/api/analytics/wrapped`, `/api/analytics/charts/{income-expense,categories,monthly-trends,account-distribution}`, `/api/analytics/insights/generated`, `/api/calculations/insights`, `/api/calculations/top-categories`, `/api/meta/{types,accounts,filters,buckets}` and `/api/analytics/v2/freshness`, with the report generator, insight engine and other code only they used.
- Unused code: password hashing and the `bcrypt` dependency (sign-in is OAuth only), the `TimeFilter` class, the upload `AccountClassifier` and `DropZone` components, a second `FYNavigator`, six unused barrel files, 290 of knip's 292 unused frontend exports and types (the other two are kept on purpose), and duplicated helpers across both stacks (jscpd: 28 clones to 12).
- Unused files and tooling: `backend/requirements.txt` (Vercel installs from `uv.lock`), `frontend/tailwind.config.js` (Tailwind 4 never loaded it), `eslint-plugin-react`, `pip-audit`, `types-openpyxl`, an uninstalled Vitest coverage provider, and three CVE constraints for packages no longer in the lockfile.

### Changed

- Python 3.14 everywhere: `requires-python` is `>=3.14,<3.15`, and CI and the migrate job run 3.14. Production already built with 3.14 because Vercel takes the newest version the range allows.
- HTTP status codes now match per condition: OAuth not configured is 503 (was 501 or 500), an inactive user refreshing a token gets 403 (was 401), a blank stored AI key is 404 (was 400), and a future `on_date` for stock prices and exchange rates is 422 (was 400).
- Chart sections share one panel component, loading skeletons share the `LoadingSkeleton` family (one fill, pulse and radius), and RSU vest-date prices are fetched in parallel.
- `AnalyticsEngine` requires a `user_id`.

### Security

- `vite` `^8.3.3` (Dependabot #120).
- `certifi` 2026.10.19 through a constraint (GHSA-6q48-6c7w-9gw2, critical: bundled emSign root CAs had compromised intermediate keys; reached via httpx).
- The recurring-note date-suffix regex is linear (CodeQL #63, polynomial ReDoS).

## 2.28.2 - 2026-10-20

The phone app installed from Safari's Add to Home Screen behaves in landscape.

### Fixed

- The installed app no longer sets `apple-mobile-web-app-status-bar-style: black-translucent`. On WebKit's tracker the mode is called deprecated, with the advice to remove the key so the status bar takes the page colour, since Home Screen web apps do not support drawing below the status bar ([webkit.org/b/317153](https://bugs.webkit.org/show_bug.cgi?id=317153)). Delete and re-add the Home Screen icon to pick up the change.
- In landscape the bottom tab bar pads the left and right safe areas, and the slide-out navigation widens by the left inset, so neither sits under the notch or Dynamic Island. The navigation's profile row now clears the home indicator in both orientations.
- Toasts clear the phone tab bar and the iOS safe areas instead of covering the tab bar.

### Changed

- The web app manifest's `orientation` is `any` instead of `portrait`. iOS never honoured the lock, and on Android it forced an installed app into portrait, which WCAG 2.1 SC 1.3.4 (AA) does not allow for content that works in both orientations.
- On a phone in landscape (500px tall or less), the tab bar shows icons only, keeping each label for screen readers and a 44px touch target, so charts and tables get more of the short screen.

## 2.28.1 - 2026-10-13

A dependency security fix.

### Security

- Transitive source-map-js moves from 1.2.1 to 1.2.2 in the frontend lockfile, fixing an event-loop denial of service through indexed source-map section offsets (Dependabot alert #119). Build tooling only; nothing shipped to the browser changes.


## 2.28.0 - 2026-10-08

Steadier period changes, a livelier landing page, border and theme-color fixes, an enforced 50/30/20 split, and TypeScript 7.

### Added

- The landing page shows an animated sample money-flow from income into spending, investments, savings and tax, counting sample KPIs, an animated income-versus-spending chart, and a feature grid that opens the matching demo page. It adds about 17 KB of JavaScript and still loads no chart library on `/`.
- Migration `recurring_user_amount_2026` adds a nullable `recurring_transactions.user_expected_amount` column (phase 1 of making edited recurring amounts stick). The app does not read or write it yet, so it is safe whether the backend or the migration deploys first.

### Changed

- Dashboard, Overview, Expense Analysis, Income Analysis and Year in Review keep the previous period's figures on screen, dimmed with a slim updating bar under the pinned filters, until every figure for the new period has loaded, then switch them together. The page no longer collapses to a skeleton or loses its scroll position on a slow network, and figures from two periods never mix.
- Needs, wants and savings must total 100% in Settings and in the preferences API (422 otherwise); Settings shows the running total and disables Save until it adds up.
- Tailwind border-color utilities now apply: the global default border and `.ledger-control` rules moved into cascade layers, so active filter pills, info banners, severity chips and error inputs show their intended borders.
- Charts and swatches that copied their colors at load or inside a memo now follow a theme toggle without a reload: Goals, Comparison KPIs, Budget rule bars, anomaly severity, the Expense and Income Analysis category bars, slices and legends, the Dashboard income and expense pies and CFP radar, Net Worth gradients, Investment allocation, the tax projection sparklines, the Cash Flow Sankey, and the Year in Review heatmap and month cells. A shared `usePaletteSnapshot` hook gives memoized chart data a palette copy that changes on each toggle.
- The frontend compiles and type-checks with TypeScript 7. typescript-eslint does not support it yet, so `frontend/.pnpmfile.cjs` gives it its own TypeScript 6.

## 2.27.0 - 2026-10-01

Filters stay in reach while you scroll, two Comparison display fixes, and the deferred major dependency upgrades.

### Added

- A shared `StickyToolbar` pins each page's period, FY, sort, and filter controls under the workspace header while the page scrolls, on 18 pages (Dashboard, Overview, Transactions, Income and Expense Analysis, Cash Flow, Net Worth, Investment Analytics, Returns, Trends, Year in Review, Income Tax, GST, Comparison, Anomaly Review, Merchant Intelligence, Bill Calendar, Budget). It gains a hairline and shadow once pinned and keeps keyboard focus clear of it.

### Changed

- Transactions, Budget, and Anomaly Review keep the previous results on screen while a new filter loads, so changing a filter no longer collapses the page to a skeleton or jumps to the top.
- Bill Calendar's month navigation moved into the pinned toolbar because the summary cards and day panel also follow the month; Income Tax and GST cross-links moved into their page headers.
- Frontend dependencies: Vitest 5, `@tanstack/react-table` 9, `@vite-pwa/assets-generator` 2, and `react-router` 8 (replaces `react-router-dom`, imports only). `rimraf` is gone from the clean script.
- Backend dependencies: the unused `mangum` Lambda shim and `python-multipart` are removed; Vercel serves the ASGI `app` directly.

### Fixed

- The Comparison spending chart anchors each period's amount at the outer end of its own bar, so the left and right labels no longer overlap at the zero line.
- The Comparison category tables name both periods once in the column header instead of repeating the period label on every row.

## 2.26.0 - 2026-09-30

Second audit pass: one definition per financial metric across pages, the AI assistant, and demo mode; a schema alignment migration; and roughly 110 findings across API contracts, validation, error states, accessibility, and docs.

### Added

- Data Health lists rows that look like realised investment losses with a one-click Classify and Un-classify; classified keys are the only rows that aggregates treat as losses.
- Migration `orm_schema_alignment_2026` makes 118 columns NOT NULL where the models already require it, adds 10 missing indexes, and drops duplicate ones. It counts NULLs first and refuses to run if any exist; it never backfills or deletes data.
- CI runs `alembic check` on SQLite and native PostgreSQL, builds the Pages bundle exactly as deploy does, and tests the frontend on Node 24. `backend/scripts/build_schema_reference.py` regenerates the schema reference.

### Changed

- One rule per metric everywhere: savings are income minus spending minus classified losses; spending excludes classified losses; cashback is cashback rows minus shared cashback (refunds and reimbursements appear as non-taxable receipts); monthly averages use complete calendar months with empty months as zero; investment accounts use your mappings, case-insensitive, with default keywords only when none are mapped; essentials, tax paid, investment income, and salary share one matcher on both backend and frontend; every negative balance is a liability.
- Net Worth excludes future-dated rows, Goals and Net Worth share a trailing 12-month savings average, and fiscal years that start in January are labelled "FY 2024".
- Spending Analysis and Year in Review read server totals instead of downloading the full ledger; `/analytics` routes, quick insights, and CSV export read only the columns they need and export streams one page at a time.
- Swagger `/docs`, `/redoc`, and `/openapi.json` are served in development only; strict token-version checking is on by default; database errors outside development hide bound parameters.
- Renovate holds SQLAlchemy below 2.1.

### Fixed

- A failed or stopped AI reply no longer leaves an empty message that made every later chat request fail; long chats are trimmed without splitting tool calls; errors read as text.
- The bulk preferences save enforces the same bounds as the section endpoints; manual transactions and recurring amounts round like uploads and reject Infinity, NaN, and oversized values; end date 9999-12-31 no longer returns 500.
- Account reset removes holdings, rules, and saved views as its mode says; anomaly badges and summaries count every match, not the first 50.
- Seven page sections show a retry error instead of a false "no data" state; the mutual fund projection no longer shows a doubled currency symbol; confirm dialogs and the command palette keep keyboard focus inside.
- Demo mode honours date, type, severity, and limit filters, and its net worth trend now matches the balance hero.
- School and tuition fees are GST-exempt; the fiscal-year list no longer shifts a day for users west of UTC.

## 2.25.0 - 2026-09-29

Covers #244 through #250 plus the full-repo audit sweep: domain tables for settings, stable ledger imports, centralized financial calculations, and 61 audit findings plus 114 SonarCloud issues fixed without schema changes.

### Added

- A persisted Full/Reduced motion preference is available from both the sidebar and Display Preferences.
- Inter and JetBrains Mono are self-hosted for consistent interface and financial metadata typography.
- Chart range controls scope time-series charts to a window, and the tax page shows a payroll and RSU withholding summary (#247).
- Account classifications, AI settings, and salary/RSU/growth inputs live in their own tables instead of JSON preference columns (#250).
- Refresh tokens rotate on every use and are single-use, with a 60-second grace window for concurrent tabs; reuse after the window revokes every session. Existing sessions upgrade on their next refresh.
- The Pages build ships a Content-Security-Policy with hashed inline scripts, and analytics refresh, AI tool execution, and the stock and exchange-rate proxies are rate limited per user.

### Changed

- Snapshot imports validate complete INR ledgers before atomic reconciliation, and OAuth plus queued mutations are bound to the initiating session (#245).
- Tax, payroll, investment, net-worth, forecast, and projection formulas have single shared owners (#246).
- Imports keep stable transaction identities, analytics refresh only affected periods, and deep transaction pages use signed cursors (#249).
- The dashboard paints its summary without waiting for the full ledger, Net Worth and Trends read server-side daily totals, and the anonymous home page no longer preloads the chart library (eager JavaScript 1,315 KB to 843 KB raw).
- Account balances, quick insights, `/insights`, and the full-ledger endpoint aggregate or project in SQL instead of building an ORM object per row, and CSV export streams.
- Preference and tag saves no longer re-download the full ledger unless excluded accounts changed.
- Category trends rebuild only affected months; the largest backend routers and frontend modules are split below 400 lines with unchanged routes and exports.
- Dependencies updated within current majors (React 19.3, Vite 8.3, Motion 13.4, Starlette 1.7, PyJWT 2.15, Alembic 1.20); SQLAlchemy stays below 2.1 and TypeScript at 6.0.3.

- Desktop routes now use a framed ledger workspace with a measured grid, active scan lines, colored KPI rails, and clearer route transitions; mobile stays edge-to-edge.
- Route, section, list, and chart motion now share a restrained token system with one owner for each entrance.
- Primary actions use a consistent blue treatment, dark surfaces are flatter and closer to black, and static cards no longer imply click behavior.
- Progress fills use transform-based animation, while sidebar utilities and safe-area spacing are more robust across desktop and mobile layouts.
- Full mode keeps visible workspace motion running, while Reduced mode removes Motion and CSS animation immediately, shows every element in its final state, uses native scrolling, and preserves active route state.
- Disclosure panels use position-only layout projection, route exits complete faster, and tablet page headers stack before controls can compress titles.
- Dark and light control foregrounds meet AA contrast, print output resets ledger surfaces to white, and the PWA now ships only the required Latin and Latin Extended font files.

### Fixed

- Accessible chart data tables are no longer hidden by outer image roles, transaction pagination fits 320px screens, and merchant note labels remain visible without clipping the merchant name.
- Reduced mode no longer pauses CSS reveals on their invisible first frame.
- Renaming, pausing, or deleting a detected recurring pattern now survives the next analytics refresh.
- Realised capital losses stay out of spending on every endpoint and AI tool, including date-filtered category breakdowns, KPIs, wrapped, behavior, trends, cohorts, and insights.
- Date-range filters include the whole end day, `/spending-rule` honours excluded accounts and no longer fails on 29 February, and FY completeness and account close dates use IST.
- Reviewing one budget anomaly no longer hides other categories' alerts; monthly report savings reconcile and top categories are grouped by category.
- AI chat labels tool amounts as INR regardless of display currency, and a personal Bedrock bearer key is never returned to the browser.
- Cash-flow forecasts no longer compound one bonus month, XIRR reports steep losses and short holdings correctly, Age of Money dequeues outgoing transfers, EPF credits interest annually, NPS uses an equivalent monthly rate, and FY 2020-21 to 2022-23 new-regime tax uses the original 115BAC rules.
- Year in Review orders FY months from the FY start and divides daily averages by elapsed days; Comparison no longer opens with both periods equal; goal ETAs ignore the partial month; bill reminder dismissals are per cycle and per user.
- KPI grids use two columns on phones and the stale-analytics dismiss button is a 44px tap target.

## 2.24.1 - 2026-09-02

Security patch release: three high-severity Dependabot alerts closed across both workspaces, with no behavioral changes.

### Security

- **react-router 7.18.1 -> 7.18.3** (via `react-router-dom`) fixes an RSC-mode CSRF bypass that allowed action execution before the 400 response (Dependabot alert #110).
- **fast-uri 4.1.1 -> 4.1.4**. The 4.1.1 the existing 3.x override mapped to was itself vulnerable to host confusion via a backslash authority introducer, so the override now maps everything below 4.1.2 to 4.1.4 (alert #107).
- **cryptography 49.0.0 -> 50.0.1** closes a Bleichenbacher oracle in PKCS#7 EnvelopedData decryption exposed through distinguishable errors and timing (alert #109). `backend/requirements.txt` is regenerated from the lockfile, which also brings the boto3, fastapi, uvicorn, websockets, and wrapt pins the lockfile already carried.

### Changed

- CLAUDE.md records the toolchain traps that cost real debugging time (#235), and ImgBot re-optimized repository images (#234).

## 2.24.0 - 2026-08-09

A route-by-route frontend polish pass, plus the dashboard, import-history, prefetch, and RSU correctness work merged since 2.23.0.

### Added

- **Monthly income versus spending on Dashboard.** A grouped bar chart uses complete months only and explicitly names an excluded in-progress month.
- **Import history on Upload and Sync.** `GET /api/upload/history` and a responsive table show recent imports, timestamps, and processed/new/updated/already-present row counts.
- **Mobile table sorting.** Shared card-style tables retain sortable-column selection and ascending/descending controls when desktop headers are hidden.
- **RSU sell-to-cover reporting.** Vestings can record the net shares received alongside the gross taxable quantity, with both values shown at the same per-share price.

### Changed

- **Full frontend visual pass.** Every public and protected route was reviewed at desktop and phone widths. Shared contrast tokens, page labels, KPI cards, category rows, controls, chart tables, settings sections, merchant rows, GST summaries, tax copy, recurring summaries, and calendar layouts now use a more consistent responsive hierarchy.
- **Theme changes preserve page state.** Filters, tabs, searches, and forms remain mounted while chart containers alone redraw against the new resolved theme.
- **Bill Calendar keeps 44px day targets.** The month grid fits common phone widths and becomes horizontally scrollable only on narrower screens.
- **Cash Flow names the configured tax regime** on computed at-source TDS, and login prefetch now warms recurring, data-health, merchant, goals, and net-worth queries used by persistent navigation and common first visits.
- Updated supported frontend and backend dependencies while retaining the compatible TypeScript and React Router lines documented in #231.

### Fixed

- **RSU vest-date values now use vest-date FX.** Historical stock prices are converted with the exchange rate published for the vest date instead of today’s rate; failed historical lookups no longer silently substitute a current fallback.
- Theme toggles no longer reset route-local state, hidden accessible chart tables no longer widen layouts, and long recurring/bill summary values no longer truncate.
- Mobile transaction pagination scrolls the actual workspace container, custom budget dates receive and restore focus, and comparison selectors expose real labels.
- Future Year in Review heatmap dates are disabled and visually distinct; income rolling-average tooltips, category-row wrapping, merchant labels, and tax fallback contrast are corrected.

### Security

- Exchange-rate base currencies are restricted to three-letter codes before reaching upstream requests or log lines, preventing newline-based log injection.

## 2.23.0 - 2026-07-27

Two waves in one release: the workspace UI rework that had been sitting unreleased, and a correctness pass over money math, API contracts, and static analysis. Two on-screen figures intentionally change value, both called out below.

### Added

- **Workspace header** with the current page label, command-palette access, notifications, and a context-aware AI entry point.
- **Merchant Intelligence page (`/merchants`)**. Transaction notes are extracted into merchant labels with an ambiguity guard, so "Milk Shake - Apple" no longer books to the tech company. On a miss the whole normalized note is kept as a descriptor rather than its first word, which stopped both the silent drops and the over-merging of unrelated purchases into one label.
- **Data Health page (`/data-health`)**. Names the exact date the ledger ends and how many days are unimported, reports rows processed / inserted / already-present from the last import, and surfaces the placeholder-note, uncategorized, and future-dated counts that previously had no home. When the analytics rollups fall behind a committed import it says the displayed numbers are from the previous import and offers an in-place recompute.
- **`GET /api/calculations/income-facets`** returns every income category and subcategory bucket with its row count and total.
- **Income classification audit** in Settings. Income categories the ledger carries but no classification list claims are now listed with their transaction count and amount, each with a one-click bucket picker and a bulk "apply suggestions" action. Saved keys that match zero transactions (drifted spellings left behind after an import) are listed separately and can be removed.
- **From-scratch migration coverage** (`backend/tests/integration/test_migrations_from_scratch.py`) runs `alembic upgrade head` against an empty database, which is the case CI never exercised because production only ever does incremental upgrades.
- **IST ledger clock.** Date boundaries are resolved in Asia/Kolkata rather than the server's timezone, so a late-evening transaction no longer lands in the next day's totals.
- **Shared page recovery state** that keeps route context visible and lets users retry failed financial-data queries without reloading the application.
- **Authentication regression coverage** for provider loading/retry states, dialog focus management, and OAuth callback failures.
- **Stricter frontend accessibility checks** for labels, keyboard interaction, semantic tables, and accessible control names.
- **Product screenshots** for desktop and mobile landing-page presentation.

### Changed

- **Days of Buffering on the Dashboard now nets out debt.** It reads the split-aware liquid pool instead of a bare total across spendable accounts. A bare total has already lost the asset/liability split, so it could neither exclude parked deposits nor subtract what is owed. On the audit ledger the figure drops from 142 to 125 days, and the entire gap is liabilities the old path could not see: 57,812.58 of credit-card debt and 4,668.14 across four overdrawn wallets. The lower number is the correct one.
- **Savings targets are split by definition.** `/budgets` measures savings as the change in the investment perimeter; `/spending-analysis` measures income minus comparable spending. These answer two legitimately different questions and are roughly 2x apart on real data, yet both were scored against a single `savings_target_percent`, so one page could say "under target" while the other said "on track". Each definition now carries its own target. The two numerators are deliberate and are not reconciled.
- **Capital loss is separated from consumption**, so a fall in investment value no longer reads as spending.
- **Every sort has an explicit comparator.** Implicit lexicographic ordering silently mis-sorted numeric and date columns.
- **Complete workspace UI rework.** The shell, sidebar, mobile navigation, page headers, metric cards, controls, transaction filters, tables, charts, settings, home page, upload flow, and key planning pages now share one compact ledger-oriented visual system.
- **Responsive behavior** was tightened across desktop and phone layouts, including stable workspace dimensions, safe-area handling in the global header, mobile transaction presentation, and non-overlapping demo/chat/navigation surfaces.
- **Home and sign-in flow** now use the real dashboard screenshots, clearer calls to action, a retryable provider-loading state, keyboard focus trapping, Escape dismissal, and focus restoration.
- **Protected data pages** now distinguish loading, empty, and failed-query states instead of rendering failed requests as valid zero-value dashboards.
- **Dense routes and controls** were split into focused components, aligned to shared buttons, inputs, selects, responsive tables, and shorter reduced-motion-aware transitions.
- **Local API routing** now defaults to the same-origin Vite proxy. `VITE_API_BASE_URL` is required only for split production hosting.
- **Theme-aware global feedback** now follows the resolved Light or Dark theme.
- **Static-analysis baseline**: ruff reports zero findings across 25 rule families, mypy is clean over 156 source files, and SonarCloud reports zero open issues. The frontend lint tier was widened to `recommendedTypeChecked` over `src/**`, which is what catches `any` flowing through a value.
- **Documentation refresh** aligns the READMEs, handbook, route catalog, API reference, database guide, calculations, architecture, testing guide, deployment notes, migration notes, and diagrams with the 2.23.0 codebase.

### Fixed

- **Goal cards no longer crash on a goal with no start date.** `financial_goals.created_at` was created nullable and never altered, and the serializer derives both `start_date` and `created_at` from that one column, so any row written before the model-level default arrives as `null`. The TypeScript type declared it non-null, so the card passed `null` into `parseLocalDate`, which threw on `.slice` -- and a throw during render unmounts the whole card, not just the on-pace indicator. The wire type now admits null and the pace calculation is skipped when there is no timeline to measure against.
- **`alembic upgrade head` works against an empty database.** It previously failed with `ValueError: No such index: 'ix_fy_summaries_fiscal_year'`, dropping an index that the creating migration never created. Not reachable in production, where only incremental upgrades run.
- **Two features that returned HTTP 422.** FastAPI reads a field's source from the handler signature, so posting query parameters to handlers that declare a request body failed validation. Found by diffing the OpenAPI schema against the client; neither TypeScript nor CI could see it.
- **`Refunds & Cashbacks` key drift** zeroed 54,700 of offsetting credits because a plural spelling never matched the singular key.
- **Anomaly deviation percentages were 100x too small** -- the formatter applied no x100.
- **Essential-category defaults were dead code**, because `"[]"` is a truthy string and so the fallback never ran.
- **Cognitive complexity** reduced below the threshold in `spending_rule.py`, `fy_summaries.py`, `summaries.py`, `investmentUtils.ts`, and `CreditCardHealth.tsx`. The `CreditCardHealth` extraction was checked against 60,000 differential cases with zero divergence and 10 of 10 injected mutants caught; the `investmentUtils` split against 85,000 cases and 29 mutants.
- **A React component defined inside its parent** was hoisted to module scope, which stops the subtree unmounting on every render.
- `/home` now redirects to `/dashboard` instead of rendering the public landing page inside the authenticated workspace.
- Demo mode returns an empty AI tool registry without making a backend request.
- Sign-in provider failures remain distinguishable from a valid but unconfigured provider list.
- Financial query failures no longer appear as empty ledgers, zero balances, or missing recurring items.
- Mobile dashboard figures, table amounts, filters, and page actions no longer clip or overflow at 320px and 375px widths.
- Calendar cells, settings controls, RSU vesting entries, upload feedback, and comparison tabs stay readable and touch-friendly across phone, tablet, and pointer-coarse layouts.

### Security

- CI and migration jobs now require committed Python lock data, reject source and project builds during dependency installation, and run tools without an implicit dependency sync.
- GitHub Pages installs the frozen pnpm lockfile with dependency lifecycle scripts disabled.
- Patched development dependency alerts for `shell-quote`, `brace-expansion`, `fast-uri`, `sharp`, and `serialize-javascript`; root and frontend pnpm audits now report zero vulnerabilities.

### Developer experience

- **Agent git worktrees no longer break the local gates.** A worktree checked out under `frontend/` gave typescript-eslint two tsconfig roots, so the pre-commit hook failed repo-wide with zero real lint problems, and vitest counted the duplicated suite (147 files / 1741 tests against CI's 116 / 1383). `eslint.config.js` now ignores `.claude` and vitest excludes `**/.claude/**`.

## 2.22.0 - 2026-07-09

RSU vesting schedule overhaul: vested vs upcoming grouping with vest-date valuation.

### Added

- **Vested / Upcoming grouping in RSU Grants** (Settings > Income & Salary Structure). Each grant's vesting table is split into a dimmed "Vested" section (date <= today) and an "Upcoming" section, both sorted chronologically. Rows re-sort on date-input blur (not per keystroke, so rows don't jump under the cursor), and grants saved before this change are normalized on load. The summary line now splits into "Vested: N shares (value)" and "Upcoming: N shares (value)" instead of one combined total.
- **Vest-date price locking (`price_at_vest`).** Once a vesting date passes, the historical close for that date is fetched once and stored on the vesting, so realized RSU income stops drifting with the current stock price. Falls back to the grant's current price when the lookup fails (delisted ticker, upstream hiccup); editing a vested row's date clears the locked price so it re-fetches. Backend: `GET /api/stock-price/{symbol}?on_date=YYYY-MM-DD` returns the close on that date (or the nearest prior trading day within a week, covering weekends/market holidays) plus an `as_of` field; `RsuVesting` schema gains optional `price_at_vest`.
- **`lib/rsuVesting.ts`** -- single source of truth for `isVested` / `sortVestings` / `vestingPrice` / `splitRsuTotals`, shared by the settings UI, the multi-year tax projection (`projectionCalculator.ts`), and the TDS schedule (`tdsScheduleCalculator.ts`).

### Changed

- **Projection math treats vested rows as realized income**: `getRsuVestingsByFY` values them at the locked vest-date price and never applies the stock-appreciation assumption to them; upcoming vestings keep the projection behavior (current price grown by appreciation %/yr).
- **RSU grant state handlers extracted** from `SalaryStructureSection.tsx` into a `useRsuGrants` hook; per-grant vesting table extracted into `VestingTable.tsx`.

## 2.21.0 - 2026-07-07

Rule-based auto-categorization, transaction tags, and saved filter views (PR #203). Backend: categorization rules engine with retroactive apply, transaction tags, saved views CRUD. Frontend: tag chips + filtering on Transactions, saved-views menu, rules settings UI.

## 2.20.0 - 2026-07-04 -- was `Unreleased` (PR #202)

Comprehensive hardening wave landing on the branch: security tightening (BYOK key split + HKDF, token-version revocation, per-user rate limits, DB-level cascade), the anomaly-detection rewrite, the 50/30/20 Budget Rule page, and a design-system consistency pass that trims data past today across every historical chart and codifies the money-cell pattern.

### Added

- **50/30/20 Budget Rule page (`/budget`).** Replaces the old category-CRUD `/budgets`. Three side-by-side bucket cards (Needs / Wants / Savings) show target vs actual with a delta score; the breakdown table caps each bucket at top-10 categories with an "Other (N more)" rollup that expands inline. Period picker covers 1yr, 2yr, 5yr, All-time, Custom. Backend endpoint `POST /api/analytics/v2/spending-rule` classifies categories via word-boundary regex (fixes Education-not-in-Needs bug), unions user overrides with defaults (fixes first-override-drops-defaults bug), and relabels compound `Transfer: Bank: X -> Y: Z` rows to instrument names (Stocks, Mutual Funds, PPF, etc.).
- **`<Money>` primitive** (`frontend/src/components/ui/Money.tsx`). Codifies the canonical amount-cell rule set: `shrink-0 text-right tabular-nums whitespace-nowrap font-medium` plus `sm|md|lg|xl` width presets so flex parents can't compress the amount. Prevents the `₹12,91` truncation bug that occurred inside 33%-wide columns. Barrel-exported from `@/components/ui`; migrated `CategoryTable`, `CategoryBreakdown` (both top-level and subcategory rows).
- **`capEndDateAtToday()` and `capSeriesToToday<T>()`** in `frontend/src/lib/dateUtils.ts`. Historical time-series charts across the app no longer render a flat-zero tail past today. `capEndDateAtToday()` is applied inside `getAnalyticsDateRange()` so it cascades through `useAnalyticsTimeFilter`, `useDashboardMetrics`, and every analytics query hook without per-caller edits. `capSeriesToToday<T>(rows, key)` is generic on row shape with ISO-string comparison for use with in-memory month or day series. Projection pages (FIRE, tax-planning multi-year, retirement forecasting) intentionally build their own future ranges and are untouched.
- **Anomaly detection rewrite** (`core/analytics/anomalies.py`). Median + MAD (Median Absolute Deviation) with a rolling per-category baseline; robust to skew and single-transaction outliers.
- **DB-level `ON DELETE CASCADE`** on every `user_id` foreign key. Migration `20260704_1100_add_ondelete_cascade_user_fks.py` handles both PostgreSQL (`ALTER TABLE ... DROP CONSTRAINT / ADD CONSTRAINT`) and SQLite (`batch_alter_table(recreate="always", naming_convention=...)` with FK-name reflection to avoid the duplicate-FK trap that happens when `create_foreign_key` runs without an explicit `drop_constraint` in the same batch).

### Changed

- **`PageContainer` migration** for 5 pages (Anomaly, FIRE, Transactions, More, SubscriptionTracker). Hand-rolled `min-h-dvh p-4 md:p-6 lg:p-8` + `max-w-7xl mx-auto space-y-6` scaffolds replaced with the shared primitive.
- **Palette drift fixes.** `year-in-review/types.ts` heatmap band colors moved from slate-tinted rgba to `${rawColors.app.red}<alpha>` template literals (theme-aware). New `rawColors.onAccent` token replaces raw `'#fff'` in `PeriodSelectors`. `CommandPalette` inline shadow moved to the shared `var(--glass-shadow-ultra)` token + app-blue ambient glow.
- **Year-in-review Monthly Breakdown** now slices `MONTHS_SHORT` at today's month for the current calendar/fiscal year (no more empty bars for future months). FY wrap handled via `((nowMonth - (fyStart - 1) + 12) % 12) + 1`.
- **CI on Python 3.13 everywhere**, added job timeouts.

### Fixed

- Cascade migration created duplicate FKs on SQLite (drop + create in same batch = REPLACE, but pure `create_foreign_key` alone APPENDS).
- Frontend crash `Cannot read properties of undefined (reading 'start')` on the budget page in demo mode -- demo axios adapter's catch-all `/analytics/v2/*` returned the wrong shape for spending-rule.
- SonarCloud MAJOR bug: identical ternary branches in a `capForBar` variable that always resolved to `target`.
- Substring false positives in investment-account classifier (`'rd'` matched inside `"weird broker xyz"`); replaced exact-match with word-boundary regex.
- Failed main deploy on 2026-07-03 (transient GitHub Pages API error); rerun succeeded.

### Security

- **Split BYOK encryption key from JWT secret.** Uses HKDF for v2 keys with per-ciphertext salt; JWT rotation no longer invalidates stored API keys.
- **Server-side token revocation via `User.token_version`.** Logout and account reset increment the stored version; outstanding access and refresh tokens then fail verification server-side.
- **Per-authenticated-user rate limits** on `/api/upload` and `/api/ai/bedrock/chat` (was IP-keyed).

---

## 2.19.0 - 2026-06-28

A premium **light theme**, a world-class UI/UX elevation pass, and an information-architecture restructure. Adds a Light/Dark/System toggle and makes the entire app render flawlessly in both themes, driven by live design-system research (Radix Colors, Material 3, Apple HIG), a 14-lens UI audit, and a 5-lens IA audit. Frontend-only -- no API, schema, or backend changes. Dark theme is preserved byte-for-byte.

### Added

- **Overview page (`/overview`).** A single "whole picture" view composing existing data (income/spending/net-saved/savings-rate KPIs, budgets-at-risk, goals progress, top income vs spending) with deep links to each detail page. Sits beside Dashboard, which stays the configurable widget board.
- **Premium light theme + Light/Dark/System toggle.** A warm off-white palette (LAB surfaces, white cards lifting off the page, soft layered shadows, AA-contrast accents) controlled entirely from `index.css` tokens. Switch via the sidebar cycle button or Settings > Appearance; an anti-flash inline script applies the stored theme before first paint, and `System` follows the OS preference live. The public landing page gets its own toggle in the header.
- **Theme-aware, reactive charts.** 20 new `--chart-*` tokens (axis, grid, tooltip, reference-line, label colors) flip with the theme; Recharts/SVG colors resolve through `rawColors` at runtime (CSS `var()` does not work in SVG attributes). All derived palettes (`CHART_COLORS`, `metricColorConfig`, category colors) rebuild in place on toggle, and the routed view remounts on theme change so charts repaint without a reload.

### Changed

- **One source of truth for color.** Swept the remaining hardcoded color literals app-wide (`text-white`/`bg-white/N`/`bg-black/N`/gray-family/SVG hex/inline rgba) onto semantic tokens (`text-foreground`, `bg-surface-*`, `bg-[var(--overlay-N)]`, `border-[var(--hairline-N)]`), so a single token edit re-themes everything. White-on-solid-accent button text is the only intentional exception.
- **World-class UI/UX elevation (52 surgical improvements):** `tabular-nums` on currency/KPI/table numbers so digits stop shifting width; accessible names on chart primitives + icon-only controls (listbox/option roles on dropdowns, aria-valuenow on the FIRE slider); page headers now mirror the canonical sidebar labels (Expense Analysis, Cash Flow, Income Tax, Budget Manager, ...); 2-column KPI grids on phones with >=44px touch targets; standardized loading/empty states; shared press/hover micro-interactions (motion kept fully visible).
- **Information-architecture restructure** (from a 5-lens IA audit). Sidebar reordered to the money decision-flow and regrouped: Net Worth + Investments merged into one **Wealth** section, the dead-end single-item Transactions section folded into a new **Data** section (with Upload & Sync elevated out of the icon bar), and Tracking renamed **Commitments**. **Settings** regrouped from 11 flat sections into four task groups (Money Setup, Categories & Classification, Profile & Display, Advanced) with essential setup expanded first. Budget Defaults and Anomaly Detection now also appear as collapsible panels on their own feature pages (context-of-use), Income Tax and GST cross-link, the mobile More tab shows an alert badge (anomalies + over-budget), and the Dashboard shows an upload CTA when a new user has no data. New tokens `--color-on-accent` / `--color-on-warning` / `--modal-backdrop` name the white-on-accent / black-on-warning / dialog-scrim conventions; the mobile browser chrome (`theme-color`) now tracks the active theme.

### Fixed

- **Theme switch desync** -- a short-lived `theme-transition` class gives every element one uniform 300ms cross-fade so the whole UI flips in sync instead of parts easing at different speeds.
- Removed an erroneous static `aria-current="page"` on sidebar/mobile-tab `NavLink`s (every item announced as the current page; `NavLink` already sets it on the active link) and a dead `getStoredTheme()` helper.

---

## 2.18.0 - 2026-06-28

A frontend-only UX, visualization, and mobile pass. No API, schema, or backend changes -- every commit touches `frontend/src`. Driven by a senior-design audit of the shared UI primitives and every page, a data-visualization-fit audit (~295 elements scored), and a live mobile audit at 390px. Desktop layout is preserved throughout (mobile changes are gated behind `sm:`/`lg:`/`useIsMobile`).

### Added

- **Shared UI primitives** so pages stop hand-rolling equivalents: `PageContainer` (one centered page scaffold), `Spinner` (one `role="status"` spinner), `ProgressBar` (fill + target tick + bullet-graph threshold bands), and chart helpers `referenceLine()` (peak/avg/target/goal/zero variants) + `currencyTooltipFormatter()`. `MetricCard` gained an opt-in `hero` size; `StandardBarChart` gained `onBarClick` + `ariaLabel`; `DataTable` gained a sticky/scrollable header.
- **Expected-return benchmark line on the SIP Growth Path chart.** A third series shows what the portfolio *should* be worth each historical month if every contribution had compounded at the assumed return from its own investment date -- so the actual-value line reads as ahead of or behind target. Plus a "Today" reference line marking the history/projection boundary. Covered by unit tests.
- **Informative context across pages, reusing data already on the page** (no new fetches): Net Worth KPIs get month-over-month deltas + trend sparklines + leverage/account-count context; Income shows primary-source share and a growth sparkline; savings-rate / utilization / recurring-coverage bare percentages become progress bars with a target tick; Comparison quick-stats get signed deltas; Bill Calendar leads with a due-date countdown; Anomalies show an expected-vs-actual mini-comparison; Budgets show a per-row month-end pace projection.
- **`DataTable` mobile card-stack mode** (`mobileCards`) -- below 640px, wide tables render as stacked label/value cards (a `mobilePrimary` column becomes the card title) instead of horizontally scrolling.

### Changed

- **Charts reshaped to fit their data** (data-viz-fit audit): the Income/Expense Flow Sankey now caps to the top 8 per side with an explicit "Other (n)" node so flows reconcile with the KPI totals (was a silent top-10 truncation); the Budget utilization radar and Year-in-Review day-of-week radar became sorted/grouped bars; the FIRE variant tiles became one shared-axis bar; the Returns monthly net became a single signed bar; Top Merchants donut became a ranked list with inline bars; the Spending multi-category / subcategory lines default to per-period (not cumulative) and cap to a top-N + "Other"; the SIP NPS allocation became a 100% stacked bar.
- **App-wide UX consistency**: every chart got an accessible name; hand-rolled spinners became the shared `Spinner`, bare empties became `EmptyState`, and `'...'`/text loaders became shaped skeletons; flat hand-rolled tables (GST, investment accounts, comparison categories) migrated to the sortable `DataTable`; the time-range brush slider got a rounded grip with grip-lines.
- **Mobile-first polish on every data page**: KPI/stat grids show 2-up on phones (were single-column), oversized hero numbers truncate instead of clipping, chart heights shrink on small screens, toggle/legend/pill rows wrap, and interactive controls (toggles, FY navigators, pagination, icon buttons, form inputs) meet the 44px touch-target minimum.

### Fixed

- **Wide data tables horizontal-scrolled on phones** (Tax slab table reached ~677px). They now card-stack or drop low-priority columns on mobile; the pivoted multi-year projection table keeps its label column pinned while FY columns scroll.
- **Dashboard expense pie/legend colors could mismatch** -- palette colors were assigned by pre-sort index. Colors are now assigned after the value sort.
- **Dashboard income/expense legends ran past the 8-slice pie** -- capped to 7 + a "+N more" row so the visible rows reconcile with the total.
- A pre-existing `QuickInsights.biggest_expense` crash in demo mode (missing optional chaining), surfaced during the live verification pass.

---

## 2.17.0 - 2026-06-27

A performance + data-architecture pass: analytics pages that pulled the entire transaction ledger into the browser now read server-side aggregations, and the `transactions` index set was rationalised. No breaking API changes; all numbers verified against the real database with independent SQL oracles.

### Added

- **`cohort_spending` rollup table** (migration `cohort_spending_2026`) -- average expense per temporal cohort (day-of-week / day-of-month / month-of-year) with occurrence-correct divisors, populated on every upload refresh. Powers the "Spending Patterns" widget server-side.
- **New read endpoints** under `/api/calculations` and `/api/transactions` that move client-side computation to the backend: `transactions/facets` (dropdowns + type counts), `quick-insights` (Dashboard band: cashback, median/avg/biggest expense, weekend split, peak weekday, transfers), `data-date-range` (time-filter nav bounds), `income-analysis` (income totals/trend/cashback), `category-monthly-history` (sparkline), `category-daily-series` (time-series charts), plus `/api/analytics/v2/cohort-spending`. See [docs/API.md](docs/API.md).

### Changed

- **Analytics pages no longer ship the full ledger to the browser.** Migrated TransactionsPage, PeriodComparison, CohortSpendingAnalysis, QuickInsights, FIRECalculator, CategoryBreakdown, MultiCategoryTimeAnalysis, EnhancedSubcategoryAnalysis, IncomeAnalysis, RecurringTransactions, BudgetTracker, and CommandPalette onto server-side aggregations / existing rollups. Computations that bundle user preferences or heavy projection math (tax, GST config table, net-worth/XIRR projections, financial-health CFP score) intentionally stay client-side to avoid duplicating preference logic.
- **`transactions` index set rationalised** (migration `optimize_tx_indexes_2026`). Replaced the drifted historical indexes with six canonical user-scoped, equality-first composites `(user_id, date)`, `(user_id, type, date)`, `(user_id, category)`, `(user_id, account)`, `(user_id, from_account)`, `(user_id, to_account)`. Non-user-scoped indexes (`date`, `type`, `category`, ...) were dropped -- the planner never used them for this app's user-scoped queries, so they only taxed writes. Verified with `EXPLAIN QUERY PLAN`. The migration is idempotent (inspects live indexes) so it converges despite prior drift.
- **`/api/calculations/monthly-aggregation`** now also returns `income_count` / `expense_count` per month (used by PeriodComparison instead of a separate full-ledger fetch).

### Fixed

- **Dashboard / analytics cards showed ₹0 or "NaN days"** when the running dev backend was stale (started before the new routes existed). The code and migrations were correct; restarting the backend with `--reload` loads the new endpoints. Net Cashback (₹41,980), Median Transaction, Weekend split, Peak Spending Day, and Days of Buffering now populate from verified server-side values.

---

## 2.16.0 - 2026-06-27

A correctness + reliability pass driven by a multi-agent audit (bug-finder matrix with adversarial verification, an assumption-research sweep cross-checked against official sources, and a 100-category scorecard). One new user-facing setting; the rest are fixes. No breaking API changes.

### Added

- **"Salary recorded net of TDS" toggle** (Settings -> Financial). Controls how Tax Planning interprets recorded salary: on (default) treats it as net of TDS and backs out the implied gross to show tax already deducted; off treats the recorded amount as the taxable gross and computes tax on it directly. New ``salary_is_net_of_tds`` column on ``user_preferences`` (migration ``salary_tds_2026``).
- **Expense Trend chart on the Spending Analysis page** -- monthly spending with a 3-month rolling average and a peak reference line, mirroring the Income Trend chart on Income Analysis (red/expense semantics, timezone-safe month labels).
- **EPF withdrawal taxability is now a user setting** (Settings -> Financial). EPF inflows are treated as exempt by default (Section 10(12) -- withdrawals after 5 years of continuous service are tax-free); a toggle plus a 0-100% field lets you count a chosen fraction as taxable. Replaces a hardcoded 50% taxable assumption that had no basis in EPF rules. New ``epf_withdrawal_taxable`` / ``epf_taxable_percent`` columns on ``user_preferences`` (migration ``a1b2c3d4e5f6``).

### Fixed

- **Chart correctness sweep (28 issues from a vigorous audit of every chart against empty/single/all-zero/negative/NaN/many-category/cross-FY data).** Highlights: the cash-flow confidence cone no longer paints a solid black wedge below zero when the forecast dips negative (rebuilt as stacked baseline+range areas); single-data-point line/area charts now show a visible dot instead of rendering blank (net worth, returns, trends, income); the effective-tax-rate "You" marker no longer vanishes (numeric X scale); many-category pies cap at 8 slices with an "Other" bucket so the palette never repeats on adjacent wedges; the seasonal and day-of-month cohort averages divide by real per-bucket occurrences (months that actually contain that day / years that contain that month) instead of a global span count; several tooltip/brush/axis date labels and the recurring next-date were shifted a day for non-UTC users; the GST-by-slab pie drops the empty 0% (exempt) slice; the Pareto chart actually two-tones its vital-few vs trivial-many bars; the budget radar caps at 8 spokes; the all-deficit savings "peak" line shows the true (negative) maximum; the net-worth stacked view is disabled (with a hint) when the range dips negative; a dead spending-donut drill-down was removed.
- **GST 2.0 (effective 2025-09-22), verified against the GST Council / incometax.gov.in.** The GST estimator used the retired 12% and 28% slabs. It is now date-aware: transactions before the cutover use the legacy table ``[0,3,5,12,18,28]``; on/after use ``[0,3,5,18,40]`` (12% and 28% removed, 40% luxury/sin de-merit added; insurance -> Nil, electricity/water -> exempt, everyday apparel/household -> 5%, electronics 28% -> 18%). The slab breakdown buckets on the union of both sets so a fiscal year spanning the cutover keeps each rate on its own slab.
- **Old-regime Section 87A is a hard cliff again.** Marginal relief was being applied to the old regime; relief exists only in the new regime, so the old regime under-taxed the band just above the 5L ceiling.
- **Tax-planning gross-from-net uses the selected regime's slabs** (was always new-regime), so the displayed gross/net/tax are mutually consistent.
- **Tax-planning no longer falls back to gross inflow** when nothing is classified as taxable -- that counted transfers/refunds/investment returns as salary. It now uses only classified taxable income (0 when nothing is classified).
- **EPF maturity projection** uses the statutory employer split (12% of basic minus the EPS diversion, 8.33% of the capped 15,000 wage) instead of mirroring the employee %, and the current 8.25% rate (was a stale 8.15%).
- **Cross-user data leak on shared browsers.** Budget, account-classification, investment-account, and preferences stores persisted to ``localStorage`` under static keys and survived logout. They are now reset on logout and account deletion.
- **Finance math:** returns-analysis monthly ROI was an annual CAGR divided by 12 (linear) -> proper monthly compounding; the period comparison's "avg daily spend" divided by a hardcoded 30 regardless of range -> actual day count; the cohort day-of-week average divided by a broken week count -> real weekday occurrences; the Sankey empty state gated on a node count that was never zero -> gates on links.
- **Backend:** amounts now convert straight to ``Decimal`` (HALF_UP) instead of rounding through ``float`` (2.675 -> 2.68); the ``list_accounts`` AI tool replaced 5N+1 queries with grouped aggregates and gained the result cap every other list tool has; three anomaly queries now honour the excluded-accounts filter; a duplicate ``NormalizationError`` class meant row-level normalization errors escaped every handler as a raw 500; transfers between two investment accounts no longer double-count as inflow; the transactions end-date filter is inclusive of the whole day; ``create_goal`` returns 400 (not 500) on a malformed target date.
- **A timezone bug class across many sites** (UTC-parsed dates read with local getters, shifting the calendar day for non-UTC users): year-in-review heatmap, recent transactions, bill calendar, ISO-week bucketing, axis tick labels, financial-health month boundary, the category sparkline, and a month-label sweep. Consolidated shared date helpers (``parseLocalDate``, ``weekdayOf``, ``toLocalDateKey``, ``formatMonthKey``, ``formatDate``).
- **Mutations no longer fail silently** -- a global error toast surfaces any failed save/delete that doesn't handle its own error (AI mode toggle, goals, recurring-item CRUD).
- **Exchange-rate fallback table** corrected to verified ECB values for its stated date (was ~13% stale).

### Changed

- CORS now uses the computed origin allowlist instead of a wildcard; logging honours ``LEDGER_SYNC_LOG_LEVEL`` instead of a hardcoded level.
- ``min-h-screen`` -> ``min-h-dvh`` across all page roots so layout tracks the mobile dynamic viewport.
- Removed the ``date-fns`` dependency (replaced its three usages with a timezone-safe ``formatDate`` helper).

### Security

- Pinned ``msgpack>=1.2.1`` to clear GHSA-6v7p-g79w-8964 (transitive, dev-only via cachecontrol).

### Accessibility

- ``aria-label`` on icon-only buttons across 13 components; the chat panel is a labelled dialog with a live region and alert role; a ``prefers-reduced-motion`` block flattens CSS animations under the OS reduce flag (motion stays visible by default).

### Tests

- Added regression coverage for the old-regime 87A cliff, the EPF statutory split, EPF taxable-fraction, GST 2.0 date-aware rates, the timezone date helpers, and the normalizer's Decimal rounding. 255 frontend + 215 backend tests pass; ruff, mypy, tsc, and eslint clean.

---

## 2.15.0 - 2026-06-07

Dependency and pipeline maintenance. Took every held-back major version to latest behind a restored CI gate, unblocked the frontend deploy, and cleared two security advisories. No feature changes; no API contract breaks.

### Changed

- **All held major dependencies bumped to latest, one verified bump per commit.** Frontend: TypeScript 5.9 -> 6.0 (removed deprecated ``baseUrl`` from ``tsconfig.app.json``; ``@/*`` resolves via ``moduleResolution: bundler``), Vite 7 -> 8 (Rolldown + Oxc engine; ``manualChunks`` object form converted to function form; build ~17s -> ~0.6s), ``@vitejs/plugin-react`` 5 -> 6, ``lucide-react`` 0.577 -> 1.17, ``jsdom`` 28 -> 29. Backend: ``cryptography`` 46 -> 48 (AES-256-GCM key-encryption round-trip re-verified), ``mypy`` 1.19 -> 2.1 (``--strict-bytes`` / ``--local-partial-types`` now default-on; zero new errors across 123 files), ``rich`` 14 -> 15, ``typer`` 0.24 -> 0.26. ``vite-plugin-pwa`` confirmed generating the service worker under Rolldown.
- **CI re-enabled as a real gate.** ``ci.yml`` was reduced to security-scan only; broken TypeScript or failing tests could pass. Restored a frontend job (lint -> ``tsc`` build -> vitest via the shared workflow) and an inline backend job (ruff + mypy + pytest, no error-swallowing), both on ``pull_request``. Pinned pnpm via ``packageManager`` and third-party actions to commit SHAs.

### Fixed

- **GitHub Pages deploy unblocked.** ``pnpm install --frozen-lockfile`` failed with ``ERR_PNPM_LOCKFILE_CONFIG_MISMATCH`` because a vite security bump changed ``pnpm.overrides`` without regenerating the lockfile. Regenerated; pinned pnpm so local, deploy, and CI resolve identically.
- **Recharts 3 + react-hooks 7 fallout.** 23 ``Tooltip`` formatter type errors (``value`` is now ``ValueType``) and 4 ``set-state-in-effect`` lint errors from the earlier major bump, which the security-only CI never caught but the deploy build did.
- **Bedrock client timeouts bounded** (Release It!). ``boto3.client('bedrock-runtime')`` inherited botocore's 60s/60s defaults; on Vercel's 10s ceiling a slow call hung the function. Set ``connect_timeout=3, read_timeout=8, max_attempts=1``.
- **Misplaced ``import pytest``** below the module docstring (F404 + E402).

### Security

- **``serialize-javascript`` pinned to >=7.0.5** via ``pnpm.overrides`` -- cleared two Dependabot alerts (1 high RCE, 1 medium DoS) on a transitive build-time dependency.

### Refactored

- Extracted ``MS_PER_DAY`` / ``MS_PER_YEAR`` / ``MONTHS_PER_YEAR`` and EPF statutory constants, replacing inlined literals (Clean Code G25/G5). ``getAnalyticsDateRange`` takes a params object (F1). Backend ``apply_excluded_accounts_filter`` helper removes a filter clause triplicated across three files (G5). Split ``ReturnsAnalysisPage`` (537 -> 391) and ``SpendingAnalysisPage`` (456 -> 260) into hook + utils per the repo convention (APOSD). Named ``NotificationCenter`` thresholds; deduped ``analyticsV2`` response-unwrap via ``getWrapped<T>``.

### Tests

- Backend 135 pass (ruff + mypy clean under the new majors); frontend 177 pass (lint + ``tsc`` + build clean under Vite 8 / TS 6).

---

## 2.14.0 - 2026-05-17

Calculation correctness audit batch -- three backend bugs caught while reviewing every aggregation path. No frontend changes; no API contract breaks.

### Fixed

- **Net Worth API: cumulative series now seeded with pre-window opening balance.** ``GET /api/calculations/daily-net-worth?start_date=...`` previously reset the cumulative ``net_worth`` series to 0 on day one of the window, ignoring all transactions before ``start_date``. A user filtering "Last 6 months" saw their chart begin at zero instead of their actual cumulative cashflow at that point. When ``start_date`` is set we now compute ``opening_balance = SUM(income) - SUM(expense)`` for transactions strictly before it and seed the cumulative series with that value. Transfers excluded (matches existing model). The response also includes ``opening_balance`` so frontends can render a starting-balance annotation.
- **Time-range filters now anchor on ``now()``, not the user's most recent transaction.** ``THIS_MONTH``, ``LAST_3_MONTHS``, etc. used to anchor on the latest transaction date so a user with stale data saw old months under "This Month" labels. Anchored on ``datetime.now(UTC)`` now: "This Month" = current calendar month, "Last 3 Months" = first day of (now - 2 months) through now (calendar-aligned, no partial-month tails). Stale data legitimately yields empty filtered results -- correct behaviour. Two parallel implementations both fixed: ``api/analytics_helpers._get_time_range_dates`` (DB query) and ``core/time_filter.TimeFilter`` (in-memory list).
- **Recurring detection: closed gaps between frequency bands.** The 2.10.0 release made ``_FREQ_BANDS`` "contiguous" via integer endpoints ``[(4, 10), (11, 19), (20, 49), ...]`` checked with ``lo <= avg_diff <= hi``. But ``avg_diff`` is a float (mean of integer day-gaps), so half-integer values 10.5, 19.5, 49.5, 79.5, 129.5, 269.5 fell through every band -> ``frequency = None`` -> recurring patterns silently dropped. Real-world example: a salary every 7 days plus a monthly top-up landing some weeks averaged 10.5 days. Bands now redefined as half-open ``[lo, next_lo)`` so they actually tile the real number line. ``_FREQ_MAX_DAYS = 400`` keeps the upper bound on yearly cadence.

### Tests

- Backend 116 -> 135 (+22 across the three fixes: 3 for daily-net-worth opening balance, 8 for time-range now-anchor, 11 for recurring band gaps).

### Documentation

- ``docs/CALCULATIONS.md`` recurring-detection algorithm section updated to match the actual current implementation: 7 half-open frequency bands (was a stale 6-band closed-interval description from a much older version), correct confidence formula (``max(0, 100 - std*penalty)`` with per-band penalties), note on why bands are half-open.

---

## 2.13.0 - 2026-05-17

UX consolidation pass (#160). Theme: make every "high-level component" interactive in a way that flows the user through the app -- donut click navigates with a category param, destination page filters end-to-end, treemap auto-drills, subcategory analysis preselects the right category. 14 commits.

### Fixed

- **Dashboard donut click routed to non-existent paths.** Click-to-navigate on income/expense donuts used hardcoded strings instead of ``ROUTES`` constants. Routes 404-ed silently when the canonical route changed. Now uses ``ROUTES.INCOME_ANALYSIS`` / ``ROUTES.SPENDING_ANALYSIS``.
- **Donut center text was clipping behind the inner ring.** Long currency strings (``Rs 57,27,353``) overflowed past ``innerRadius``. Switched to ``formatCurrencyShort`` and added an adaptive font size that shrinks for longer strings (22px -> 13px across 6-12 chars). Visual polish: ``innerRadius 60%``, ``outerRadius 85%``, ``paddingAngle 3``, ``cornerRadius 4``.
- **Donut whole-Pie click selected every slice at once.** ``onClick`` was attached to the ``<Pie>`` element and the cursor was ``pointer`` everywhere including the gaps between slices. Each ``<Cell>`` now owns its own ``onClick`` / ``onMouseEnter`` / ``onMouseLeave``. Hover state: active slice brightens 18%, others fade to 40% opacity. Cursor is ``default`` over gaps.

### Added

- **Click-through journey on the dashboard donuts.** Clicking a slice (e.g. "Food & Dining" on the Expense donut) navigates to ``/spending?category=Food%20%26%20Dining``. The destination page reads the URL parameter and applies the filter end-to-end:
  - New ``FilterBanner`` component appears at the top with a "Clear" button
  - MetricCards (Total Spending, Monthly Avg, Top Category, Categories) reflect only the filtered category
  - Pareto Analysis bars + cumulative line filtered
  - Top Merchants section narrowed to merchants in that category
  - Expense Breakdown (treemap) auto-expands to show subcategories of the filtered category
  - Subcategory Deep-Dive opens with the filtered category preselected
  - Same flow on Income Analysis side (``?category=Salary`` etc.)
- **MetricCard becomes opt-in clickable.** New ``href`` and ``onClick`` props turn the card into a ``Link`` or ``button`` with a subtle ``y:-2`` hover lift, stronger border, and a top-right ``ArrowUpRight`` indicator that fades in on hover. Default behavior unchanged when neither prop is set, so all 37 existing callsites are zero-regression.
- **Pareto Analysis chart on Spending Analysis.** Bars sorted by category total, cumulative-percentage line overlaid, 80% reference line marking the Pareto threshold. Helps users see the typical "20% of categories cause 80% of spending" pattern at a glance.
- **Brush slider on more charts.** Drag-to-zoom across the timeline. New shared ``BRUSH_DEFAULTS`` (height 32, traveller width 10, app-blue stroke) used by Net Worth Trend, Investment Analytics Growth Over Time, and Returns Analysis Monthly P&L combo. Default window is the most-recent third so the chart reads at full fidelity on first paint.
- **Spending Patterns insight strip.** Below the Cohort Spending bar chart on Spending Analysis. Two pills: "Peak Day/Date/Month" (shows the bucket name + percent above average) and "Quietest". Removes the need to hover to find the takeaway.
- **Day of Week chart on Year in Review goes radar.** Replaces the flat side-by-side bar chart. Spending and earning overlay each other on a 7-spoke polar grid so weekend-vs-weekday patterns pop visually. Two insight pills below: biggest spending day (with amount) and weekend-vs-weekday delta percent.
- **Year in Review Monthly Breakdown gains a Net cash-flow line.** ``BarChart`` -> ``ComposedChart`` with a dashed blue line overlaying the Spending and Earning bars. Saving months show as peaks; overspending months as troughs. Subtitle clarifies what the chart shows.
- **Auto-granularity on time-series category charts.** ``MultiCategoryTimeAnalysis`` and ``EnhancedSubcategoryAnalysis`` were always daily granularity, which is unreadable past ~3 months. Now auto-buckets day -> week -> month based on the actual data span (90-day, 2-year thresholds). New Auto/Daily/Weekly/Monthly selector lets users override. Header shows the active bucket. Shared helpers (``bucketDate``, ``formatBucketLabel``, ``pickGranularity``) live in ``lib/chartPeriodUtils.ts``.

### Changed

- **Comparison Spending Distribution butterfly chart polish.** Capacity 10 -> 15 categories. Inline value labels on both ends (left for periodA, right for periodB) so users see amounts without hovering. Winner-side opacity boost: the bigger-spend side of each row gets full opacity (0.95), the loser dims to 45% so the eye lands on the larger spender.
- **Holdings Map (Returns Analysis) replaced with horizontal bar chart.** The 2D scatter (transactions vs current value) had weak insight; ranked horizontal bars make winners obvious. Top 12 with overflow footer; top-holding callout in the subtitle.
- **Asset Allocation chart (Investment Analytics) migrated to ``StandardPieChart``.** Gains the same per-slice hover dimming and centre-value polish as the dashboard donuts. Centre shows total invested; header shows asset-class count.

### Removed

- **Monthly Net Worth Change waterfall chart on /net-worth.** Added clutter without unique insight -- the Net Worth Trend chart immediately above already shows month-over-month direction. The chart, the unused ``monthlyChanges`` memo, and ``MonthlyChangesChart.tsx`` are gone.

### Refactored

- **Two ``SummaryCard`` duplicates merged into ``shared/SummaryCard``.** ``bill-calendar/components/SummaryCard.tsx`` and ``subscription-tracker/components/SummaryCard.tsx`` were 95% identical. Single shared component now consumed by both pages.
- **``CategorySparkline`` merged into ``Sparkline`` as a compact variant.** ``CategorySparkline.tsx`` was a thin wrapper that dropped axes and added compact dimensions. Now a ``compact`` prop on the canonical ``Sparkline`` component.

### Tests

- Frontend: 177 vitest tests pass throughout (no regressions).

---

## 2.12.0 - 2026-05-17

Audit batch 2 (#159). Ten small UX/a11y/perf fixes consolidated into one PR.

### Added

- **Dashed reference lines for upcoming Net Worth milestones.** 1L, 5L, 10L, 25L, 50L, 1Cr etc. drawn below the Net Worth trend so users see "I'll cross 1Cr around month X" at a glance. Recharts auto-clips outside the y-axis range so all milestones can be rendered without filtering.
- **Upload CTA on empty states.** Comparison page and Dashboard page empty states now have a clear "Upload Data" action (``EmptyState`` already supported ``actionLabel``/``actionHref``; two visible callsites just hadn't wired them).
- **12-month trend sparkline per row in CategoryBreakdown.** Pure SVG, no chart-lib overhead. Each category row shows its 12-month trajectory inline.
- **Drag-to-zoom Brush below Net Worth chart.** Pre-zoomed to the most-recent third of data on first render. ``StandardAreaChart`` gains an opt-in ``showBrush`` prop for future migrations.
- **Holdings scatter on Returns Analysis.** Activity vs value plot. Pragmatic scope; rigorous CAGR scatter deferred. (Note: subsequently replaced by horizontal bar chart in 2.13.0 -- the scatter axis was confusing in practice.)
- **``LazySection`` wrapper using IntersectionObserver.** Forward-looking helper for below-the-fold lazy mounting. Deferred consumers will land in future PRs.
- **``ChartContainer`` ``ariaLabel`` prop.** Wraps in ``role='img'`` so screen readers get a single descriptive summary instead of walking every SVG path. Improves a11y for analytics pages dramatically.
- **``StaleDataBadge`` component + first consumer in ``InstrumentProjections``.** Tiny amber pill that lights up when a hook lands on compiled-in fallback data (e.g. ``/api/rates/instruments`` is unreachable and the EPF rate comes from the bundled JSON). Users now know when they're seeing fallback values.

### Changed

- **Comparison page: stacked bars collapsed into overlaid bars.** Two-bars-per-metric -> one overlaid bar (faded ghost layer for period A, solid layer for B on top). Reads at a glance instead of forcing the eye to compare two adjacent rectangles.

### Infrastructure

- **GitHub Actions cron** pinging ``/health`` every 30 minutes to keep the Neon free-tier branch unarchived. Eliminates the 20s cold-start the first user of the day was hitting.

---

## 2.11.0 - 2026-05-16

Cleanup + refactor sweep across the codebase. Twelve focused PRs landed in one day.

### Fixed

- **``excluded_accounts`` preference now applied consistently.** Three transaction-query code paths (analytics, calculations, ai_tools) were missing the ``excluded_accounts`` filter so a hidden account could leak into Sankey flows, monthly aggregates, and AI tool responses. Centralized via a shared ``apply_excluded_accounts`` helper on ``build_transaction_query``.

### Added

- **Net Worth projection: 1-stddev confidence band** (#149). Replaced the single dashed projection line (which gave a false impression of certainty) with a band that widens as ``sqrt(time)`` under a geometric Brownian motion model. New ``computeMonthlyGrowthStats`` returns ``{rate, logSigma}`` based on log-return variance over a configurable lookback. New ``projectNetWorthCompoundBand`` paints the band as a Recharts ``<Area>`` with ``[lower, upper]`` tuple values (Recharts paints range areas natively when given a tuple). Median dashed line still renders on top. 7 new unit tests for the math.

### Changed

- **Frontend file-size discipline: every file under 500 LOC** (#141). Big ones split into focused modules. No behaviour change; just easier code review and faster IDE navigation.
- **``SEMANTIC_COLORS`` is canonical for income/expense/savings** (#146). All previously-hardcoded green/red/blue references in analytics components route through the design tokens.
- **Status semantic palette wired as Tailwind tokens** (#147). ``bg-app-green``, ``text-app-red``, etc. resolved via the design tokens instead of arbitrary hex strings.
- **Expense category colors centralized** (#145). ``EXPENSE_CATEGORY_COLORS`` is the single source; consumed by treemaps, donuts, and bar charts.
- **KPI typography standardized** (#148). MetricCard, KpiCard, StatCard, and SummaryCard all use the same type-scale tokens for value, label, and subtitle.

### Removed

- **In-memory token blacklist** (#139). Was a no-op on Vercel multi-worker deploys (each worker has its own dict, so a "logged-out" token is still valid on every other worker). Logout now just relies on the frontend dropping the token. Real token revocation needs a shared store (Redis) and is a future-PR if SSO ever requires it.

### Documentation

- **README overhaul + version sync** (#142). README badges, examples, and architecture sections reorganized; versions synced to the prevailing 2.10.0 across CHANGELOG, package.json, and pyproject.toml.
- **Post-2.10.0 cleanup pass** (#144). Doc drift fixes uncovered while reading the 2.10.0 audit notes.

### Internal / tooling (no user impact)

- ``indian-finance-expert`` skill converted to CSV-backed lookups (#143).
- 8 project-level Claude skills + ``.claude`` config committed (#137).
- Codebase-atlas skills + task-skills consolidation (#139, mixed with the token-blacklist removal).

---

## 2.10.0 - 2026-05-13

Hardcoded-values audit. Fixes silently-wrong defaults, reduces classifier brittleness, and centralizes policy constants that were scattered across the codebase. Two full rescan passes; every finding either fixed, deliberately skipped with reason, or tracked for a follow-up PR that needs a schema migration.

### Fixed

- **Convenience spending bug: `calculate_convenience_spending()` was returning 0 for every user.** The lowercase-token list (`{shopping, dining, food, ...}`) was compared via exact equality against normalized category names (`"Food & Dining"`, `"Entertainment & Recreations"`), so it never matched. The "Significant Convenience Spending" insight has never fired in production. Switched to substring match; dropped `food` (over-matched essentials), added `recreation`, `leisure`, `travel`, `subscription`. `core/calculator.py`.
- **Recurring detection dead zones.** Frequency bands had gaps at 19d, 46-49d, 76-79d, 111-149d, 211-339d -- any subscription with those cycles was silently invisible to the detector. Bands are now contiguous (each starts at previous_end + 1): WEEKLY 4-10, BIWEEKLY 11-19, MONTHLY 20-49, BIMONTHLY 50-79, QUARTERLY 80-129, SEMIANNUAL 130-269, YEARLY 270-400. `core/analytics/recurring.py`.
- **Merchant regex missed most real UPI narrations.** `^Swiggy` only matched when the merchant name was the first token; `"UPI/Swiggy/ref123"` and `"Payment to Zomato"` never hit the merchant list. Switched to `\b(...)\b` with `pattern.search()`. `core/analytics/merchants.py`.
- **Bank-name normalizer covered only 5 banks with case-sensitive exact match.** `"SBI Bank"` (correctly cased) didn't normalize because the lookup key was lowercase-only. Rewritten as case-insensitive word-boundary regex covering 20+ banks (SBI, HDFC, ICICI, Axis, Kotak, Yes, IDFC First, IndusInd, PNB, BOB, BOI, Canara, Union, Federal, RBL, IDBI, Citi, HSBC, Standard Chartered, DBS, AU Small Finance). Longest-match-first so `"IDFC First"` beats `"IDFC"`. Does not match `"axis"` inside `"taxis"`. `ingest/normalizer.py`.
- **Default investment-account mappings leaked the maintainer's personal account names** (`"Grow Stocks"`, `"IND money"`, `"RSUs"`, `"FD/Bonds"`) into every install. Shipped empty -- users configure their own mappings via Settings > Account Classifications. `core/_analytics_helpers.py`.
- **Frontend account-type classifier was substring-matching without case folding and collision-prone.** `"DEMAT"` (uppercase) didn't match, `"ICICI Investment"` matched both `invest` and `bank` with undefined ordering. Rewritten with priority-ordered rules (credit_card → investment → loan → deposit), word-boundary regex, case-insensitive. New `credit_card` type distinguishes `"HDFC CC"` → credit_card from `"HDFC Bank"` → deposit from `"HDFC Stocks"` → investment. `constants/accountTypes.ts` with 41 new unit tests.
- **Fallback exchange rates were stale by >1 month.** Refreshed all 14 currency pairs to 2026-05-13 values. Response now includes `fallback_as_of` so the frontend can warn users when they're seeing fallback data. `api/exchange_rates.py`.
- **EPF default rate was ~3 years out of date** (8.15% labelled "FY 2023-24" in a 2026 codebase). Bumped to 8.25% (FY 2024-25 notification) and sourced from the new backend config.

### Added

- **`/api/rates/instruments` endpoint** serving EPF/PPF/NPS rates from `backend/src/ledger_sync/config/instrument_rates.json` with `effective_from`, `effective_until`, `source_url`, and free-text `notes` per instrument. There is no reliable public JSON API for Indian EPF/PPF rates (EPFO publishes via PDF notification yearly, Ministry of Finance publishes PPF quarterly via press release), so this file is the source of truth -- updating a rate is a one-line PR. `api/rates.py` + `config/instrument_rates.json`.
- **`useInstrumentRates()` hook** on the frontend with a compiled-in fallback so `InstrumentProjections` renders zero-network before the query resolves. `hooks/api/useInstrumentRates.ts` + `services/api/rates.ts`.
- **Rate limiting** (`slowapi`) on `/api/upload` (10/minute) and `/api/ai/bedrock/chat` (30/minute). `auth.py` and `oauth.py` already had limiters; these were the remaining sensitive endpoints without protection.
- **Database pool settings are now env-configurable** via `LEDGER_SYNC_DB_POOL_SIZE`, `LEDGER_SYNC_DB_MAX_OVERFLOW`, `LEDGER_SYNC_DB_POOL_RECYCLE_SECONDS`, `LEDGER_SYNC_DB_CONNECT_TIMEOUT_SECONDS`, `LEDGER_SYNC_DB_STATEMENT_TIMEOUT_SECONDS`, `LEDGER_SYNC_DB_IDLE_TRANSACTION_TIMEOUT_SECONDS`. Defaults unchanged (5/3/300/10/30/60) and sized for Neon free tier. Production tuning no longer requires a code edit.
- **`LEDGER_SYNC_AI_MAX_TOOL_ROUNDS` setting** to reconcile the frontend `MAX_TOOL_ROUNDS=6` runaway-loop cap with the backend `UsageLogRequest.tool_rounds` max=20 reporting limit. Single documented source.

### Changed

- **Tax calculator refactored to a dated-config layout.** All year-specific rules (slabs, surcharge bands, 87A rebate, standard deduction, cess, professional tax) now live in `frontend/src/lib/tax-config/index.ts` as per-FY blocks (FY 2023-24, 2024-25, 2025-26) with `source` field referencing the Budget notification. `getTaxConfig(fyStartYear)` resolves the config with graceful fallback (newest known for future years, oldest known for ancient years). `taxCalculator.ts` is unchanged at the public API level -- every exported symbol (slab arrays, `calculateTax`, `getStandardDeduction`, `getTaxSlabs`) still works, but sources its constants from the config loader. Adding Budget 2026 when it lands is a single new entry in `tax-config/index.ts`.
- **Insight thresholds given names.** 10 magic numbers in `core/insights.py` (`40` / `80` consistency cutoffs, `40%` category concentration, `30%` convenience, `1.2` / `0.8` trend ratios, `20%` / `-10%` lifestyle inflation, `1.3` / `0.7` velocity) extracted to module-level named constants with a header comment explaining they are heuristics, not policy. Zero behavior change; makes future tuning discoverable.
- **Health-score rubric centralized.** `HEALTH_SCORE_RUBRIC` const in `healthScoreUtils.ts` holds the 8 primary targets (savings 20%, essential 50%, emergency 6mo, investment 15%, DTI ≤36%, savings consistency 90%, income CV ≤25%) and is referenced by `scoreSpendLessThanIncome`, `scoreEssentialRatio`, `scoreEmergencyFund`, `scoreInvestment`, `scoreDebtToIncome`, `scoreSavingsConsistency`, `scoreIncomeStability`. Score-curve math unchanged -- pure refactor.
- **AI tool default limits centralized.** Replaced six scattered literals (`20` / `100` for `search_transactions`, `15` / `50` for `list_categories`, `6` / `24` for `list_recent_months`) with named module-level constants in `api/ai_tools.py`. The executor clamp and the JSON Schema shown to the LLM now read from the same source -- no more drift risk between the two.
- **Fallback exchange-rate response shape:** now includes `fallback_as_of` for frontend staleness banner.

### Tests

- **Frontend test count 123 -> 170** (+47). 41 new cases in `constants/__tests__/accountTypes.test.ts` covering CC-vs-bank priority, word boundaries, ambiguous names, case insensitivity, edge cases. 6 new cases in `lib/tax-config/__tests__/taxConfig.test.ts` covering FY lookup, future-year fallback, ancient-year fallback, cess/prof-tax stability across FYs.
- **Backend test count 93 -> 98** (+5). 4 new bank-name normalizer tests covering case insensitivity, extended bank coverage, longest-match-first, word boundaries (`"axis"` inside `"taxis"` must not match). 1 new test on the `/api/rates/instruments` endpoint.

### Skipped (follow-up PRs; each needs a DB migration or changes equality semantics)

- FIRE/NPS assumption persistence, per-state professional tax, user-defined category aliases, always-title-case categories, transfer amount-tolerance pairing, user-tunable insight thresholds, AI pricing-table freshness automation.

---

## 2.9.0 - 2026-04-29

Follow-up to 2.8.0's audit-driven cleanup: execute the remaining feature gaps that were flagged and delete the Insights page outright.

### Removed

- **Insights page deleted entirely.** The 2.8.0 audit already trimmed it to 3 widgets; user feedback confirmed the whole page was cruft. The route (`/insights`), sidebar + CommandPalette + MorePage entries, `ROUTES.INSIGHTS`, and the remaining three components (`IncomeStabilityIndex`, `LifestyleCreepDetection`, `SavingsMilestonesTimeline`) all gone. The dashboard, comparison, and individual analytics pages cover these insights natively -- there's no single "Insights" page anymore.

### Added

- **Barista FIRE variant** on the FIRE Calculator. New `computeBaristaFIRE()` helper and a monthly-income slider. Shows how much smaller your corpus needs to be when a part-time income covers some of your expenses (soft landing rather than cold stop). Layout bumped from 3 variant cards to 4 (Lean / Barista / Standard / Fat).
- **Portfolio-level XIRR** metric on the Investment Analytics page. New shared `frontend/src/lib/xirr.ts` helper (extracted from the per-account implementation on the MF projection page, now DRY). Builds cashflows from every investment transfer plus the current total value as a final liquidation event, then solves via Newton-Raphson. Shows `-` when fewer than one dated flow exists or solver diverges.
- **Subscription Tracker: "Saved per month" KPI** showing estimated monthly savings from deactivated recurring expense items (cancelled subs, paid-off EMIs, stopped gyms). Only appears once the user has at least one deactivated expense item.
- **"Tune detection" link** from the Anomaly Review page header to Settings, making the existing sensitivity / threshold / anomaly-type preferences discoverable.

### Changed

- **Comparison page default is now FY-over-FY, not month-over-month.** FY is the cadence that drives tax + saving-rate decisions; month-over-month is dominated by one-off rent / bonus / travel noise.
- **Comparison FY mode truncates both sides to the elapsed-day count when the selected period is the current (in-progress) FY.** Prevents the "last FY ₹24L vs this FY ₹4L = 83 % down!" artifact that appeared when the current FY was only two months in. Labels now read `FY24-25 (to same date)` vs `FY25-26 (YTD)` to make the truncation explicit.
- **GST Analysis disclaimer rewritten** to be more explicit about what the numbers represent ("approximate only -- GST isn't line-itemed in bank statements, use for lifestyle-scale awareness, not for filing") and coloured warning-orange instead of info-blue so users don't mistake estimates for precision.
- **XIRR implementation extracted to `frontend/src/lib/xirr.ts`** and consumed by both the MF projection page and the new Investment Analytics XIRR metric. Signature-identical to what was already in the MF projection file; now unit-tested.

### Tests

- **Frontend test count 114 -> 123.** 5 new tests for `computeBaristaFIRE` (zero / partial / full-coverage / zero-SWR edge cases), 4 for `calculateXIRR` (empty, two-flow 10 %, monthly-SIP, divergent same-sign), updated the `computeFIRE` orchestrator test to cover the new `baristaFIRE` field.

---

## 2.8.0 - 2026-04-29

Audit-driven cleanup and accuracy pass. Comes out of `docs/AUDIT.md` which rated every page, chart, and calculation against the gold-standard personal-finance feature set.

### Removed

- **8 underperforming / dead analytics components** (~1 400 lines total) all in `frontend/src/components/analytics/`:
  - Dead code: `SubcategoryAnalysis.tsx` (superseded by `EnhancedSubcategoryAnalysis`), `YearOverYearComparison.tsx` (never consumed).
  - Retired from the Insights page: `SpendingVelocityGauge` (gauges are wrong for continuous metrics), `PeerComparisonBenchmarks` (invented peer data), `CategoryCorrelationAnalysis` (correlation numbers without actionable next step), `AccountActivityScore` (paired with the correlation widget - niche), `MonthlyFinancialReportCard` (duplicated Year-in-Review), `ExpenseElasticityChart` (duplicated lifestyle-creep signal).

### Changed

- **Insights page trimmed from 9 widgets to 3 strong ones** -- `IncomeStabilityIndex`, `LifestyleCreepDetection`, `SavingsMilestonesTimeline`. Each drives a specific decision: "how predictable is my cash flow", "is my spending quietly growing faster than income", "what big milestones have I hit / am I nearing".
- **Net worth projection math fixed: linear regression -> compound (geometric)**. Savings and asset returns both compound, so the old linear model dramatically underestimated time-to-target. A user with ₹50L at 12 % annualized reaches ₹1Cr in ~6 years by compound, but ~17 years by linear extrapolation -- the UI now shows the realistic number. New helpers `computeMonthlyGrowthRate`, `projectNetWorthCompound`, and `buildMilestoneRowsCompound` use geometric mean over the last 12 monthly data points. Old linear helpers kept for backwards compatibility.
- **Milestone summary bar now shows annualized %** instead of an absolute monthly rupee delta (the delta number was meaningless once the rate is compound). Hover tooltip still shows the approximate rupee gain at the current net worth.
- **Dashboard default widget count reduced from 14 to 6** on first visit. Power users can still turn on the other 8 from Settings > Dashboard Widgets. New default set: Savings Rate, Top Spending, Top Income, Burn Rate, Daily Spending, Biggest Transaction.

### Added

- **Section 80CCD(1B) deduction input** in Tax Planning's regime-comparison form. Lets users claim the standalone ₹50 000 NPS Tier-1 deduction that sits over and above the 80C ₹1.5L cap -- a common miss for salaried NPS contributors.
- **Advance Tax Schedule panel** on the Tax Planning page showing the four Indian deadlines (15 Jun / 15 Sep / 15 Dec / 15 Mar) with cumulative percentages (15 / 45 / 75 / 100) and amounts due based on the projected annual liability. Current-FY deadlines within 30 days are highlighted to dodge penalty interest under Sections 234B / 234C. Hidden when total tax is zero.

### Tests

- **10 new tests** for the compound-growth helpers in `netWorthProjection.test.ts`: edge cases (empty / single-point / zero / negative start), geometric-mean recovery against a synthetic 4 %/month series, compound-vs-linear horizon comparison, and compound milestone ETA solver accuracy. Frontend test count 104 -> 114.

---

## 2.7.1 - 2026-04-29

Polish patch -- Settings UX cleanup and a smarter auto-classifier, plus a new data-focused page catalog.

### Changed

- **Settings sections are collapsed by default.** The `Section` primitive (`frontend/src/pages/settings/sectionPrimitives.tsx`) now defaults `defaultCollapsed` to `true`, so opening Settings shows a scannable list of headers instead of a wall of forms. Dropped the per-mode override on `AIAssistantSection` (was auto-expanding when the user was configured) and the redundant `defaultCollapsed={true}` on `AdvancedSection` so every section has a uniform baseline.
- **Auto-account classification now studies balance data, not just account names.** `getDefaultClassifications` in `frontend/src/pages/settings/helpers.ts` gained an optional `accountStats` argument and applies a balance-sign refinement pass to anything the keyword dictionary couldn't classify: negative balance + activity -> **Credit Cards**, positive balance + >=3 transactions -> **Bank Accounts**. User-saved classifications still win. Keyword list also extended with Indian card brands the old dictionary missed (Jupiter Edge, OneCard, Slice, Millennia, SimplyCLICK, Regalia, Swiggy HDFC, Amazon Pay ICICI, Flipkart Axis, Amex, Diners) and `jupiter` (neobank).

### Added

- **`docs/PAGES.md`** -- new data-focused catalog documenting every page in the app: what each one shows, the tables/endpoints it reads from, the decisions it helps the user make. Written for "what can I learn from this app?" rather than "how is this built?".

---

## 2.7.0 - 2026-04-29

Mobile PWA polish landed in three stacked PRs (#126 / #127 / #128). The app now installs on iPhone/Android home screens like a native-feeling finance app, with a bottom tab bar for phone-sized viewports and a full-bleed app icon.

### Added

- **Bottom tab bar** for phone viewports (`<lg` breakpoint). `Home / Txns / Flow / More` with a Framer-Motion shared-element active pill and 52px touch targets. Hidden on desktop (sidebar stays).
- **`/more` route + `MorePage.tsx`** -- grid menu grouping every route that didn't earn a tab slot (Analytics, Investments, Planning, Tax, Data) + sign-out.
- **Full-bleed PWA app icon** -- uses the same PiggyBank glyph on the blue-to-indigo gradient as the web header. `pwa-assets.config.ts` overrides `minimal-2023`'s apple-touch transform to `padding: 0` + transparent background so the gradient paints edge-to-edge; iOS applies its own squircle mask on install. No more white-ring "unfinished-app" look on the home screen.

### Changed

- **`h-screen` -> `h-dvh`** in `AppLayout` and `Sidebar` so the viewport tracks the dynamic-viewport-height API. No more content jumping when the mobile address bar toggles.
- **Safe-area insets wired through** the sticky `PageHeader`, chat widget / panel, and `HomePage` header. Sticky headers bake `env(safe-area-inset-top)` into padding so titles don't render behind the iOS notch in standalone mode. Main content gets `pb-safe` so the last row clears the home indicator. `ChatWidget` respects `safe-area-inset-right` for landscape on notched devices.
- **`theme-color` aligned to `#000000`** (was `#09090b`) in both `index.html` and the PWA manifest so the status bar blends into the app black -- no seam at the top.
- **Body/HTML reset** in `index.css`: `margin: 0`, `padding: 0`, `overscroll-behavior-y: none`, `html { height: 100% }`, `#root { height: 100% }` so `h-dvh` has a reference and rubber-band scroll doesn't flash white behind the notch.
- **`CreditCardHealth` widget now uses the user's account classifications** instead of substring-grepping `"credit"` in account names. Cards like "HDFC Millennia" or "Amazon Pay Card" were silently dropped before. Falls back to the old name match only when an account has no classification yet.

### Fixed

- **HomePage layout shifted up behind the notch in PWA standalone mode.** The landing page lives outside `AppLayout` so the safe-area work from the other PRs didn't reach it. Header padding-top/left/right now use `env(safe-area-inset-*)`; main's `pt-20` becomes `calc(5rem + inset-top)`; footer gets `inset-bottom`. `h-screen` -> `min-h-dvh` matches the rest of the app.

---

## 2.5.0 - 2026-04-26

Major AI chat upgrade: the bot can now answer questions about *any* of your data, not just the six fields we pre-loaded into the system prompt.

### Changed

- **AI chat now uses tool calling instead of context stuffing.** Previously `buildFinancialContext` fetched six endpoints up front and crammed them into the system prompt; anything outside that fixed slice ("when did I last go for a haircut?", "how many bank accounts?") was unanswerable. The model now has nine read-only tools it can call on demand -- `list_accounts`, `search_transactions`, `get_monthly_summary`, `list_categories`, `get_category_spending`, `get_net_worth`, `list_recurring`, `list_goals`, `list_recent_months` -- and picks the right one based on the question. Same tool schema works across OpenAI, Anthropic, and Bedrock (each provider has a native tool-calling API).
- **System prompt shrunk to ~10 lines.** Just preferences (currency, fiscal year), today's date, tool-usage guidance, and an anti-hallucination nudge. The bot reaches for tools instead of making up plausible numbers.
- **All three providers are now non-streaming.** Tool-calling requires handling `tool_use` events between turns, which makes SSE streaming awkward. Plain JSON for all providers keeps the tool loop simple (one HTTP call per round). Users see "processing... 2-5s... full reply" instead of token-by-token streaming.

### Added

- **`backend/src/ledger_sync/api/ai_tools.py`** -- tool registry + `/api/ai/tools` (list) + `/api/ai/tools/execute` (run). Every tool is user-scoped via `CurrentUser`, enforced at the FastAPI dependency level so the LLM can never see another user's data.
- **`search_transactions` tool** -- full-text search over transaction notes/category/subcategory/account with optional date, category, account, type, and amount filters. Capped at 100 results to prevent runaway queries.
- **Frontend tool-calling loop in `useChat`.** Sends -> receives `tool_use` -> executes tools in parallel -> appends `tool_result` -> sends again -> repeats until `end_turn` or 6-round limit (prevents runaway loops).
- **16 new tests:** 7 for `/api/ai/bedrock/chat` covering the new block-based message shape (`tool_use`/`tool_result` round-trip, `toolConfig` forwarding), 9 for `/api/ai/tools` covering every tool against a real SQLite DB with seeded data. Backend test count 75 -> 91.
- **6 rewritten frontend tests** for `chatAdapters.ts` covering OpenAI/Anthropic/Bedrock request + response mapping including tool-call conversion.

### Why tool calling over a vector database (RAG)

Finance data is structured. `SELECT category, SUM(amount) FROM transactions WHERE user_id = ? AND date >= ?` beats any embedding similarity search when the question is a database query in disguise. Vector DBs are worth adding later for semantic merchant matching ("food delivery", "subscriptions I don't use") but are overkill for 90% of finance questions.

---

## 2.6.0 - 2026-04-27

Follow-up to 2.5.0's tool-calling chat. Biggest change: the chat now has a **two-mode split** so users can either use the app's shared Bedrock key (free, rate-limited) or bring their own API key (unlimited, they pay). Plus six new tools expose tax / FY / cash-flow / budget data to the assistant, and every LLM round-trip is now logged for cost transparency.

### Added -- App vs BYOK mode split

- **`ai_mode` column** on `user_preferences` (`'app_bedrock'` default or `'byok'`) + `PATCH /api/preferences/ai-config/mode` to toggle.
- **App mode (default):** new users get a working chatbot immediately. No provider picker, no key input -- the server uses the app's shared Bedrock bearer token and a fixed cheap default model (`us.anthropic.claude-haiku-4-5-20251001-v1:0`). Rate-limited to `LEDGER_SYNC_AI_DAILY_MESSAGE_LIMIT` messages per day (default 10) so our AWS bill stays predictable. Users who hit the cap get a clear 429 with a "switch to BYOK" pointer.
- **BYOK mode:** existing provider/model/key picker + per-user token limits. Users pay their own provider. No app-level cap (the provider bills them directly).
- **`settings.py`** gains three new env-overridable fields: `ai_default_bedrock_model`, `ai_default_bedrock_region`, `ai_daily_message_limit`.
- **Settings → AI Assistant** rebuilt with a stacked two-card mode picker. App-mode panel shows a live "3 / 10 left" counter with a small explainer. BYOK-mode panel unchanged from 2.5.0 (provider/model/key + optional token limits).
- **ChatPanel usage badge** is now mode-aware: `"· 3 / 10 left"` in app mode (messages), `"· 1.2k / 50k"` in BYOK mode (tokens).
- **ChatWidget gating:** opening the chat no longer requires `has_key` when the user is in app mode -- they can chat out of the box.

### Added -- 6 new AI tools (registry now 15)

- `get_fy_summary` -- fiscal-year rollup (income by source, tax paid, savings, YoY change) from the `fy_summaries` table.
- `get_tax_summary` -- prefers uploaded `TaxRecord` filings (gross/TDS/advance/self-assessment/80C/80D/standard deductions) and falls back to transaction-derived totals when no filing is available.
- `get_cash_flow` -- monthly income-vs-expense time series with totals and averages.
- `list_budgets` -- active budgets with current-month usage %, ranked by usage.
- `list_anomalies` -- recent unusual-spending alerts the system detected.
- `get_preferences_summary` -- currency, fiscal-year start, and salary-structure components so the LLM can reason about context.

### Added -- Usage tracking

- **`ai_usage_log` table** with `(provider, model, input_tokens, output_tokens, tool_rounds, cost_usd, timestamp)`. Bedrock usage is recorded server-side after `converse()`; OpenAI and Anthropic report back from the browser via `POST /api/ai/usage/log`.
- **Cost estimation** via `core/ai_pricing.py` -- per-provider, per-model-prefix USD-per-1M-token table with longest-prefix match. Unknown models fall through to a conservative 10/40 USD-per-1M fallback so we never under-report cost.
- **`GET /api/ai/usage`** returns today / MTD / all-time rollups + current limits + today's message count (for app mode). Used by Settings and the chat header.
- **BYOK per-user token limits:** two nullable columns `ai_daily_token_limit` / `ai_monthly_token_limit` plus `PATCH /api/preferences/ai-config/limits`. Applies only in BYOK mode; app mode uses the server-wide message cap instead.

### Changed

- **System prompt stays minimal.** The LLM fetches data via tools instead of having summaries pre-stuffed. ~300 tokens per round instead of ~2K.
- **Default mode is `app_bedrock`.** Existing users stay in their previous configuration (the migration sets `app_bedrock` as the server-default, but the save-config endpoint now flips prefs to `byok` whenever someone configures a provider key, so migrating users who already had BYOK remain BYOK after their next save).

### Migrations

- **`20260427_1200_add_ai_usage_log_and_token_limits.py`** creates the `ai_usage_log` table + two indexes, adds `ai_daily_token_limit` + `ai_monthly_token_limit` to `user_preferences`, and adds the `ai_mode` column (default `'app_bedrock'`). Downgrade is intentionally empty per project convention.

### Tests

- **Backend:** 32 new tests across 3 suites (`test_ai_tools.py`, `test_ai_usage.py`, `test_ai_chat.py` extensions). Coverage of the 6 new tools, usage rollups, cost estimation edge cases, app-mode message cap enforcement, BYOK bypass of the app cap, and default-model selection. Test count 75 → 107.

### Deploy notes

- Run `uv run alembic upgrade head` once after deploy (auto-runs on Vercel via the migrate workflow).
- No new required env vars. Optional overrides: `LEDGER_SYNC_AI_DAILY_MESSAGE_LIMIT` (default 10), `LEDGER_SYNC_AI_DEFAULT_BEDROCK_MODEL` (default Haiku), `LEDGER_SYNC_AI_DEFAULT_BEDROCK_REGION` (default `us-east-1`).

---

## 2.4.2 - 2026-04-25

Further mobile polish after device testing, plus an AI chat fix.

### Changed

- **PWA icon redesigned.** The old mark had the L-glyph in the top-left corner leaving dead space bottom-right, and read as "a bar chart" at 64px. New icon is a centered bold rupee (₹) glyph over a subtle ascending trend line on the blue-to-indigo gradient. Reads clearly at 64px home-screen badge size, stays recognisable after Android's maskable crop, and ties the mark to the product (personal finance) rather than a generic chart. Regenerated all 6 PNG sizes from [pwa-icon-source.svg](frontend/public/pwa-icon-source.svg).
- **Cash Flow on mobile is now a dedicated vertical view, not a shrunken Sankey.** Phones get [MobileFlowView](frontend/src/pages/income-expense-flow/components/MobileFlowView.tsx) -- a stacked sequence (Income sources -> Total Income -> Savings + Expenses split -> Expense categories) with proportional bars and currency-accurate labels. No swipe, no cramped nodes, no horizontal scroll. Desktop still gets the full Sankey.
- **Bedrock chat path switched from SSE streaming to plain JSON.** The backend runs on Vercel via Mangum, which buffers streaming responses end-to-end, so the old `StreamingResponse` made the UI sit on "processing" forever -- the browser only saw the first byte once boto3's `converse_stream` generator had fully drained (or the function timed out). Now the backend calls `converse` (non-streaming), collects the full reply, and returns `{ "content": "..." }`. The frontend [Bedrock adapter](frontend/src/lib/chatAdapters.ts) fetches once, parses JSON, emits the full text as a single token. Anthropic and OpenAI paths still stream browser-direct since they don't go through Mangum.

### Fixed

- **Chart heights auto-cap at 280px on mobile** via a new `MOBILE_HEIGHT_CAP` in [ChartContainer](frontend/src/components/ui/ChartContainer.tsx). Previously a chart with `height={400}` would eat ~60% of a 667px iPhone viewport. Now any numeric height greater than 280 is auto-shrunk on `max-width: 639px`; percent heights (`'100%'`) and the new optional `mobileHeight` prop still win. Affects ~20 chart call-sites with one change.
- **Numeric columns in DataTable now use `tabular-nums`.** Right-aligned cells align digit-by-digit vertically, which matters most on mobile where rows are tight.
- **CommandPalette (Cmd+K) on mobile** -- panel no longer pushed off-screen by the on-screen keyboard. Top offset drops from `15vh` to `8vh` below `sm`, panel is `flex flex-col max-h-[80vh]` so the input stays visible while results scroll inside.
- **AI chat "processing... forever" with no error surfaced.** Silent `{"error": "..."}` SSE events from the Bedrock proxy were dropped by the token extractor (it only looked for `{"token": "..."}`), so invalid model IDs, auth failures, region mismatches, and timeouts all looked identical to the user: a spinner that never resolved. With the new JSON path, Bedrock errors surface as HTTP 502 with a readable `detail`; the existing `onError` path displays them in the chat UI.
- **AI model selection now has a "Custom model ID" free-text field.** The hardcoded Bedrock model list can get stale the moment AWS ships a new inference profile; a user who picked "Claude Opus 4.7 (Bedrock)" was sending `us.anthropic.claude-opus-4-7-v1` which Bedrock no longer accepts. The dropdown now includes "Custom model ID..." which enables a text input below so the exact `modelId` (e.g. `us.anthropic.claude-opus-4-1-20250805-v1:0`) can be pasted straight from the Bedrock console -- no code update required. Also shown/editable when a saved value doesn't match any dropdown option, so users migrating from stale IDs can see and fix them.
- **Bedrock auth via bearer token (`AWS_BEARER_TOKEN_BEDROCK`).** The server-side proxy used `boto3.client("bedrock-runtime").converse(...)` expecting SigV4 credentials, but production was running with only a Bedrock API key. boto3 1.39+ auto-detects `AWS_BEARER_TOKEN_BEDROCK`, so the existing code works once the env var is in place. Added a [`LEDGER_SYNC_BEDROCK_API_KEY`](backend/src/ledger_sync/config/settings.py) setting that gets injected into `AWS_BEARER_TOKEN_BEDROCK` on startup, so all app secrets stay under the `LEDGER_SYNC_` prefix on Vercel. Added a pre-flight check that returns HTTP 503 with a clear "server not configured" message when no auth mechanism is present -- before, boto3's error surfaced as the misleading "model identifier is invalid".
- **AI chat had zero financial context despite `buildFinancialContext`.** The context builder expected flat response shapes like `summary.income` as a number, but every V2 endpoint returns `{ data: [...], count: N }` with **nested** objects (`summary.income.total`). Every section's `Array.isArray && length > 0` guard silently skipped, `Promise.allSettled` swallowed fetch errors, and the chat shipped with an empty system prompt -- which is why the assistant replied "I don't have access to your financial data." Rewrote [chatContext.ts](frontend/src/lib/chatContext.ts) to match the actual V2 response shapes (monthly-summaries, category-breakdown `{ categories, total }`, recurring, net-worth `current`, goals) and log failures with `console.warn` so future endpoint shape changes don't silently return an empty prompt.

### New

- **5 unit tests for the Bedrock chat endpoint** ([test_ai_chat.py](backend/tests/unit/test_ai_chat.py)): no-preferences, wrong-provider, missing-AWS-auth, successful converse, boto3-exception handling. Uses `StaticPool` + `check_same_thread=False` so `TestClient` (httpx worker thread) can reuse the in-memory SQLite DB.
- **Live Bedrock smoke-test script** ([backend/scripts/bedrock_smoke_test.py](backend/scripts/bedrock_smoke_test.py)): runs `converse()` against a real model to verify the bearer-token path end-to-end. Kept out of the pytest suite because CI has no AWS access; invoked with `AWS_BEARER_TOKEN_BEDROCK=... uv run python scripts/bedrock_smoke_test.py`.
- **Shared `useIsMobile` hook** extracted to [hooks/useIsMobile.ts](frontend/src/hooks/useIsMobile.ts) from the inline definition in `IncomeExpenseFlowPage`.

---

## 2.4.1 - 2026-04-25

Follow-up mobile-UX pass after installing the 2.4.0 PWA on iPhone and finding real-device issues.

### Fixed

- **iOS Safari auto-zoom on input focus.** Every input with `font-size < 16px` (all of them -- default was `text-sm` = 14px) triggered a viewport zoom on tap, which then failed to zoom back out. Now any input/select/textarea on viewports below the `sm` breakpoint (640px) renders at 16px. Desktop typography is unchanged. Single global rule in [frontend/src/index.css](frontend/src/index.css).
- **PWA content hidden under the iPhone notch.** Added `viewport-fit=cover` to [index.html](frontend/index.html) and safe-area-inset utility classes (`pt-safe`, `pb-safe`, etc.) to index.css. The sidebar hamburger button and chat widget now offset themselves by `env(safe-area-inset-top)` / `env(safe-area-inset-bottom)` so they clear the notch and home indicator in standalone PWA mode.
- **Hamburger button overlapping page titles.** The `lg:hidden` hamburger at `fixed top-4 left-4` was sitting directly over every page's `<h1>`. [PageHeader](frontend/src/components/ui/PageHeader.tsx) now adds `px-12 text-center` on mobile (reserves 48px for the hamburger on each side and centers the title) and reverts to `px-0 text-left` at `sm+`. Affects all 22 pages that use `PageHeader`.
- **ChatPanel overflowing 375px viewport.** Hardcoded `w-[380px]` caused horizontal scroll on iPhone SE. Now `w-[calc(100vw-2rem)] max-w-[380px]` and `max-h-[70vh] sm:max-h-[500px]` in [ChatPanel.tsx](frontend/src/components/chat/ChatPanel.tsx).
- **Below-minimum touch targets (< 44px).** Grew the sidebar bottom-bar icons, chat-panel header buttons, and profile-modal close button from 32-36px to 44px on mobile, keeping the desktop dimensions via `sm:` resets. Apple HIG minimum is 44x44 px.
- **Numeric inputs showing full alphabetic keyboard on mobile.** Added `inputMode="decimal"` to 31 currency/percentage inputs across 14 files (budget forms, goals, settings, tax deductions, mutual fund projections, etc.). Mobile users now see a decimal-optimized keypad.
- **DataTable horizontal scroll invisible on mobile.** Existing 4px scrollbar was invisible on touch devices. The scroll wrapper now shows a thin scrollbar on viewports below `sm` and uses `-webkit-overflow-scrolling: touch` + `overscroll-behavior-x: contain` for smooth momentum scroll without rubber-banding the outer page.

---

## 2.4.0 - 2026-04-25

### Added

- **PWA support** (#80) -- Ledger Sync is now installable on iOS, Android, and desktop. Closes #80.
  - Web App Manifest with `display: standalone`, dark theme color (`#09090b`), portrait orientation, and 4 icon sizes (64, 192, 512, maskable 512). Manifest `start_url` and `scope` are relative so they resolve correctly under the `/ledger-sync/` GitHub Pages base path.
  - Service worker via `vite-plugin-pwa` (Workbox under the hood) with `registerType: 'autoUpdate'` -- updates propagate within one app restart. Precaches the entire app shell (JS/CSS/HTML/fonts/icons, ~2.4 MiB across 68 entries) so the UI loads instantly offline.
  - **Financial data is intentionally NOT cached.** The service worker explicitly denies `/api/*` via `navigateFallbackDenylist` -- API calls always hit the network, so account balances and analytics are never stale. TanStack Query's existing error-state handling covers the offline-with-data case.
  - iOS/Android install meta tags: `apple-mobile-web-app-capable`, `mobile-web-app-capable`, `apple-mobile-web-app-status-bar-style=black-translucent`, `apple-mobile-web-app-title`, and a 180x180 apple-touch-icon.
  - Icon source committed as [frontend/public/pwa-icon-source.svg](frontend/public/pwa-icon-source.svg). Regenerate with `pnpm run generate:icons` (uses `@vite-pwa/assets-generator` + sharp).

---

## 2.3.2 - 2026-04-25

### Fixed

- **Earning-start-date is now a view filter, not a data filter.** Previously, toggling "Use as analytics start" silently dropped pre-earning-start transactions from every backend query, which corrupted factual totals: Net Worth dropped (because pre-earning balance-affecting transactions vanished), account balances shrank, and historical milestone crossings disappeared. Earning-start is now applied only at the chart x-axis layer via `useAnalyticsTimeFilter`; all underlying data (account balances, totals, milestone history) is always computed from the user's full transaction history.
  - Backend: `build_transaction_query(apply_earning_start=True)` default flipped to `False`. `_apply_date_range` in `transactions.py` no longer accepts a `user` parameter. Three analytics_v2 endpoints (`/monthly-summaries`, `/daily-summaries`, `/category-trends`) no longer clamp their period by earning-start. The `_get_earning_start_period` helper is removed (dead code).
  - Frontend: `useTransactions` hook no longer filters by earning-start-date. `useAnalyticsTimeFilter` now applies earning-start to `dateRange.start_date` (the chart-window lower bound) via a new `clampStartToEarningStart` helper with unit tests. Net Worth milestones scan the full unfiltered history, so "First Reached" dates from before earning-start remain visible.
  - 8 new unit tests (7 for `clampStartToEarningStart`, 1 regression for milestone preservation).

---

## 2.3.1 - 2026-04-25

Milestones table redesigned around the question users actually ask: "have I *held* this threshold, or just touched it once?"

### Changed

- **[Milestones table](frontend/src/pages/net-worth/components/MilestonesTable.tsx) columns simplified to 5:** Target | Status | First Reached | Stable Since | Expected to Reach. Dropped the separate Amount column (now shown as a subline under the Target label) and the Notes column (replaced by structured date columns).
- **Status now has 3 states** instead of 2:
  - **Stable** (green ✓) -- crossed the threshold and never dipped below since.
  - **Reached** (yellow ○) -- crossed once but later dipped below; still achieved, not stable.
  - **Upcoming** (muted ⊙) -- not yet crossed.
- **Header stats** now show Current net worth, Avg monthly growth, Stable X/N, and Reached X/N (was: Achieved X/N only).

### Added

- **`stableSince` field** on [`MilestoneRow`](frontend/src/pages/net-worth/netWorthProjection.ts). Computed by scanning backward for the last dip below target; returns the crossing date that stuck. `null` when the milestone is upcoming or when net worth is currently below threshold after a prior crossing. 5 new unit tests covering never-dipped, dipped-then-recovered, dipped-still-below, upcoming, and multiple-dips scenarios.

---

## 2.3.0 - 2026-04-25

Codebase-wide consolidation of hand-rolled UI primitives. A component-reuse audit found 35+ files re-implementing identical `<table>` + `<thead>` + `<tbody>` boilerplate and ~30 files repeating recharts config. This release ships a shared table primitive, a new radar chart wrapper, and migrates every file where the chart shape cleanly fits the existing wrappers.

### Added

- **`DataTable` primitive** ([frontend/src/components/ui/DataTable.tsx](frontend/src/components/ui/DataTable.tsx)) -- a generic, type-safe `<table>` with column-driven config, internal sort state (click or keyboard Enter/Space), optional row animation (auto-disabled above 200 rows), and per-row/per-cell className callbacks. 10 unit tests.
- **`StandardRadarChart`** ([frontend/src/components/analytics/StandardRadarChart.tsx](frontend/src/components/analytics/StandardRadarChart.tsx)) -- wraps the PolarGrid/PolarAngleAxis/PolarRadiusAxis/Radar boilerplate previously duplicated in `FinancialHealthScore` and `CFPScoreView`.

### Changed

- **Tables migrated to `DataTable`:** [MilestonesTable](frontend/src/pages/net-worth/components/MilestonesTable.tsx) (Net Worth), [MonthlyBreakdownTable](frontend/src/pages/trends-forecasts/components/MonthlyBreakdownTable.tsx) (Trends & Forecasts). Sort state moved off `useTrendsForecasts` into the table; hook now exposes `recentChartData` directly. `ariaSort` helper removed from `trendsUtils.tsx` (was only used by this table).
- **Charts migrated to `StandardBarChart`:** `YearOverYearComparison`, [DayOfWeekChart](frontend/src/pages/year-in-review/components/DayOfWeekChart.tsx), [CohortSpendingAnalysis](frontend/src/components/analytics/CohortSpendingAnalysis.tsx), `PeerComparisonBenchmarks`, `IncomeStabilityIndex` (horizontal), `LifestyleCreepDetection` (horizontal), `ExpenseElasticityChart` (horizontal).
- **Charts migrated to `StandardAreaChart`:** `SavingsMilestonesTimeline`, [InstrumentProjections](frontend/src/components/analytics/InstrumentProjections.tsx), [FIRECalculatorPage](frontend/src/pages/FIRECalculatorPage.tsx) projection chart.
- **Charts migrated to `StandardPieChart`:** [TopMerchants](frontend/src/components/analytics/TopMerchants.tsx), [DashboardPage](frontend/src/pages/DashboardPage.tsx) (income + expense donuts with center labels), [IncomeAnalysisPage](frontend/src/pages/income-analysis/IncomeAnalysisPage.tsx) income-by-category donut.
- **Charts migrated to `StandardRadarChart`:** [FinancialHealthScore](frontend/src/components/analytics/FinancialHealthScore.tsx), [CFPScoreView](frontend/src/components/analytics/health/CFPScoreView.tsx).

### Internal

- Wrapper extensions needed during migration: `StandardBarChart` gained `referenceLines`, `xDomain`/`xType`/`yType`, `yCategoryKey` for horizontal layout, `xTickFormatter`/`yTickFormatter`, `tooltipValueWithPayload` (advanced tooltip that sees the row), `barGap`, `hideVerticalGrid`/`hideHorizontalGrid`, per-bar `fillOpacity`/`getCellColor`. `StandardAreaChart` gained `strokeDasharray` on `AreaConfig`. `StandardPieChart` gained `onSliceClick` (with auto pointer cursor). All extensions are generic -- no file-specific escape hatches.
- Net ~160 LOC deleted across migrated files. Same visual output, one source of truth for chart styling.
- Deferred as intentionally custom (shape doesn't fit a generic wrapper): [CashFlowForecast](frontend/src/components/analytics/CashFlowForecast.tsx) (8 Areas with per-area strokeDasharray + confidence-cone trick), [EffectiveTaxRateChart](frontend/src/components/analytics/EffectiveTaxRateChart.tsx) (uses ReferenceDot), `SubcategoryAnalysis` (non-date x-axis with dots), [NetWorthPage](frontend/src/pages/net-worth/NetWorthPage.tsx) waterfall, [IncomeExpenseFlowPage](frontend/src/pages/income-expense-flow/IncomeExpenseFlowPage.tsx) Sankey, [SpendingAnalysisPage](frontend/src/pages/spending-analysis/SpendingAnalysisPage.tsx) nested pies, [TaxPlanningPage](frontend/src/pages/tax-planning/TaxPlanningPage.tsx) bar+line composed chart, [BudgetPage](frontend/src/pages/budget/BudgetPage.tsx) (mix of bar/area/radar in one page), plus IncomeAnalysisPage's area chart. Also deferred: the two non-flat tables ([TaxableIncomeTable](frontend/src/components/analytics/TaxableIncomeTable.tsx) expandable tree, [MultiYearProjectionTable](frontend/src/pages/tax-planning/components/MultiYearProjectionTable.tsx) pivot) -- these need dedicated primitives, not `DataTable` flags.

---

## 2.2.1 - 2026-04-25

Rework of the net-worth milestones feature from 2.2.0 after self-review found two visual issues.

### Changed

- **Unified Milestones table** -- the separate "Milestones Achieved" and "Next Targets" tables from 2.2.0 have been merged into a single "Net Worth Milestones" table. One row per threshold (₹1L through ₹10Cr), sorted low-to-high, with a Status column (✓ Achieved / ⏳ Upcoming), date, and notes. Removes visual redundancy and makes the "just crossed" → "next target" progression obvious at a glance.

### Fixed

- **Projection used a different "current" value than the chart ends at.** Old code passed the account-balance-derived `netWorth` (MetricCard value) into `projectNetWorth`, while the chart ended at the cumulative income-minus-expense series. On users where those two differ, the projection line had a vertical discontinuity at "today". Fixed: the projection anchor is now the last point of the filtered chart series, and milestones/ETAs use the same anchor value. No more jump.
- **Daily + monthly dates mixed on a categorical x-axis stretched the projection horizontally.** With 1,448 historical daily points + 60 monthly points on the same string-keyed axis, Recharts gave each unique date equal width, so the 60 projected months visually occupied ~4% of the axis while representing 5 years. When projection is ON, historical data is now downsampled to month-end points so the whole timeline renders at uniform monthly spacing.
- **"Today" reference line moved to the actual projection start** (the last historical point, labeled "Now") -- previously it used `new Date()` which could sit a few days past the last data point and create tiny visual artifacts.

### Internal

- `netWorthProjection.ts` API simplified: one `buildMilestoneRows(series, anchor, growth)` replaces the previous `detectMilestonesAchieved` + `computeMilestoneETAs` pair. Added `downsampleToMonthly` helper. `projectNetWorth` signature now takes an anchor point (date + value) instead of just a number.
- Removed `MilestonesAchieved.tsx` and `TargetProjectionsTable.tsx`. New `MilestonesTable.tsx` is the single consumer. 18 unit tests.

---

## 2.2.0 - 2026-04-25

### Added

- **Net Worth milestones + projection** -- see 2.2.1 for the shipped design. This version had two separate tables and a projection anchor mismatch; fixed immediately in 2.2.1.

### Fixed (production data)

After the transfer-dedup fix in 2.1.6, production user 1's Cashback Shared account still showed a phantom -₹39 balance because the broken raw transactions never got re-ingested. Re-uploading through the UI (with force=true) recovers the 3 silently-dropped same-day duplicate transfer pairs and restores the 0.00 balance.

---

## 2.1.6 - 2026-04-25

Ingestion-pipeline fixes from a full xlsx vs DB audit.

### Fixed

- **Transfer reconciliation silently dropped duplicate same-day transfers** -- each real transfer appears in the source export as two rows (Transfer-In + Transfer-Out). The reconciler correctly collapsed paired legs via a set-based hash but couldn't distinguish "paired leg of same transfer" from "genuine second transfer of the same amount on the same day between the same accounts", so any user who recorded two identical transfers on one day lost one. Fix: normalizer emits `transfer_leg = "in" | "out"`; reconciler uses per-direction occurrence counters keyed on the canonical (date, amount, from, to, category, subcategory, note) tuple. 5 new regression tests in `test_transfer_reconciliation.py`.
- **`POST /api/analytics/v2/refresh` returned HTTP 500 on SQLite** -- the endpoint ran `SET statement_timeout = '120s'` (Postgres-only) unconditionally. Now guarded with `if session.bind.dialect.name == "postgresql"`.
- **`_calculate_category_trends` scrambled subcategory attribution** -- grouping key was `(period, category, type)` and stored subcategory was whichever sub appeared last in the loop. "Refund & Cashbacks" INCOME showed Credit Card Cashbacks ₹18,706 and Other Cashbacks ₹27,773 instead of the correct ₹43,774 / ₹4,086. Now groups by `(period, category, subcategory, type)`; 1,110 rows in `category_trends` instead of 594, matching raw totals exactly.
- **`AuditLog.user_id` was always NULL** -- `AnalyticsEngine._log_audit` never passed `user_id=self.user_id`. Now scoped per user so the audit trail can distinguish runs.
- **`/api/upload` now auto-triggers `run_full_analytics`** -- the defense-in-depth fix ensures pre-aggregated tables never drift from raw transactions when a client skips the explicit `/refresh` call.

---

## 2.1.5 - 2026-04-24

Second-pass audit: fixed calculation bugs, tightened user scoping, and hardened a few response/log paths. No UX changes.

### Fixed

- **FIRE monthly SIP compounding** -- `computeRetirementCorpus` used the naive `expectedReturn/12` as the monthly rate. At 12% effective annual that compounded to ~12.68% and understated the required SIP by ~12-13% for long horizons. Now uses `(1+r)^(1/12) - 1`. The year-by-year projection loop was rewritten to use the same monthly annuity-due so the final projected corpus converges on `requiredCorpus` within 1%
- **Tax surcharge order** -- surcharge is now computed on base tax (before Section 87A rebate), matching Indian tax code. Cess still applies on tax-after-rebate + surcharge. No practical impact today (87A ceiling sits well below any surcharge threshold) but the ordering is now correct for any future rule change
- **FY string year-2100 wraparound** -- `(year+1) % 100` formatted FY 2099-2100 as "2099-00"; now uses a helper that zero-pads the last two digits so 2100 renders as "00" correctly and collisions can't happen
- **Anomaly detection division-by-zero** -- `_detect_high_expense_months` raised `ZeroDivisionError` when all months had zero expenses. Guarded at the top: zero average short-circuits with no anomalies

### Security

- **Cross-user aggregation hardened** -- `AnalyticsEngine` now refuses per-user aggregations when `user_id` is `None` (previously those queries silently scanned every user's data). Three anomaly/budget queries (`_detect_high_expense_months`, `_detect_large_transactions`, `_update_budget_tracking`) rewired to use the explicit user filter
- **`/api/preferences/ai-config/key` response hardened** -- sets `Cache-Control: no-store, no-cache, private, max-age=0` and `Pragma: no-cache` so the decrypted API key can't land in intermediary proxy caches, disk cache, or service workers
- **OAuth error logs redacted** -- Google and GitHub token-exchange failures now log only `status` and the provider's `error` field instead of the full response body. Prevents leaking short-lived codes or internal provider URLs into log aggregators
- **Upload row cap** -- `TransactionUploadRequest.rows` bounded at 100_000 (well above a busy user's annual volume). Prevents an authenticated client from DoS-ing via arbitrarily large request bodies

### Tests

- Added `taxCalculator.test.ts` (12 tests: slab math, 87A rebate, surcharge-on-base-tax, cess, FY helpers)
- Extended `fireCalculator.test.ts` (+3 tests: effective-monthly-rate regression, projection convergence, zero-return handling)
- Extended `projectionCalculator.test.ts` (+1 test: FY 2099/2100 wrap)
- Added `test_analytics_user_scoping.py` (5 integration tests: zero-guard, user-id-required, cross-user isolation for two anomaly paths)
- Added `test_upload_schema.py` (3 unit tests: row cap enforcement)
- Totals: backend 70 tests (up from 62), frontend 65 tests (up from 49)

---

## 2.1.4 - 2026-04-24

### Security

- **python-dotenv CVE-2026-28684** -- constrained to `>=1.2.2` via `pyproject.toml`'s `constraint-dependencies`. The vulnerability allowed arbitrary file overwrite via cross-device rename fallback when `.env` was a symlink
- **PBKDF2 salt hardened** -- initial encryption implementation used a hardcoded salt; now uses a random 128-bit salt per ciphertext (salt + nonce + ciphertext base64-encoded into a single field). Flagged by SonarCloud

---

## 2.1.3 - 2026-04-24

### Changed

- **Pages folder structure standardized** -- every multi-file page now has the same layout: `<page>/PageName.tsx + use<Page>.ts + types.ts + *utils.ts + components/`. Kebab-case for directories, PascalCase for single-file pages
- **`pages/IncomeExpenseFlowPage/`** renamed to `pages/income-expense-flow/` for kebab-case consistency
- **`pages/SettingsPage.tsx`** moved into `pages/settings/SettingsPage.tsx`; sub-sections moved into `pages/settings/sections/`; `settings/components.tsx` renamed to `settings/sectionPrimitives.tsx`
- **Frontend page-level `index.ts` barrel files** removed per CLAUDE.md policy ("no barrel files")
- **Thin re-export stubs deleted** -- `ComparisonPage.tsx`, `GoalsPage.tsx`, `SubscriptionTrackerPage.tsx` were one-line re-exports and have been removed; routes now import directly from the page folders
- Every multi-file page folder now has a `components/` subfolder (settings uses `sections/` instead because "section" is the domain term)

### Removed

- **Issue backlog cleanup** -- closed #70 (Recharts -> Nivo migration, no user benefit for massive work), #13 (Finance Levels gamification page, out of scope), #84 (Lighthouse CI, maintenance burden > value for solo-dev project), #89 (already fixed by enum migration)

---

## 2.1.2 - 2026-04-24

### Changed

- **`pages/BillCalendarPage.tsx`** (799 lines) split into `pages/bill-calendar/` module: `BillCalendarPage.tsx`, `useBillCalendar.ts`, `billUtils.ts`, `types.ts`, plus 4 sub-components in `components/`
- **`pages/YearInReviewPage.tsx`** (757 lines) split into `pages/year-in-review/` module: orchestrator + `useYearInReview.ts` + `heatmapUtils.ts` + `types.ts` + 4 new sub-components; existing StatCard/InsightRow/DayOfWeekChart moved into `components/`
- **`pages/TrendsForecastsPage.tsx`** (775 lines) split into `pages/trends-forecasts/` module with same structure
- **`pages/TaxPlanningPage.tsx`** (1,090 lines) split into `pages/tax-planning/` module with `useTaxPlanning.ts` + `taxPlanningUtils.ts` + `types.ts` + 6 sub-components (TaxPageActions, TaxTip, RegimeVerdictDetail, RegimeComparison, DeductionInput, MultiYearProjectionTable)
- **`core/analytics_engine.py`** -- extracted module-level helpers (`_group_txns_by_pattern`, `_resolve_pattern_display`, `_aggregate_holdings_data`) and constants (`DEFAULT_ESSENTIAL_CATEGORIES`, `DEFAULT_INVESTMENT_ACCOUNT_PATTERNS`) into new `core/_analytics_helpers.py` (full class-level mixin split deferred to a future PR)

---

## 2.1.1 - 2026-04-24

### Changed

- **`db/models.py`** split into `db/_models/` package (7 domain files: `enums.py`, `user.py`, `transactions.py`, `investments.py`, `analytics.py`, `planning.py` + `__init__.py` facade). `db/models.py` is now a 21-line re-export facade; consumer imports unchanged

---

## 2.1.0 - 2026-04-24

### Added

- **AI Finance Chatbot** (closes #90) -- floating chat widget (bottom-right) with glass-morphism UI, streaming token-by-token responses, and conversation history per session
- **Bring Your Own Key (BYOK)** -- configure OpenAI, Anthropic, or AWS Bedrock in Settings > AI Assistant; provider list updated to current models (O3, O4 Mini, GPT-4.1 family, GPT-4o family, Claude Opus 4.7, Sonnet 4.6, Haiku 4.5, Bedrock `us.anthropic.claude-*-v1` variants)
- **AES-256-GCM encryption** (`core/encryption.py`) -- API keys encrypted at rest with PBKDF2-HMAC-SHA256 key derivation
- **Bedrock streaming proxy** (`api/ai_chat.py`) -- browser cannot call Bedrock directly (requires SigV4 + binary EventStream parsing), so `POST /api/ai/bedrock/chat` proxies via `boto3.client('bedrock-runtime').converse_stream()`
- **Financial context builder** (`lib/chatContext.ts`) -- fetches monthly summaries, category breakdowns, recurring bills, net worth, and goals from existing V2 endpoints; compresses into a ~2-4K token system prompt so the AI has full financial context
- **Chat adapters** (`lib/chatAdapters.ts`) -- provider-specific streaming request builders and SSE stream parsers (OpenAI/Anthropic go browser-direct, Bedrock goes through backend proxy)
- **AI config endpoints** -- `PUT/GET/DELETE /api/preferences/ai-config` for configuring provider/model/api_key; `GET /api/preferences/ai-config/key` returns decrypted key for frontend streaming calls
- **`DecryptionError` class** -- raised when the JWT secret rotates between saving and using a key; frontend shows "re-enter your API key" prompt
- **Alembic migration** -- adds `ai_provider`, `ai_model`, `ai_api_key_encrypted` columns to `user_preferences`

### Fixed

- **Bedrock 400 errors from browser** -- bearer tokens don't work for Bedrock inference (SigV4 required), CORS is not supported. Proxy through backend fixed both issues
- **Chat widget double-rendering** -- `doStream()` was being called inside a `setMessages` updater, causing React StrictMode to spawn two parallel streams in dev; rewritten to call streaming outside the state updater
- **Recurring frequency enum bug** (closes #89) -- semi-annual, weekly, biweekly, quarterly recurring transactions were returning 500 errors due to missing enum values in PostgreSQL; fixed by migration `20260412_1200_add_missing_recurrence_enum_values.py`

---

## 2.0.0 - 2026-04-12

### Added

- **Client-side file parsing** -- Excel/CSV files are now parsed in the browser using SheetJS; only structured JSON rows are sent to the backend (breaking API change: `POST /api/upload` now accepts JSON body instead of multipart file)
- **Frontend file parser** (`lib/fileParser.ts`) -- lazy-loads SheetJS, computes SHA-256 hash via `crypto.subtle`, maps flexible column names, validates dates/amounts/types
- **Column mapping constants** (`constants/columns.ts`) -- shared column name mappings and valid transaction types
- **CSV upload support** -- dropzone now accepts `.csv` files in addition to `.xlsx` and `.xls`
- **Backend JSON upload schema** (`schemas/upload.py`) -- `TransactionRow` and `TransactionUploadRequest` Pydantic models for structured upload validation
- **`SyncEngine.import_rows()`** -- new method accepting pre-parsed JSON rows from the frontend
- **`DataNormalizer.normalize_from_dict()`** -- normalizes plain dicts (category corrections, account standardization, transfer resolution) for the JSON upload path
- **Income & tax projections** -- input your salary CTC structure (basic, HRA, special allowance, EPF, NPS, professional tax, variable pay) per fiscal year, with FY-to-FY navigation and editing
- **RSU grant management** -- add stock grants with vesting schedules; vesting amounts auto-projected with stock appreciation
- **Growth assumptions** -- configure annual salary hike, variable pay growth, stock appreciation, and projection horizon; projections compound from the latest salary FY
- **Multi-year tax comparison table** -- side-by-side projected gross, tax, and net across future fiscal years
- **Projection calculator** (`lib/projectionCalculator.ts`) -- pure functions for multi-year salary/RSU/tax projection with full TDD test coverage
- **Salary Pydantic schemas** -- `SalaryComponents`, `RsuGrant`, `GrowthAssumptions` with backend validation
- **Three new preference endpoints** -- `PUT /api/preferences/salary-structure`, `PUT /api/preferences/rsu-grants`, `PUT /api/preferences/growth-assumptions`
- **Alembic migration** -- adds `salary_structure`, `rsu_grants`, `growth_assumptions` JSON columns to `user_preferences`
- **Indirect Tax (GST) analysis page** -- estimates GST paid on expenses using official slab rates (0/3/5/18/28%) with subcategory-first lookup; includes summary cards, GST-by-slab donut chart, monthly trend bar chart, and category breakdown table
- **YoY % change badges** -- Tax Planning summary cards show year-over-year percentage changes comparing actual data for past FYs and salary projections for current/future FYs
- **Stock price auto-fetch** -- RSU grant editor fetches live stock prices via Yahoo Finance backend proxy (`GET /api/stock-price/{symbol}`), auto-converts from stock currency to display currency
- **Account reset modes** -- Profile modal offers two reset options: "Reset Transactions" (preserves preferences, budgets, goals) and "Complete Reset" (wipes everything); backend `POST /api/auth/account/reset` accepts `mode` query parameter (`full` | `transactions`)
- **Analytics refresh endpoint** -- `POST /api/analytics/v2/refresh` recomputes all pre-aggregated analytics tables synchronously; called by the frontend after upload instead of relying on `BackgroundTasks` (which can be killed on Vercel serverless before completion)

### Changed

- **Upload pipeline** -- four-phase UX (Parsing -> Processing -> Uploading -> Computing Analytics) with clear progress indication; analytics refresh is a separate synchronous request for serverless reliability; force-reupload reuses already-parsed data without re-parsing the file
- **Upload API** -- `POST /api/upload` now accepts `{ file_name, file_hash, rows, force }` JSON body instead of multipart file upload (breaking change)
- **SheetJS upgraded** -- migrated from vulnerable npm `xlsx@0.18.5` to CDN-distributed `xlsx@0.20.3` (fixes GHSA-4r6h-8v6p-xvw6 Prototype Pollution and GHSA-5pgg-2g8v-p4x9 ReDoS)
- **Upload error handling** -- Axios error codes mapped to user-friendly toast messages; conflict state (duplicate file) now offers force-reupload inline
- **Tax Planning page** -- extended with salary-based projection toggle, stacked paid-vs-projected tax bars in yearly chart, projection-aware labels throughout
- **Settings page** -- new "Income & Salary Structure" section with salary grid, RSU grant editor, and growth assumption sliders
- **preferencesStore** -- hydration logic extracted into standalone pure helpers to reduce cognitive complexity (SonarCloud finding)
- **tsconfig** -- bumped `lib` from ES2022 to ES2023 for `Array.findLast()` support

### Removed

- **Server-side file parsing for uploads** -- backend no longer receives raw Excel/CSV files via the upload endpoint (CLI still uses the old file-based `import_file()` path)
- **Temporary file handling** -- removed temp file creation, magic byte validation, and chunked file reading from upload endpoint

### Fixed

- **Upload stuck with no feedback in production** (issue #72) -- root cause was Vercel 60s serverless timeout combined with large file uploads; solved by moving parsing to the client
- **Zero-amount transactions rejected** -- `TransactionRow.amount` validation changed from `gt=0` to `ge=0` to accept valid zero-amount rows
- **Sticky PageHeader consistency** -- moved PageHeader outside Framer Motion containers on InsightsPage, FIRECalculatorPage, and TaxPlanningPage so `position: sticky` works correctly (CSS `transform` from animations was breaking it)
- **Scroll-to-top on navigation** -- reset `#main-content` scroll position on route change so every page starts from the top
- **Returns Analysis FY switching** (issue #88) -- CAGR and Monthly ROI now update when changing fiscal year
- **TaxPlanningPage cognitive complexity** -- extracted helper functions to bring SonarCloud score under threshold
- **Chart hover/tooltip standardization** -- consistent hover states and tooltip styling across all chart pages
- **SonarCloud findings** -- `localeCompare` for string sorts, `Number.parseInt` over global `parseInt`, `findLast` over `filter().at(-1)`, extracted nested ternaries, composite keys for RSU vesting rows
- **SonarCloud cognitive complexity** -- extracted helper functions in QuickInsights, InvestmentAnalyticsPage, generateDerivedData, and generateTransactions to bring S3776 scores under threshold
- **Projected tax bar color** -- uses orange instead of green to distinguish from paid tax
- **Sidebar double-highlight** -- NavLink `end` prop prevents parent route from highlighting alongside child

---

## 1.9.0 - 2026-04-10

### Added

- **Multi-currency display conversion** -- view all financial data in 15 currencies (USD, EUR, GBP, JPY, CAD, AUD, CHF, SGD, AED, CNY, KRW, SEK, NZD, HKD) with live ECB exchange rates via frankfurter.dev
- **Currency quick-switcher** -- sidebar currency selector for instant switching with rate indicator pill showing live conversion (e.g., "1 USD = INR 92.68")
- **Exchange rate proxy** -- backend endpoint (`GET /api/exchange-rates`) with 24h in-memory cache and three-tier fallback (fresh cache, stale cache, hardcoded rates)
- **Currency metadata constants** -- `currencies.ts` with locale, symbol, number format, and short unit configuration per currency
- **`display_currency` preference** -- persisted per-user with Alembic migration, auto-derives number format, symbol, and symbol position
- **Google Analytics tracking** -- site-wide analytics via G-PFFMG7D8DP

### Changed

- **Settings: Display Preferences** -- replaced manual number format, currency symbol, and symbol position fields with a single "Display Currency" dropdown that auto-derives all three
- **Formatters** -- `formatCurrency`, `formatCurrencyCompact`, and `formatCurrencyShort` now apply exchange rate conversion automatically via `convertAmount()` helper
- **Formatter short units** -- `formatCurrencyShort` switches between Cr/L/K (INR) and B/M/K (international) based on display currency metadata
- **Documentation rebrand** -- updated README, CLAUDE.md, package.json, pyproject.toml, and all docs with "Personal Finance Dashboard" tagline and compelling descriptions

---

## 1.8.1 - 2026-04-05

### Added

- **Pre-computed daily summaries** -- `daily_summaries` table for instant heatmap rendering (income, expenses, net, transaction counts, top category per day)
- **Daily summaries API** -- `GET /api/analytics/v2/daily-summaries` with date range and limit filters
- **Investment holdings API** -- `GET /api/analytics/v2/investment-holdings` with active_only filter and portfolio summary
- **Auto-populate investment holdings** -- analytics engine dynamically detects investment accounts from preferences and computes net invested amounts from transfer flows

### Changed

- **Calculations fast path** -- `get_totals`, `get_monthly_aggregation`, and `get_category_breakdown` read from pre-computed tables when no date filter is active
- **YearInReview heatmap** -- uses daily summaries instead of scanning all transactions

### Fixed

- User scoping, cascades, indexes, and constraints added to database schema
- Regex backtracking risk eliminated in note normalization
- SonarCloud findings resolved, mobile responsiveness improved

---

## 1.8.0 - 2026-04-05

### Added

- **Demo mode** -- "Try Demo" on landing page lets visitors explore the full dashboard with ~500 realistic sample transactions, zero signup
- **Smart account classification** -- keyword matching for EPF, PPF, Mutual Funds, Groww, Zerodha, FD, Stocks, Gold, Crypto as Investments; Indian banks as Bank Accounts; EMI/mortgage as Loans
- **Auto income classification** -- Salary/Freelance to Taxable, Dividends/Interest to Investment Returns, Cashbacks to Non-taxable
- **Auto investment account mapping** -- MF/Groww to mutual_funds, Stocks/Zerodha to stocks, FD to fixed_deposits, PPF/EPF/NPS to ppf_epf
- **Backend migrated to Vercel** -- replaced Render (30-50s cold starts) with Vercel serverless via Mangum for zero cold start latency
- **Neon database via Vercel integration** -- unified dashboard management
- **Alembic migration workflow** -- GitHub Actions runs migrations on push to main when schema files change

### Changed

- Demo banner uses floating pill overlay instead of sticky bar
- Account classification priority: Credit Cards > Investments > Loans > Bank Accounts > Cash > Other

---

## 1.7.0 - 2026-03-14

### Added

- Code quality rules in CLAUDE.md (200-line file limit, import conventions, design system constraints)
- `CHART_TEXT`, `CHART_SURFACE`, `CHART_INPUT` constants for centralized chart styling

### Changed

- Split 4 oversized pages into focused components: SettingsPage (20 files), SubscriptionTrackerPage (13), GoalsPage (13), ComparisonPage (13)
- Chart styling migrated from raw hex values to shared constants

---

## 1.6.0 - 2026-03-03

### Added

- **Analytics V2 API** -- stored aggregations for monthly summaries, category trends, transfer flows, recurring transactions, merchant intelligence, net worth, fiscal year summaries
- **Sidebar** -- collapsible navigation groups, user profile, search, dynamic badge counts, notification center
- 11 new pages: Returns Analysis, Tax Planning, Cash Flow Forecast, Net Worth, Budget, Subscription Tracker, Spending Analysis, Investment Analytics, Dashboard, Profile Modal
- Standard chart components: `StandardAreaChart`, `StandardBarChart`, `StandardPieChart`

### Changed

- Backend restructured with FastAPI routers, middleware, and error handling
- OAuth authentication (Google, GitHub) via authorization code flow

---

## 1.5.0 - 2026-03-01

### Added

- Settings page complete rebuild (single scrollable page with glass cards)
- Financial Goals with savings pool allocation and smart projections
- Subscription Tracker with confirm/manual add, Bill Calendar integration
- User preferences API with comprehensive Pydantic models
- OAuth fields in users table

### Changed

- UI/UX polish across 18+ pages (hover states, visual hierarchy, animations)
- Goal/budget/anomaly creation refreshes lists without page reload

### Fixed

- Goal/budget creation sent as JSON body instead of query params
- GoalsPage vertical gaps and animation issues

---

## 1.4.0 - 2026-02-28

### Added

- Deployment configuration (GitHub Pages + Neon PostgreSQL + backend hosting)
- `AnalyticsEngine` for post-upload data analytics
- CI workflow with GitHub Actions (lint, type-check, build, deploy)

### Changed

- SQLite-only `strftime` replaced with database-agnostic date formatting
- React Router basename configured for `/ledger-sync/` subpath
- GitHub Actions pinned to commit SHAs for security

### Fixed

- `email-validator` dependency for Pydantic `EmailStr`
- `frontend/src/lib/` tracked (previously ignored by `.gitignore`)
- TypeScript errors and SonarQube findings resolved

---

## 1.3.0 - 2026-02-21

### Added

- Core architecture: React Router, authentication, page routing, dark theme
- iOS-inspired color palette with Framer Motion animations
- `AppLayout`, `PageHeader`, `MetricCard`, `Sparkline`, `ChartEmptyState`
- Investment Analytics, Comparison, and Net Worth pages
- Financial calculation APIs (totals, aggregations, category breakdowns)
- `useAnalyticsTimeFilter` hook for centralized time filter state
- Command palette, comprehensive documentation

### Changed

- Frontend rebuilt from Next.js to React + Vite SPA
- Backend migrated to layered architecture with SQLAlchemy 2.0

---

## 1.2.0 - 2026-02-04

### Added

- Authentication flow (login, registration, protected routes)
- ComparisonPage, Year in Review (heatmap + insights), Budget management, Anomaly review
- Settings tabs (essential categories, income classification, investment mappings)
- Financial Health Score (8 metrics across 4 pillars)
- Spending Velocity Gauge

### Changed

- Chart tooltips with glass styling, natural line types, enhanced animations
- Date handling standardized with `getDateKey`
- API endpoints migrated to `DatabaseSession`

---

## 1.1.0 - 2026-01-27

### Added

- PostgreSQL support
- Recurring transaction detection, top merchants analytics
- Year-over-year comparison charts
- Tax Planning page, Trends & Forecasts page
- CSV export endpoint, period navigation

### Changed

- Currency formatting refactored for consistency across all pages

---

## 1.0.0 - 2026-01-09

### Added

- Backend sync engine for Excel imports (Money Manager Pro format)
- Data ingestion pipeline: `excel_loader` -> `normalizer` -> `validator` -> `hash_id`
- SHA-256 transaction deduplication (idempotent re-uploads)
- Frontend with layout, homepage, and initial analytics
- Income-Expense Flow page, ExpenseTreemap, QuickInsights
- Date range filtering, income categorization with financial year grouping
- Testing setup, structured logging, comprehensive documentation
- MIT License