# Bill Tracker — Changelog

## v0.25.0

### Added
- **Roadmap Page** — Kanban-style priority lanes (CRITICAL → NICE TO HAVE) with collapsible items, lazy-loaded activity log tab, admin-only `/api/about/roadmap` and `/api/about/dev-log` endpoints. Replaces AdminDashboard.

### Fixed
- **Import CSRF failure** — XLSX, SQLite, and backup file imports now include `x-csrf-token` header in all three raw `fetch()` calls (`importAdminBackup`, `previewSpreadsheetImport`, `previewUserDbImport`). Previously returned "session expired or fraudulent" 403 on every import attempt.

### Removed
- **AdminDashboard.jsx** — Replaced by RoadmapPage with kanban layout.

## v0.24.4

### Changed
- **Analytics page mobile layout** — Charts, heatmap, controls, donut chart, and checkbox grid now display properly on mobile screens. Heatmap columns narrowed, responsive breakpoints added throughout.

### Fixed
- **Previous month payment toggle** — Clicking payment badges (Missed, Late, Due Soon, Upcoming) on previous months now creates/removes payments for the correct month instead of always using today's date. Backend scopes payment lookup to the viewed year/month; frontend passes year/month context.
- **Mobile tracker row toggle** — MobileTrackerRow StatusBadge was missing clickable/onClick props; now wired up to toggle paid/unpaid.

## v0.24.3

### Changed
- **Status badge toggle is instant** — removed the AlertDialog confirmation popup. Clicking Late/Due Soon/Upcoming/Missed badges now toggles paid/unpaid directly.

## v0.24.2

### Fixed
- **StatusBadge toggle-paid broken** — `handleTogglePaid()` was using `row.bill_id` instead of `row.id`, causing the API call to fail with an undefined bill ID. Clicking Late/Due Soon/Upcoming/Missed badges now correctly toggles paid/unpaid status.

## v0.24.1

### Added
- **Export privacy warning** — Amber alert banner on Download My Data section warning that exports may contain sensitive account metadata (website URLs, usernames, account info). Updated "What's included" list to show monthly starting amounts and history ranges.

## v0.24.0

### Fixed
- **Admin toggle-paid restricted** — Admins can no longer toggle payments on other users' bills. All bill payment mutations now require ownership (`routes/bills.js`).
- **Analytics crash fix** — Imported missing `standardizeError` in `routes/analytics.js`. Invalid query params now return 400 instead of crashing with ReferenceError.
- **Export data integrity** — User exports (Excel and SQLite) now include `cycle_type`, `cycle_day`, and `bill_history_ranges`. Previously, non-monthly recurrence settings and history visibility ranges were lost on export/import.
- **Single-user mode lockout** — Fixed `getSingleModeUser()` joining sessions table unnecessarily. When a configured user had only expired sessions, the join excluded them. Now validates only user existence, active status, and role.
- **Password rate limiter scoped** — `passwordLimiter` moved from all `/api/profile` routes to only `POST /change-password`. Normal profile reads/updates no longer hit password-change rate limits.
- **Profile password change session invalidation** — Fixed `routes/profile.js` referencing `req.sessionId` (never set by requireAuth). Now uses `req.cookies?.[COOKIE_NAME]` consistent with the auth route, so other sessions are properly invalidated on password change.
- **CSRF defaults aligned** — `CSRF_HTTP_ONLY` default changed from `true` to `false`. The SPA uses a double-submit pattern reading `document.cookie`, so `httpOnly=true` was always broken without the docker-compose override. Default now matches actual usage.
- **CSRF protection on password change** — Removed `csrfSkip` exemptions for `/api/auth/change-password` and `/api/profile/change-password`. These are sensitive state mutations and should have CSRF protection like all other authenticated writes.
- **Notification due-day math** — Fixed `runNotifications()` comparing raw timestamps instead of calendar days. Bills due today could be classified as `overdue` instead of `due_today` when checked after midnight had passed. Now normalizes both dates to local date-only before comparison.
- **Upcoming bills validation** — `GET /api/tracker/upcoming` now clamps `days` to `1–365` and defaults invalid/NaN input to 30. Negative or non-numeric values no longer produce empty results.

## v0.23.4

### Fixed
- **Clear Demo Data button now works** — Removed misleading "coming soon" placeholder text. The Clear Demo Data button with AlertDialog confirmation is now accessible from the seeded state view.
- **Seed script user ID bug** — Fixed `seedDemoData.js` creating bills with wrong user ID (`userId` instead of `targetUserId`).
- **Removed duplicate seed endpoint** — Deleted redundant `/api/settings/seed-demo-data` route (canonical endpoint is `/api/user/seed-demo-data`).

## v0.23.3

### Changed
- **Replaced native `confirm()` with shadcn/ui AlertDialog** — TrackerPage (mark as paid) and DataPage (import confirmation) now use themed, accessible AlertDialog components instead of browser-native `confirm()` dialogs. Consistent with the app's design system and supports dark/light mode.
- **STRUCTURE.md tech stack corrected** — Updated from "Next.js App Router" to the actual stack (Vite + React + Tailwind + shadcn/ui + Sonner)

## v0.23.2

### Security
- **CRITICAL: Notification privacy leak fix** — In per-user notification mode, bills were sent to all opted-in recipients regardless of ownership. Added ownership filter (`bill.user_id !== recipient.id`) and orphaned bill guard. Security audit by Private_Hudson confirmed the fix is airtight.
- **Duplicate login route removed** — Deleted `routes/authLogin.js`, consolidating login logic into `routes/auth.js` only.

### Changed
- `services/notificationService.js`: Added per-user ownership filter and null `user_id` guard in notification runner
- `routes/authLogin.js`: Removed (consolidated into `routes/auth.js`)
- `docs/Engineering_Reference_Manual.md`: Removed stale `authLogin.js` duplicate route note, updated version to 0.23.2
- `README.md`: Updated to reflect current features, env vars, security notes, project structure, and known limitations

---

## v0.23.1

### Added
- **Migration Rollback** — New `rollbackMigration()` function in database.js and `POST /api/admin/migrations/rollback` endpoint for admin-only migration rollback
- Rollback support for v0.44 (performance indexes), v0.45 (audit_log table), v0.46 (cycle columns)
- Transaction-wrapped rollback with detailed logging (`[rollback]`, `[rollback-error]`)
- Audit logging for rollback events: `migration.rollback` and `migration.rollback.failure`
- Error codes: `NOT_APPLIED` (404), `ROLLBACK_NOT_SUPPORTED` (422)

### Fixed
- **Duplicate migrationStartTime declaration** — Removed duplicate variable declaration causing syntax error
- **Duplicate else block** — Removed duplicated migration skip branch in `runMigrations()`
- **DB path exposure** — Changed `Opening DB at:` log to use `path.basename()` instead of full path

### Changed
- `routes/admin.js`: Added `rollbackMigration` import and `/migrations/rollback` endpoint
- `db/database.js`: Added `rollbackMigration()` function with transaction support and rollback SQL map

---

## v0.23.0

### Added
- **Migration Logging Enhancement** — Detailed logging for each migration step including timing, error logging with timing, and total migration time reporting
- **Circular Dependency Fix** — Lazy import pattern via `getLogAudit()` function prevents circular dependency with auditService
- **Logging Categories** — `[migration]`, `[migration-error]`, `[migration-failure]` with timing in milliseconds

### Changed
- `db/database.js`: Added `[migration] Applying {version}` log before each migration
- `db/database.js`: Added `[migration] {version} completed in Xms` log after each migration
- `db/database.js`: Added `[migration] All migrations completed in Xms` log after all migrations
- `db/database.js`: Added `[migration-error] Failed after Xms: ...` log on migration failures
- `db/database.js`: Added lazy `getLogAudit()` function with try/catch to avoid circular dependency
- `db/database.js`: Unversioned user notification columns migration now logs timing

### Security
- Audit log injection: ✅ PASS — getLogAudit() only used after initSchema completes
- Lazy import safety: ✅ PASS — try/catch wrapper, fallback empty function
- SQL injection: ✅ PASS — logging only, no dynamic SQL
- Timing manipulation: ✅ PASS — Date.now() local to migration loop
- Circular dependency: ✅ PASS — lazy import avoids require cycle
- Error logging completeness: ✅ PASS — both success and failure paths logged
- Audit logging safety: ✅ PASS — try/catch prevents audit errors from crashing migration

---

## v0.22.3

### Fixed
- **ENV-Seeded Users First-Login Bug** — Admin and regular users created via `INIT_ADMIN_USER`/`INIT_ADMIN_PASS` and `INIT_REGULAR_USER`/`INIT_REGULAR_PASS` environment variables no longer see the first-login/force-password-change flow on container restarts

### Changed
- `setup/firstRun.js`: `runFromEnv()` now resets `first_login=0, must_change_password=0` when updating existing admin and regular users
- `server.js`: Seed logic resets `first_login=0, must_change_password=0` when updating existing regular users  
- `db/database.js`: `[init] Reset password` code now sets `must_change_password=0` instead of `1` to match intended behavior

### Added
- Audit logging (`seed.flag_reset` action) for flag resets in `setup/firstRun.js` and `server.js`
- `db/database.js` init-time flag resets use `console.log` (avoids circular dependency with auditService during DB initialization)

---

## v0.22.2

### Added
- **Session Invalidation on Password Change** — All other sessions are terminated when you change your password; current session gets a new ID
- **Logout All Devices** — New `POST /api/auth/logout-all` endpoint to sign out from every device at once

