78 KiB
78 KiB
Bill Tracker — Changelog
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 intoroutes/auth.jsonly.
Changed
services/notificationService.js: Added per-user ownership filter and nulluser_idguard in notification runnerroutes/authLogin.js: Removed (consolidated intoroutes/auth.js)docs/Engineering_Reference_Manual.md: Removed staleauthLogin.jsduplicate route note, updated version to 0.23.2README.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 andPOST /api/admin/migrations/rollbackendpoint 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.rollbackandmigration.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 usepath.basename()instead of full path
Changed
routes/admin.js: AddedrollbackMigrationimport and/migrations/rollbackendpointdb/database.js: AddedrollbackMigration()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 migrationdb/database.js: Added[migration] {version} completed in Xmslog after each migrationdb/database.js: Added[migration] All migrations completed in Xmslog after all migrationsdb/database.js: Added[migration-error] Failed after Xms: ...log on migration failuresdb/database.js: Added lazygetLogAudit()function with try/catch to avoid circular dependencydb/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_PASSandINIT_REGULAR_USER/INIT_REGULAR_PASSenvironment variables no longer see the first-login/force-password-change flow on container restarts
Changed
setup/firstRun.js:runFromEnv()now resetsfirst_login=0, must_change_password=0when updating existing admin and regular usersserver.js: Seed logic resetsfirst_login=0, must_change_password=0when updating existing regular usersdb/database.js:[init] Reset passwordcode now setsmust_change_password=0instead of1to match intended behavior
Added
- Audit logging (
seed.flag_resetaction) for flag resets insetup/firstRun.jsandserver.js db/database.jsinit-time flag resets useconsole.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-allendpoint 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_atto auth.js change-password for consistency with profile.js - Audit logging for
logout.allandpassword.changeevents
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 > 0guards
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()inclient/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/useEffectwithuseTracker()hook load()callback replaced byrefetch()from React Query- Error handling:
useEffect+useRefpattern prevents duplicate toast notifications
v0.21.1
Added
- Loading Skeletons — Tracker and Bills pages show skeleton placeholders during data loading with
aria-busyattributes - Reusable
Skeletoncomponent 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_paidper bill row,previous_month_totalin summary, year-wrapping for January
v0.20.8
Added
- Billing Cycle Sub-categories —
cycle_type(monthly/weekly/biweekly/quarterly/annual) andcycle_daycolumns 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_typeandcycle_daycolumns - 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-expandedandaria-haspopupon Tracker menu,aria-labelon footer,role="main"on layout wrapper - Main landmark — proper
<main>element with uniqueidfor skip navigation target
v0.20.6
Added
- Audit logging — security event tracking via
audit_logtable (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_userandidx_audit_log_actionfor query performance
v0.20.5
Added
- Bulk payment validation —
/api/payments/bulknow requires{ payments: [...] }format - Max 50 items per request — prevents abuse via oversized bulk requests
- Per-item input validation —
bill_idmust be integer,paid_datemust be YYYY-MM-DD,amountmust be >= 0 - Duplicate detection — payments with same
bill_id + paid_date + amountare skipped, not duplicated - Structured response —
{ created: [...], skipped: [...], errors: [...] }
v0.20.4
Added
- Migration dependency management — All 17 versioned migrations now have explicit
dependsOnfields 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] — satisfiedwhen 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_nameonbills(user_id, name)— user-scoped bill lookupsidx_payments_methodonpayments(method)— payment method filteringidx_monthly_starting_amounts_useronmonthly_starting_amounts(user_id)— user starting amountsidx_import_history_imported_atonimport_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
sessionstable bloat and potential token reuse. created_atcolumn on sessions — v0.43 migration addscreated_atto the sessions table for better cleanup targeting.SESSION_CLEANUP_INTERVAL_MSenv 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_PASSis set, the default admin's password is reset andmust_change_password=1is 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 arun()function. Migrations whose changes aren't present in the DB (likeis_seededcolumns) 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 userno longer includes the username (Hudson finding) - Password reset is always explicit — If
INIT_ADMIN_PASSis set, the reset happens. If not set, no reset. No silent side-effects.
v0.19.2
Added
- React Error Boundaries —
ErrorBoundarycomponent wraps all routes inApp.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_migrationsis 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 COLUMNfailures on legacy databases.
Security
- Migration reconciliation is read-only — No user data is modified or deleted during legacy detection. All
PRAGMA table_info()andsqlite_masterqueries 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_USERandINIT_REGULAR_PASScreate a non-admin user on first run for role-based testing - Non-admin Test User — Added
INIT_REGULAR_USERandINIT_REGULAR_PASSenv vars for role-based testing
Changed
- Database Migration v0.42 —
bill_history_rangestable creation moved into versioned migration system
Security
- Admin-only
/aboutendpoint — Added/api/about-adminendpoint 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-sanitizeadded to ReactMarkdown component in AboutPage.jsx - Route guards —
/admin/aboutroute protected withRequireAuth 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_USERandINIT_REGULAR_PASSare set - Regular users are created with
role='user'andis_default_admin=0 - Migration system now handles
bill_history_rangestable 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_SITEenv 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/aboutroute guard — ReactRequireAuthmiddleware protects/admin/aboutroute - Rate limiting on
/api/about-admin—adminActionLimiter(30 req/15min per IP) applied to prevent brute-force attempts - XSS prevention —
rehype-sanitizeadded to ReactMarkdown component in AboutPage.jsx - Content redaction —
routes/aboutAdmin.jssanitizes paths, redacts internal IPs, passwords, API keys - Error sanitization — Error messages exclude paths to prevent path disclosure
- Non-admin test user — Added
INIT_REGULAR_USERandINIT_REGULAR_PASSenv 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_seededcolumn 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
Othermonthly starting amount alongside the 1st and 15th amounts. - New
monthly_starting_amountsrecords store user-scoped, month-specific starting cash withfirst_amount,fifteenth_amount, andother_amount. - New
GET /api/monthly-starting-amountsandPUT /api/monthly-starting-amountsendpoints 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/summaryendpoint 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/calendarendpoint 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
lgand 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.pngBillTracker 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.pngartwork to the served/img/logo.pngasset 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.htmlnow redirects to the modern React/loginscreen 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 legacypublic/*.html, CSS, and JS files from being emitted intodist. - 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/summaryusing 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@5for full cryptographic validation via JWKS: signature, issuer, audience, expiry, nonce, andsubpresence — 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
Securesolely becauseNODE_ENV=production; this preserves login on plain-HTTP Docker deployments while still supportingCOOKIE_SECURE=true,HTTPS=true, and HTTPS reverse-proxy detection.
Added
- Docker startup volume repair: runtime now starts through
docker-entrypoint.sh, creates/data/dband/data/backups, fixes/dataownership for the non-rootbilluser, then drops privileges before launching Node. This prevents SQLite migrations from failing withSQLITE_READONLYon mounted volumes. - Docker startup migrations: entrypoint now runs
scripts/migrate-db.jsas the non-root app user before starting the server, so required SQLite schema migrations and seeded defaults complete before the app listens for requests. SetRUN_DB_MIGRATIONS=falseonly for special maintenance runs. - Database writability preflight: startup now checks that
DB_PATHand 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_posttoken endpoint authentication method selector. The default remainsclient_secret_basic, matching the previousopenid-clientbehavior. - Admin user role management: Admin Users table now lets an admin promote another user to
adminor demote an admin back touser, 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;
/adminuses the same top nav so admins can return to Tracker/Bills/Categories/Profile/Settings/Status without typing a URL. Backend/adminprotection 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-modeextended: returnslocal_login_enabled,oidc_login_enabled,oidc_configured,can_disable_local,warnings, safe OIDC settings, and the client-secret markerPUT /api/admin/auth-modeextended: accepts all new provider settings, allows setting a new client secret, keeps the existing secret when blank, and supports explicit saved-secret clearingGET /api/auth/modeextended: returnslocal_enabled; returns OIDC provider name and login URL only when OIDC is enabled and fully configuredPOST /api/auth/loginnow checkslocal_login_enabledsetting and returns 403 if admin has disabled local login- Login page OIDC button uses
/api/auth/modeso 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_enabledbefore 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.jsrewritten to useopenid-client@5throughout:getOidcClient(config)— builds and caches an openid-clientClientafter OIDC discoverybuildAuthorizationUrl()usesclient.authorizationUrl()(uses discoveredauthorization_endpoint)exchangeAndVerifyTokens()replaces manual exchange + claims-only validation withclient.callback()which does the full PKCE exchange, JWKS signature verification, and all claim checks in one callgetOidcConfig()resolves provider config from DB settings first, env fallback second, safe defaults lastmapRoleFromClaims()reads the effective admin group at runtime; default role remainsuser, and admin is granted only by explicit group matchfindOrProvisionUser()uses effectiveoidc_auto_provisionand 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
settingstable, 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@5JWKS-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-Byremoved;Strict-Transport-Securityadded whenHTTPS=true - CORS locked down:
cors({ credentials: true })(wide-open) replaced with opt-in viaCORS_ORIGINenv var; without it, no CORS headers are sent and the browser's same-origin policy applies - Cookie
secureflag: session cookie now setssecure: truein production (NODE_ENV=productionorHTTPS=true), preventing transmission over plain HTTP in deployed environments - Settings endpoint hardened:
GET /api/settingsnow 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.filefields removed fromGET /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.messageinternally but no longer callsconsole.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=trueplus all required env vars are present:GET /api/auth/oidc/login— generates PKCE code verifier + challenge, stores one-time state inoidc_statesDB table, redirects to the identity provider's authorization endpointGET /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 frontendGET /api/auth/modenow includesoidc_enabled,oidc_provider_name, andoidc_login_urlwhen OIDC is configuredservices/oidcService.js: OIDC discovery caching, PKCE helpers, login-state management, token exchange, claim validation, role/group mapping, user auto-provisioningmiddleware/rateLimiter.js: rate limiter factory for all endpoint categoriesmiddleware/securityHeaders.js: global security headers middlewareauthService.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 OIDCsubclaim for stable identity mappingusers.email TEXT— stores email for OIDC-linked accounts and optional local-user email linkingusers.last_login_at TEXT— updated on every successful login (local or OIDC)oidc_statestable: short-lived (5 min TTL) PKCE/nonce state for in-flight OIDC logins; pruned on each new login attempt
Changed
authService.login()now updateslast_login_aton each successful local loginrequireAuthsingle-user-mode query now includesdisplay_nameso 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@5and upgradevalidateIdTokeninservices/oidcService.jsfor 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/previewPOST /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_ratefield 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_dayfield and clamps to shorter months using the existing month-end handling
Notes
- The legacy
override_due_datecolumn 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/menow returnsdisplay_nameso the top-nav user menu always shows the current display name after Profile save without requiring logout/loginGET /api/statusnow includes a safe read-onlycleanupsection:last_run_atandlast_resulttask 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_namesave already merged correctly into local state; the auth session fix ensuresrefresh()also returns the updated value
v0.15
Added
services/cleanupService.js— new cleanup service with four independent tasks:- Expired import sessions — deletes
import_sessionsrows 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-*.sqlitefiles from the OS temp directory that were not deleted after an interrupted download; configurable max age (default 2 hours) - Orphaned backup partials — removes
.partialand.uploadfiles 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_historyrows older than a configurable threshold; disabled by default
- Expired import sessions — deletes
- 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 resultPUT /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 thesettingstable with safe defaults (seeded viaINSERT 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
:rootnow 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/:idnow permanently removes the bill and all associated payments, monthly state, and history ranges — inactivation (PUTwithactive: 0) remains the safer non-destructive alternative - Bill history visibility: bills now carry a
history_visibilityfield (default,all,ranges,none) for future UI control over which historical data is shown for inactive bills bill_history_rangestable: per-bill, multi-range date records for fine-grained history visibility controlGET /api/bills/:id/history-ranges— list all history ranges for a billPOST /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 rangeDELETE /api/bills/:id/history-ranges/:rangeId— remove a history range- Bills list and detail responses now include
history_visibilityandhas_history_rangesflag for future UI icon support
Changed
DELETE /api/bills/:idchanged from soft-delete (set active=0) to hard-delete; clients that need deactivation should usePUT /api/bills/:idwith{ active: 0 }(unchanged behavior)- AP flag badge on the Bills page is now emerald/green and uses boolean coercion to prevent the SQLite integer
0from 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;
/datanow redirects to/profilefor 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
/profilewith 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 aUNIQUE(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/profilereturns safe user data (id, username, display_name, role, created_at, updated_at, last_password_change_at, notification preferences, export links) PATCH /api/profile— updatesdisplay_name(only safe user-owned field)GET /api/profile/settings— returns user-owned notification preferences from the users tablePATCH /api/profile/settings— updates user notification preferences (partial update; omitted fields are preserved)POST /api/profile/change-password— strict password change requiringcurrent_password,new_password, andconfirm_new_password; always verifies the current password regardless of account state; recordslast_password_change_aton successGET /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_nameandlast_password_change_atcolumns added to the users table via additive migration
Changed
PUT /api/settingsno 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_idis 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-passwordalways 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-passwordpreserved 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 Paidcolumns 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 genericInternal 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, andSeptember2017 - Added tolerance for known month-name typos found in the test workbook, including
Januaru,Febuary, andNovevmber - All-sheets XLSX preview now skips obvious non-bill tabs such as
info, tax/debt summary sheets, genericSheet13-style tabs, andhome ownership expenses
Notes
- Electric rows from
Test_Data/monthly bills.xlsxnow 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
recommendationobject 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 OneandDiscover 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/previewandPOST /api/import/user-db/applyare still needed - User data exports (SQLite + Excel) remain Coming Soon; backend endpoints
GET /api/export/user-dbandGET /api/export/user-excelare still needed
v0.10.1
Added
- Multi-sheet preview mode: pass
?parse_all_sheets=truetoPOST /api/import/spreadsheet/previewto 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; newyear_month_sourcefield on every preview row:row_date,sheet_name,default, orambiguous- Every row now includes
sheet_nameidentifying which worksheet it came from - Multi-sheet workbook response includes a
sheetsarray 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 resolveYearMonthandparseSheetNameexported 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 togetSheetRows();parseSheetRows()extracted as reusable helper- All preview rows now include
sheet_nameandyear_month_sourcefields (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 existingrow_{N}format
Notes
- Apply behavior unchanged —
previewRow.detected_yearanddetected_monthalready 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=Ndefault 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 dataPOST /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 confirmationGET /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: trueis 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
billstable does not have auser_idcolumn (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 toexceljsif 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.jsdocumenting the neededGET /api/export/user-dbandGET /api/export/user-excelendpoints
v0.8.2
Fixed
- Docker runtime image now creates writable
/data/db,/data/backups, and fallback/app/backupsdirectories for the non-root app user - Docker Compose now builds the project Dockerfile and mounts persistent storage at
/datainstead of bypassing the image setup with a plain Node image - Docker Compose no longer requires a present
.envfile; explicit service environment defaults remain in the compose file - Docker Compose now stores the SQLite database at
/data/db/bills.db, matching the persistent/datavolume and Dockerfile defaults
v0.8.1
Fixed
- Backup storage now respects
BACKUP_PATHand 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-...sqliteIDs 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_atcorrectly 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/importendpoint for importing uploaded SQLite backup files into the managed backup directory - Imported backups use server-generated
imported-backup-...sqliteIDs, 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/historyreturns the fullHISTORY.mdcontents, current package version, and changelog last-modified timestamp- Dedicated Release Notes page at
/release-noteswith 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/statusnow returns backward-compatible operational status sections:worker,notifications,backups,server,tracker, andrecent_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_mbfrom 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_statetable: stores per-bill, per-month overrides for actual_amount, notes, is_skippedGET /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, andis_skippedfields per row from monthly_bill_state - Export CSV now includes
Actual AmountandMonthly Notescolumns
Changed
GET /api/trackerrows now include monthly override fields when set; fall back to bill defaults when not setGET /api/exportCSV output includes two new columns (backward-compatible, new columns appended)
v0.2.1
Fixed
dailyWorker.js: Payment query for auto-draft status detection was missingdeleted_at IS NULL, causing soft-deleted payments to count toward bill status in the daily workernotificationService.js: Payment query for notification suppression was missingdeleted_at IS NULL, allowing a soft-deleted payment to incorrectly suppress due/overdue email notificationspayments.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 validationpayments.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 400tracker.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 rangepayments.js GET /: Year and month query parameters are now validated with the same rules; partial provision of only one is rejected with HTTP 400export.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_atcolumn +POST /api/payments/:id/restoreendpoint GET /api/tracker/upcoming?days=30— upcoming bills feed sorted by due dateGET /api/bills/:id/payments?page=&limit=— paginated payment history per billGET /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_atcolumn added (migration runs automatically on startup) DELETE /api/payments/:idnow soft-deletes (setsdeleted_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: 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.