## Why
The `/accounts` page was making two extra client-side API calls via the
`useDashboardData()` hook:
- `/api/dashboard/net-worth-evolution` (~14.5s in dev)
- `/api/dashboard/top-categories` (~119ms, completely unused on this
page)
The backend logic itself is fast (~34ms), but the full middleware stack
cost of extra HTTP roundtrips in the dev environment was adding
significant overhead.
## What
### Backend (`AccountController.php`)
- Added `ExchangeRateService` constructor injection
- Added `Inertia::defer()` prop `accountMetrics` to the `index()` action
- Added `getAccountMetrics()`, `formatMonth()`, `convertBalance()`
private methods
- Uses `BalanceLookup::forAccounts()` for efficient batch loading (2 SQL
queries regardless of account count)
### Frontend (`Accounts/Index.tsx`)
- Removed `useDashboardData()` hook import and usage
- Added `accountMetrics` as an optional deferred prop (undefined until
resolved)
- Loading state derived from `!accountMetrics`, which naturally drives
the existing `loading` prop on `AccountListCard` (skeleton UI already
existed)
- Added `handleBalanceUpdated` callback using `router.reload({ only:
['accountMetrics'] })` for targeted refresh
## Verification
### Tests
- 3 new Pest tests covering deferred prop behavior:
- `accounts index defers account metrics` — verifies metrics are missing
from initial response, present after `loadDeferredProps()`, with correct
balance values
- `accounts index deferred metrics includes invested amount for
investment accounts` — verifies invested amount for investment account
types
- `accounts index deferred metrics returns null invested amount for
non-investment accounts` — verifies non-investment accounts get null
invested amount
- All 15 tests in `AccountControllerTest.php` pass
## Summary
- Store account names in plaintext instead of encrypting them
client-side. Encryption remains only for transaction descriptions and
notes.
- Existing encrypted account names are silently migrated when the user
unlocks their encryption key, via a new `useDecryptAccountNames` hook
that calls API endpoints to update each account.
- New `AccountName` component handles rendering both encrypted (legacy)
and plaintext accounts across the app.
## Test plan
- [x] Create a new account — name should be stored in plaintext
(`encrypted = false`, `name_iv = null`)
- [x] Unlock encryption key with existing encrypted accounts — they
should auto-migrate silently
- [x] After migration, verify accounts show `encrypted = false` in DB
and names are readable plaintext
- [x] Edit a migrated account — should work without encryption
- [x] Verify all account name displays work across dashboard, account
details, transactions, settings