### Changed
- `invalidateOtherSessions()` helper in authService.js
- Both change-password routes (auth + profile) now rotate session ID
- Added `last_password_change_at` to auth.js change-password for consistency with profile.js
- Audit logging for `logout.all` and `password.change` events

## v0.22.1

### Changed
- **N+1 Query Optimization** — Batch queries replace per-bill loops in tracker and analytics (monthly states, payments, previous month, upcoming)
- Empty bill list edge case handled with `billIds.length > 0` guards

## v0.22.0

### Added
- **React Query Migration** — TrackerPage now uses TanStack Query (useQuery) for data fetching with caching, stale-while-revalidate, and auto-refetch
- **Custom Query Hooks** — `useTracker()`, `useBills()`, `useCategories()` in `client/hooks/useQueries.js`
- **Query DevTools** — React Query DevTools available in development mode
- **QueryClientProvider** — Global config with 2min staleTime, 1 retry, refetchOnWindowFocus disabled

### Changed
- TrackerPage: replaced manual `useState`/`useEffect` with `useTracker()` hook
- `load()` callback replaced by `refetch()` from React Query
- Error handling: `useEffect` + `useRef` pattern prevents duplicate toast notifications

## v0.21.1

### Added
- **Loading Skeletons** — Tracker and Bills pages show skeleton placeholders during data loading with `aria-busy` attributes
- Reusable `Skeleton` component with line, circle, card, button, input variants

## v0.21.0

### Added
- **3-Month Trend Indicator** — Tracker shows up/down/flat trend vs 3-month average with percentage change (↑ green, ↓ red, → gray)
- Trend card with purple gradient header and TrendingUp icon
- Backend: 3-month payment aggregation with year-wrapping, ±2% threshold for "flat"

## v0.20.9

### Added
- **Previous Month Paid** — "Last Month" column on Tracker shows last month's paid amount per bill; summary card shows previous month total
- Backend: `previous_month_paid` per bill row, `previous_month_total` in summary, year-wrapping for January

## v0.20.8

### Added
- **Billing Cycle Sub-categories** — `cycle_type` (monthly/weekly/biweekly/quarterly/annual) and `cycle_day` columns on bills, conditional day selector in UI (ordinal dropdown for monthly, weekday dropdown for weekly/biweekly, free text for quarterly/annual)
- Migration v0.46 adds `cycle_type` and `cycle_day` columns
- Server-side validation of cycle_type values
- Smart defaults: cycle_day auto-sets when cycle_type changes

## v0.20.7

### Added
- **Skip-to-content link** — keyboard users can skip navigation directly to main content
- **ARIA accessibility** — `aria-expanded` and `aria-haspopup` on Tracker menu, `aria-label` on footer, `role="main"` on layout wrapper
- **Main landmark** — proper `<main>` element with unique `id` for skip navigation target

## v0.20.6

### Added
- **Audit logging** — security event tracking via `audit_log` table (migration v0.45)
- **`logAudit()` service** — safe logging function that never crashes the app
- **Logged events:** `login.success`, `login.failure`, `logout`, `password.change`, `role.change`, `csrf.failure`, `profile.update`, `profile.settings.update`
- **Indexes:** `idx_audit_log_user` and `idx_audit_log_action` for query performance

## v0.20.5

### Added
- **Bulk payment validation** — `/api/payments/bulk` now requires `{ payments: [...] }` format
- **Max 50 items per request** — prevents abuse via oversized bulk requests
- **Per-item input validation** — `bill_id` must be integer, `paid_date` must be YYYY-MM-DD, `amount` must be >= 0
- **Duplicate detection** — payments with same `bill_id + paid_date + amount` are skipped, not duplicated
- **Structured response** — `{ created: [...], skipped: [...], errors: [...] }`

## v0.20.4

### Added
- **Migration dependency management** — All 17 versioned migrations now have explicit `dependsOn` fields defining their dependency chain
- **`validateMigrationDependencies()` function** — Validates that a migration's prerequisites have been applied before running it
- **Dependency check logging** — Migrations log `[migration] vX depends on [vY] — satisfied` when dependencies are met
- **Missing dependency handling** — Migrations with unmet dependencies are skipped with a clear error log instead of crashing

## v0.20.3

### Added
- **Database performance indexes** — v0.44 migration adds 4 indexes on frequently queried columns:
  - `idx_bills_user_name` on `bills(user_id, name)` — user-scoped bill lookups
  - `idx_payments_method` on `payments(method)` — payment method filtering
  - `idx_monthly_starting_amounts_user` on `monthly_starting_amounts(user_id)` — user starting amounts
  - `idx_import_history_imported_at` on `import_history(imported_at)` — time-based import queries

## v0.20.2

### Added
- **Transaction wrapping for database migrations** — All migrations (versioned, legacy, and unversioned) now run within BEGIN/COMMIT transactions with ROLLBACK on failure, ensuring atomic schema changes
- **PRAGMA foreign_keys safety** — v0.40 migration uses try/finally to guarantee FK checks are always re-enabled, even on failure

### Fixed
- **Hudson audit fix** — v0.40 migration now restores foreign_keys = ON in a finally block, preventing FK checks from being left disabled if migration fails

## v0.20.1

### Added
- **Code splitting** — All page components (except LoginPage) now lazy-load via React.lazy + Suspense, reducing initial bundle size
- **PageLoader component** — Minimal loading spinner for lazy-loaded routes
- **Version badge on Roadmap page** — Admins see the current version at the top of the dashboard
- **Version in /api/about-admin** — API now returns version from package.json
- **Roadmap nav link** — Admins see "Roadmap" in dropdown menu and admin sidebar
- **/admin/roadmap route** — Direct URL to admin dashboard

## v0.20.0

### Added
- **Admin Dashboard** — New admin-only dashboard with roadmap and activity log sections:
  - **Roadmap section**: Parses FUTURE.md with color-coded priority cards (🔴🟠🟡🔵💭), collapsible, CRITICAL/HIGH expanded by default
  - **Activity Log section**: Parses DEVELOPMENT_LOG.md, reverse chronological, collapsible entries
  - SimpleCollapsible component (custom, no external deps)
- **Priority color coding**: CRITICAL (🔴), HIGH (🟠), MEDIUM (🟡), LOW (🔵), NICE TO HAVE (💭)
- **Responsive scrollbars**: Smooth scrolling for roadmap and activity log sections

### Changed
- **AboutPage.jsx**: Modified to conditionally render AdminDashboard for admin users only; non-admin users see standard About page
- **FUTURE.md**: Updated to v0.20.0

### Security
- **Admin-only access**: AdminDashboard only accessible to authenticated admin users
- **Input validation**: Markdown parsing handles all FUTURE.md and DEVELOPMENT_LOG.md content safely

---

## v0.19.4

### Added
- **Session token expiry cleanup** — Expired sessions are now purged automatically on startup, every 24 hours, and per-user on login. Prevents `sessions` table bloat and potential token reuse.
- **`created_at` column on sessions** — v0.43 migration adds `created_at` to the sessions table for better cleanup targeting.
- **`SESSION_CLEANUP_INTERVAL_MS` env var** — Configurable cleanup interval (default 24h, max 7 days). Invalid values are rejected with a warning.

### Security
- **Input validation on `SESSION_CLEANUP_INTERVAL_MS`** — Rejects 0, negative, and >7-day values to prevent DoS via event loop starvation (Hudson finding).

## v0.19.3

### Fixed
- **Legacy database login now works** — When `INIT_ADMIN_PASS` is set, the default admin's password is reset and `must_change_password=1` is enforced. This solves the case where a legacy DB has users with unknown passwords.
- **Legacy migrations now actually run** — Every entry in `reconcileLegacyMigrations()` now has a `run()` function. Migrations whose changes aren't present in the DB (like `is_seeded` columns) are executed instead of silently skipped.
- **v0.40 ownership migration assigns to admin** — Unowned bills/categories now go to the first admin user instead of the first regular user. Prevents data being assigned to a non-admin account.

### Security
- **Removed username from password reset log** — `[init] Reset password for default admin user` no longer includes the username (Hudson finding)
- **Password reset is always explicit** — If `INIT_ADMIN_PASS` is set, the reset happens. If not set, no reset. No silent side-effects.

## v0.19.2

### Added
- **React Error Boundaries** — `ErrorBoundary` component wraps all routes in `App.jsx`. Shows friendly fallback UI with "Try Again" and "Reload Page" buttons instead of a white screen crash. Logs component stack to console for debugging.

### Fixed
- **Legacy database migration login failure** — Users upgrading from pre-migration-tracking databases (before v0.19.1) now log in successfully. The startup flow now detects legacy databases (tables exist but `schema_migrations` is empty), reconciles all previously-applied migrations by checking actual DB state, and marks them as applied without re-running destructive operations.
- **Migration idempotency** — All migrations now check whether their changes are already present before applying, preventing `ALTER TABLE ADD COLUMN` failures on legacy databases.

### Security
- **Migration reconciliation is read-only** — No user data is modified or deleted during legacy detection. All `PRAGMA table_info()` and `sqlite_master` queries use hardcoded identifiers (no user input). Try/catch wrappers prevent partial state on failure. (Verified by Private_Hudson)

## v0.19.1

### Added
- **Regular User Seed Environment Variables** — `INIT_REGULAR_USER` and `INIT_REGULAR_PASS` create a non-admin user on first run for role-based testing
- **Non-admin Test User** — Added `INIT_REGULAR_USER` and `INIT_REGULAR_PASS` env vars for role-based testing

