diff --git a/HISTORY.md b/HISTORY.md
index 09d8f12..1956168 100644
--- a/HISTORY.md
+++ b/HISTORY.md
@@ -10,6 +10,16 @@
- **Migration version sync assertion** — `_runMigrationVersions` module-level variable is now populated by `runMigrations()` before its loop runs. `reconcileLegacyMigrations()` — which runs after `runMigrations()` on legacy-DB upgrade paths — compares its own version array against the stored list and throws a descriptive error if any version appears in one array but not the other. Catches drift between the two migration arrays at startup rather than silently misconfiguring a legacy schema.
+- **Encryption key fully app-managed — no env var required** — `TOKEN_ENCRYPTION_KEY` environment variable support removed entirely. The auto-generated DB key (`_auto_encryption_key` in the settings table) is now the primary mechanism, not a fallback. The `[security]` warning that fired on every startup when no env var was set is gone. On first startup a 48-byte cryptographically random key is generated and persisted to the database; subsequent restarts reuse it. All existing encrypted data (SMTP password, OIDC secret, SimpleFIN tokens, push notification tokens) continues to decrypt correctly.
+
+- **TrackerPage crash fixed — `activeTotalExpected` temporal dead zone** — The `cashflow` block added in an earlier change referenced `activeTotalExpected` and `activePaidTowardDue` before their `const` declarations. JavaScript's temporal dead zone caused `Cannot access 'activeTotalExpected' before initialization` on every tracker load. Fixed by moving the four `active*` declarations above the cashflow block that depends on them.
+
+- **Bank merchant rule matching — `balance_delta`, `current_balance`, and error handling** — `billMerchantRuleService.js` was the only payment creation path still missing `balance_delta` computation and `current_balance` update after the #49 fix. Both `applyMerchantRules` (full-user batch matching, called on every sync) and `syncBillPaymentsFromSimplefin` (single-bill retroactive match) now read the bill fresh before each payment, compute `computeBalanceDelta`, include `balance_delta` in the INSERT, and update `bills.current_balance`. `payment_source` corrected from `'auto_match'` (not in `VALID_PAYMENT_SOURCES`) to `'provider_sync'`. DB migration `v0.82` updates all historical `auto_match` records. Both functions also gained full error handling — `txRows` queries, the fallback notes query, and `db.transaction()` blocks are all wrapped with try/catch that logs to console and returns safe zero-match defaults instead of propagating exceptions to the sync worker or API caller.
+
+- **`applyMerchantRules` returns matched bill names** — The function now returns `{ matched, matched_bills: ['AT&T', 'Netflix'] }` instead of just `{ matched }`. Bill name is fetched via JOIN in the rules query. The name set is deduplicated via `Map` keyed by bill_id (so one bill matched by multiple transactions still appears once). The name list is bubbled through `bankSyncService.runSync` and the `POST /api/data-sources/sync-all` endpoint (using a `Set` to dedupe across multiple SimpleFIN sources). TrackerPage Sync Bank toast now reads **"Synced — AT&T, Netflix ✓"** instead of "3 payments matched", with a `(+N more)` suffix when there are more matched bills than bill names to display. Toast duration extended to 5 seconds.
+
+- **BillModal — bank matching Sync button moved, fixed, and made reactive** — The old "SimpleFIN payment history" sync button in the Subscriptions tab was removed (it didn't call `loadLinkedTransactions()` after success and was less discoverable). A new **Sync** button lives in the Bank Matching Rules section header of the Transactions tab, visible whenever `localHasRules` is true. A local `localHasRules` state initialises from `sourceBill.has_merchant_rule` but flips to `true` immediately when `onRulesChanged` fires — so the button appears right after adding the first rule without closing and reopening the modal. After a successful sync, `loadLinkedTransactions()` is called to refresh the Linked transactions list below, and `refetch()` updates the parent tracker view.
+
- **Pin Due — urgent bills float to top of tracker** — A "Pin Due" toggle button in the TrackerPage header sorts overdue and due-soon bills to the top of each bucket when enabled. Priority order: `missed` → `late` → `due_soon` → `upcoming` → everything else; ties broken by `due_day`. The sort runs after filtering but before the bucket split, so each half-month bucket is sorted independently. The button uses `variant="default"` (solid) when active and `variant="outline"` when off so the current mode is always visible. Preference persists across sessions via `localStorage` under `tracker_pin_upcoming`. Drag reorder is automatically disabled while the toggle is on (`reorderEnabled` now also requires `!pinUpcoming`) since the two modes conflict.
- **Tracker row keyboard navigation** — Tracker rows (desktop table view) are now keyboard navigable. Each row has `tabIndex={0}`, `data-tracker-row`, `aria-rowindex`, and an `aria-label` announcing the bill name, status, and due day. A `focus-visible:ring-2 ring-primary/60 ring-inset` focus ring appears on keyboard focus only. Key bindings: `↓`/`j` focuses the next row, `↑`/`k` the previous (both cross bucket boundaries via `querySelectorAll('[data-tracker-row]')`), `Enter` opens the edit modal, `P` toggles paid/unpaid (skipped bills ignored, `Ctrl+P`/`Cmd+P` passes through to the browser), `Esc` blurs the row. The `onKeyDown` handler guards against firing on nested interactive elements with `if (e.target !== e.currentTarget) return`.
diff --git a/client/components/BillModal.jsx b/client/components/BillModal.jsx
index 689df6a..827adb4 100644
--- a/client/components/BillModal.jsx
+++ b/client/components/BillModal.jsx
@@ -143,6 +143,9 @@ export default function BillModal({ bill, initialBill, categories, onClose, onSa
const [snowballInclude, setSnowballInclude] = useState(!!sourceBill?.snowball_include);
const [snowballExempt, setSnowballExempt] = useState(!!sourceBill?.snowball_exempt);
const [syncingPayments, setSyncingPayments] = useState(false);
+ // Track whether rules exist locally so the Sync button appears immediately
+ // after the first rule is added without waiting for sourceBill to refetch.
+ const [localHasRules, setLocalHasRules] = useState(!!sourceBill?.has_merchant_rule);
const [showDebtSection, setShowDebtSection] = useState(
() => isDebtCat(categories, sourceBill?.category_id ? String(sourceBill.category_id) : CAT_NONE)
|| !!sourceBill?.snowball_include
@@ -694,44 +697,7 @@ export default function BillModal({ bill, initialBill, categories, onClose, onSa
/>