diff --git a/.env.example b/.env.example
index a43ea32..00f34bc 100644
--- a/.env.example
+++ b/.env.example
@@ -7,11 +7,23 @@
PORT=3000
NODE_ENV=production
+# ── Reverse proxy ──────────────────────────────────────────────────────────────
+# Set when running behind a reverse proxy (Docker, nginx, Traefik, Caddy, etc.)
+# so req.ip reflects the real client (rate limiting, audit logs, login history)
+# and req.secure reflects the original protocol (Secure cookies).
+# TRUST_PROXY=true (trust first proxy hop — most common)
+# TRUST_PROXY=2 (trust two hops, e.g. CDN + nginx)
+# TRUST_PROXY=loopback (trust loopback addresses only)
+# Leave unset for direct deployments with no proxy.
+# TRUST_PROXY=true
+
# ── CSRF Cookie httpOnly Setting ──────────────────────────────────────────────
# CSRF cookie httpOnly setting (default: true)
-# Set CSRF_HTTP_ONLY=false to allow JavaScript access for SPA CSRF patterns
-# CSRF_HTTP_ONLY: "true" (secure, default - cookie not readable by JS)
-# CSRF_HTTP_ONLY: "false" (SPA mode - allows JavaScript to read cookie)
+# The SPA fetches the token from GET /api/auth/csrf-token and stores it in
+# memory — JavaScript does not need to read the cookie directly. httpOnly=true
+# removes the token from the XSS-accessible cookie surface.
+# CSRF_HTTP_ONLY: "true" (default — cookie not readable by document.cookie)
+# CSRF_HTTP_ONLY: "false" (legacy — only if a custom client reads document.cookie)
#
# ── CSRF Cookie sameSite Setting ──────────────────────────────────────────────
# CSRF cookie sameSite setting (default: strict)
@@ -37,6 +49,22 @@ NODE_ENV=production
# DB_PATH=/opt/bill-tracker/data/db/bills.db
# BACKUP_PATH=/opt/bill-tracker/data/backups
+# ── Encryption key ────────────────────────────────────────────────────────────
+# AES-256-GCM key used to encrypt secrets at rest (SimpleFIN tokens, SMTP passwords).
+# Must be at least 32 bytes. Any printable string works; a random hex string is best.
+#
+# Generate one with: node -e "console.log(require('crypto').randomBytes(48).toString('hex'))"
+#
+# If not set, Bill Tracker auto-generates a key and stores it in the database
+# next to the encrypted data — anyone with database read access can decrypt.
+# Set this variable in production to keep the key separate from the data.
+#
+# TOKEN_ENCRYPTION_KEY=replace-with-a-long-random-string-at-least-32-chars
+
+# ── Bank Sync (SimpleFIN) ─────────────────────────────────────────────────────
+# Enable/disable bank sync from the Admin panel. Users connect their own
+# SimpleFIN Bridge from the Data page. No environment config required.
+
# ── First-run admin account ────────────────────────────────────────────────────
# Set BOTH on first start to create the admin account automatically.
# Remove or comment out after the server has started once — they are not
diff --git a/.forgejo/workflows/ci.yml b/.forgejo/workflows/ci.yml
new file mode 100644
index 0000000..b77bef3
--- /dev/null
+++ b/.forgejo/workflows/ci.yml
@@ -0,0 +1,35 @@
+# CI — syntax check, server tests (node:test), client tests (Vitest), Vite build.
+# Runs on every push and pull request so no change lands unverified.
+#
+# Forgejo Actions reads .forgejo/workflows/. The container image matches the
+# Dockerfile runtime (node:22) so better-sqlite3 prebuilds resolve identically.
+
+name: CI
+
+on:
+ push:
+ pull_request:
+
+jobs:
+ ci:
+ runs-on: docker
+ container:
+ image: node:22-bookworm
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v4
+
+ - name: Install dependencies
+ run: npm ci
+
+ - name: Syntax check (server)
+ run: npm run check:server
+
+ - name: Server tests (node:test)
+ run: npm run test
+
+ - name: Client tests (Vitest)
+ run: npm run test:client
+
+ - name: Build (Vite)
+ run: npm run build
diff --git a/.gitignore b/.gitignore
index a9d2e87..4c4446e 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,3 +1,14 @@
+# Private project/agent docs — never commit
+DEVELOPMENT_LOG.md
+PROJECT.md
+STRUCTURE.md
+BUILD_SUMMARY.md
+SCRIPTS.md
+project-requirements.md
+.learnings/
+simplefin_no_git/
+
+# Dependencies
node_modules/
dist/
db/*.db
@@ -6,3 +17,26 @@ db/*.db-wal
backups/
.env
*.log
+simplefin-bank-sync-issue.md
+project-wide-data-input-and-sync-issue.md
+docs/SIMPLEFIN_CONSUMER_GUARDRAILS.md
+
+# Playwright E2E run artifacts (visual baselines under e2e/**/*-snapshots/ ARE committed)
+test-results/
+playwright-report/
+blob-report/
+.playwright/
+e2e/.auth/
+db/e2e.db*
+
+# MkDocs docs site (auto-generated, not part of app source)
+mkdocs/
+
+# Root bill tracker DB (empty artifact, never commit)
+/bills.db
+Mobile App/PLAN.md
+mobile/
+
+# LIVE production DB copy for SimpleFIN testing — REAL financial data + secrets.
+# NEVER commit. Whole dir is ignored.
+tests/Site-BKUP-Test/
diff --git a/.learnings/bishop/ERRORS.md b/.learnings/bishop/ERRORS.md
new file mode 100644
index 0000000..6225533
--- /dev/null
+++ b/.learnings/bishop/ERRORS.md
@@ -0,0 +1,55 @@
+# Errors Logged During Phase 1 Verification
+
+No errors encountered during Build-Verify Phase 1.
+
+---
+
+## v0.40.0 Documentation Update
+
+No errors encountered during release notes and API reference update.
+
+**Date**: 2026-06-14
+**Task**: Documentation update for v0.40.0
+**Status**: Complete
+
+
+---
+
+## Release Notes and API Documentation Update v0.40
+
+### Errors Encountered
+
+1. **Initial edit failed - exact match required**
+ - Attempted to add new version sections to release-notes/index.md using `edit` tool
+ - Error: `Could not find edits[2] in ... The oldText must match exactly including all whitespace and newlines.`
+ - Resolution: Read the current file to understand its structure, then made targeted edits for version header changes
+
+2. **API Reference nav placement verification**
+ - Checked if api-v040.md was already in mkdocs.yml nav
+ - Found already present with correct alphabetical ordering
+ - No action needed
+
+### Non-Obvious Findings
+
+1. **Release notes already had v0.38.x sections**
+ - File already contained v0.38.0 through v0.38.4 subsections
+ - Only needed to update version header from 0.37.0 to 0.40.0
+ - The file was already partially updated
+
+2. **mkdocs.yml already has api-v040.md entry**
+ - `API Reference (v0.40): technical/api-v040.md` was already present
+ - Ordered alphabetically correctly (v0.40 before v0.37)
+ - No action needed
+
+3. **New bank transactions endpoint path**
+ - Endpoint is `/api/transactions/bank-ledger`, not `/api/bank-transactions`
+ - Confirmed by `grep -E "router\\.(get|post|patch|put|delete)" routes/transactions.js`
+
+4. **cents-migration-plan.md location**
+ - File exists at `./docs/cents-migration-plan.md` (not in technical/ subdirectory)
+ - Correct relative link from api-v040.md is `../cents-migration-plan.md`
+
+---
+
+**Date**: 2026-06-14
+**Task**: Documentation update for v0.38-v0.40 release notes and API reference
diff --git a/.learnings/bishop/LEARNINGS.md b/.learnings/bishop/LEARNINGS.md
new file mode 100644
index 0000000..b96c1da
--- /dev/null
+++ b/.learnings/bishop/LEARNINGS.md
@@ -0,0 +1,109 @@
+# Learnings from Phase 1 Verification
+
+## Business Logic Extraction — Verification Summary
+
+### What Was Verified
+
+1. **Build Success**: ✅ `docker build --no-cache -t bill-tracker:local .` completed successfully
+ - 1764 modules transformed
+ - Build time: 1.91s
+ - Output: 35 JS chunks for code splitting
+
+2. **Container Start**: ✅ Container starts cleanly with migrations applied
+ - All 46 migrations applied correctly
+ - Database initialization successful
+ - No errors in startup logs
+
+3. **Services/billsService.js Existance**: ✅ Verified
+ - All 8 expected exports present:
+ - `parseDueDay()`
+ - `parseInterestRate()`
+ - `validateCycleDay()`
+ - `getDefaultCycleDay()`
+ - `validateBillData()`
+ - `getValidCycleTypes()`
+ - `VALID_VISIBILITY`
+ - `validateCycleDayOnly()`
+
+4. **Routes/bills.js Integration**: ✅ Verified
+ - Imports from `../services/billsService`
+ - `validateBillData()` call in POST `/api/bills` endpoint
+ - `validateBillData()` call in PUT `/api/bills/:id` endpoint
+ - No inline validation logic remaining in routes
+
+### No Errors Found
+
+The business logic extraction is complete and working correctly. All validation logic has been moved from routes to the service layer, maintaining the same behavior.
+
+### Test Notes
+
+- Docker client version (1.42) is older than required (1.44) for docker compose
+- Workaround: Used `docker run` directly instead of `docker compose`
+- Existing container stopped and removed before starting fresh build
+
+### Files Created
+
+- `.learnings/bishop/ERRORS.md` — Error log (empty - no errors)
+- `.learnings/bishop/LEARNINGS.md` — This file
+
+---
+
+## Release Notes and API Documentation Update v0.40
+
+### What Was Done
+
+1. **Updated Release Notes** (`mkdocs/docs/release-notes/index.md`)
+ - Current Release version updated from `0.37.0` to `0.40.0`
+ - Added v0.38.x section with sub-sections for v0.38.0, v0.38.1, v0.38.2, v0.38.3, v0.38.4
+ - Added v0.39.0 section documenting settings auto-save, safe-to-spend toggle, and notifications UI move
+ - Added v0.40.0 section documenting bank transactions ledger page, merchant/store matching service, and transaction matching refactor
+
+2. **Created API Reference v0.40** (`mkdocs/docs/technical/api-v040.md`)
+ - Documented new endpoints added in v0.38-v0.40
+ - Settings endpoints (`PUT /api/settings`)
+ - Transactions endpoints (`GET /api/transactions/bank-ledger`, `POST /api/transactions/auto-categorize`, `POST /api/transactions/:id/apply-merchant-match`, etc.)
+ - Bank Transactions ledger page endpoints
+ - Matching service endpoints
+ - Included worked curl examples for high-impact endpoints
+
+3. **Verified mkdocs.yml**
+ - `API Reference (v0.40): technical/api-v040.md` already present under Reference section
+ - Alphabetically ordered correctly (v0.40 before v0.37)
+
+4. **Internal Link Verification**
+ - All user-guide links point to existing files
+ - All technical doc links point to existing files
+ - `cents-migration-plan.md` link corrected from `cents-migration-plan.md` to `../cents-migration-plan.md`
+
+### Files Modified
+
+- `mkdocs/docs/release-notes/index.md` — Updated release notes with v0.38.x, v0.39.0, v0.40.0 sections
+- `mkdocs/docs/technical/api-v040.md` — Created new API reference file
+
+### No Errors Found
+
+All documentation updates completed successfully with proper internal linking.
+
+### Test Notes
+
+- No mkdocs build attempted in sandbox (mkdocs not installable)
+- Manual verification of nav structure and internal links performed
+- Git commit avoided per instructions (Ripley owns git)
+
+### Files Created
+
+- `mkdocs/docs/technical/api-v040.md` — New API reference for v0.38-v0.40
+
+### Learnings
+
+- The release notes structure needed granular v0.38.x sub-sections per version (v0.38.0 through v0.38.4)
+- The new bank transactions endpoint is at `/api/transactions/bank-ledger`, not `/api/bank-transactions`
+- The settings auto-save endpoint is `PUT /api/settings` with partial update support
+- The merchant/store matching service is invoked via `/api/transactions/auto-categorize` and `/api/transactions/:id/apply-merchant-match`
+
+---
+
+**Verified By**: Bishop (subagent)
+**Date**: 2026-06-14
+**Version**: v0.40.0
+**Task**: Documentation update for v0.38-v0.40 release notes and API reference
diff --git a/.learnings/neo/ERRORS.md b/.learnings/neo/ERRORS.md
new file mode 100644
index 0000000..f05d136
--- /dev/null
+++ b/.learnings/neo/ERRORS.md
@@ -0,0 +1,9 @@
+# Bill Tracker - Neo Errors Log
+
+## 2026-05-11 - Phase 1 Business Logic Extraction
+
+### Errors Encountered
+- None - extraction completed successfully on first attempt
+
+### Notes
+-工程参考手册 does not exist in the project directory (expected to be under `Projects/bill-tracker/`)
diff --git a/.learnings/neo/LEARNINGS.md b/.learnings/neo/LEARNINGS.md
new file mode 100644
index 0000000..35b0000
--- /dev/null
+++ b/.learnings/neo/LEARNINGS.md
@@ -0,0 +1,45 @@
+# Bill Tracker - Backend Refactoring Learnings
+
+## 2026-05-11 - Phase 1 Business Logic Extraction
+
+### Task
+Extract business logic from `routes/bills.js` into a dedicated service layer (`services/billsService.js`).
+
+### Functions Extracted to `services/billsService.js`
+- `getDefaultCycleDay(cycleType)` - Returns default cycle day based on cycle type
+- `validateCycleDay(cycleType, cycleDay)` - Validates cycle_day based on cycle_type rules
+- `parseDueDay(value)` - Parses and validates due_day (must be 1-31 integer)
+- `parseInterestRate(value)` - Parses and validates interest_rate (0-100 range)
+- `getValidCycleTypes()` - Returns array of valid cycle types
+- `validateBillData(data, existingBill)` - Comprehensive validation and normalization for bill create/update
+- `validateCycleDayOnly(cycleType, cycleDay)` - Convenience wrapper for cycle_day validation
+
+### Functions Remaining in `routes/bills.js`
+- Route handlers only - parse request, call service, send response
+- DB queries remain in routes (tightly coupled to HTTP flow, not pure business logic)
+- Error formatting with `standardizeError` (HTTP-layer concern)
+
+### Design Decisions
+1. **`validateBillData`** - Centralized validation function that handles both create and update scenarios
+ - Takes optional `existingBill` parameter to support partial updates
+ - Returns `{ errors, normalized }` structure
+ - Validates all bill fields including category_id, history_visibility, cycle_type/cycle_day
+
+2. **`getValidCycleTypes()`** - Exported constant array for consistency across files
+
+3. **`VALID_VISIBILITY`** - Exported from service for reuse in other files if needed
+
+### Benefits
+- Business logic is now testable in isolation without mocking Express request/response
+- Route handlers are thinner and easier to read
+- Validation rules are centralized in one place
+- Easier to add new bill-related operations without touching routes
+
+### Files Modified
+- `routes/bills.js` - Removed ~80 lines of business logic, replaced with service imports and calls
+- `services/billsService.js` - New file created with extracted business logic
+
+### No Breaking Changes
+- All API endpoints maintain exact same behavior
+- Same validation rules applied
+- Same error messages returned
diff --git a/.learnings/scarlett/BILL_TRACKER_ACTIVE.md b/.learnings/scarlett/BILL_TRACKER_ACTIVE.md
new file mode 100644
index 0000000..8166859
--- /dev/null
+++ b/.learnings/scarlett/BILL_TRACKER_ACTIVE.md
@@ -0,0 +1,52 @@
+# Bill Tracker — Scarlett's Active Notes
+
+**Last updated:** 2026-05-11
+
+## Task 2: RoadmapPage UI — Kanban Priority Lanes
+
+### What Changed
+
+| File | Action | Description |
+|------|--------|-------------|
+| `client/pages/RoadmapPage.jsx` | **NEW** | Standalone kanban-style roadmap page with 2 tabs (Roadmap + Activity Log) |
+| `client/App.jsx` | **MODIFIED** | Added lazy import for RoadmapPage; `/admin/roadmap` route now renders ``; `/admin/about` route uses `` without admin prop |
+| `client/pages/AboutPage.jsx` | **MODIFIED** | Removed `admin` prop, removed `AdminDashboard` import, removed conditional render block — AboutPage is now public-only |
+| `client/components/AdminDashboard.jsx` | **DELETED** | Replaced entirely by RoadmapPage |
+| `client/components/ui/collapsible.jsx` | **NEW** | shadcn Collapsible component (Radix-based) |
+| `tailwind.config.js` | **MODIFIED** | Added `collapsible-down`/`collapsible-up` keyframes and animations |
+| `package.json` | **MODIFIED** | Added `@radix-ui/react-collapsible` dependency |
+
+### Architecture
+
+- **RoadmapPage** is a standalone page rendered at `/admin/roadmap` (behind `` + ``)
+- Uses **shadcn Tabs** for Roadmap / Activity Log tab switching
+- **Roadmap tab**: 5-column kanban grid on desktop (`lg+`), 2-column on tablet (`sm–lg`), single column on mobile (`` elements (via shadcn Collapsible)
+- `aria-expanded` on all collapsible triggers (Radix handles this)
+- `aria-label` on priority badges (e.g., "Critical priority")
+- `role="region"` + `aria-label` on each priority lane section
+- Keyboard-focusable throughout
+
+### Responsive
+
+- Desktop (`lg+`): 5-column grid
+- Tablet (`sm–lg`): 2-column grid (CRITICAL+HIGH | MEDIUM+LOW+NICE TO HAVE)
+- Mobile (` {}` appropriate for audit failures
-
-**Final Verdict: SECURE** — No blocking issues
-
----
-
-### v0.22.3 — Skip First-Login for ENV-Seeded Users
-**Status:** ✅ COMPLETED
-**Date:** 2026-05-10
-**Priority:** HIGH
-
-| Agent | Status | Time | Notes |
-|-------|--------|------|-------|
-| Neo | ✅ COMPLETED | 2m18s | Reset first_login & must_change_password flags in setup/firstRun.js and server.js |
-| Bishop | ✅ COMPLETED | 25m30s | Fixed db/database.js [init] code to reset flags, all tests passed |
-| Hudson | ✅ COMPLETED | 45s | 5/6 PASS, 1 FAIL: missing audit logging for flag resets |
-| Neo | ✅ COMPLETED | 2m3s | Added logAudit calls to setup/firstRun.js and server.js |
-| Ripley | ✅ COMPLETED | — | Added logAudit to server.js, fixed circular dep in database.js, built & tested |
-
-**Files modified:** `setup/firstRun.js`, `server.js`, `db/database.js`
-
-**Work Completed:**
-- [x] `runFromEnv()` in firstRun.js resets `first_login=0, must_change_password=0` when updating existing admin/regular users
-- [x] Seed logic in server.js resets `first_login=0, must_change_password=0` when updating existing regular users
-- [x] Fixed db/database.js [init] code to reset `first_login=0, must_change_password=0` when updating admin password
-- [x] Verified ENV-seeded users (admin, regular) do NOT see first-login flow on container restart
-- [x] Verified non-ENV users still see first-login flow
-- [x] Version bumped to 0.22.3 in package.json and client/lib/version.js
-- [x] Audit logging added for flag resets in setup/firstRun.js and server.js
-- [x] database.js uses console.log for init-time flag resets (avoids circular dep with auditService)
-
-**Bug Found & Fixed:**
-The `db/database.js` [init] code was setting `must_change_password = 1` when resetting the password, which was overriding the flags reset by firstRun.js. Changed to `must_change_password = 0` to match the intended behavior.
-
-**Security Audit (Hudson):**
-1. Flag reset correctness: ✅ PASS
-2. No privilege escalation: ✅ PASS
-3. Container restart safety: ✅ PASS
-4. SQL injection: ✅ PASS
-5. Authorization scoping: ✅ PASS
-6. Audit trail: ✅ FIXED (added logAudit calls)
-
----
-
-### v0.22.2 — Session Token Rotation on Auth Events
-**Status:** ✅ COMPLETED
-**Date:** 2026-05-10
-**Priority:** MEDIUM
-
-| Agent | Status | Time | Notes |
-|-------|--------|------|-------|
-| Neo | ✅ COMPLETED | 6m45s | invalidateOtherSessions, rotateSessionId, logout-all endpoint |
-| Ripley | ✅ COMPLETED | — | Fixed profile.js cookie bug, added audit logging, added last_password_change_at to auth.js |
-| Bishop | ✅ COMPLETED | 12m1s | All API tests passed |
-| Hudson | ✅ COMPLETED | 21s | 6/6 PASS |
-
-**Files modified:** `services/authService.js`, `routes/auth.js`, `routes/profile.js`
-
-**Work Completed:**
-- [x] `invalidateOtherSessions(userId, keepSessionId)` — deletes all sessions except current
-- [x] Password change (auth.js + profile.js) invalidates all other sessions
-- [x] Password change rotates current session ID (sets new cookie)
-- [x] New `POST /api/auth/logout-all` endpoint
-- [x] Audit logging for `logout.all` and `password.change`
-- [x] Added `last_password_change_at` to auth.js for consistency with profile.js
-
-**Security Audit (Hudson):**
-1. Session invalidation completeness: ✅ PASS
-2. Session rotation security: ✅ PASS — atomic transaction
-3. Logout-all security: ✅ PASS — all sessions deleted, cookie cleared
-4. No session fixation: ✅ PASS — transaction ensures atomicity
-5. Authorization scoping: ✅ PASS — uses req.user.id only
-6. Audit logging: ✅ PASS
-
----
-
-### v0.22.1 — N+1 Query Optimization
-**Status:** ✅ COMPLETED
-**Date:** 2026-05-10
-**Priority:** MEDIUM
-
-| Agent | Status | Time | Notes |
-|-------|--------|------|-------|
-| Neo | ✅ COMPLETED | 6m7s | Batch queries for tracker + analytics |
-| Ripley | ✅ COMPLETED | — | Reviewed changes, version bump 0.22.0 → 0.22.1 |
-| Bishop | ✅ COMPLETED | 2m13s | 6/6 PASS |
-| Hudson | ✅ COMPLETED | 18s | 5/5 PASS |
-
-**Files modified:** `routes/tracker.js`, `routes/analytics.js`
-
-**Work Completed:**
-- [x] Tracker: batch monthly_bill_state, payments, prev month payments, upcoming payments
-- [x] Analytics: added empty billIds guards
-- [x] All batch queries guarded by `billIds.length > 0` for empty list safety
-- [x] IN clause built with parameterized placeholders (no SQL injection)
-
-**Security Audit (Hudson):**
-1. SQL injection: ✅ PASS — parameterized placeholders only
-2. Empty IN clause: ✅ PASS — all guarded
-3. User scoping: ✅ PASS — bills scoped by req.user.id
-4. No data leakage: ✅ PASS — bills filtered before extracting IDs
-5. Type safety: ✅ PASS — bill.id from SQLite auto-increment
-
----
-
-### v0.22.0 — React Query Migration
-**Status:** ✅ COMPLETED
-**Date:** 2026-05-10
-**Priority:** MEDIUM
-
-| Agent | Status | Time | Notes |
-|-------|--------|------|-------|
-| Neo | ❌ FAILED | 2s | Rate-limited, partial work only (installed deps, started TrackerPage migration) |
-| Ripley | ✅ COMPLETED | — | Completed React Query migration, fixed error handling, version bump |
-| Bishop | ✅ COMPLETED | 2m57s | 8/8 PASS |
-| Hudson | ✅ COMPLETED | 26s | 4/5 PASS (1 FAIL fixed: error handling toast duplication) |
-
-**Files modified:** `client/App.jsx`, `client/hooks/useQueries.js` (new), `client/pages/TrackerPage.jsx`, `package.json`, `package-lock.json`
-
-**Work Completed:**
-- [x] Installed @tanstack/react-query + @tanstack/react-query-devtools
-- [x] Created custom hooks: useTracker, useBills, useCategories
-- [x] Migrated TrackerPage from useState/useEffect to useTracker() hook
-- [x] Added QueryClientProvider with sensible defaults
-- [x] Added ReactQueryDevtools for development
-- [x] Fixed load→refetch callback references
-- [x] Fixed error handling: useRef pattern prevents duplicate toasts
-
-**Security Audit (Hudson):**
-1. Query key injection: ✅ PASS — safe numeric params
-2. DevTools exposure: ✅ PASS — only API data, dev-only
-3. Refetch callback safety: ✅ PASS — no uncontrolled loops
-4. Error handling: ❌ FAIL → ✅ FIXED — useRef pattern prevents duplicate toasts
-5. Cache configuration: ⚠️ INFO — long cache acceptable for UX
-
----
-
-### v0.21.0 — 3-Month Trend Indicator
-**Status:** 🔄 IN PROGRESS
-**Date:** 2026-05-10
-**Priority:** MEDIUM
-
-| Agent | Status | Time | Notes |
-|-------|--------|------|-------|
-| Neo | ✅ COMPLETED | 19m | Backend trend calculation, TrendIndicator + TrendCard components |
-| Ripley | ✅ COMPLETED | — | Fixed duplicate TrendIndicator, version bump, Bishop bug fix |
-| Bishop | ✅ COMPLETED | 4m55s | 4/4 PASS, fixed user_id query bug (JOIN through bills) |
-| Hudson | ✅ COMPLETED | 12s | 5/5 PASS (SQL injection, user scoping, date wrapping, division by zero, XSS) |
-
-**Files modified:** `routes/tracker.js`, `client/pages/TrackerPage.jsx`, `client/lib/version.js`, `package.json`
-
-**Work Completed:**
-- [x] Backend: 3-month trend calculation with year-wrapping
-- [x] Backend: trend object in API response (direction, percent_change, 3_month_avg)
-- [x] Frontend: TrendIndicator component (arrow + percentage + label)
-- [x] Frontend: TrendCard component (purple gradient card)
-- [x] Bug fix: removed duplicate TrendIndicator definition
-- [x] Version bumped to 0.21.0
-
-**Security Audit (Hudson):**
-1. SQL injection: ✅ PASS — parameterized queries only
-2. User scoping: ✅ PASS — JOIN through bills for user_id filtering
-3. Date wrapping: ✅ PASS — handles year boundaries correctly
-4. Division by zero: ✅ PASS — checks threeMonthAvg > 0 before division
-5. No frontend XSS: ✅ PASS — direction is server-computed enum
-
----
-
-### v0.21.1 — Loading Skeletons & Async State
-**Status:** ✅ COMPLETED
-**Date:** 2026-05-10
-**Priority:** MEDIUM
-
-| Agent | Status | Time | Notes |
-|-------|--------|------|-------|
-| Scarlett | ✅ COMPLETED | 1m2s | Skeleton component, TrackerPage/BillsPage skeleton loaders |
-| Ripley | ✅ COMPLETED | — | Fixed `/>}}` syntax error on Bucket component |
-| Bishop | ✅ COMPLETED | 1m58s | 11/11 PASS |
-| Hudson | ✅ COMPLETED | 17s | 5/5 PASS |
-
-**Files modified:** `client/components/ui/Skeleton.jsx` (new), `client/pages/TrackerPage.jsx`, `client/pages/BillsPage.jsx`
-
-**Work Completed:**
-- [x] Reusable Skeleton component (line, circle, card, button, input variants)
-- [x] TrackerPage skeleton cards, rows, buckets with aria-busy
-- [x] BillsPage skeleton rows during loading
-- [x] Bug fix: double closing brace `/>}}` on second Bucket component
-
-**Security Audit (Hudson):**
-1. XSS via className: ✅ PASS
-2. No sensitive data in skeleton: ✅ PASS
-3. aria-busy correctness: ✅ PASS
-4. No validation bypass: ✅ PASS
-5. Skeleton presentational only: ✅ PASS
-
----
-
-### v0.20.9 — Previous Month Paid on Tracker
-**Status:** 🔄 IN PROGRESS
-**Date:** 2026-05-10
-**Priority:** MEDIUM
-
-| Agent | Status | Time | Notes |
-|-------|--------|------|-------|
-| Neo | ✅ COMPLETED | 7m40s | Previous month backend + frontend column + summary card |
-| Ripley | ✅ COMPLETED | — | Version bump, doc updates, deploy |
-| Bishop | ✅ COMPLETED | 2m22s | 5/5 PASS (Docker build, API, version, frontend, previous_month fields) |
-| Hudson | ✅ COMPLETED | 23s | 5/5 PASS (SQL injection, date wrapping, user scoping, auth, XSS) |
-
-**Files modified:** `routes/tracker.js`, `client/pages/TrackerPage.jsx`, `client/lib/version.js`, `package.json`
-
-**Work Completed:**
-- [x] Backend: previous month calculation with year wrapping
-- [x] Backend: `previous_month_paid` per bill, `previous_month_total` in summary
-- [x] Frontend: "Last Month" column in desktop table
-- [x] Frontend: "Last Month" row in mobile view
-- [x] Frontend: Previous month summary card
-- [x] Version bumped to 0.20.9
-
-**Security Audit (Hudson):**
-1. SQL injection in prev month query: ✅ PASS — parameterized queries
-2. Date range year wrapping: ✅ PASS — Jan→Dec correctly handled
-3. Data leakage / user scoping: ✅ PASS — bills scoped to user_id
-4. Authentication: ✅ PASS — req.user.id used
-5. XSS via monetary amounts: ✅ PASS — numeric fmt() rendering
-
----
-
-### v0.20.8 — Billing Cycle Sub-categories
-**Status:** 🔄 IN PROGRESS
-**Date:** 2026-05-10
-**Priority:** MEDIUM
-
-| Agent | Status | Time | Notes |
-|-------|--------|------|-------|
-| Neo | ✅ COMPLETED | 8m42s | Migration v0.46, cycle_type/cycle_day validation, BillModal UI |
-| Ripley | ✅ COMPLETED | — | Version bump, Hudson fix (validateCycleDay server-side), build, push |
-| Bishop | ✅ COMPLETED | 56s | Container running, migration v0.46 applied, columns confirmed |
-| Hudson | ✅ COMPLETED | 26s | 4/5 PASS, found medium-risk cycle_day gap (fixed by Ripley) |
-
-**Files modified:** `db/database.js`, `routes/bills.js`, `client/components/BillModal.jsx`, `client/lib/version.js`, `package.json`
-
-**Work Completed:**
-- [x] Migration v0.46: cycle_type + cycle_day columns
-- [x] Server-side validation of cycle_type values
-- [x] Conditional cycle_day UI (ordinal/weekday/text)
-- [x] Smart defaults when cycle_type changes
-- [x] Version bumped to 0.20.8
-
-**Security Audit (Hudson):**
-1. cycle_type whitelist validation: ✅ PASS
-2. cycle_day server-side validation: ⚠️ MEDIUM (fixed — added validateCycleDay with type-specific checks)
-3. SQL injection: ✅ PASS (parameterized queries)
-4. Default value safety: ✅ PASS
-5. Authorization (user-scoped updates): ✅ PASS
-
----
-
-### v0.20.7 — Keyboard Navigation & ARIA Accessibility
-**Status:** 🔄 IN PROGRESS
-**Date:** 2026-05-10
-**Priority:** HIGH
-
-| Agent | Status | Time | Notes |
-|-------|--------|------|-------|
-| Scarlett | ✅ COMPLETED | 5m5s | Skip-to-content, aria-expanded/hasPopup, aria labels, main landmark |
-| Ripley | ✅ COMPLETED | — | Fixed useId import (react-router-dom → react), verified vite build |
-| Bishop | ✅ COMPLETED | 5m10s | 11/11 PASS (all accessibility checks verified) |
-| Hudson | ✅ COMPLETED | 19s | Security audit: 5/5 PASS, no XSS/DOM clobbering/injection |
-
-**Files modified:** `client/App.jsx`, `client/components/layout/Layout.jsx`, `client/components/layout/Sidebar.jsx`, `client/main.jsx`, `client/lib/version.js`, `package.json`
-
-**Work Completed:**
-- [x] Skip-to-content link with sr-only/focus:not-sr-only pattern
-- [x] `aria-expanded` and `aria-haspopup` on Tracker menu dropdown
-- [x] `aria-label="Footer"` on footer element
-- [x] `role="main"` and `aria-labelledby` on layout wrapper
-- [x] Main content wrapped in `` with unique id from useId()
-- [x] Fixed build error: useId imported from react, not react-router-dom
-- [x] Version bumped to 0.20.7
-
-**Security Audit (Hudson):**
-1. XSS via ARIA attributes: ✅ PASS — hardcoded strings + useId(), no user data
-2. DOM clobbering: ✅ PASS — useId() generates unique unpredictable IDs
-3. Skip link injection: ✅ PASS — useId() output not user-controllable
-4. aria-expanded state: ✅ PASS — computed from route state, not hardcoded
-5. No backend changes: ✅ PASS — only frontend JSX files modified
-
----
-
-### v0.20.6 — Audit Logging for Critical Operations
-**Status:** 🔄 IN PROGRESS
-**Date:** 2026-05-10
-**Priority:** HIGH
-
-| Agent | Status | Time | Notes |
-|-------|--------|------|-------|
-| Neo | ✅ COMPLETED | 9m19s | Created auditService.js, migration v0.45, audit calls in 4 route files |
-| Bishop | ✅ COMPLETED | 7m26s | 6/6 PASS, also fixed authLogin.js missing audit calls |
-| Hudson | ✅ COMPLETED | 40s | Security audit: 7/7 PASS, no vulnerabilities |
-
-**Files modified:** `services/auditService.js` (new), `db/database.js`, `routes/auth.js`, `routes/admin.js`, `middleware/csrf.js`, `routes/profile.js`, `client/lib/version.js`, `package.json`
-
-**Work Completed:**
-- [x] Created `audit_log` table migration (v0.45) with indexes
-- [x] Created `logAudit()` service with try/catch safety
-- [x] Added audit calls: login.success, login.failure, logout, password.change
-- [x] Added audit calls: role.change (with old/new role), csrf.failure
-- [x] Added audit calls: profile.update, profile.settings.update
-- [x] Version bumped to 0.20.6
-
-**Security Audit (Hudson):**
-1. Sensitive data logging: ✅ PASS — no passwords/tokens/session IDs logged
-2. SQL injection: ✅ PASS — prepared statements, no string interpolation
-3. Denial of service: ✅ PASS — try/catch prevents app crash
-4. Failed login info disclosure: ✅ PASS — username only, no credentials
-5. Audit log integrity: ✅ PASS — no UPDATE/DELETE endpoints
-6. CSRF bypass: ✅ PASS — no feedback loop
-7. Role change audit: ✅ PASS — server-validated values, not user-controlled
-
----
-
-### v0.20.5 — Bulk Payment Input Validation
-**Status:** 🔄 IN PROGRESS
-**Date:** 2026-05-10
-**Priority:** HIGH
-
-| Agent | Status | Time | Notes |
-|-------|--------|------|-------|
-| Neo | ✅ COMPLETED | 2m6s | Added max 50 items, duplicate detection, input validation |
-| Bishop | ✅ COMPLETED | 6m44s | 13/13 PASS (all endpoint tests verified) |
-| Hudson | ✅ COMPLETED (2 FAIL → fixed) | 29s | Type coercion + Infinity bypass found, both fixed by Ripley |
-
-**Files modified:** `routes/payments.js`, `client/lib/version.js`, `package.json`
-
-**Objective:**
-Add input validation on /api/payments/bulk endpoint.
-
-**Work Completed:**
-- [x] Request body must contain `payments` array
-- [x] Max 50 items per request
-- [x] Per-item validation (bill_id integer, paid_date YYYY-MM-DD, amount >= 0)
-- [x] Duplicate detection using bill_id + paid_date + amount composite key
-- [x] Response includes `skipped` array for duplicates
-- [x] Comment block with validation rules
-- [x] Version bumped to 0.20.5
-
-**Security Audit (Hudson):**
-1. Max items bypass: ✅ PASS
-2. Type coercion attack (bill_id): ❌ FAIL → Fixed (regex `/^\d+$/` check added)
-3. Date regex bypass: ⚠️ MEDIUM (not critical, format-only check)
-4. Amount validation (Infinity): ❌ FAIL → Fixed (`!isFinite()` check added)
-5. SQL injection: ✅ PASS
-6. Authorization bypass: ✅ PASS
-7. Breaking change: ✅ PASS
-
-**Fixes applied by Ripley:**
-- `bill_id`: Added `/^\d+$/` regex check before parseInt to prevent `"1abc"` → `1` coercion
-- `amount`: Added `!isFinite(parsedAmt)` check to reject `Infinity` values
-- Also fixed `skipped.push()` to use `parsedAmt` instead of raw `amount`
-
----
-
-### v0.20.4 — Migration Dependency Management
-**Status:** 🔄 IN PROGRESS
-**Date:** 2026-05-10
-**Priority:** HIGH
-
-| Agent | Status | Time | Notes |
-|-------|--------|------|-------|
-| Neo | ❌ FAILED | 2m22s | Read docs, ran out of time, no code written |
-| Ripley | ✅ COMPLETED | — | Implemented dependsOn fields, validation function, loop integration |
-| Ripley | ✅ COMPLETED | — | Implemented dependsOn fields, validation function, loop integration |
-| Bishop | ✅ COMPLETED | 2m31s | Verified all 9 checks PASS |
-| Hudson | ✅ COMPLETED | 1m10s | Security audit: 7/7 PASS |
-
-**Files modified:** `db/database.js`, `client/lib/version.js`, `package.json`
-
-**Objective:**
-Add explicit dependency management to all 17 versioned migrations with validation.
-
-**Work Completed:**
-- [x] Added `dependsOn` array to all 17 versioned migrations (v0.2 → v0.44)
-- [x] Added `validateMigrationDependencies()` function
-- [x] Integrated dependency check into migration loop
-- [x] Logs `[migration] vX depends on [vY] — satisfied` when deps are met
-- [x] Skips migrations with unmet deps with clear error log
-- [x] Adds newly applied versions to `appliedVersions` Set for subsequent checks
-- [x] Version bumped to 0.20.4
-- [x] Docker build passes, login works, dependency logging confirmed
-
-**Security Audit (Hudson):**
-1. Dependency bypass: ✅ PASS — all dependsOn are hardcoded string literals
-2. SQL injection: ✅ PASS — appliedVersions from trusted immutable schema_migrations
-3. Denial of service: ✅ PASS — continue (skip) not throw on unmet deps
-4. Array injection: ✅ PASS — no dynamic input in dependsOn arrays
-5. Race condition: ✅ PASS — single-process SQLite, no concurrent access
-6. Circular deps: ✅ PASS — linear chain verified, no cycles
-7. Edge cases: ✅ PASS — empty/undefined/missing deps handled
-
----
-
-### v0.20.3 — Missing Database Indexes
-**Status:** ✅ COMPLETED
-**Date:** 2026-05-10
-**Priority:** HIGH
-
-| Agent | Status | Time | Notes |
-|-------|--------|------|-------|
-| Neo | ✅ COMPLETED | 2m40s | Added v0.44 migration with 4 indexes |
-| Bishop | ✅ COMPLETED | 2m33s | Docker build, all indexes verified, version bumped |
-| Hudson | ✅ COMPLETED | 1m1s | Security audit: 7/7 PASS |
-| Ripley | ✅ COMPLETED | — | Fixed nested transaction bug, committed, pushed, deployed |
-
-**Files modified:** `db/database.js`, `client/lib/version.js`, `package.json`
-
-**Task ID:** missing-indexes-003
-
-**Objective:**
-Add performance indexes on frequently queried columns to eliminate full table scans.
-
-**Work Completed:**
-- [x] Added v0.44 migration with 4 CREATE INDEX statements
-- [x] Fixed nested transaction bug (migration run() should NOT have its own BEGIN/COMMIT)
-- [x] All indexes use IF NOT EXISTS for idempotency
-- [x] Docker build passes, login works, no errors
-- [x] Version bumped to 0.20.3
-
-**Security Audit (Hudson):**
-1. SQL injection: ✅ PASS — all hardcoded names, no dynamic input
-2. Index naming collision: ✅ PASS — IF NOT EXISTS prevents duplicates
-3. Correct columns: ✅ PASS — all 4 match spec
-4. Performance impact: ✅ PASS — idempotent, created once
-5. Migration ordering: ✅ PASS — v0.44 after v0.43
-6. Transaction nesting: ✅ PASS — no nested BEGIN/COMMIT in run()
-7. Migration recorded: ✅ PASS — correct entry in schema_migrations
-
----
-
-### v0.20.2 — Transaction Wrapping for Migrations
-**Status:** ✅ COMPLETED
-**Date:** 2026-05-10
-**Priority:** CRITICAL
-
-| Agent | Status | Time | Notes |
-|-------|--------|------|-------|
-| Neo | ✅ COMPLETED | 9m | Implemented transaction wrapping for all migrations |
-| Bishop | ✅ COMPLETED | 2m | Verified Docker build, migrations, login, version bump |
-| Hudson | ✅ COMPLETED | 31s | Security audit: 6/7 PASS, 1 FAIL (FK re-enable) — Ripley fixed |
-| Ripley | ✅ COMPLETED | — | Fixed v0.40 FK issue, committed, pushed, deployed |
-
-**Files modified:** `db/database.js`, `client/lib/version.js`, `package.json`, `FUTURE.md`, `HISTORY.md`
-
-**Task ID:** migration-transactions-002
-
-**Objective:**
-Wrap all database migrations in BEGIN/COMMIT/ROLLBACK transactions so partial failures don't leave the schema in an inconsistent state.
-
-**Work Completed:**
-- [x] Wrapped versioned migrations loop in BEGIN/COMMIT/ROLLBACK
-- [x] Wrapped legacy reconciliation migrations in BEGIN/COMMIT/ROLLBACK
-- [x] Wrapped unversioned user notification columns in BEGIN/COMMIT/ROLLBACK
-- [x] Special handling for v0.40 PRAGMA foreign_keys (OFF before BEGIN, ON in finally block)
-- [x] Fixed Hudson finding: FK re-enable now uses try/finally to guarantee restoration even on error
-- [x] Hudson security audit: 6/7 PASS, 1 FAIL → fixed → all clear
-- [x] Docker build + fresh DB test: all migrations apply correctly with transaction logging
-- [x] Version bumped to 0.20.2
-
-**Security Audit (Hudson):**
-1. Transaction atomicity: ✅ PASS
-2. PRAGMA foreign_keys handling: ❌ FAIL → ✅ FIXED (try/finally)
-3. SQLite WAL mode: ✅ PASS
-4. Error propagation: ✅ PASS
-5. recordMigration inside transaction: ✅ PASS
-6. SQL injection: ✅ PASS
-7. Concurrent access: ✅ PASS
-
----
-
-## Current Work (In Progress)
-
-### v0.20.1 — Code Splitting + Admin Dashboard + Version Bump
-**Status:** ✅ COMPLETED
-**Date:** 2026-05-09
-**Priority:** MEDIUM
-
-| Agent | Status | Time | Notes |
-|-------|--------|------|-------|
-| Bishop | ✅ COMPLETED | — | Code splitting verified, version bump applied |
-
-**Files modified:** `client/lib/version.js`, `package.json`, `DEVELOPMENT_LOG.md`
-
-**Task ID:** code-splitting-version-bump-001
-
-**Objective:**
-Verify code splitting implementation (React.lazy + Suspense) and bump version to 0.20.1 for significant performance improvement.
-
-**Work Completed:**
-- [x] Verified code splitting in `client/App.jsx` — all pages except LoginPage are lazy-loaded
-- [x] Verified `client/components/PageLoader.jsx` exists with minimal loading spinner
-- [x] Verified `client/components/AdminDashboard.jsx` imports `APP_VERSION` from `@/lib/version`
-- [x] Verified `routes/aboutAdmin.js` returns version from package.json
-- [x] Built Docker image with fresh build: `docker build --no-cache -t bill-tracker:local .`
-- [x] Container started and verified with `docker run -p 3036:3000`
-- [x] Verified `/api/about-admin` returns version `0.20.1`
-- [x] Verified 35 JS chunks generated (code splitting working)
-- [x] Version bumped to 0.20.1 in `package.json` and `client/lib/version.js`
-
-**Test Results:**
-
-**Docker Build:** ✅ PASSED
-```
-Successfully built cf550f4ed581
-Successfully tagged bill-tracker:local
-```
-
-**Container Start:** ✅ PASSED
-```
-Database initialized successfully
-Bill Tracker running on port 3000
-Users found: 2
-```
-
-**API Test:** ✅ PASSED
-```
-$ curl -s -b /tmp/bt-cookies-v21.txt http://localhost:3036/api/about-admin
-{"version":"0.20.1","future":"...20513 chars..."}
-```
-
-**Login Test:** ✅ PASSED
-```
-$ curl -s -c /tmp/bt-cookies-v21.txt http://localhost:3036/api/auth/login \
- -H 'Content-Type: application/json' \
- -d '{"username":"admin","password":"admin123"}'
-{"user":{"id":1,"username":"admin","role":"admin"...}}
-```
-
-**Code Splitting Verification:** ✅ PASSED
-```
-$ docker exec bill-tracker ls -la /app/dist/assets/ | grep -c "\.js"
-35
-```
-
-**Files Modified:**
-- `client/lib/version.js` — Version bumped to 0.20.1 with updated RELEASE_NOTES
-- `package.json` — Version bumped to 0.20.1
-- `DEVELOPMENT_LOG.md` — Added v0.20.1 entry
-
-**Deliverables:**
-- Code splitting verified with React.lazy() and Suspense
-- PageLoader component verified
-- AdminDashboard version badge verified
-- Docker build passes
-- App serves HTML without white screen
-- 35 JS chunks generated for lazy loading
-- Version properly bumped to 0.20.1
-- Documentation updated
-
----
-- [x] Verified AdminDashboard component parses FUTURE.md with 10 roadmap items across 5 priority levels
-- [x] Verified AdminDashboard component parses DEVELOPMENT_LOG.md with version entries
-- [x] Verified SimpleCollapsible component renders collapsible sections
-- [x] Verified priority color coding: 🔴🟠🟡🔵💭 with correct CSS classes
-- [x] Verified scrollbar styles in client/index.css for smooth scrolling
-- [x] Version bumped to 0.20.0 in package.json and client/lib/version.js
-- [x] FUTURE.md updated to v0.20.0
-
-**Test Results:**
-
-**Docker Build:** ✅ PASSED
-```
-Successfully built ab7a1c3a3a72
-Successfully tagged bill-tracker:local
-```
-
-**Container Start:** ✅ PASSED
-```
-Database initialized successfully
-Bill Tracker running on port 3000
-Users found: 2
-```
-
-**API Test:** ✅ PASSED
-```
-$ curl -s -b /tmp/admin-cookies-v20.txt http://localhost:3036/api/about-admin
-{"future":"...20513 chars...","developmentLog":"...23092 chars..."}
-```
-
-**Login Test:** ✅ PASSED
-```
-$ curl -s -c /tmp/test-cookies.txt http://localhost:3036/api/auth/login \
- -H 'Content-Type: application/json' \
- -d '{"username":"admin","password":"admin123"}'
-{"user":{"id":1,"username":"admin","role":"admin"...}}
-```
-
-**Code Verification:** ✅ PASSED
-- AdminDashboard.jsx exists and imports correctly
-- AboutPage.jsx renders AdminDashboard for admin users
-- SimpleCollapsible component present
-- Priority color coding implemented
-- Scrollbar styles added
-
-**Files Modified:**
-- `client/components/AdminDashboard.jsx` — New admin dashboard with roadmap and activity log
-- `client/pages/AboutPage.jsx` — Conditional rendering of AdminDashboard
-- `client/index.css` — Scrollbar styles for smooth scrolling
-- `client/lib/version.js` — Version bumped to 0.20.0
-- `package.json` — Version bumped to 0.20.0
-- `FUTURE.md` — Updated to v0.20.0
-- `DEVELOPMENT_LOG.md` — Added v0.20.0 entry
-
-**Deliverables:**
-- Admin Dashboard with roadmap and activity log implemented
-- Priority color coding with collapsible sections
-- Mobile responsive design with scrollbar customization
-- Admin users see AdminDashboard; non-admins see standard About page
-- Version properly bumped to 0.20.0
-- Documentation updated
-
----
-
-## Current Work (In Progress)
-
-_No current active work._
-
----
-
-## Completed Work
-
-### v0.19.3 — Legacy DB Login Fix + Migration Run Functions + Security Hardening
-**Date:** 2026-05-09
-
-| Agent | Status | Time | Notes |
-|-------|--------|------|-------|
-| Neo | ✅ COMPLETED | 1m 38s | Added `run()` functions to all legacy migrations, admin password reset logic |
-| Bishop | ✅ COMPLETED | 3m 22s | All 4 tests passed. Updated Engineering Reference Manual |
-| Hudson | ✅ COMPLETED | 1m 21s | Security audit — log disclosure, reset timing, v0.40 ownership |
-| Ripley | ✅ COMPLETED | — | Fixed Hudson findings, built, tested, committed, pushed v0.19.3 |
-
-**Files modified:** `db/database.js`, `docs/Engineering_Reference_Manual.md`, `HISTORY.md`, `FUTURE.md`
-
----
-**Task ID:** error-boundaries-verify-001
-**Priority:** MEDIUM
-**Started:** 2026-05-09 18:28 CDT
-**Completed:** 2026-05-09 18:30 CDT
-
-**Objective:**
-Verify Scarlett's Error Boundary implementation, build, test, and update documentation.
-
-**Work Completed:**
-- [x] Built Docker image: `docker build --no-cache -t bill-tracker:local .`
-- [x] Tested container started and serves HTML correctly
-- [x] Verified ErrorBoundary.jsx exists at `client/components/ErrorBoundary.jsx`
-- [x] Verified all routes wrapped with `` in App.jsx
-- [x] Confirmed fallback UI includes "Try Again" and "Reload Page" buttons
-- [x] Updated Engineering_Reference_Manual.md with Error Boundaries section
-- [x] Updated DEVELOPMENT_LOG.md with completion entry
-
-**Test Results:**
-
-**Docker Build:** ✅ PASSED
-```
-Step 19/19 : CMD ["node", "server.js"]
---
-Successfully built ff23244dc5af
-Successfully tagged bill-tracker:local
-```
-
-**Container Start:** ✅ PASSED
-```
-Database initialized successfully
-Bill Tracker running on port 3000
-Users found: 2
-```
-
-**Login Test:** ✅ PASSED
-```
-$ curl -s -c /tmp/bt-err-test.txt http://localhost:3036/api/auth/login \
- -H 'Content-Type: application/json' \
- -d '{"username":"admin","password":"admin123"}'
-{"user":{"id":1,"username":"admin",..."role":"admin"...}}
-```
-
-**HTML Response:** ✅ PASSED
-```
-$ curl -s http://localhost:3036/ | head -5
-
-
-
-
-
-```
-
-**Files Modified:**
-- `docs/Engineering_Reference_Manual.md` — Error Boundaries section added
-- `DEVELOPMENT_LOG.md` — this entry added
-
-**Deliverables:**
-- Error boundary component verified
-- All routes wrapped correctly
-- Fallback UI verified with recovery buttons
-- Docker build passes
-- App serves HTML without white screen
-- Documentation updated
-
----
-
----
-
-## Current Work (In Progress)
-
-### Bishop — Security Hardening Verification & Documentation Update
-**Status:** ✅ COMPLETED
-**Task ID:** security-doc-update-001
-**Priority:** HIGH
-**Started:** 2026-05-09 17:30 CDT
-**Completed:** 2026-05-09 17:31 CDT
-
-**Objective:**
-Verify Neo's 6 security fixes and update Engineering_Reference_Manual.md accordingly.
-
-**Work Completed:**
-- [x] Verified #1: Path traversal fix (ALLOWED_FILES map in routes/aboutAdmin.js)
-- [x] Verified #2: Admin route bypass fix (admin prop, dual API calls)
-- [x] Verified #3: Sensitive info redaction (expanded patterns)
-- [x] Verified #4: Error message leaks (generic error only)
-- [x] Verified #5: Race condition fix (transaction wrapping)
-- [x] Verified #6: Password validation (8-char minimum)
-- [x] Updated Engineering_Reference_Manual.md with v0.19.2 section
-- [x] Updated DEVELOPMENT_LOG.md with completion entry
-
-**Files Modified:**
-- `docs/Engineering_Reference_Manual.md` — v0.19.2 security fixes section added
-- `DEVELOPMENT_LOG.md` — this entry added
-
-**Deliverables:**
-- All 6 security fixes verified and documented
-- Engineering Reference Manual updated with detailed fix explanations
-- Development Log current with Bishop's review completion
-
----
-
-**Last Updated:** 2026-05-09 17:31 CDT
-
----
-
-## Current Work (In Progress)
-
-### Bishop — Code Review + Documentation Update
-**Status:** ✅ COMPLETED
-**Task ID:** code-review-doc-update-001
-**Priority:** HIGH
-**Started:** 2026-05-09 16:20 CDT
-**Completed:** 2026-05-09 16:25 CDT
-
-**Objective:**
-Verify security fixes and update documentation for v0.19.0 release.
-
-**Work Completed:**
-- [x] Verified security fixes in all modified files
-- [x] Reviewed `routes/aboutAdmin.js` — path traversal fix, redaction, error sanitization
-- [x] Reviewed `server.js` — adminActionLimiter on about-admin route
-- [x] Reviewed `client/App.jsx` — admin route guard at /admin/about
-- [x] Reviewed `client/pages/AboutPage.jsx` — rehype-sanitize for XSS prevention
-- [x] Reviewed `client/api.js` — aboutAdmin endpoint
-- [x] Updated Engineering_Reference_Manual.md with new endpoint and security measures
-- [x] Updated HISTORY.md with v0.19.0 security fixes and version bump convention
-- [x] Documented environment variables: INIT_REGULAR_USER, INIT_REGULAR_PASS
-- [x] Established version bump convention (Patch/Minor/Major rules)
-
-**Files Modified:**
-- `docs/Engineering_Reference_Manual.md` — comprehensive security documentation added
-- `HISTORY.md` — v0.19.0 security fixes section added, version bump convention added
-- `DEVELOPMENT_LOG.md` — this entry added
-
-**Deliverables:**
-- Security fixes verified and documented
-- Engineering Reference Manual updated with about-admin endpoint and security measures
-- HISTORY.md established version bump convention and current version
-- Non-admin test user support added for role-based testing
-
----
-
-**Last Updated:** 2026-05-09 16:25 CDT
-
----
-
-## Current Work (In Progress)
-
-### Bishop — Engineering Reference Manual Update
-**Status:** ✅ COMPLETED
-**Task ID:** eng-ref-manual-update-001
-**Priority:** HIGH
-**Started:** 2026-05-09 15:05 CDT
-**Completed:** 2026-05-09 15:10 CDT
-
-**Objective:**
-Update Engineering_Reference_Manual.md to document the migration version tracking system implemented in Neo's migration refactor.
-
-**Work Completed:**
-- [x] Read current Engineering_Reference_Manual.md
-- [x] Read db/database.js migration implementation
-- [x] Read DEVELOPMENT_LOG.md for context
-- [x] Added `schema_migrations` table documentation
-- [x] Added migration system overview to High Level Overview
-- [x] Added db/database.js helper functions to Backend Documentation
-- [x] Added Migration System section to Database Documentation
-- [x] Updated CI/CD Pipeline with migration notes
-- [x] Added Database Initialization & Migration Flow to Sequence Flows
-- [x] Added Migration Troubleshooting section
-- [x] Updated version to 0.19.1 with migration note
-
-**Files Modified:**
-- `docs/Engineering_Reference_Manual.md` — comprehensive migration documentation added
-- `DEVELOPMENT_LOG.md` — updated with Bishop's update completion
-
-**Deliverables:**
-- Complete migration system documentation in Engineering Reference Manual
-- Deployment teams can now understand and troubleshoot the migration system
-- Version tracking is clearly documented for ops teams
-
----
-
-## Current Work (In Progress)
-
-### Neo — Migration Version Tracking System
-**Status:** ✅ COMPLETED
-**Task ID:** migration-v-tracking-001
-**Priority:** CRITICAL
-**Started:** 2026-05-09 14:45 CDT
-**Completed:** 2026-05-09 15:00 CDT
-
-**Objective:**
-Implement explicit version tracking for database migrations so users can safely upgrade via `git pull && npm start` without migration state issues.
-
-**Work Completed:**
-- [x] Create `schema_migrations` tracking table in `db/database.js`
-- [x] Refactor `runMigrations()` to query and apply only pending migrations
-- [x] Convert existing inline migrations to versioned migration objects
-- [x] Add detailed logging for each migration step
-- [x] Add `hasMigrationBeenApplied()` and `recordMigration()` helper functions
-
-**Files Modified:**
-- `db/database.js` — migration system refactor
-
-**Deliverables:**
-- Version tracking implementation complete
-- Migrations are now trackable, repeatable, and resilient
-- Users can `git pull && npm start` safely
-
----
-
-## Completed Work
-
-### Neo — Migration Version Tracking System (2026-05-09)
-**Files Modified:** `db/database.js`
-- Created `schema_migrations` tracking table (id, version UNIQUE, description, applied_at)
-- Added `hasMigrationBeenApplied()` and `recordMigration()` helper functions
-- Refactored `runMigrations()` to skip already-applied migrations
-- Converted inline migrations to versioned objects with version/description/run
-- Added detailed logging for migration steps
-
----
-
-## Notes for Bishop
-
-**COMPLETED (2026-05-09 15:05 CDT):** Engineering_Reference_Manual.md updated to reflect migration version tracking system changes.
-
-**Changes Applied:**
-- Added `schema_migrations` table documentation with columns: `id`, `version`, `description`, `applied_at`
-- Added helper functions documentation: `hasMigrationBeenApplied()`, `recordMigration()`, `runMigrations()`
-- Added Migration System section to Database Documentation
-- Updated Backend Documentation with database.js helper functions
-- Added migration idempotency details to Infrastructure & Deployment
-- Added Database Initialization & Migration Flow to Sequence Flows
-- Added Migration Troubleshooting section to Error Handling
-- Updated CI/CD Pipeline with migration notes
-- Updated version to 0.19.1
-
-**Files Modified:**
-- `/home/kaspa/.openclaw/Projects/bill-tracker/docs/Engineering_Reference_Manual.md`
-
----
-
----
-
-## Historical Context
-
-**Migration System Issues Identified (Neo's Audit):**
-1. ❌ CRITICAL: No explicit version tracking
-2. ❌ CRITICAL: No transaction wrapping
-3. ⚠️ HIGH: No dependency management
-4. ⚠️ MEDIUM: No rollback capability
-5. ⚠️ MEDIUM: Limited error handling
-
-All issues documented in `/FUTURE.md` with implementation notes.
-
-**Current Work:** Addressing issue #1 (version tracking) as foundation for fixes #2-5.
-
-## Current Work (In Progress)
-
-### Neo — Admin-Only /about Endpoint for FUTURE.md and DEVELOPMENT_LOG.md
-**Status:** ✅ COMPLETED
-**Task ID:** admin-about-endpoint-001
-**Priority:** MEDIUM
-**Started:** 2026-05-09 15:25 CDT
-**Completed:** 2026-05-09 15:30 CDT
-
-**Objective:**
-Create a backend endpoint that serves FUTURE.md and DEVELOPMENT_LOG.md content to admin users only.
-
-**Work Completed:**
-- [x] Created new route file `routes/aboutAdmin.js` with file reading logic
-- [x] Implemented admin-only access using existing `requireAuth` and `requireAdmin` middleware
-- [x] Added proper error handling for file read operations
-- [x] Mounted new route at `/api/about-admin` in `server.js`
-- [x] Used `fs.readFileSync` with UTF-8 encoding for file reading
-- [x] Added path resolution relative to the routes file
-
-**Files Modified:**
-- `routes/aboutAdmin.js` — New file containing the admin-only endpoint implementation
-- `server.js` — Added route registration for `/api/about-admin`
-
-**Deliverables:**
-- Admins can now access FUTURE.md and DEVELOPMENT_LOG.md content via a secure API endpoint
-- Endpoint returns structured JSON with both file contents
-- Non-admin users get 403 Forbidden
-- Unauthenticated users get 401 Unauthorized
-- File reading errors return 500 with meaningful message
-
----
-
-## Current Work (In Progress)
-
-### Neo — Security Fixes Implementation
-**Status:** ✅ COMPLETED
-**Task ID:** security-fixes-implementation-001
-**Priority:** HIGH
-**Started:** 2026-05-09 16:00 CDT
-**Completed:** 2026-05-09 16:15 CDT
-
-**Objective:**
-Implement 4 security fixes for the Bill Tracker application:
-1. Add `/admin/about` route guard in `client/App.jsx`
-2. Add rate limiting to `/api/about-admin` in `server.js`
-3. Add rehype-sanitize to `client/pages/AboutPage.jsx`
-4. Add aboutAdmin to `client/api.js`
-
-**Work Completed:**
-- [x] Added `` to client/App.jsx with admin protection
-- [x] Added `adminActionLimiter` to the `/api/about-admin` route in server.js
-- [x] Installed `rehype-sanitize` package and added it to ReactMarkdown component in client/pages/AboutPage.jsx
-- [x] Added `aboutAdmin: () => get('/about-admin')` to client/api.js
-
-**Files Modified:**
-- `client/App.jsx` — Added admin route protection for AboutPage
-- `server.js` — Added rate limiting to about-admin endpoint
-- `client/pages/AboutPage.jsx` — Added rehype-sanitize for content sanitization
-- `client/api.js` — Added aboutAdmin API function
-
-**Deliverables:**
-- Admin-only access to AboutPage at `/admin/about` with proper authentication
-- Rate limiting protection on admin about endpoint
-- Sanitized rendering of markdown content in AboutPage
-- Client-side API access to admin about endpoint
-
----
-
-### Neo — Security Hardening (Round 2)
-**Status:** ✅ COMPLETED
-**Task ID:** security-hardening-002
-**Priority:** CRITICAL → MEDIUM
-**Started:** 2026-05-09 17:05 CDT
-**Completed:** 2026-05-09 17:28 CDT
-
-**Objective:**
-Fix 6 security issues identified by Private_Hudson's audit and user-reported vulnerability list.
-
-**Work Completed:**
-- [x] 🔴 #1: Replaced `sanitizePath()` with hardcoded filename allowlist in `routes/aboutAdmin.js`
-- [x] 🟠 #2: Added `admin` prop to `AboutPage.jsx`, updated `App.jsx` to pass it via `/admin/about` route
-- [x] 🟠 #3: Expanded `redactSensitiveContent()` with file path, connection string, env var, and internal URL patterns
-- [x] 🟠 #4: Removed `err.message` from console.error in `routes/aboutAdmin.js`, generic HTTP 500 only
-- [x] 🟡 #5: Wrapped regular user creation in `db.transaction()` in `server.js` to prevent race condition
-- [x] 🟡 #6: Added 8-character minimum password validation for `INIT_REGULAR_PASS` in `server.js`
-
-**Files Modified:**
-- `routes/aboutAdmin.js` — allowlist, enhanced redaction, error sanitization
-- `client/App.jsx` — `` prop on `/admin/about` route
-- `client/pages/AboutPage.jsx` — `admin` prop, conditional API call, admin content rendering
-- `server.js` — transaction wrapping for user creation, password validation
-
-**Deliverables:**
-- Path traversal eliminated (allowlist approach)
-- Public/admin AboutPage properly separated
-- Sensitive info redaction expanded
-- Error logs sanitized
-- Race condition prevented
-- Password validation enforced
-
----
-
-### Private_Hudson — Security Audit
-**Status:** ✅ COMPLETED
-**Task ID:** security-audit-001
-**Priority:** HIGH
-**Started:** 2026-05-09 17:05 CDT
-**Completed:** 2026-05-09 17:07 CDT
-
-**Objective:**
-Security-focused review of all recent Neo changes.
-
-**Work Completed:**
-- [x] Audited `server.js` and `setup/firstRun.js` for INIT_REGULAR_USER credential handling
-- [x] Audited `db/database.js` migration v0.42 for SQL injection and idempotency
-- [x] Audited `routes/aboutAdmin.js` for path traversal, auth bypass, information disclosure
-- [x] Audited `client/App.jsx` route guards
-- [x] Audited `client/pages/AboutPage.jsx` for XSS via markdown
-- [x] Wrote full findings to `SECURITY_AUDIT.md`
-
-**Files Modified:**
-- `SECURITY_AUDIT.md` — New file with detailed findings and remediation recommendations
-
-**Deliverables:**
-- 9 findings across CRITICAL/HIGH/MEDIUM/LOW/INFO severities
-- Recommended fixes for each finding
-- OWASP Top 10 mapping
-
----
-
-### Bishop — FUTURE.md Reorganization
-**Status:** ✅ COMPLETED
-**Task ID:** future-reorg-001
-**Priority:** MEDIUM
-**Started:** 2026-05-09 17:19 CDT
-**Completed:** 2026-05-09 17:30 CDT
-
-**Objective:**
-Reorganize FUTURE.md into strict priority order with emoji headings.
-
-**Work Completed:**
-- [x] Consolidated 37 pending items into priority tiers
-- [x] Grouped under 🔴 CRITICAL, 🟠 HIGH, 🟡 MEDIUM, 🔵 LOW, 💭 NICE TO HAVE
-- [x] Removed duplicate sections and empty headers
-- [x] Kept Completed Items and Template sections
-
-**Files Modified:**
-- `FUTURE.md` — Full reorganization
-
-**Deliverables:**
-- Clean, prioritized planning document
-- Consistent format with emoji priority markers
-
----
-
-## Current Work (In Progress)
-
-### Bishop — Migration Fix Verification & Documentation
-**Status:** ✅ COMPLETED
-**Task ID:** migration-fix-verification-001
-**Priority:** CRITICAL
-**Started:** 2026-05-09 18:10 CDT
-**Completed:** 2026-05-09 18:15 CDT
-
-**Objective:**
-Verify Neo's 🔴 CRITICAL migration login fix in `db/database.js` and update documentation.
-
-**Work Completed:**
-- [x] Built Docker image with `docker build --no-cache -t bill-tracker:local .`
-- [x] Tested with FRESH database — migrations applied correctly
-- [x] Tested with SIMULATED LEGACY database — detection, reconciliation, and migration completed successfully
-- [x] Verified LOGIN works in both scenarios
-- [x] Updated Engineering_Reference_Manual.md with migration fix documentation
-- [x] Updated DEVELOPMENT_LOG.md with completion entry
-
-**Test Results:**
-
-**Test 1: Fresh Database** ✅
-- Container started with new data volume
-- Migrations applied in order (v0.2 through v0.42)
-- Admin user created
-- Regular user created
-- Login successful
-
-**Test 2: Simulated Legacy Database** ✅
-- Database created with tables but NO `schema_migrations` table
-- Container detected legacy database
-- Reconciliation logged: `[migration] Detected legacy database, reconciling schema migrations...`
-- All existing migrations recorded: `v0.4`, `v0.14.4`, `v0.38`, `v0.40`
-- Remaining migrations applied: `v0.2`, `v0.3`, `v0.13`, `v0.14`, `v0.15`, `v0.17`, `v0.18.1`, `v0.18.2`, `v0.18.3`, `v0.41`, `v0.42`
-- Login successful
-
-**Log Output:**
-```
-[migration] Detected legacy database, reconciling schema migrations...
-[migration] Applied v0.4: monthly_bill_state: per-bill per-month overrides
-[migration] Recorded legacy migration v0.4: monthly_bill_state: per-bill per-month overrides
-[migration] Applied v0.14.4: bills: optional credit-card APR / interest rate
-[migration] Recorded legacy migration v0.14.4: bills: optional credit-card APR / interest rate
-[migration] Applied v0.38: import_history: per-user audit log
-[migration] Recorded legacy migration v0.38: import_history: per-user audit log
-[migration] Applied v0.40: ownership: user-scoped bills/categories
-[migration] Recorded legacy migration v0.40: ownership: user-scoped bills/categories
-[migration] Legacy database reconciliation complete
-[migration] Applying v0.2: payments: soft-delete column
-[migration] payments.deleted_at column added
-[migration] Applied v0.2: payments: soft-delete column
-[migration] Applying v0.3: payments: compound index for tracker query
-[migration] Applied v0.3: payments: compound index for tracker query
-[migration] Skipping already applied v0.4: monthly_bill_state: per-bill per-month overrides
-[migration] Applying v0.13: users: profile columns
-[migration] Applied v0.13: users: profile columns
-[migration] Applying v0.14: bills: history visibility mode
-[migration] bills.history_visibility column added
-[migration] Applied v0.14: bills: history visibility mode
-[migration] Skipping already applied v0.14.4: bills: optional credit-card APR / interest rate
-[migration] Applying v0.15: import_sessions and import_history tables
-[migration] Applied v0.15: import_sessions and import_history tables
-[migration] Applying v0.17: users: external identity / OIDC columns
-[migration] Applied v0.17: users: external identity / OIDC columns
-[migration] Applying v0.18.1: monthly_income: per-user monthly income for Summary planning
-[migration] Applied v0.18.1: monthly_income: per-user monthly income for Summary planning
-[migration] Applying v0.18.2: monthly_starting_amounts: per-user monthly starting amounts for 1st and 15th
-[migration] Applied v0.18.2: monthly_starting_amounts: per-user monthly starting amounts for 1st and 15th
-[migration] Applying v0.18.3: monthly_starting_amounts: add other_amount column
-[migration] Applied v0.18.3: monthly_starting_amounts: add other_amount column
-[migration] Skipping already applied v0.38: import_history: per-user audit log
-[migration] Skipping already applied v0.40: ownership: user-scoped bills/categories
-[migration] Applying v0.41: bills and categories: is_seeded flag for demo data cleanup
-[migration] bills.is_seeded column added
-[migration] categories.is_seeded column added
-[migration] Applied v0.41: bills and categories: is_seeded flag for demo data cleanup
-[migration] Applying v0.42: bill_history_ranges: per-bill date ranges for history visibility
-[migration] Applied v0.42: bill_history_ranges: per-bill date ranges for history visibility
-Database migrations complete for /data/db/bills.db
-```
-
-**Files Modified:**
-- `docs/Engineering_Reference_Manual.md` — Migration system update documentation added
-- `DEVELOPMENT_LOG.md` — this entry added
-
-**Deliverables:**
-- Build verification complete
-- Fresh database migrations verified
-- Legacy database reconciliation verified
-- Login functionality confirmed in both scenarios
-- Documentation updated for ops teams
-
----
-
-### Private_Hudson — Security Verification of Migration Login Fix
-**Status:** ✅ COMPLETED
-**Task ID:** migration-login-fix-security-verification-001
-**Priority:** CRITICAL
-**Started:** 2026-05-09 18:20 CDT
-**Completed:** 2026-05-09 18:25 CDT
-
-**Objective:**
-Verify security implications of Neo's migration fix in `db/database.js`, specifically the `handleLegacyDatabase()` and `reconcileLegacyMigrations()` functions.
-
-**Security Verification Checklist:**
-- [x] SQL Injection: All queries use hardcoded table/column names, no user input
-- [x] Data Integrity: Reconciliation only records migration status, no data modification
-- [x] Authorization Bypass: All migrations applied; no mechanism to skip security migrations
-- [x] Race Condition: SQLite WAL mode + busy_timeout prevents corruption
-- [x] Error Handling: Try/catch wrappers prevent partial state, idempotent operations
-
-**Test Results:**
-
-**Login Test (admin/admin123):** ✅
-```
-$ curl -s http://localhost:3036/api/auth/login -H 'Content-Type: application/json' -d '{"username":"admin","password":"admin123"}'
-{"user":{"id":1,"username":"admin","display_name":null,"role":"admin","active":true,"is_default_admin":true,"must_change_password":false,"first_login":true}}
-```
-
-**Legacy Database Detection Test:** ✅
-- Confirmed `schema_migrations` table does not exist in current DB
-- Confirmed all 5 core tables exist (users, bills, payments, categories, settings)
-- Legacy database correctly identified by `handleLegacyDatabase()`
-
-**Query Safety Verification:**
-- `PRAGMA table_info()` queries use hardcoded table names
-- `sqlite_master` queries use `IN ('users', 'bills', 'payments', 'categories', 'settings')`
-- No dynamic SQL construction from user input
-- Column name validation via `isValidColumnName()` whitelist in `runMigrations()`
-
-**Security Verdict: PASS**
-
-All 5 security focus areas verified:
-1. **SQL Injection** — PASS (no user input reaches migration queries)
-2. **Data Integrity** — PASS (reconciliation is read-only, idempotent)
-3. **Authorization Bypass** — PASS (all migrations apply; no skipping mechanism)
-4. **Race Condition** — PASS (SQLite WAL + atomic INSERT prevents corruption)
-5. **Error Handling** — PASS (no partial state, errors logged cleanly)
-
-**Files Reviewed:**
-- `db/database.js` — All migration functions
-- `server.js` — Startup/initialization logic
-
-**Deliverables:**
-- Security verification report complete
-- No blocking issues found
-- Migration system passes security audit
-
----
-
-**Last Updated:** 2026-05-09 18:25 CDT
-
-**Implementation Note:**
-The `handleLegacyDatabase()` function in `db/database.js` checks for a database with existing tables but an empty or missing `schema_migrations` table. When detected, it runs `reconcileLegacyMigrations()` which:
-1. Checks if core tables exist (users, bills, payments, categories, settings)
-2. Iterates through all migrations and marks already-applied ones as "recorded"
-3. Then `runMigrations()` applies any remaining migrations
-
-This ensures backward compatibility with existing deployments while preventing duplicate migrations.
-
----
-
----
-
-## v0.19.4 — Session Token Expiry Cleanup
-
-**Date:** 2026-05-09
-**Status:** COMPLETED
-
-### Agents
-- **Neo** — Implemented cleanupExpiredSessions(), v0.43 migration, periodic purge, per-user login cleanup (19m)
-- **Bishop** — Verified all tests pass: Docker build, migration, startup logs, login, interval (3m 5s)
-- **Hudson** — Security audit: 5 PASS, 1 FAIL (SESSION_CLEANUP_INTERVAL_MS validation — fixed by Ripley)
-- **Ripley** — Fixed Hudson finding (interval validation), committed v0.19.4, pushed, deployed
-
-### Files Modified
-- `db/database.js` — cleanupExpiredSessions(), v0.43 migration, COLUMN_WHITELIST
-- `server.js` — Startup cleanup, periodic interval, input validation for SESSION_CLEANUP_INTERVAL_MS
-- `services/authService.js` — Per-user expired session cleanup on login and createSession
-- `docs/Engineering_Reference_Manual.md` — Session cleanup documentation
-
-### Commits
-- `399882f` — v0.19.4: session token expiry cleanup
-- `3a1d613` — docs: v0.19.4 changelog, remove completed item from FUTURE.md
diff --git a/Dockerfile b/Dockerfile
index da684ab..6d34e91 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,5 +1,5 @@
# ── Stage 1: Build ─────────────────────────────────────────
-FROM node:18-alpine AS builder
+FROM node:22-alpine AS builder
WORKDIR /app
# native build deps (better-sqlite3 etc)
@@ -17,7 +17,7 @@ RUN npm run build
# ── Stage 2: Runtime ───────────────────────────────────────
-FROM node:18-alpine
+FROM node:22-alpine
WORKDIR /app
# minimal tools for debugging inside container + privilege drop in entrypoint
diff --git a/FUTURE.md b/FUTURE.md
index 6c96c89..44f1c26 100644
--- a/FUTURE.md
+++ b/FUTURE.md
@@ -2,8 +2,8 @@
**This document tracks potential future enhancements for Bill Tracker.**
-**Last Updated:** 2026-05-10
-**Current Version:** v0.24.3
+**Last Updated:** 2026-05-30
+**Current Version:** v0.34.2
## How to Use This Document
@@ -12,7 +12,7 @@ This file is a living document. Agents should:
2. Add new recommendations with priority levels
3. Never add completed items — move those to HISTORY.md instead
4. Reference this file when dispatching improvement tasks
-5. Only Ripley can remove items from this list.
+5. Only Ripley can remove items from this list. Notify Ripley if something needs to be removed.
### Priority Format
@@ -31,229 +31,158 @@ Items are grouped under their priority section heading (`## 🔴 CRITICAL`, `##
## Pending Recommendations
-### 🔴 CRITICAL
+## 🟠 HIGH
-
-### ~~🔴 Notification Runner Leaks Bill Details Across Users — CRITICAL~~ ✅ FIXED (v0.23.2)
-**Moved to HISTORY.md**
-
-
-### 🟠 HIGH
-
-### ~~🟠 Admin Can Toggle Payments on Any User Bill — HIGH~~ ✅ FIXED (v0.24.0)
-**Moved to HISTORY.md**
-
-### ~~🟠 Analytics Validation Errors Crash Instead of Returning 400 — HIGH~~ ✅ FIXED (v0.24.0)
-**Moved to HISTORY.md**
-
-### ~~🟠 User Export Drops Recurrence and History-Range Data — HIGH~~ ✅ FIXED (v0.24.0)
-**Moved to HISTORY.md**
-
-### ~~🟠 Single-User Mode Can Lock Itself Out When Expired Sessions Exist — HIGH~~ ✅ FIXED (v0.24.0)
-**Moved to HISTORY.md**
-
-
-### 🟡 MEDIUM
-
-
-### ~~🟡 Password Change Rate Limiter Applies to Every Profile Endpoint — MEDIUM~~ ✅ FIXED (v0.24.0)
-**Moved to HISTORY.md**
-
-### ~~🟡 Profile Password Change Does Not Invalidate Other Sessions — MEDIUM~~ ✅ FIXED (v0.24.0)
-**Moved to HISTORY.md**
-
-### ~~🟡 CSRF Defaults Conflict with SPA Token Loading — MEDIUM~~ ✅ FIXED (v0.24.0)
-**Moved to HISTORY.md**
-
-### ~~🟡 Change-Password Routes Are Globally Exempted from CSRF — MEDIUM~~ ✅ FIXED (v0.24.0)
-**Moved to HISTORY.md**
-
-### ~~🟡 Notification Due-Day Math Can Miss Same-Day Reminders — MEDIUM~~ ✅ FIXED (v0.24.0)
-**Moved to HISTORY.md**
-
-### ~~🟡 Upcoming Bills Allows Negative Day Windows — MEDIUM~~ ✅ FIXED (v0.24.0)
-**Moved to HISTORY.md**
-
-
-### Architecture: Business Logic Mixed with Route Handlers
+### 🟡 Projected Cash Flow — MEDIUM
**Priority:** MEDIUM
-**Added:** 2026-05-08 by Neo
+**Added:** 2026-05-16 by Ripley (from _null's prioritized roadmap)
**Description:**
-Many routes contain business logic that should be extracted to service layers.
+Show users what's coming: "You'll have $X left before the 15th", "Upcoming bills before next paycheck", and a "Safe-to-spend" estimate based on starting amount, unpaid bills, and scheduled income. Fits naturally with the existing 1st/15th bucket model.
+
+**Scope:**
+- "Remaining after bills" projection per bucket (1st half / 15th half)
+- "Upcoming bills before next paycheck" list
+- "Safe-to-spend" estimate based on starting balance minus unpaid bills
+- Scheduled income support (payday amounts)
**Rationale:**
-- `bills.js` contains `parseDueDay()`, `parseInterestRate()` — validation logic
-- `tracker.js` contains date/range calculations that are reused across routes
-- `admin.js` has complex OIDC config building mixed with routing
-- `analytics.js` has complex date-building logic (`buildMonths`, `monthKey`, etc.)
+- The 1st/15th bucket model is already built — cash flow projection is the natural next step
+- Most valuable feature for day-to-day money management
+- Turns a bill tracker into a financial planning tool
**Implementation Notes:**
-- Files to modify: Multiple route files + new service files in `/services/`
-- Estimated effort: 8 hours
-- Proposed structure:
- ```
- /services/billsService.js
- /services/trackerService.js
- /services/analyticsService.js
- /services/authService.js (existing)
- /services/oidcService.js (existing)
- /services/cleanupService.js (existing)
- ```
-- Route handlers should call services, not contain business logic
-
-### ~~Skip First-Login User Creation When ENV Seeds Users~~ ✅ COMPLETED (v0.22.3)
-**Moved to HISTORY.md**
-
-### ~~No Rollback Capability for Failed Migrations~~ ✅ COMPLETED (v0.23.1)
-**Moved to HISTORY.md**
-
-### ~~Limited Error Handling and Logging for Migrations~~ ✅ COMPLETED (v0.23.0)
-**Moved to HISTORY.md**
-
-**Rationale:**
-- Migration errors are silent or unclear
-- No logging of which migration failed or why
-- No way to diagnose schema inconsistencies
-- Risk: slow debugging on production issues
-
-**Implementation Notes:**
-- Add detailed logging: `[migration] Applying v0.20.0: Add user_groups table`
-- Include timing: `[migration] v0.20.0 completed in 234ms`
-- Log precondition checks: `[migration] Checking: table_exists('users')`
-- Error log with context: `[migration-error] v0.20.0 failed: UNIQUE constraint failed on users.username`
+- Requires user to enter starting balance and payday amounts (new settings fields)
+- Calculate: starting amount - unpaid bills due before next payday = safe-to-spend
+- Files to modify: `TrackerPage.jsx`, `routes/tracker.js`, `user_settings` table (new fields)
+- Estimated effort: 8-10 hours
---
-### 🔵 LOW
+### 🟡 Recurring Payment Rules — MEDIUM
+**Priority:** MEDIUM
+**Added:** 2026-05-16 by Ripley (from _null's prioritized roadmap)
+**Status:** Partial — infrastructure built (auto_mark_paid column, confirm/dismiss APIs, UI for suggestions), but no proactive suggestion scheduler generating payments on due date.
-### ~~🔵 Export Formats Include Sensitive Bill Credential Fields by Default — LOW~~ ✅ FIXED (v0.24.1)
-**Moved to HISTORY.md**
+**Description:**
+Auto-mark certain bills as paid on due date if `autodraft_status = assumed_paid`. Or create suggested payments awaiting confirmation. Good for autopay-heavy users.
-### ~~🔵 Duplicate Local Login Route Increases Auth Drift Risk — LOW~~ ✅ FIXED (v0.23.2)
-**Moved to HISTORY.md**
+**Scope:**
+- Bills with autopay/autodraft get a "suggested payment" on their due date
+- User confirms or dismisses the suggestion
+- Auto-mark option: bills can be set to automatically mark as paid on due date
+**Implementation Notes:**
+- ✅ `auto_mark_paid` column + bill edit checkbox
+- ✅ `applyAutopaySuggestions()` in trackerService handles auto-mark + suggestion generation
+- ✅ Confirm (`POST /api/payments/autopay-suggestions/:billId/confirm`) and dismiss (`POST /.../dismiss`) endpoints
+- ✅ Suggestion UI in TrackerPage with badge + confirm/dismiss buttons
+- ❌ No proactive suggestion engine — only runs when tracker loads
+- ❌ No scheduled task/cron to evaluate bills and create suggestions on due date
+- Estimated effort remaining: 2-3 hours
-### Add comprehensive unit and integration tests
+---
+
+### 🟡 Calendar Agenda Mode — MEDIUM
+**Priority:** MEDIUM
+**Added:** 2026-05-16 by Ripley (from _null's prioritized roadmap)
+
+**Description:**
+Replace the month-grid calendar with an agenda view: Today / This Week / Next 14 Days. Group bills by "needs action," "autopay," "already paid." More useful when actually paying bills.
+
+**Rationale:**
+- Month grids are pretty but not actionable
+- Agenda mode answers "what do I need to do right now?"
+- Groups by status makes it immediately clear what needs attention
+
+**Implementation Notes:**
+- New view toggle on CalendarPage: Grid vs Agenda
+- Agenda shows: Overdue → Today → This Week → Next 14 Days
+- Each group sorted by due date, with action status badges
+- Files to modify: `CalendarPage.jsx`, `routes/calendar.js`
+- Estimated effort: 6-8 hours
+
+---
+
+### 🟡 Filtered Exports — MEDIUM
+**Priority:** MEDIUM (upgraded from LOW)
+**Added:** 2026-05-11 by Ripley (from _null's prioritized roadmap)
+
+**Description:**
+Export only utilities, debts, overdue, date range, tax-relevant categories. Currently exports everything with no filtering.
+
+**Rationale:**
+- Users need "all Q1 utility bills" or "overdue payments this year" for reconciliation and tax prep
+- `/api/export/user-excel` exports everything — no query params for date range, category, or status
+
+**Implementation Notes:**
+- Add query params to export endpoints: `category_id`, `start`, `end`, `status` (paid/unpaid/overdue)
+- Files to modify: `routes/export.js`, `client/pages/DataPage.jsx`
+- Estimated effort: 6 hours
+
+---
+
+## 🔵 LOW
+
+### 🔵 Payment Method Tracking and Summary — LOW
**Priority:** LOW
+**Added:** 2026-05-11 by Ripley
+
+**Description:**
+The `payments` table has a `method` column (free-text) but no way to see "how much did I pay via autopay vs manual vs credit card this month."
+
+**Implementation Notes:**
+- Standardize payment methods: enum or controlled list (autopay, bank_transfer, credit_card, check, cash, other)
+- Add payment method breakdown to analytics or summary page
+- Files to modify: `routes/payments.js`, `AnalyticsPage.jsx`, schema migration
+- Estimated effort: 4-6 hours
+
+---
+
+### 🔵 No Keyboard Navigation or Shortcuts — LOW
+**Added:** 2026-05-11 by Ripley
+
+**Status:** Partial — Esc closes modals ✅, Cmd+K opens command palette ✅, arrow key tracker navigation ❌
+
+**Description:**
+Only a skip link exists for keyboard accessibility. No `Cmd+K` to find a bill, no `Esc` to close modals, no arrow keys to navigate the tracker grid.
+
+**Implementation Notes:**
+- ✅ `Esc` closes any open modal/dialog (via Radix Dialog default)
+- ✅ `Cmd+K` / `Ctrl+K` opens command palette (`CommandPalette.jsx`)
+- ❌ Arrow keys navigate tracker rows when grid is focused
+- Remaining effort: 1-2 hours
+
+---
+
+### 🔵 Add comprehensive unit and integration tests
**Added:** 2026-05-08 by Scarlett
**Description:**
-Currently no unit tests exist for components or hooks. The only testing appears to be functional tests in `test-functional.js`. Component-level testing is missing.
-
-**Rationale:**
-Code quality and maintainability. Unit tests catch regressions and document component behavior. Bill Tracker has complex business logic (bill calculations, monthly state, analytics) that should be tested.
+Currently no unit tests exist for components or hooks. The only testing is functional tests in `test-functional.js`.
**Implementation Notes:**
-- Set up Jest + React Testing Library
+- Set up Jest + React Testing Library (or vitest)
- Test key components: BillModal, TrackerPage row, BillsTableInner
- Test hooks: useAuth, custom form hooks
- Test utility functions in `client/lib/utils.js`
-- Consider vitest for faster test execution
-- Add CI integration for test execution
-- Files likely to be modified: Add `client/test/` directory, add `jest.config.cjs`
- Estimated effort: 8-12 hours for baseline coverage
-### Features: Missing Export for User-Specific Reports
-**Priority:** LOW
-**Added:** 2026-05-08 by Neo
-
-**Description:**
-No built-in way to export filtered data (e.g., "all bills in category X for last 6 months").
-
-**Rationale:**
-- `/api/analytics/summary` exists but returns JSON only
-- Users cannot generate Excel/PDF reports
-- No programmatic way to get export links for specific filters
-- `/api/export/user-excel` exports everything, not filtered views
-
-**Implementation Notes:**
-- Files to modify: `/home/kaspa/.openclaw/Projects/bill-tracker/routes/export.js`
-- Estimated effort: 6 hours
-- Add endpoints:
- - `GET /api/export/user-excel?category_id=1&start=2026-01&end=2026-06`
- - `GET /api/export/user-json?filter=bills&status=missed`
- - Add report title/description to export metadata
-
-### Features: Missing Bill Grouping and Reorganization API
-**Priority:** LOW
-**Added:** 2026-05-08 by Neo
-
-**Description:**
-No way to reorder bills, drag-and-drop, or group by custom criteria.
-
-**Rationale:**
-- `bills` table has `due_day` ordering but no manual sort order
-- Frontend likely orders by `due_day` only
-- Users cannot create bill groups or categories for bills
-- No way to mark bills as "hidden" or "archived" without deactivating
-
-**Implementation Notes:**
-- Files to modify: `/home/kaspa/.openclaw/Projects/bill-tracker/db/schema.sql`, `/routes/bills.js`
-- Estimated effort: 6 hours
-- Add:
- - `sort_order` column to bills table (default NULL, ordered first by sort_order then due_day)
- - `PUT /api/bills/reorder` endpoint accepting `{bill_id: new_index}`
- - `PUT /api/bills/:id/archived` to soft-dearchive (sets `archived` flag)
-
---
-### 💭 NICE TO HAVE
-### Add consistent form state management pattern
+## 💭 NICE TO HAVE
+
+### 💭 Add consistent form state management pattern
**Priority:** MEH
**Added:** 2026-05-08 by Scarlett
**Description:**
-Form state management is inconsistent across components. Some use `useState` for each field, others use form libraries. Validation patterns vary.
-
-**Rationale:**
-Consistency and maintainability. A consistent pattern makes it easier to add new forms and reduce bugs.
+Form state management is inconsistent across components. Some use `useState` for each field, others use form libraries.
**Implementation Notes:**
- Consider react-hook-form for complex forms
- Create reusable form field components (InputField, SelectField, etc.)
- Standardize validation approach
-- Files likely to be modified: `client/components/*.jsx`
-- Estimated effort: 4-6 hours for migration
-
----
-
-## Template for New Recommendations
-
-```markdown
-### [Feature Name]
-**Priority:** CRITICAL / HIGH / MEDIUM / LOW / MEH
-**Added:** YYYY-MM-DD by [Agent]
-
-**Description:**
-Brief description of the improvement.
-
-**Rationale:**
-Why this matters.
-
-**Implementation Notes:**
-- Technical approach
-- Files likely to be modified
-- Estimated effort
-
-**Depends On:**
-Any prerequisites or blocking issues.
-```
-
-## Completed Items
-
-### ✅ Security: Rate Limiting on /api/about-admin — MEDIUM
-**Completed:** 2026-05-09 (v0.19.0)
-**Fix:** `adminActionLimiter` (30 req/15min) applied to `/api/about-admin` route.
-
-### ✅ Security: Markdown Sanitization in AboutPage — MEDIUM
-**Completed:** 2026-05-09 (v0.19.0)
-**Fix:** `rehype-sanitize` added to `AboutPage.jsx` ReactMarkdown component.
-
-### ✅ Security: aboutAdmin() in API Client — LOW
-**Completed:** 2026-05-09 (v0.19.0)
-**Fix:** `aboutAdmin` endpoint function added to `client/api.js`.
-
----
+- Estimated effort: 4-6 hours
diff --git a/HISTORY.md b/HISTORY.md
index e79523d..bb1c363 100644
--- a/HISTORY.md
+++ b/HISTORY.md
@@ -1,4 +1,869 @@
# Bill Tracker — Changelog
+## v0.41.0
+
+### 🧪 Money-service test coverage
+
+- **[Tests] Covered two untested money-critical services** — the manual-vs-bank payment source-of-truth (`paymentAccountingService`) and the analytics month-window math (`analyticsService`) had no dedicated tests. Added `tests/paymentAccountingService.test.js` (bank-backed predicate, the accounting-active SQL fragment, and the core invariant: a bank payment overrides a provisional manual payment so it isn't double-counted, restores the balance, and the manual payment counts again with its balance re-applied when the override is removed) and `tests/analyticsService.test.js` (month key/label/addMonths/monthEndDate/buildMonths with year-boundary + leap-year edges, and `validateSummaryQuery` defaults/range errors). (Tracker X2)
+
+### ⚡ Tracker performance
+
+- **[Tracker] Killed the getTracker N+1 (was ~2–3 DB round-trips × N bills every home-page load)** — inside `bills.map`, `getTracker` ran a payments query per bill (`fetchPaymentsForBillCycle`) plus `computeAmountSuggestion` per bill, and the suggestion alone fired up to 12 queries per bill (6 months × 2) — roughly 70–450 queries for a 35-bill account on every Tracker load. Now one query fetches all bills' cycle payments (grouped in JS by each bill's own range) and two queries compute all amount suggestions (`computeAmountSuggestionsBatch`), replacing the per-bill loops. Behavior-preserving — `tests/amountSuggestionService.test.js` pins the batched suggestion to be byte-identical to the per-bill function, and the `trackerService` tests still pass unchanged. (Tracker P1)
+
+### ⚛️ React Query — deeper adoption
+
+- **[Client] Completed the React Query adoption with global error handling, prefetching, and mutations** — building on the page migration: (1) a global `QueryCache.onError` handler surfaces a toast when a *background refetch* fails while stale data is on screen (pages still render inline errors on the initial load), restoring the load-error feedback centrally; (2) `usePrefetchTracker()` warms the adjacent month's cache on month-nav hover/focus, so clicking prev/next is instant; (3) the quick-pay action — previously duplicated verbatim across the desktop and mobile tracker rows — is now a shared `useQuickPay()` `useMutation` hook (its `isPending` replaces a manual busy flag, and the tracker/badge caches invalidate on settle). (React Query mutation follow-ups: pay/skip and other writes can adopt the same hook pattern incrementally.)
+
+### ⚛️ React Query migration (all manual-fetch pages)
+
+- **[Client] Moved the 7 remaining manual-fetch pages onto React Query** — Analytics, Bills, Subscriptions, Summary, Snowball, Spending, and Bank transactions each fetched with hand-rolled `useEffect` + `load()` + `useState(data/loading/error)`. Migrated them all to `useQuery` hooks (in `client/hooks/useQueries.js`) whose keys encode the params, so React Query now provides caching, request dedup, cancellation, and out-of-order-response safety for free — the manual request-sequence guards added earlier were removed. `keepPreviousData` keeps the last result visible while a new month/filter/page loads. Mutations that previously called `load()` now `invalidateQueries`, and optimistic edits (list reorder/delete, categorize, budget/plan changes) write through `queryClient.setQueryData` wrappers so every existing call site works unchanged. Editable form fields seeded from the data (Summary starting-amounts/income, Snowball settings, Spending budgets) are now seeded via a data-synced effect; pagination (Spending/Bank) is a page-keyed query. Bonus: because Bills reuses the shared `['bills']` cache, bill mutations there now also refresh the Tracker/overdue badge live. Each page was its own commit, verified with `npm run lint` (0 errors) + `npm run test:client` + `npm run build`. (R5)
+
+### ✅ React correctness audit + ESLint enforcement
+
+- **[Client] Added ESLint + react-hooks enforcement and fixed everything it found** — there was no linting at all, so nothing enforced the two rules that catch real React bugs across 125 components/pages: **rules-of-hooks** (conditional-hook crashes) and **exhaustive-deps** (stale closures). Added an ESLint 9 flat config (`eslint.config.mjs`) with `eslint-plugin-react-hooks`/`react-refresh`, an `npm run lint` script, and wired it into `npm run ci`. First run: **0 rules-of-hooks violations** (clean), and the rest fixed:
+ - **Real latent bugs (were runtime-throwing / dead):** `ImportTransactionCsvSection` called `importErrorState()` without importing it (its own error handler would `ReferenceError` on a failed import); a dead duplicate `client/components/MobileTrackerRow.jsx` (unused, with undefined-setter refs) was deleted; `StatusPage`'s `dbOk` had a dead `?? true` branch; plus orphaned dead logic (an unrendered autopay-badge `useMemo`, an unwired categories keyboard handler).
+ - **All 13 `exhaustive-deps` fixed**, incl. a real stale-value bug: `BankSyncSection`'s save read `btLateGraceDays` outside its `useCallback` deps, so it could persist a stale late-grace value. Others stabilized derived arrays/objects feeding `useMemo` (TrackerPage `filters`/`rows`, HealthPage `bills`, BankTransactionsPage `transactions`) and documented intentional id/filter-scoped effects.
+ - **Out-of-order response guards (R3):** the month/filter-driven loaders on Analytics, Summary, and Spending now use the same request-sequence guard as the Bank ledger, so a slow response for old params can't overwrite fresher data on rapid navigation.
+ - Verified-clean (no change needed): event-listener cleanup is balanced everywhere, the always-mounted dialogs' `open`-guarded reset effects are correct, and the CSV preview's index-keys are fine (read-only sample). *(The larger React Query migration of the 7 manual-fetch pages remains as a follow-up enhancement — the async-race correctness is already handled.)*
+
+### 🧹 Bill modal decompose
+
+- **[Bill modal] Broke the 1,733-line God-component into focused sub-components** — `BillModal.jsx` did bill fields + payment CRUD + linked-transaction unmatch + merchant rules + templates + debt/autopay all in one file. Extracted seven presentational components under `client/components/bill-modal/` (`DebtDetailsSection`, `AutopayTrustIndicator`, `PaymentHistoryList`, `PaymentFormFields`, `UnmatchDialogs`, `LinkedTransactionsSection`, `TemplateSection`) plus a shared `transactionDisplay.js` helper module, threading state/handlers via props. State stays in the parent (the save action is unchanged), so it's **behavior-preserving** — each extraction was its own commit, verified with `npm run build` + `npm run test:client`. BillModal is now **1,090 lines (from 1,733, −37%)** and the big interwoven blocks (payments, linked transactions, unmatch flows, debt) are each their own testable file. (Tracker BM2)
+
+### 🎨 Tracker summary cards
+
+- **[Tracker] Fixed the two identical "Paid" boxes + surfaced Remaining + month-progress** — the summary row rendered `SummaryCard type="paid"` **twice** (this month and last month), visually indistinguishable, while the actionable *remaining* number only lived in the CashFlow card below. Now the previous-month figure is a muted **"Last month: $X" hint** under Total Paid (not a second green box), and a **Remaining** card (the existing-but-unused blue card type) surfaces `summary.remaining` when there's no bank hero (in bank mode the hero already shows the projected remaining, so it isn't duplicated). Added a compact **"$X of $Y paid · N bills left"** progress line under the cards. Respects the `tracker_show_summary_cards` setting; light/dark + a11y unchanged. *(The Playwright visual baseline for `/` may need a refresh via `npm run test:e2e:update` since the card row changed intentionally.)* (Tracker T3)
+
+### 🚀 Tracker modern UX
+
+- **[Tracker] Optimistic pay/skip + `toast.promise` on long syncs** — marking a bill paid/unpaid now flips the row **instantly** (optimistic local state) and rolls back on error, instead of waiting for the server round-trip before updating — on both the desktop and mobile rows. And the bank sync (Tracker) and the bill-modal "Sync" now use sonner's `toast.promise` — one toast that transitions loading → done → error, replacing the manual spinner-flag + separate success/error toasts. (Tracker M1/M2)
+
+### ✨ Tracker features & toast quality
+
+- **[Tracker] "Pay all due" per bucket + reversible quick-pay with specific toasts** — each bucket header now has a **Pay all due (N)** action that records a payment for every unpaid, occurrence-gated bill in that bucket in one `bulkPay` call, behind a confirm dialog (showing the count + total) and a single **Undo** toast that deletes the just-created payments. Quick-pay (the inline "Add" on a row) and mark-paid now show a **specific** toast ("Rent — $1,200 paid") and quick-pay gained an **Undo** action to match un-pay — it was the only reversible action without one. (Per-bill snooze on overdue rows already exists in the Overdue Command Center.) (Tracker T4)
+
+### 🧹 Tracker cleanup
+
+- **[Tracker] Consolidated the "paid or autodraft = done" check + tidied a few spots** — the settled-status test (`status === 'paid' || status === 'autodraft'`) was copy-pasted across the server and client; added a single `isPaidStatus(status)` (+ `PAID_STATUSES`) to `services/statusService.js` and a matching `isPaidStatus` to `client/lib/trackerUtils.js`, and routed the unambiguous call sites through it (`trackerService`, `StatusBadge`, `CalendarPage`, and `rowIsPaid`) — the intentionally paid-*only* counts (`count_paid`, `count_autodraft`) are left distinct. Also replaced two inline `Math.max(r.balance || 0, 0)` sums in `getTracker` with the existing `rowOutstanding` helper, and gave the Tracker's display-settings load a quiet toast on failure instead of a silent swallow. Behavior-preserving; full server + client suites green. (Tracker T5)
+
+### 🐛 Tracker & bill-modal hardening
+
+- **[Bill modal] Correctness + error/toast + validator cleanup** — several small fixes in `BillModal`: `handleBlur` used positional guessing that defaulted every unmapped field to `interestRate`'s value (latent, masked by inline validators) — now takes the field value explicitly; the three copy-pasted money validators collapsed into one shared `validateNonNegativeMoney(val, label)` in `client/lib/money.js` (the expected-amount message also went from "positive number" to "non-negative", since 0 is allowed); the save action's duplicate due-day/interest-rate re-validation (which re-checked with toasts what `validateForm` already field-validated) was removed; the save/deactivate/verify-autopay `toast.error(err.message)` calls got fallbacks so a missing message never shows "undefined"; and the save toasts now name the bill ("Rent added" / "Rent updated"). Tests: `client/lib/money.test.js` covers the shared validator. (Tracker BM1)
+- **[Tracker] Route error handling + autopay write atomicity** — `routes/tracker.js` had no try/catch and returned a plain `{error}` shape unlike the rest of the API; its three GET handlers now wrap in try/catch and return `standardizeError` (`{error, message, field, code}`), including the invalid-month validation path. Separately, `applyAutopaySuggestions` (which runs on a `GET /tracker`) inserted the auto-mark-paid payment and applied the balance delta as two un-transactional steps — a mid-way failure could leave a payment without its balance adjustment. Wrapped the INSERT + `applyBalanceDelta` in a single `db.transaction`. Covered by new cases in `tests/trackerService.test.js` (auto-mark creates one payment + drops the balance, idempotent on re-load; route returns the standardized error). (Tracker T2)
+- **[Tracker] Sidebar overdue badge (and drift/bills) went stale after row actions** — the app never called `queryClient.invalidateQueries`; a Tracker mutation only `refetch()`'d the single tracker query passed down as `refresh`. So after paying/skipping/editing a bill, the sidebar **overdue badge** (`['overdue-count']`, 2-minute staleTime) kept its old number for up to two minutes — you could clear your last overdue bill and still see "3" — and the drift report / bills list didn't update either. Added a `useInvalidateTrackerData()` hook (invalidates `tracker` + `overdue-count` + `drift-report` + `bills`) and wired it in place of the bare `refetch` handed to the rows, `BillModal.onSave`, bank-sync, reorder, and the payment/late-attribution handlers, so the whole shell updates live. (Tracker X3)
+- **[Notifications] "Reminder days before" was a dead setting — the notifier ignored it** — every bill has a `reminder_days_before` column (default 3) and the bill modal exposed a "Reminder Days" control for subscriptions, but `services/notificationService.js` used a hard-coded schedule (early reminder always at exactly 3 days out) and never read the column. A user who set "remind me 7 days before" still only got the fixed 3-day/1-day/today reminders. The early reminder now fires at the bill's own `reminder_days_before` lead (only when ≥ 2 days, so it never collides with the 1-day/same-day reminders), and the email subject + body say "due in N days" using that value. The lead-time selection was pulled into a pure, exported `reminderTypeFor(bill, diffDays)` so it's unit-tested directly (`tests/notificationLeadTime.test.js`) — default 3 stays backwards-compatible. The **"Reminder Days Before" control now shows for every bill** (not just subscriptions), and saving a non-subscription bill no longer clobbers the column back to 3. (Tracker BM3)
+- **[Bill modal/SimpleFIN] Importing bank payments didn't refresh the payment list or the Tracker** — the two flows in the bill modal that *create* payments — **Sync** (`syncBillSimplefinPayments`) and a **merchant-rule historical import** (`onRulesChanged` → `importHistoricalPayments`) — only reloaded the linked-transactions list, unlike the unmatch handlers which correctly reload payments *and* linked transactions *and* call `onSave`. So after importing, say, 3 payments from bank history, the modal's Payment History stayed stale and the Tracker row behind it kept showing "due/overdue" even though the bill was now covered — until you closed and reopened. Both paths now `await Promise.all([loadPayments(), loadLinkedTransactions()])` then `onSave?.()`, matching the unmatch pattern, so imported payments appear immediately and the Tracker updates live. (The SimpleFIN *search/preview/candidate* flow was already correct.) (Tracker BM4)
+- **[Tracker/SimpleFIN] Bank card's "unpaid this month" and "remaining" over-counted off-month bills** — `buildBankTracking` (`services/trackerService.js`) summed `expected_amount` for *all* active unpaid bills via SQL with no occurrence gate, so an annual or off-month quarterly bill inflated `unpaid_this_month` (and therefore the bank `remaining`) even though the Tracker rows beside it correctly excluded it — the same class of bug as QA-B5-02, still live on the bank path. `getTracker` now derives the unpaid total from the already-gated rows (via `resolveDueDate`), netting partial payments, and passes it into `buildBankTracking`. Also made `summary.remaining` / `total_remaining` use the bank card's own remaining when bank tracking is on (they previously used manual starting-amount math even in bank mode, disagreeing with safe-to-spend), and switched a stray `balance / 100` to `fromCents`. New test file `tests/trackerService.test.js` covers the gating fix, summary totals, the bank-mode remaining agreement, cents↔dollars integrity, and `getOverdueCount` gating — the dense Tracker aggregation had no dedicated tests before. (Tracker T1)
+- **[Payments] Quick-pay could create duplicate payments and double-drop the balance** — `POST /api/payments/quick` (the one-click "pay" behind every Tracker row) had **no duplicate guard** and its INSERT + balance update weren't atomic, unlike `POST /api/payments/bulk`. A double-click, a retry, or two open tabs made a *second* payment for the same bill/date/amount and applied the balance drop twice; a failure between the INSERT and the balance write left a payment with no balance adjustment. Quick-pay now checks the same `bill_id + paid_date + amount` composite key (returning the existing payment idempotently, HTTP 200) and wraps the INSERT + `applyBalanceDelta` in a single `db.transaction`. A different amount on the same day is still a legitimate new payment. Test: `tests/paymentsQuickRoute.test.js`. (Tracker X1)
+
+### ✨ Data page overhaul
+
+- **[Data] Redesigned the Data page (modern, goal-oriented, 2026)** — replaced the dense tabbed/collapsible-card layout with a settings-style **two-pane** shell: a sticky goal-nav (Bank sync · Transactions · Import · Export & backups) beside one active pane, collapsing to a segmented control on mobile. Adds a **connection hero** with five distinct states (loading / server-disabled / error+retry / not-connected / connected±needs-attention) so a network blip is never mistaken for "not connected" and a server without SimpleFIN never shows a dead Connect button; **`?section=` deep-linking** (URL source of truth, back-button friendly, migrates the old tab key); plain-language titles + icons; a live **"N to review"** badge and **health dot** on the nav; at-a-glance transaction count + "syncs automatically" reassurance; **lazy-loaded** import panes; reduced-motion-aware transitions; and command-palette section links. `/data` is now covered by the authed axe sweep (zero critical/serious). Shell only — every section component (and the SimpleFIN sync buttons) is reused unchanged. `SectionCard` chrome modernized (icon chip, sentence-case titles).
+- **[Import] OFX / QFX transaction import** — many banks export `.ofx`/`.qfx`; you can now import them (OFX 1.x SGML, OFX 2.x XML, and QFX). `services/ofxImportService.js` parses to the same normalized shape as the CSV path and reuses the shared session + dedupe (`user_id, data_source_id, provider_transaction_id`) + `import_history` primitives. Routes `POST /api/import/ofx/{preview,commit}`; UI in the Import pane (upload → preview → import). Test `tests/ofxImportService.test.js`.
+- **[Export] Richer export — JSON + date ranges** — `GET /api/export` now accepts a `from`/`to` date range (in addition to `year`) for CSV or JSON, and there's a new `GET /api/export/user-json` full portable JSON export (same assembly as the SQLite/Excel exports). The Download pane gains a JSON card and a filtered "Payments export" (date range + CSV/JSON). Test `tests/exportRicher.test.js`.
+- **[Data] "Erase my data" (danger zone)** — a self-service reset: `POST /api/user/erase-data` (rate-limited, type-to-confirm) transactionally wipes your financial + imported data (bills, payments, transactions, bank connections, categories, imports, rules) while **preserving your account, login, 2FA and preferences**, then re-seeds default categories and audits the action. Red-accented card in Export & backups with a type-to-confirm dialog. Test `tests/eraseUserData.test.js` (wipes only the requesting user, preserves others + the account).
+
+### 🔍 Snowball review
+
+- **[Debt] Snowball page surfaces projection failures + smaller polish** — the payoff-projection panel silently swallowed fetch errors (`catch { /* non-fatal */ }`), so a failed load showed nothing with no way to recover; it now shows a "Couldn't load … Try again" state (and a subtle "showing the last result" banner when a *refresh* fails). `loadProjection` now uses the amount currently typed (not just the last-saved value), matching the debounced live preview. The over-50-years banner now reads "one or more debts won't pay off at this rate" (accurate for the unpayable-debt case too), and the extra-payment validation copy says "non-negative" (0 is allowed). (Snowball review #2, #3, #8; #9 was already handled by the input's on-blur auto-save.)
+- **[Debt] Payoff projection no longer fakes a "freedom" date when a debt can't be paid off** — if a debt's minimum payment never overcomes its interest and the rolling snowball can't cover it either, it never reaches $0. The simulation previously excluded that debt from `months_to_freedom` (a `Math.max` over payoff months treated "never" as 0), so it reported the *other* debts' last month as your payoff date — misleadingly optimistic. Now `months_to_freedom`/`payoff_date` are `null` when any active debt never clears, the result is flagged `capped`, and each such debt is marked `never_paid` (`services/snowballService.js`, `services/aprService.js`). (Snowball review #1)
+- **[Debt] Snowball plan API: consistent errors, less duplication, no N+1** — the four plan-lifecycle endpoints (pause/resume/complete/abandon) were near-identical copies and returned a plain `{error}` shape unlike the rest of the API; folded them into one `transitionPlan` helper returning `standardizeError` `{message, code}` with proper state guards + ownership scoping. `enrichPlanWithProgress` fetched each snapshot bill in its own query (`GET /plans` = plans × debts queries) and wasn't user-scoped — now one `WHERE id IN (…) AND user_id = ?` batch. Test: `tests/snowballPlanRoute.test.js`. (Snowball review #4, #6, #7)
+- **[Debt] Added the missing test coverage for all payoff math** — the debt-payoff engine (`calculateSnowball`/`calculateAvalanche`/`calculateMinimumOnly`/`amortizationSchedule`/`monthsToPayoff`/`debtAprSnapshot`) had **zero** automated tests despite being the most math-heavy code in the app. Added `tests/snowballMath.test.js` (12 tests) with hand-calculated examples (0% and 12% APR amortization rows, snowball rolling, avalanche-beats-snowball interest, skip reasons, APR breakdown) and the unpayable-debt edge above. (Snowball review #5)
+
+### 🐛 QA Fixes
+
+- **[SimpleFIN] Purging a soft-deleted bill orphaned its matched transactions** — found on the live SimpleFIN DB: 3 transactions were `match_status='matched'` with `matched_bill_id=NULL`. Root cause: bills are soft-deleted (retained for recovery), then the retention GC (`pruneSoftDeletedFinancialRecords`, `services/cleanupService.js`) hard-deletes them past the 30-day window. `transactions.matched_bill_id` is `ON DELETE SET NULL`, so the purge nulled the pointer but left `match_status='matched'` — a limbo row **excluded from spending/analytics (`match_status != 'matched'`) yet attributed to no bill**, silently dropping that spend. The purge now releases those matches back to `'unmatched'` in the same transaction and self-heals any pre-existing orphans; retention behaviour is unchanged. Verified on a copy of the live DB (3→0 orphans, 0 transactions lost). Regression: 3 tests in `tests/backupAndCleanup.test.js`. (QA-B5-04)
+- **[Security] SQLite DB was created world-readable (644)** — `docker-entrypoint.sh` locked the data *directory* (`chmod 700`) but never the DB *file*, and SQLite created `bills.db`/`-wal`/`-shm` under the default umask (644). On a real deploy the DB (financial data + encrypted SimpleFIN token, sessions, SMTP/OIDC secrets) was world-readable, shielded only by the parent dir's 700. Added `umask 077` to the entrypoint (DB, WAL/SHM, backups, exports now created 600/700) plus an explicit `chmod 600` for pre-existing files on upgrade. (QA-B16-02)
+- **[Privacy] The version check is now opt-out-able** — the privacy policy described the external version check as "optional", but there was no way to disable it (it hit a hardcoded upstream host whenever the About/Status/version page loaded). Added an **admin toggle**: an `update_check_enabled` setting gates the request in `services/updateCheckService.js` (default on — when off, **no external request is made**), exposed via `GET`/`PUT /api/about-admin/update-check-setting` and a switch on the admin **System Status** page. Privacy policy updated to state an admin can disable it. Test: `tests/updateCheckOptOut.test.js`. (was QA-B16-01)
+- **[Security] Bill name could inject HTML into reminder emails** — `buildEmailHtml` (`services/notificationService.js`) escaped the bill name in the detail table but interpolated it **raw** into the reminder message line (`${bill.name} is due…`), so a bill named `` landed unescaped in the email HTML. Self-XSS (reminder emails go to the bill's owner), but a clear inconsistent-escaping bug — now escaped everywhere. Covered by `tests/notificationDelivery.test.js`. (was QA-B14-04)
+- **[Notifications] "Send test push" was completely broken** — `services/notificationService.js` attached its `_push` export (the ntfy/Gotify/Discord/Telegram helpers) *before* the final `module.exports = {…}`, which clobbered it, so `require('…/notificationService')._push` was `undefined`. `routes/notifications.js` (`const { sendTestPush } = require(…)._push || {}`) therefore always hit `throw 'Push service not initialised'` → **`POST /api/notifications/test-push` always returned 500** for every user testing their push channel. Scheduled reminders were unaffected (they call `sendPushToUser` in-scope). Moved the `_push` assignment after the reassignment. Covered by `tests/notificationDelivery.test.js` (per-channel payloads, dispatch, error handling, and a check that the auth token never leaks into the message body). (was QA-B10-01)
+- **[Summary/Analytics] Non-monthly bills were counted in every month** — the Summary expense list/total and the Analytics "expected vs actual" line both counted annual (and off-month quarterly) bills for months they weren't due, over-stating the obligation and disagreeing with the Tracker (e.g. a yearly insurance bill inflated every month). Both `routes/summary.js` and `services/analyticsService.js` now gate bills by `resolveDueDate` — the same occurrence check the Tracker uses. Guarded by Tracker↔Summary and Tracker↔Analytics reconciliation checks in `e2e/api.probe.spec.js`. The SimpleFIN bank-tracking `unpaid_this_month` metric had the same gap and is fixed the same way (fetch + JS `resolveDueDate` filter, since SQL can't call it), covered by `tests/summaryBankTracking.test.js`. (was QA-B5-01, QA-B5-02, QA-B5-03)
+- **[Data] Seed Demo Data amounts were 100× too small** — `scripts/seedDemoData.js` inserted demo dollar amounts straight into `bills.expected_amount` / `current_balance` / `minimum_payment`, which became integer-cents columns in the v1.03 migration, so a seeded "$85" bill showed as $0.85 (a whole demo month totalled ~$32 instead of ~$3,200). Now wraps demo values in `toCents()` before insert. Regression guard added in `e2e/api.probe.spec.js`. (was QA-B9-01)
+- **[Bills] `expected_amount` was unvalidated** — POST/PUT `/api/bills` accepted negative amounts, non-numeric input (silently coerced to $0), and absurd values (`1e15` → cents past `Number.MAX_SAFE_INTEGER`). `validateBillData` (`services/billsService.js`) now rejects non-numeric / negative / out-of-range with a structured `VALIDATION_ERROR`, accepting `0`…`$100,000,000`. Regression assertions in `e2e/api.probe.spec.js`. (was QA-B13-01)
+- **[UI] Negative amounts rendered as "$-50.00"** — client `fmt()` (`client/lib/utils.js`) and server `formatUSD()` (`utils/money.js`) placed the minus sign after the currency symbol; now render the conventional "-$50.00". Test added in `client/lib/utils.test.js`. (was QA-B6-01)
+- **[a11y] Icon-only controls and chart SVGs had no accessible name** — Radix Select filter/sort triggers (Tracker, Bills) and the Spending month-nav buttons rendered with no discernible text (screen readers announced a bare "button"); Analytics chart `