### Changed
- **Database Migration v0.42** — `bill_history_ranges` table creation moved into versioned migration system

### Security
- **Admin-only `/about` endpoint** — Added `/api/about-admin` endpoint serving FUTURE.md and DEVELOPMENT_LOG.md to admins only
- **Rate limiting** — `adminActionLimiter` (30 req/15min per IP) applied to `/api/about-admin`
- **Content sanitization** — Path traversal protection, internal IP/password redaction, error sanitization in `routes/aboutAdmin.js`
- **XSS prevention** — `rehype-sanitize` added to ReactMarkdown component in AboutPage.jsx
- **Route guards** — `/admin/about` route protected with `RequireAuth role="admin"` in client/App.jsx

### Fixed
- First-time login rate limiting bypass when no users exist
- Password change rate limiter only applies to actual password change routes (not login)
- CSRF middleware properly exempts login endpoint
- Admin user auto-creation using bcryptjs
- Backup operation rate limiter scoped to backup routes only

### Notes
- Regular user seed occurs only if both `INIT_REGULAR_USER` and `INIT_REGULAR_PASS` are set
- Regular users are created with `role='user'` and `is_default_admin=0`
- Migration system now handles `bill_history_ranges` table creation via v0.42
- Admin about endpoint is fully protected and only serves project documentation files

## v0.19.0

### Added
- **Demo Data Seeding** — Users can seed their account with 20 realistic demo bills and 8 demo categories from the Data section for testing purposes
- **Demo Data Removal** — Users can clear only their seeded demo data (user-created bills remain unaffected)
- **CSRF Protection** — Configurable CSRF token handling for SPA mode (`CSRF_HTTP_ONLY`, `CSRF_SAME_SITE` env vars)
- **UI Improvements** — Mobile-responsive sidebar navigation, loading skeletons for Settings, improved BillModal mobile layout
- **Click-to-Toggle Paid Status** — Users can click on Paid/Unpaid status in Tracker to toggle payment status with confirmation dialog
- **Performance** — React.memo() optimization applied to StatusBadge, SummaryCard, MobileBillRow, MobileTrackerRow, NavPill, and BrandBlock components to prevent unnecessary re-renders
- **Documentation** — Added CSRF-SPA-Setup.md, Authentik-Integration.md, UI_IMPROVEMENTS.md, RATE_LIMITING_ENHANCEMENT.md

### Security
- Rate limiting applied to demo data operations (3 per 15 minutes)
- Audit logging for demo data clear operations
- Private_Hudson security review completed — all critical/high issues resolved

### Security (2026-05-09)
- **Admin-only `/admin/about` route guard** — React `RequireAuth` middleware protects `/admin/about` route
- **Rate limiting on `/api/about-admin`** — `adminActionLimiter` (30 req/15min per IP) applied to prevent brute-force attempts
- **XSS prevention** — `rehype-sanitize` added to ReactMarkdown component in AboutPage.jsx
- **Content redaction** — `routes/aboutAdmin.js` sanitizes paths, redacts internal IPs, passwords, API keys
- **Error sanitization** — Error messages exclude paths to prevent path disclosure
- **Non-admin test user** — Added `INIT_REGULAR_USER` and `INIT_REGULAR_PASS` env vars for role-based testing

### Fixed
- First-time login rate limiting bypass when no users exist
- Password change rate limiter only applies to actual password change routes (not login)
- CSRF middleware properly exempts login endpoint
- Admin user auto-creation using bcryptjs
- Backup operation rate limiter scoped to backup routes only

### Notes
- Toaster notifications now use Tailwind CSS exclusively (removed inline styles)
- Seed data is user-scoped with `is_seeded` column tracking
- All agent contributions documented in REVIEW.md

## v0.18.4

### Added
- Added user active/inactive management in Admin. Inactive users cannot log in and active sessions are invalidated when they are disabled.
- Added a durable default-admin marker so the built-in default admin remains an admin-only operator account.
- Admin users created after first run can now sign in directly to Tracker while retaining Admin Panel access from the menu.
- Admins can delete any other user, including other admins, with a destructive 2026 warning that all user-owned bill data will be permanently removed.
- Added an `Other` monthly starting amount alongside the 1st and 15th amounts.
- New `monthly_starting_amounts` records store user-scoped, month-specific starting cash with `first_amount`, `fifteenth_amount`, and `other_amount`.
- New `GET /api/monthly-starting-amounts` and `PUT /api/monthly-starting-amounts` endpoints manage monthly starting balances.
- Tracker renamed the “Total Expected” card to “Starting” and shows the selected month’s combined starting amount.
- The Tracker Starting card now has an edit control for setting 1st, 15th, and Other monthly amounts.
- Summary now uses monthly starting balances as the planning base and shows a Starting Balance section.
- Remaining balances deduct paid bills: due days 1-14 from the 1st bucket, due days 15-31 from the 15th bucket, and total paid from combined remaining.
- Added monthly starting amounts to user SQLite and Excel exports, and to user SQLite imports.
- Added a public About page with app version, stack, AI-assistance note, and Release Notes access.
- Release Notes are now available without login.

### Notes
- Starting balances are not bills and are not payments.
- Remaining values can go negative when paid bills exceed starting cash; overages are not blocked.
- Previous month remaining is exposed to Summary as informational context only when available.
- Navigation now groups Overview, Summary, Bills, and Categories under Tracker, and groups Profile, Settings, and Data in the user menu.
- System Status is admin-only and appears in the Admin Panel navigation.
- Profile now focuses on account details, display name, password, and notification preferences in a narrower modern layout.
- Data is restored as a dedicated import/export/history page instead of redirecting into Profile.
- Fixed Admin Panel availability when all managed users have been promoted to admin.
- The default admin account is blocked from Tracker/user-data routes; non-default admins keep regular Tracker access.
- No payment behavior, bill behavior, Calendar, or Analytics behavior was changed.

## v0.18.1

### Changed
- Updated Admin authentik/OIDC issuer help text to show the authentik discovery URL example and clarify that issuer base or full discovery URL can be used.
- Updated the default category seed list to the top 10 common bill categories, safely filling missing user-scoped defaults without renaming or deleting existing categories.
- Categories now return user-scoped active/inactive bill counts, payment counts, bill name previews, and compact bill detail data.
- Categories page now shows compact stat chips for active bills, inactive bills, and payments with a subtle legend.
- Removed the category-level total paid chip from Categories while keeping bill-level paid totals in expanded details.
- Category rows now expand to show bills in that category, with hover/tap summaries for chips and bill names.
- Improved Categories page mobile and tablet layout so chips wrap cleanly and expanded bill details stay readable without page-level horizontal scrolling.
- Added a Summary page for monthly planning with income, expenses, paid expense count, result/savings, and browser Print / PDF output.
- Added minimal user-scoped monthly income support for the Summary page.
- Added a user-scoped `GET /api/summary` endpoint and income save endpoint using existing bills, payments, and monthly bill state data.
- Summary includes a simple income, expenses, and savings chart without adding a new chart library.
- Cleaned up the Summary page layout with a centered planner view, display-first Monthly Plan card, compact income editing, cleaner expense rows, and a calmer chart card.
- Summary Print / PDF behavior remains browser-based and no backend/payment behavior was changed.
- Added a Calendar page with a month grid for user-owned bills and payments, compact day indicators, a legend, monthly progress summary, and day detail dialog.
- Added a user-scoped `GET /api/calendar` endpoint for one-month calendar data using existing bills, payments, categories, and monthly bill state records without schema changes.
- Calendar status and totals respect monthly actual amount overrides, skipped bills, existing due-day clamping, and existing tracker-style late/missed status behavior.
- Added Calendar to the top navigation after Tracker while preserving the existing desktop and mobile nav behavior.
- Improved mobile and tablet responsive rendering across the top navigation, page headers, dialogs, dense tables, Tracker, Bills, Categories, Settings, Status, Admin, Analytics, and Login views.
- Preserved the current desktop layout by keeping existing desktop-oriented layouts at `lg` and above while adding mobile/tablet stacking, scrolling, and tap-friendly controls below that breakpoint.
- Tablet navigation now uses the compact menu to avoid horizontal overflow; user menu, theme toggle, and admin-only navigation remain reachable.
- Dialogs and destructive confirmations now respect mobile viewport width/height and scroll internally when content is long.
- Dense Tracker, Bills, Admin, Analytics, and import/history style tables use horizontal scrolling or mobile stacking so actions remain reachable on smaller screens.
- Tracker and Bills now use stacked mobile/tablet bill rows below `lg`, reducing sideways scrolling for normal bill review, quick payment, and bill actions while preserving the desktop table layouts.
- Tracker mobile notes stay contained in each bill row, so long notes can truncate or scroll locally without forcing the whole bill list sideways.

### Notes
- No auth behavior, tracker/payment/bill business logic, admin permissions, or desktop redesign changes were made.
- No Tracker, Bills, payment, analytics, calendar, auth, or admin behavior was changed for the Categories page updates.
- No Tracker, Bills, payment, Calendar, Analytics, auth, or admin behavior was changed for the Summary page updates.

## v0.18

