## Why
### Problem
As new features are added, dashboard and settings pages have been
accumulating extra database queries — risking N+1 regressions and
degraded response times. There was no automated way to detect these
regressions before they reach production.
## What
### Changes
- **New `Performance` test suite** (`tests/Performance/`) with 25 tests
that enforce query count ceilings on every page and API endpoint
accessible from the sidebar
- `PageQueryCountTest.php` — 15 tests covering Dashboard, Accounts,
Transactions, Budgets, Cashflow, and all Settings pages, plus scaling
tests that prove query count stays constant as data volume grows
- `ApiQueryCountTest.php` — 10 tests covering all Dashboard and Cashflow
analytics API endpoints, plus scaling tests
- **Separate CI job** (`performance-tests`) that runs in parallel with
`tests` and `linter`, and gates both `build-image` and `deploy`
- **Excluded from the main `tests` job** to avoid running them twice
(`--exclude-testsuite=Performance`)
- Helper functions (`performanceSeedUser`, `countQueries`,
`assertMaxQueries`) added to `tests/Pest.php` for reuse
- `phpunit.xml` updated with the new `Performance` testsuite entry
### How it works
Each test seeds a user with realistic data (3 accounts, 30 transactions,
15 balances, 5 categories, 3 labels, 1 budget) and asserts the endpoint
executes **at most N queries**. Thresholds have a small buffer (~3
queries) above current counts — tight enough to catch N+1 regressions
(which add dozens of queries) but loose enough to not break on minor
legitimate changes. On failure, every executed SQL query is dumped for
easy debugging.
### Run locally
```bash
php artisan test --testsuite=Performance
```
## Verification
### Tests
All 25 performance tests pass. Existing Feature (633) and Unit (22)
suites unaffected.
## Summary
- Removed all `setupEncryptionKey()` / `visitWithEncryptionKey()` calls
from browser tests — encryption key setup in localStorage is no longer
needed since new users don't have encryption
- Removed `encryption_salt` from `UserFactory::onboarded()` state and
`OnboardingFlowTest` user creation
- Removed `name_iv` from `Account::factory()` calls in
`BankAccountsTest`
- Deleted `DemoEncryptionService` and its unit test — demo command now
stores plaintext account names and transaction descriptions
- Removed `demoEncryptionKey` Inertia shared prop and `encryption_key`
from demo config
- Removed encryption helper methods from `TestCase.php` and global
`setupEncryptionKey()` from `Pest.php`
## Test plan
- [x] Run `php artisan test --exclude-testsuite=Browser` — all
non-browser tests pass
- [x] Run `php artisan test --testsuite=Browser` — browser tests pass
without encryption key setup
- [x] Run `php artisan demo:reset` — demo account created with plaintext
data
- [x] Verify existing encryption migration tests still pass
(`EncryptionTest`, `DecryptTransactionsTest`,
`PlaintextTransactionsTest`)
This PR simplifies the IndexedDB synchronization mechanism by removing
individual sync controllers and services, and instead using Inertia.js
shared props to provide data globally across the application.
## Benefits
1. **Simplified Architecture**: Removed complex sync logic for
static/semi-static data (accounts, categories, banks, labels, automation
rules)
2. **Better Performance**: Data is now shared via Inertia props,
eliminating unnecessary API calls and IndexedDB operations
3. **Reduced Complexity**: Significantly reduced codebase size (~2000
lines removed)
4. **Better UX**: Data is immediately available on page load without
waiting for sync operations
5. **Maintainability**: Fewer moving parts means easier to maintain and
debug
## Migration Notes
- Transaction syncing still uses IndexedDB for offline support
- All other data (accounts, categories, banks, labels, automation rules)
is now fetched via Inertia shared props
- Components automatically receive updated data on navigation without
manual sync operations