### Branding
- Replaced the top-navbar dollar-sign placeholder and duplicate text/version brand stack with the selected `/img/logo.png` BillTracker logo.
- The logo now serves as the BillTracker brand in the top navigation while preserving the existing navbar height and route behavior.
- Login now uses the BillTracker logo, shows linked build/version information near the login actions, and uses the authentik icon for OIDC login.
- Admin Authentication Methods now uses subtle authentik branding in the OIDC toggle/configuration/test-login controls.
- Cropped transparent padding from the BillTracker logo asset so it renders larger and more readably in the unchanged-height navbar.
- Promoted the transparent `logo_cut.png` artwork to the served `/img/logo.png` asset and enlarged the login-page logo while keeping the login card layout compact.
- Login logo sizing now follows the login form width so the brand grows and shrinks with the sign-in column instead of rendering too small.
- Legacy `/login.html` now redirects to the modern React `/login` screen so the old static login page is no longer served by stale links.
- Vite now copies only modern React public assets from `client/public`, preventing legacy `public/*.html`, CSS, and JS files from being emitted into `dist`.
- No backend, auth, tracker, bills, categories, settings, status, admin, or navigation-link behavior was changed.

### Analytics
- Added a user-scoped Analytics API at `GET /api/analytics/summary` using existing bills, payments, categories, and monthly bill state data without schema changes.
- Added an Analytics page with date range controls, category and bill filters, inactive/skipped toggles, chart visibility toggles, and a line/area trend option.
- Added monthly spending trend, expected vs actual spend, category spending donut, and pay-on-time heatmap views.
- Added print and browser save-as-PDF report output with print CSS that hides navigation, controls, and interactive actions.
- Analytics queries are scoped to the signed-in user and do not accept or expose cross-user aggregation.

### Security
- **OIDC ID token signature verification** now uses `openid-client@5` for full cryptographic validation via JWKS: signature, issuer, audience, expiry, nonce, and `sub` presence — tokens without a valid signature are rejected
- **OIDC client cache** invalidation path added; cache is keyed by issuer/client/redirect so Admin panel credential changes pick up a fresh client
- OIDC-provisioned accounts (empty `password_hash`, `auth_provider='oidc'`) continue to be blocked from local password login
- Tokens, auth codes, and client secrets are never logged at any point in the OIDC flow
- Session cookies no longer become `Secure` solely because `NODE_ENV=production`; this preserves login on plain-HTTP Docker deployments while still supporting `COOKIE_SECURE=true`, `HTTPS=true`, and HTTPS reverse-proxy detection.

### Added
- **Docker startup volume repair**: runtime now starts through `docker-entrypoint.sh`, creates `/data/db` and `/data/backups`, fixes `/data` ownership for the non-root `bill` user, then drops privileges before launching Node. This prevents SQLite migrations from failing with `SQLITE_READONLY` on mounted volumes.
- **Docker startup migrations**: entrypoint now runs `scripts/migrate-db.js` as the non-root app user before starting the server, so required SQLite schema migrations and seeded defaults complete before the app listens for requests. Set `RUN_DB_MIGRATIONS=false` only for special maintenance runs.
- **Database writability preflight**: startup now checks that `DB_PATH` and its parent directory are writable before opening SQLite, producing a clearer error if a bind mount or volume is genuinely read-only.
- **Release notes Markdown rendering**: the release notes viewer now renders inline Markdown such as `**bold**`, backticked code, and HTTPS links instead of showing the raw markers.
- **authentik configuration testing**: Admin Authentication Methods now includes a live OIDC discovery test for the entered issuer/client/redirect settings and a direct authentik login test button once OIDC is enabled.
- **authentik setup guardrails**: the Admin form now fills the Redirect URI from the current app origin, offers a "Use Current" reset, and warns when the Issuer URL looks like an authorize/token/userinfo endpoint instead of the provider issuer.
- **authentik client auth method**: Admin OIDC settings now include an advanced `client_secret_basic` / `client_secret_post` token endpoint authentication method selector. The default remains `client_secret_basic`, matching the previous `openid-client` behavior.
- **Admin user role management**: Admin Users table now lets an admin promote another user to `admin` or demote an admin back to `user`, with protections against changing your own role or removing the last admin account.
- **Single-user mode recovery**: User Settings now shows a Login Mode section while single-user mode is active, allowing the default user to restore multi-user login without needing access to Admin routes.
- **Admin navigation parity**: Admin users now keep the normal app navigation and get an Admin link after Status; `/admin` uses the same top nav so admins can return to Tracker/Bills/Categories/Profile/Settings/Status without typing a URL. Backend `/admin` protection remains unchanged.
- **Admin-controlled auth method toggles** in Admin panel (Authentication Methods card):
  - `local_login_enabled` — enable/disable local username/password login (default: enabled)
  - `oidc_login_enabled` — enable/disable OIDC/authentik login (default: disabled)
  - Database-backed authentik/OIDC provider settings: provider name, issuer URL, client ID, client secret, redirect URI, scopes, auto-provision, admin group, default role
  - Lockout protection: admin cannot disable all login methods; cannot disable local login unless OIDC is configured, enabled, and has an admin group mapping
  - Client secret is write-only in the UI/API; Admin GET returns only `oidc_client_secret_set`
- **`GET /api/admin/auth-mode`** extended: returns `local_login_enabled`, `oidc_login_enabled`, `oidc_configured`, `can_disable_local`, `warnings`, safe OIDC settings, and the client-secret marker
- **`PUT /api/admin/auth-mode`** extended: accepts all new provider settings, allows setting a new client secret, keeps the existing secret when blank, and supports explicit saved-secret clearing
- **`GET /api/auth/mode`** extended: returns `local_enabled`; returns OIDC provider name and login URL only when OIDC is enabled and fully configured
- **`POST /api/auth/login`** now checks `local_login_enabled` setting and returns 403 if admin has disabled local login
- **Login page OIDC button** uses `/api/auth/mode` so local-only, OIDC-only, and mixed login states are reflected safely
- **OIDC login and callback routes** now check both DB-backed effective OIDC config and `oidc_login_enabled` before proceeding
- Eleven auth settings keys are seeded: `local_login_enabled`, `oidc_login_enabled`, `oidc_provider_name`, `oidc_issuer_url`, `oidc_client_id`, `oidc_client_secret`, `oidc_redirect_uri`, `oidc_scopes`, `oidc_auto_provision`, `oidc_admin_group`, `oidc_default_role`
- **`scripts/test-oidc-smoke.js`** — 42 smoke tests covering PKCE, redirect sanitization, DB/env OIDC config precedence, safe secret handling, incomplete config behavior, provisioning, email linking, role/group mapping, OIDC-only local-login denial, and lockout logic; all pass

### Changed
- `services/oidcService.js` rewritten to use `openid-client@5` throughout:
  - `getOidcClient(config)` — builds and caches an openid-client `Client` after OIDC discovery
  - `buildAuthorizationUrl()` uses `client.authorizationUrl()` (uses discovered `authorization_endpoint`)
  - `exchangeAndVerifyTokens()` replaces manual exchange + claims-only validation with `client.callback()` which does the full PKCE exchange, JWKS signature verification, and all claim checks in one call
  - `getOidcConfig()` resolves provider config from DB settings first, env fallback second, safe defaults last
  - `mapRoleFromClaims()` reads the effective admin group at runtime; default role remains `user`, and admin is granted only by explicit group match
  - `findOrProvisionUser()` uses effective `oidc_auto_provision` and creates local OIDC users on first valid authentik login when enabled

### Notes
- Local username/password login remains supported and protected by lockout checks
- OIDC environment variables remain optional fallback/bootstrap values only; once a DB field is set, the DB value takes precedence
- Client secret is stored in the existing `settings` table, never returned through public endpoints, and never displayed in the UI
- Valid authentik users auto-provision local app users when enabled; unknown users receive a safe 403 when disabled
- Admin role is never granted by default; requires explicit OIDC admin group membership or local admin account
- OIDC signature verification/security is preserved through `openid-client@5` JWKS-backed validation, issuer/audience/expiry/nonce checks, PKCE, and state replay protection
- Live authentik SSO cannot be verified without a running authentik instance; all local testable code paths are covered by the smoke test script
- Admin single/multi user mode behavior was not changed

---

## v0.17

### Security
- **Rate limiting** added via `express-rate-limit`: login (10/15 min), password change (5/15 min), import (20/15 min), export (30/15 min), admin mutations (30/15 min), OIDC (20/15 min) — all per-IP, in-memory
- **Security headers** added globally: `X-Content-Type-Options: nosniff`, `X-Frame-Options: SAMEORIGIN`, `Referrer-Policy: strict-origin-when-cross-origin`, `X-Permitted-Cross-Domain-Policies: none`; `X-Powered-By` removed; `Strict-Transport-Security` added when `HTTPS=true`
- **CORS locked down**: `cors({ credentials: true })` (wide-open) replaced with opt-in via `CORS_ORIGIN` env var; without it, no CORS headers are sent and the browser's same-origin policy applies
- **Cookie `secure` flag**: session cookie now sets `secure: true` in production (`NODE_ENV=production` or `HTTPS=true`), preventing transmission over plain HTTP in deployed environments
- **Settings endpoint hardened**: `GET /api/settings` now returns only the four user-facing keys (`currency`, `date_format`, `grace_period_days`, `notify_days_before`); previously returned all settings rows including SMTP password hash and backup paths
- **DB path removed from status response**: `database.path`/`database.file` fields removed from `GET /api/status`; filesystem paths are no longer exposed to any authenticated user
- **OIDC-only account protection**: `login()` now rejects local-password login attempts for accounts provisioned via external OIDC, preventing bypass of SSO-only accounts
- **Error handler hardened**: global Express error handler logs `err.message` internally but no longer calls `console.error(err)` (which could log full stack traces with paths); user-facing errors remain useful but safe
- **CSP deferred**: Content-Security-Policy requires auditing inline styles from Tailwind and Radix event handlers; deferred to a dedicated hardening pass

### Added
- **Backend OIDC/authentik preparation** — disabled by default; activated only when `OIDC_ENABLED=true` plus all required env vars are present:
  - `GET /api/auth/oidc/login` — generates PKCE code verifier + challenge, stores one-time state in `oidc_states` DB table, redirects to the identity provider's authorization endpoint
  - `GET /api/auth/oidc/callback` — validates state, exchanges authorization code for tokens (PKCE), validates ID token claims (issuer, audience, expiry, nonce), maps to local user, creates session, redirects to frontend
  - `GET /api/auth/mode` now includes `oidc_enabled`, `oidc_provider_name`, and `oidc_login_url` when OIDC is configured
  - `services/oidcService.js`: OIDC discovery caching, PKCE helpers, login-state management, token exchange, claim validation, role/group mapping, user auto-provisioning
  - `middleware/rateLimiter.js`: rate limiter factory for all endpoint categories
  - `middleware/securityHeaders.js`: global security headers middleware
  - `authService.createSession(userId)`: creates a server-side session for a user who has already been authenticated externally (used by OIDC callback)
- **Schema migrations** (v0.17, additive — safe on existing databases):
  - `users.auth_provider TEXT DEFAULT 'local'` — tracks identity source (`local` | `oidc`)
  - `users.external_subject TEXT` — stores OIDC `sub` claim for stable identity mapping
  - `users.email TEXT` — stores email for OIDC-linked accounts and optional local-user email linking
  - `users.last_login_at TEXT` — updated on every successful login (local or OIDC)
  - `oidc_states` table: short-lived (5 min TTL) PKCE/nonce state for in-flight OIDC logins; pruned on each new login attempt

### Changed
- `authService.login()` now updates `last_login_at` on each successful local login
- `requireAuth` single-user-mode query now includes `display_name` so the top nav shows correctly in single-user mode

### Notes
- Local username/password authentication continues to work regardless of OIDC configuration
- OIDC requires `OIDC_ENABLED=true`, `OIDC_ISSUER_URL`, `OIDC_CLIENT_ID`, `OIDC_CLIENT_SECRET`, `OIDC_REDIRECT_URI`
- JWT signature verification (JWKS) is not implemented in this pass — ID token *claims* are validated but the cryptographic signature is not. Install `openid-client@5` and upgrade `validateIdToken` in `services/oidcService.js` for full production-grade signature verification
- Admin full-database backup/restore was not changed
- No frontend SSO login button was added in this pass

---

## v0.16.2

### Added
- User SQLite data import backend for exports created by this app:
  - `POST /api/import/user-db/preview`
  - `POST /api/import/user-db/apply`
- SQLite import preview validates the uploaded file, confirms it is a BillTracker user data export, summarizes counts, warnings, and proposed create/skip/conflict actions, and writes no live data
- SQLite import apply uses the preview session, imports only into the signed-in user's account, creates missing user-owned records, skips duplicates/conflicts by default, and records import history
- Regression coverage for user SQLite preview, apply, conflict skipping, and invalid file rejection

### Changed
- Profile > My Data now shows Import Spreadsheet History and Import SQLite Data Export side by side on desktop and stacked on mobile
- Export My Data now appears below the two import tools
- The SQLite import UI includes file selection, preview summary, warnings/conflicts, confirmation before apply, and result/error states

### Security
- User SQLite import does not use admin backup/restore endpoints and does not perform a full system restore
- User SQLite import does not import users, password hashes, sessions, cookies, admin/global settings, SMTP credentials, backup files, server paths, or other users' data
- Uploaded SQLite files are read as data through parameterized queries, stored temporarily only for preview parsing, and cleaned up without exposing server paths

---

## v0.16.1

### Added
- Bills now support an optional `interest_rate` field for credit-card APR values; blank remains allowed for non-credit-card bills
- The bills database schema, API create/update routes, bill responses, and user exports now preserve `interest_rate`
- Bills page bill names now open the shared Edit Bill dialog directly

### Changed
- Edit Bill due-date editing now uses a recurring day-of-month number instead of a full calendar date picker
- Removed the old due-date wording from the shared Edit Bill UI; the field is now labeled "Due day of month"
- Bills page editing no longer relies on a separate Edit button as the primary action
- Bills page and Tracker both use the same shared Edit Bill dialog with the corrected due-day and APR fields
- Tracker due-date calculation now uses the recurring `due_day` field and clamps to shorter months using the existing month-end handling

### Notes
- The legacy `override_due_date` column remains in the database for compatibility, but the shared Edit Bill dialog no longer edits it and current tracker due-date calculation ignores it
- Payment, monthly state, bill delete, deactivate, reactivate, and global grace-period behavior were not changed
- Per-bill grace periods were not added

---

## v0.16

### Added
- Admin Cleanup / Maintenance panel in the Admin area with settings for all four cleanup tasks, last-run summary, Save Settings, and Run Cleanup Now button
- Import history trimming shows an explicit destructive-action warning when enabled
- Read-only Maintenance card on the user Status page showing last cleanup run timestamp and per-task result counts (import sessions pruned, temp files removed, backup partials removed); no admin controls exposed
- Tracker page: clicking a bill name opens the existing Edit Bill dialog for that bill; saving refreshes the tracker for the current month; monthly gear (⚙) still opens monthly state, not global bill edit
- Edit Bill dialog is now a shared component (`components/BillModal.jsx`); Bills page and Tracker page both import it — no duplicate dialog

### Changed
- `GET /api/auth/me` now returns `display_name` so the top-nav user menu always shows the current display name after Profile save without requiring logout/login
- `GET /api/status` now includes a safe read-only `cleanup` section: `last_run_at` and `last_result` task counts
- Edit Bill due-date helper text clarified that grace period is a global Setting

### Notes
- Per-bill grace period days are not a backend-supported field; grace period remains a global app setting in Settings
- Admin cleanup controls (Save Settings, Run Cleanup Now) are admin-only and do not appear on the user Status page
- Profile `display_name` save already merged correctly into local state; the auth session fix ensures `refresh()` also returns the updated value

---

## v0.15

### Added
- `services/cleanupService.js` — new cleanup service with four independent tasks:
  - **Expired import sessions** — deletes `import_sessions` rows past their 24-hour expiry (previously only pruned on next preview request; now also runs daily)
  - **Stale export temp files** — removes orphaned `bill-tracker-user-*.sqlite` files from the OS temp directory that were not deleted after an interrupted download; configurable max age (default 2 hours)
  - **Orphaned backup partials** — removes `.partial` and `.upload` files left in the backup directory after a server crash; uses a fixed 2-hour safety cutoff so in-progress operations are never interrupted
  - **Import history trimming** — optionally deletes `import_history` rows older than a configurable threshold; disabled by default
- Daily worker now runs all enabled cleanup tasks each morning after notifications; cleanup errors are caught and logged but do not fail the worker
- Admin cleanup API:
  - `GET /api/admin/cleanup` — returns current cleanup settings and last run result
  - `PUT /api/admin/cleanup` — update any combination of cleanup settings (partial updates supported)
  - `POST /api/admin/cleanup/run` — trigger all enabled cleanup tasks immediately and return the result
- Eight new `cleanup_*` keys in the `settings` table with safe defaults (seeded via `INSERT OR IGNORE`)

### Notes
- No frontend admin UI for cleanup settings in this pass — backend and APIs only
- All cleanup tasks are independently toggled; disabling one does not affect others
- Import history trimming is off by default to preserve audit history; enable explicitly via `PUT /api/admin/cleanup`
- Backup partial pruning always uses a 2-hour minimum age regardless of settings so a live backup in progress is never removed

---

## v0.14.3

### Changed
- Added the shadcn Material Design theme registry setup and made Material Design the default light theme for the app
- `:root` now represents the Material Design light tokens used by the existing Tailwind/shadcn CSS variable system
- Aligned dark mode to the same Material-inspired design language so light and dark mode feel related
- Modernized shared theme surfaces and app shell styling, including the top navigation, cards, dialogs, dropdowns, buttons, inputs, badges, tables, and focus/hover/disabled states

### Notes
- The app still uses the existing Tailwind, shadcn, and Radix framework; no new UI framework was introduced
- Material Design is not an optional user-selected variant; it is the default light theme
- Backend behavior, routes, tracker, bills, payments, import/export logic, and database behavior were not changed

---

## v0.14.2

### Added
- Inactive bills now have a History Visibility editor on the Bills page
- History Visibility supports Default, Show all history, Show no history, and Show only selected date ranges
- Selected ranges mode supports adding, editing, deleting, labeling, and saving multiple year/month ranges

### Changed
- Bills with non-default history visibility or saved history ranges continue to show the history visibility indicator after saving

### Notes
- The editor is available for inactive bills only; active bills may show the indicator but do not expose the editor
- Delete, deactivate, and reactivate behavior was not changed
- No backend behavior changed

---

## v0.14.1

### Added
- Bills page now exposes permanent bill deletion behind a strong confirmation dialog
- Delete confirmation explains that payments, monthly history, notes, and history ranges are permanently deleted and cannot be undone
- Delete confirmation requires an explicit acknowledgement checkbox before the destructive action is enabled
- Bills with historical visibility metadata now show a small history visibility indicator in the Bills table

### Notes
- Deactivate/reactivate remains the safe non-destructive option and still uses the existing active/inactive update behavior
- The delete dialog offers Deactivate instead for active bills and Activate instead for inactive bills
- No backend delete, deactivate, reactivate, payment, monthly state, or history range behavior changed

---

## v0.14

### Added
- Bill hard-delete: `DELETE /api/bills/:id` now permanently removes the bill and all associated payments, monthly state, and history ranges — inactivation (`PUT` with `active: 0`) remains the safer non-destructive alternative
- Bill history visibility: bills now carry a `history_visibility` field (`default`, `all`, `ranges`, `none`) for future UI control over which historical data is shown for inactive bills
- `bill_history_ranges` table: per-bill, multi-range date records for fine-grained history visibility control
- `GET /api/bills/:id/history-ranges` — list all history ranges for a bill
- `POST /api/bills/:id/history-ranges` — add a date range (start year/month, optional end year/month, optional label)
- `PUT /api/bills/:id/history-ranges/:rangeId` — update a history range
- `DELETE /api/bills/:id/history-ranges/:rangeId` — remove a history range
- Bills list and detail responses now include `history_visibility` and `has_history_ranges` flag for future UI icon support

### Changed
- `DELETE /api/bills/:id` changed from soft-delete (set active=0) to hard-delete; clients that need deactivation should use `PUT /api/bills/:id` with `{ active: 0 }` (unchanged behavior)
- AP flag badge on the Bills page is now emerald/green and uses boolean coercion to prevent the SQLite integer `0` from rendering as a visible "0" next to the badge

### Notes
- No delete-confirmation UI added in this pass — backend only for the delete/history changes
- No history-visibility UI added — backend and data model only
- All history-range queries are scoped to the bill's owning user
- Deleting a bill cascades automatically via database foreign keys (`ON DELETE CASCADE`)

---

## v0.13.3

### Changed
- Moved authenticated navigation from a left-sidebar-first shell to a top-navigation-first app header
- Aligned authenticated pages under the new shared top-nav shell with consistent page width, padding, background, and card surface styling
- Modernized the shared app background, top nav active states, user menu, theme toggle placement, and mobile navigation menu

### Notes
- Profile remains the user/account hub, and user Data tools remain under Profile > My Data
- Settings remains focused on app-level preferences
- Admin/system controls remain separated from regular user Profile and continue to be shown only in the admin area
- No backend import/export, tracker, bill, category, or payment behavior changed

---

## v0.13.2

### Changed
- User-owned spreadsheet import, user data export, user data import placeholder, and import history tools now live under Profile > My Data
- Removed the top-level Data item from the regular sidebar; `/data` now redirects to `/profile` for backward-compatible deep links
- Settings is narrowed to app-level preferences: appearance, currency, date format, and billing grace period

### Notes
- Admin/system backup and restore controls remain separate from regular user Profile
- Status remains a standalone operational/system health page

---

## v0.13.1

### Added
- Profile page frontend at `/profile` with profile summary, display-name editing, notification preferences, password change, user-owned exports, and import history
- Sidebar signed-in user name now links to the Profile page
- Profile API helpers for profile details, profile settings, password changes, export metadata, and import history
- User export download buttons for SQLite and Excel exports from the Profile page

### Fixed
- Startup crash: `UPDATE categories SET user_id = ?` now removes orphaned NULL-owner categories whose names already exist for the target user before assigning ownership, preventing a `UNIQUE(user_id, name)` constraint violation on databases that had previously completed the v0.12 migration

### Notes
- Profile page uses user-owned export endpoints only and does not include admin backup controls or backup paths
- Password values are only kept in local form state for submission and are cleared after successful password change

---

## v0.13

### Added
- Profile backend foundation: `GET /api/profile` returns safe user data (id, username, display_name, role, created_at, updated_at, last_password_change_at, notification preferences, export links)
- `PATCH /api/profile` — updates `display_name` (only safe user-owned field)
- `GET /api/profile/settings` — returns user-owned notification preferences from the users table
- `PATCH /api/profile/settings` — updates user notification preferences (partial update; omitted fields are preserved)
- `POST /api/profile/change-password` — strict password change requiring `current_password`, `new_password`, and `confirm_new_password`; always verifies the current password regardless of account state; records `last_password_change_at` on success
- `GET /api/profile/exports` — returns metadata and links for user data export actions (SQLite and Excel)
- `GET /api/profile/import-history` — returns the signed-in user's import history (delegates to existing import service)
- `display_name` and `last_password_change_at` columns added to the users table via additive migration

### Changed
- `PUT /api/settings` no longer accepts backup-related keys (`backup_enabled`, `backup_frequency_days`, `backup_keep_count`, `backup_path`) from regular users; those settings are admin-only and remain manageable through the admin backup routes

### Security
- All `/api/profile/*` endpoints require authentication; user identity is always derived from the session — `user_id` is never accepted from the request body
- Profile responses never include password hashes, session tokens, SMTP credentials, backup paths, or other users' data
- `POST /api/profile/change-password` always requires the current password; no bypass path exists

### Notes
- No frontend Profile UI in this pass — backend APIs only
- Admin full-database backup/restore behavior was not changed
- Existing `POST /api/auth/change-password` preserved for the first-login forced-reset flow

---

## v0.12

### Added
- Bills and categories now have database ownership fields so imported and manually created bills belong to the signed-in user
- User data exports are now enabled for SQLite and Excel formats
- User exports include only the signed-in user's bills, payments, categories, monthly bill state, notes, and export metadata

### Fixed
- Bill, category, payment, tracker, spreadsheet import, and export routes now filter user-owned data instead of using global bill/category records

### Notes
- Existing unowned bills/categories are assigned to the first regular user during migration when one exists
- Admin full-system backup/export behavior was not changed

---

## v0.11.4

### Added
- Creating a new bill from an XLSX import row now also imports other paid months for the same detected bill name from the current preview
- Related paid months create monthly state and payment records on the newly created bill

### Notes
- Related-month import is limited to exact normalized bill-name matches in the reviewed XLSX preview
- Rows for other bill names remain skipped and are not imported automatically

---

## v0.11.3.2

### Fixed
- Login now updates the shared auth state before navigating, preventing the app from sitting on the login flow after successful sign-in
- First-login password change now sends the backend field name expected by the auth route
- Privacy acknowledgment refreshes auth state before continuing to the tracker

---

## v0.11.3.1

### Fixed
- XLSX import history and apply summary now record the number of rows skipped during review even though skipped rows are not sent as apply decisions
- Import review still applies only confirmed non-skipped rows, preserving the safer focused apply payload

### Tests
- Added regression coverage that omitted reviewed skips are counted in both apply results and import history

---

## v0.11.3

### Added
- XLSX import now detects `Paid Date` / `Date Paid` columns separately from due-date columns
- Confirmed XLSX imports now create payment records from detected paid dates and paid amounts so imported bills can appear paid in the tracker
- Import review rows now show and allow editing detected paid date and paid amount before Apply

### Notes
- Payment creation still only happens after the user confirms and applies the reviewed row
- Duplicate payment records with the same bill, amount, and paid date are skipped unless overwrite is enabled

---

## v0.11.2.5

### Fixed
- XLSX import apply now sends only rows the user chose to import instead of also sending every skipped preview row
- Import request parser failures on `/api/import/*` now return safe import-specific JSON errors instead of the generic `Internal server error`

### Notes
- Skipped preview rows remain visible and editable on the review screen, but they are not applied or created silently
- User confirmation is still required before creating a new bill, matching an existing bill, or applying any XLSX import row

---

## v0.11.2.4

### Fixed
- Fixed XLSX import month detection for real workbook tabs that combine month and year without a separator, such as `July2017`, `August2017`, and `September2017`
- Added tolerance for known month-name typos found in the test workbook, including `Januaru`, `Febuary`, and `Novevmber`
- All-sheets XLSX preview now skips obvious non-bill tabs such as `info`, tax/debt summary sheets, generic `Sheet13`-style tabs, and `home ownership expenses`

### Notes
- Electric rows from `Test_Data/monthly bills.xlsx` now resolve to the correct 2017 month values instead of becoming ambiguous because of tab-name parsing
- Import recommendations and apply remain confirmation-only; no auto-apply behavior was added

---

## v0.11.2.3

### Fixed
- Fixed remaining XLSX import apply error paths by normalizing and validating import decision fields before SQL writes
- Import apply now returns structured validation errors with row/field details when possible instead of generic Internal Server Error responses
- Import review now keeps the preview visible after apply failure so decisions can be corrected and retried

### Tests
- Added regression coverage for unsupported actions, unknown row IDs, missing create-new-bill names, invalid year/month values, bulk-style create-new-bill payloads without category IDs, frontend/backend match payloads, and skip rows without bill/month fields

---

## v0.11.2.2

### Fixed
- Fixed XLSX import apply validation for matching rows to existing bills
- Invalid match-existing-bill decisions now return clear validation errors instead of falling through to generic server errors

### Tests
- Added regression coverage for matched existing bill applies with numeric and string bill IDs, invalid match payloads, monthly state writes, bill template preservation, create-new-bill, and skip-row behavior

---

## v0.11.2.1

### Changed
- XLSX import bulk row tools are now visually scoped inside the XLSX Review Table, directly above the preview rows
- Bulk controls no longer use page-level sticky positioning, making it clearer they belong only to the current XLSX preview

### Notes
- Bulk actions still affect only selected XLSX preview rows and do not auto-apply imports

---

## v0.11.2

### Added
- Import review now supports bulk row selection and bulk row actions for the current XLSX preview
- Users can bulk mark selected preview rows as Skip
- Users can bulk mark selected preview rows as Create New Bill with editable prefilled name, category, due day, expected amount, actual amount, and notes when available
- Selected rows can be reset back to their recommendation/default decision

### Notes
- Bulk actions only update review decisions; no auto-apply or silent bill creation was added
- Per-row confirmation and editing remain available before Apply
- Rows without usable bill names remain unresolved after bulk Create New Bill until the user enters a name or skips the row
- Ambiguous rows are not auto-matched by bulk actions

---

## v0.11.1.1

### Changed
- Import review rows now let users override recommended matches and choose Create New Bill even when a possible existing bill match is suggested
- Create New Bill action switching now keeps the row expanded, hides the existing-bill selector, and sends a create-new-bill payload without the recommended bill ID

### Notes
- Recommendations remain confirmation-only and are not auto-applied
- Ambiguous rows still block Apply until the user chooses a valid action or skips the row

---

## v0.11.1

### Added
- XLSX import preview rows now include a `recommendation` object for pre-filling the review screen without applying changes automatically
- Smarter bill matching tolerates casing, punctuation, token order, and common abbreviations, including examples like `Capital` / `Cap One` → `Capital One` and `Discover Austin` → `Austin Discover`
- Create-new-bill recommendations now prefill likely bill name, due day, expected amount, and detected monthly amount when available
- Category suggestions use existing categories only, from explicit category columns, obvious keywords, or strongly matched similar bills
- Due-day suggestions are parsed from spreadsheet date/due-date cells when a valid day can be detected
- Amount recommendations carry detected spreadsheet amounts into confirmed monthly state decisions, and create-new-bill expected amounts when appropriate

### Changed
- Import review UI now shows the recommendation action, confidence, reason, and warnings before apply
- Apply payloads now include confirmed `category_id`, `due_day`, `expected_amount`, `actual_amount`, month/year, and notes when the user accepts or adjusts recommendations
- Weak or multiple possible bill matches remain unresolved until the user chooses a bill, creates a new bill, or skips the row

### Notes
- Recommendations are never auto-applied; the user must still review the preview screen and press Apply
- Ambiguous rows are not auto-applied and continue to block Apply until resolved
- Categories are not auto-created in this pass
- Existing bills are not silently updated, including due days or expected amounts; mismatches are shown as warnings
- Date intent is still heuristic: columns that look like payment dates lower confidence and warn rather than treating the date as a definite due date

---

## v0.11

### Added
- New dedicated **Data** page (`/data`) accessible from the sidebar with a FolderInput icon
- **Import Spreadsheet History** section: XLSX file picker with parse-all-sheets toggle, default year/month inputs, and a Preview Import workflow
- Preview UI shows workbook summary (sheet names, detected year/month, row counts, status badges), grouped by sheet tab in multi-sheet mode
- Per-row decision controls: auto-preselects high-confidence bill matches; ambiguous rows surface as "Needs decision" and block apply until resolved; user can choose match existing bill, create new bill, update monthly record, add monthly note, record as payment, or skip
- Suggested-match quick buttons and bill selector using optgroup (suggested matches / all bills) for fast selection
- Apply bar shows live summary of rows to apply, skipped, and unresolved; Apply button disabled until all rows are resolved
- Post-apply result card with created/updated/skipped/error counts; "New Import" button to start over
- **Download My Data** section (moved from Settings): SQLite and Excel export cards (Coming soon badges while backend endpoints are pending)
- **Import My Data Export** section: placeholder card explaining user-scoped SQLite import with Coming Soon state; clearly labelled as distinct from admin DB restore
- **Import History** section: table of all past imports for the current user with timestamps, file names, sheet names, and row outcome counts
- API helpers added: `api.previewSpreadsheetImport()`, `api.applySpreadsheetImport()`, `api.importHistory()`

### Changed
- Removed "Download My Data" section from Settings page — now lives exclusively on the Data page
- Settings page icon imports trimmed to only what the page uses

### Notes
- Admin full-database backup/restore remains exclusively in the Admin panel and is not accessible from the Data page
- User SQL import (user-scoped SQLite restore) shows a Coming Soon card; backend endpoints `POST /api/import/user-db/preview` and `POST /api/import/user-db/apply` are still needed
- User data exports (SQLite + Excel) remain Coming Soon; backend endpoints `GET /api/export/user-db` and `GET /api/export/user-excel` are still needed

---

## v0.10.1

### Added
- Multi-sheet preview mode: pass `?parse_all_sheets=true` to `POST /api/import/spreadsheet/preview` to parse every tab in one XLSX upload
- `parseSheetName()` — detects year/month from worksheet tab names supporting: `Jan 2026`, `January 2026`, `2026-01`, `01-2026`, `May`, `May Bills`, `Bills May 2026`, `2026 May`, and any combination of month name (full or abbreviated) with an optional 4-digit year
- Known non-data sheet names (Summary, Totals, Dashboard, Notes, Categories, Settings, Overview, Template, etc.) are automatically skipped in multi-sheet mode with `status: "skipped"` in the response
- `resolveYearMonth()` — tracks where each row's year/month came from; new `year_month_source` field on every preview row: `row_date`, `sheet_name`, `default`, or `ambiguous`
- Every row now includes `sheet_name` identifying which worksheet it came from
- Multi-sheet workbook response includes a `sheets` array with per-tab metadata: detected year, detected month, status (`parsed`, `parsed_month_only`, `ambiguous`, `skipped`), and row count
- Rows on ambiguous tabs (no detectable month) carry `year_month_source: "ambiguous"` and a warning; apply treats them normally using decision-level year/month override if provided
- `resolveYearMonth` and `parseSheetName` exported from service module for direct testing without DB
- Multi-sheet XLSX fixture (`scripts/test-import-multi-fixture.xlsx`) with Jan 2026, Feb 2026, Summary (skipped), May (month-only), and Misc Data (ambiguous) tabs

### Changed
- Single-sheet preview response now includes `parse_mode: "single_sheet"` in workbook metadata for consistency
- `parseXlsxBuffer()` now returns the full workbook object; raw-row extraction moved to `getSheetRows()`; `parseSheetRows()` extracted as reusable helper
- All preview rows now include `sheet_name` and `year_month_source` fields (single-sheet mode gets the selected sheet name and correct source)
- Row IDs in multi-sheet mode use `s{sheetIndex}_r{rowNumber}` format to guarantee uniqueness across tabs; single-sheet mode keeps existing `row_{N}` format

### Notes
- Apply behavior unchanged — `previewRow.detected_year` and `detected_month` already carried sheet-derived values through the session; no apply-path code changes needed
- Single-sheet preview behaviour is fully backward-compatible
- "1st–14th" style bucket names are treated as ambiguous (no month detected); provide `?month=N` default if needed

---

## v0.10

### Added
- XLSX spreadsheet import backend for importing historical bill data from Google Sheets exports (no direct Sheets API connection; user uploads an exported XLSX file)
- `POST /api/import/spreadsheet/preview` — parses an uploaded XLSX file safely, classifies rows, detects bill names/amounts/dates/labels, matches against existing bills, and returns a structured preview with proposed actions; writes no data
- `POST /api/import/spreadsheet/apply` — accepts confirmed import decisions from a preview session and applies only those decisions in a single transaction; ambiguous, conflicting, and skipped rows are never applied without explicit user confirmation
- `GET /api/import/history` — returns the authenticated user's import history (last 100 imports)
- Import session table (`import_sessions`) stores temporary preview state scoped to the uploading user; sessions expire after 24 hours and are cleaned up on next preview
- Import history table (`import_history`) records a per-user audit log of every apply: filename, sheet, row counts by outcome, and a decision summary
- Bill matching: exact normalized-name matches proposed automatically; partial/token-overlap matches require user confirmation; multiple matches mark row as requires_user_decision
- Duplicate detection for payments and monthly bill state; existing records are preserved by default unless `overwrite: true` is passed
- XLSX magic-bytes validation and formula parsing disabled (`cellFormula: false`) to prevent formula execution
- Test script (`scripts/test-import.js`) covering parseAmount, parseDate, detectLabels, normalizeName, row classification, XLSX round-trip, and fixture generation; also saves a test XLSX file for manual API testing

### Security
- Import endpoint requires authenticated user session; user_id is always taken from session, never from request body
- XLSX formula parsing disabled; all cells treated as plain string data
- File size limited to 10 MB; magic-bytes check rejects non-XLSX uploads
- Import sessions and history are scoped by user_id; sessions validate user ownership on load
- Temp/session data is not the original binary file; parsed row data is stored in the session table and cleaned up after apply or expiry

### Notes
- No frontend UI yet; this is the backend foundation for the import workflow
- No direct Google Sheets API integration in this pass; input is a user-exported XLSX file
- The `bills` table does not have a `user_id` column (shared household design); imported bills enter the shared pool, consistent with existing app behavior. Import history and sessions are per-user
- The `xlsx` (SheetJS) Community Edition has known prototype-pollution and ReDoS CVEs with no available OSS patch; mitigations applied (formula parsing off, size cap, magic-bytes check, authenticated-only access). Consider migrating to `exceljs` if stricter isolation is required

---

## v0.9

### Added
- "Download My Data" section in Settings with user-facing export cards for SQLite and Excel formats
- Export cards display "Coming soon" status pills and disabled buttons until backend endpoints are implemented
- "What's included" and "What's not included" info panels clarify that exports contain only the signed-in user's own data, not system backups
- Placeholder comments in `api.js` documenting the needed `GET /api/export/user-db` and `GET /api/export/user-excel` endpoints

---

## v0.8.2

### Fixed
- Docker runtime image now creates writable `/data/db`, `/data/backups`, and fallback `/app/backups` directories for the non-root app user
- Docker Compose now builds the project Dockerfile and mounts persistent storage at `/data` instead of bypassing the image setup with a plain Node image
- Docker Compose no longer requires a present `.env` file; explicit service environment defaults remain in the compose file
- Docker Compose now stores the SQLite database at `/data/db/bills.db`, matching the persistent `/data` volume and Dockerfile defaults

---

## v0.8.1

### Fixed
- Backup storage now respects `BACKUP_PATH` and otherwise derives a writable backup directory from the configured database path, preventing container permission errors from attempts to create `/app/backups`

---

## v0.8

### Added
- Admin backup management UI for creating, importing, listing, downloading, restoring, and deleting managed SQLite backups
- Admin scheduled-backup controls for enabling daily or weekly scheduled backups, choosing run time, setting scheduled-backup retention, saving settings, and running a scheduled backup immediately
- Scheduled backup worker using existing cron support; scheduled backups use managed `scheduled-backup-...sqlite` IDs and the same safe backup directory
- Admin backup settings endpoints for reading/saving schedule settings and running a scheduled backup on demand

### Changed
- Backup metadata now includes a backup type: manual, scheduled, imported, or pre-restore
- Backup status includes scheduled backup settings, next run, retention count, and last error when available
- Settings writes now update `updated_at` correctly so scheduled backup settings can be saved

### Security
- Backup delete is admin-only and only deletes managed backup IDs inside the controlled backup directory
- Scheduled retention only deletes old scheduled backups, never manual, imported, or pre-restore backups

---

## v0.7

### Added
- Admin-only `POST /api/admin/backups/import` endpoint for importing uploaded SQLite backup files into the managed backup directory
- Imported backups use server-generated `imported-backup-...sqlite` IDs, checksum metadata, and the same path validation as managed backups

### Security
- Uploaded backup imports are written to controlled temporary files, SQLite integrity-checked, and only promoted after validation succeeds
- Invalid or empty uploaded backup files are rejected without leaving temporary artifacts behind

---

## v0.6.1

### Security
- Backup status metadata now uses managed backup IDs instead of exposing filesystem paths
- Invalid SQLite backup files now fail restore validation with a safe HTTP 400 error

### Changed
- Backup status counts now use the backup service's managed backup list instead of scanning arbitrary files in the backup directory

---

## v0.6

### Added
- Admin-only SQLite database backup endpoints for creating, listing, downloading, and restoring backups
- Secure backup service with generated timestamped backup IDs, checksum metadata, SQLite integrity validation, and controlled backup directory handling
- Restore flow that creates a pre-restore safety backup before replacing the live database

### Security
- Backup download and restore IDs are strictly validated to prevent path traversal, absolute paths, nested paths, and arbitrary file access
- Backup API returns safe metadata only and does not expose backup directory paths

---

## v0.5

### Added
- `GET /api/version/history` returns the full `HISTORY.md` contents, current package version, and changelog last-modified timestamp
- Dedicated Release Notes page at `/release-notes` with loading, error, empty, and full-history display states
- Status page Release Notes card with current version, changelog last-updated timestamp, preview, and link to the full Release Notes page
- Frontend API helper `releaseHistory()` for fetching full release history

### Changed
- Status page release notes area now stays compact and links to the dedicated full-history view

---

## v0.4

### Added
- Status page operations cards for daily worker, notifications, backups, server clock, tracker health, and recent errors
- `/api/status` now returns backward-compatible operational status sections: `worker`, `notifications`, `backups`, `server`, `tracker`, and `recent_errors`
- Lightweight in-memory status runtime for worker state, notification test/error state, and recent backend errors

### Changed
- Quick Pay and inline payment creation on the Tracker page now scope new payments to the selected tracker month/year
- Status page runtime memory now reads `runtime.memory_mb` from the backend
- Status backend now includes database `last_modified`, server time, timezone, and current-month tracker counts

### Fixed
- Quick Pay no longer records payments against today's real month when viewing a previous or future tracker month

---

## v0.3

### Added
- `monthly_bill_state` table: stores per-bill, per-month overrides for actual_amount, notes, is_skipped
- `GET /api/bills/:id/monthly-state?year=&month=` — retrieve monthly state for a bill (returns defaults if no state set)
- `PUT /api/bills/:id/monthly-state` — upsert monthly state (actual_amount, notes, is_skipped) for a specific bill+month
- Tracker response now includes `actual_amount`, `monthly_notes`, and `is_skipped` fields per row from monthly_bill_state
- Export CSV now includes `Actual Amount` and `Monthly Notes` columns

### Changed
- `GET /api/tracker` rows now include monthly override fields when set; fall back to bill defaults when not set
- `GET /api/export` CSV output includes two new columns (backward-compatible, new columns appended)

---

## v0.2.1

### Fixed

- `dailyWorker.js`: Payment query for auto-draft status detection was missing `deleted_at IS NULL`, causing soft-deleted payments to count toward bill status in the daily worker
- `notificationService.js`: Payment query for notification suppression was missing `deleted_at IS NULL`, allowing a soft-deleted payment to incorrectly suppress due/overdue email notifications
- `payments.js GET /`: Year and month query parameters were interpolated directly into SQL string instead of using parameterized queries (SQL injection risk); replaced with bound parameters and proper validation
- `payments.js GET /`: End-of-month boundary was hardcoded as day 31 for all months; now computed using actual days-in-month per year/month

### Changed

- `payments.js POST /`, `POST /quick`, `POST /bulk`: Amount is now validated as a positive number (> 0); zero and negative amounts are rejected with HTTP 400
- `tracker.js GET /`: Year and month query parameters are now validated (year 2000–2100, month 1–12); invalid values return HTTP 400 instead of silently computing an invalid date range
- `payments.js GET /`: Year and month query parameters are now validated with the same rules; partial provision of only one is rejected with HTTP 400
- `export.js GET /`: Year query parameter is now validated (2000–2100); invalid values return HTTP 400
- DB schema: Added compound index `idx_payments_bill_date_del ON payments(bill_id, paid_date, deleted_at)` to accelerate the core tracker query pattern (`WHERE bill_id = ? AND paid_date BETWEEN ? AND ? AND deleted_at IS NULL`)

---

## v0.2

### Added

- `GET /api/version` — serves version and structured release notes from HISTORY.md
- **Paid Date column** in tracker table — shows payment date in green for paid bills
- `POST /api/payments/bulk` — batch payment recording in a single request
- Soft-delete for payments: `deleted_at` column + `POST /api/payments/:id/restore` endpoint
- `GET /api/tracker/upcoming?days=30` — upcoming bills feed sorted by due date
- `GET /api/bills/:id/payments?page=&limit=` — paginated payment history per bill
- `GET /api/export?year=YYYY&format=csv` — CSV export of all payments for a year
- Status page now displays current version and release notes from HISTORY.md

### Changed

- Payments schema: `deleted_at` column added (migration runs automatically on startup)
- `DELETE /api/payments/:id` now soft-deletes (sets `deleted_at`) instead of hard-deleting
- All payment queries now exclude soft-deleted records

---

## v0.1

### Added

- Initial release: React + Vite + Tailwind CSS + shadcn/ui frontend
- Three-theme system: Light, Dark, Dark Purple — persisted to localStorage
- Collapsible sidebar with Ctrl+B / ⌘+B keyboard shortcut
- Stripe-style layered layout with centered max-width container
- Full page set: Tracker, Bills, Categories, Settings, Status
- Admin panel with user management and onboarding wizard
- First-run terminal wizard + env-var non-interactive setup
- Single-user mode (bypass login for household use)
- Email notification system via SMTP (per-user or global recipient)
- Three-level auth: admin (user management only), user (full tracker access)
- First-login privacy notice informing users of admin limitations
- Docker deployment with persistent volume for DB and backups
- Legacy UI preserved at /legacy ("Remember When" mode)
- Release notes one-time dialog on version upgrade

---

## Version Bump Convention

### Version Format

Bill Tracker follows [Semantic Versioning](https://semver.org/): `MAJOR.MINOR.PATCH`

### Version Bump Rules

| Bump Type | When to Bump | Examples |
|-----------|-------------|----------|
| **Patch (x.y.Z)** | Bug fixes, security patches, hotfixes | v0.19.0 → v0.19.1 |
| **Minor (x.Y.z)** | New features, new endpoints, new environment variables | v0.19.0 → v0.20.0 |
| **Major (X.y.z)** | Breaking changes, schema changes, API changes | v0.19.0 → v1.0.0 |

### Version Updates

| Change | Version Bump | HISTORY.md Entry |
|--------|--------------|------------------|
| Security fix in `routes/*.js` | Patch | Under current minor version |
| New API endpoint | Minor | Under current minor version |
| New env variable (`INIT_REGULAR_USER`) | Minor | Under current minor version |
| Breaking change to frontend | Major | Under new major version |
| Database schema change | Major | Under new major version |

### Current Version

- **Current Version**: v0.19.0
- **Package.json**: `version: "0.19.0"`
- **HISTORY.md**: Top entry matches current version

### Version Sync

The version in `package.json` and top of `HISTORY.md` must always be in sync. After any change that qualifies for a bump, update both files and document in HISTORY.md under the appropriate version section.
