BillTracker/HISTORY.md

203 KiB
Raw Blame History

Bill Tracker — Changelog

v0.37.0

Added

  • Tracker table sorting — The Tracker page now supports URL-backed sorting by bill name, due date, expected amount, last-month paid, paid amount, remaining amount, paid date, and status. Desktop table headers are clickable with active direction indicators, while the filter bar provides a compact sort selector for mobile and tablet. Status sorting uses the tracker lifecycle order (missed, late, due soon, upcoming, autodraft, paid, skipped) instead of plain alphabetical labels, and manual bill reordering is paused while a sorted view is active.

  • Bank payments override provisional manual tracker payments — Manual payments entered from the Tracker count immediately while waiting for bank sync. When a matching bank-backed payment clears for the same bill cycle, the bank payment becomes the accounting source of truth and the manual payment is preserved as history only with override metadata and a BillModal badge/note. Overridden manual payments are excluded consistently from Tracker, Summary, Calendar, Analytics, Categories, starting amount summaries, drift checks, notifications, status counts, bank pending deductions, trends, overdue checks, and debt balance deltas. If the bank match is undone, the provisional manual payment is reactivated.

  • Improved unmatch flow — choice dialog + bulk deselect — Clicking Unmatch on a linked transaction in the Bill modal now opens a two-option choice dialog instead of immediately removing the match. Option 1 ("Unmatch this payment only") confirms via a single AlertDialog and removes only that transaction. Option 2 ("Review all similar matches") fetches all linked transactions for the bill whose payee normalizes to the same prefix and opens a checklist dialog where each similar match is pre-checked. Users can deselect individual transactions to keep them matched, use All/None quick-selects, and optionally check a "Remove merchant rule" checkbox (shown only when a merchant rule matching the payee pattern exists on the bill). The confirm button shows the count of selected transactions and is disabled when nothing is selected. New backend endpoint POST /api/transactions/unmatch-bulk handles mixed provider_sync (restores balance + soft-deletes payment) and transaction_match (standard unmatch service) entries in a single database transaction.

  • Service Catalog page for subscription matching — The full known-service catalog moved out of the main Subscriptions page and into its own dedicated route at /subscriptions/catalog. The catalog now acts as an advanced matching tool instead of a second subscriptions list: tracked entries appear under a "Tracking" header with price drift indicators, each linked entry can be edited in BillModal, Re-link opens a searchable dialog to swap or remove the catalog link, and Custom bank descriptors let users add exact payee strings from their bank statements to improve future matching. Untracked catalog entries remain searchable/filterable and can still be tracked individually or in bulk. The Subscriptions page now shows a compact "Improve Matching" card that links to the Service Catalog when users need to tune descriptors, fix a wrong service link, or connect an existing bill to a known service. Catalog load failures now show both inline error state and toast feedback. New migration v0.96 adds bills.catalog_id FK (backfilled for existing subscriptions via name matching) and the user_catalog_descriptors table for per-user custom payee strings; user descriptors are merged into loadCatalog so they improve auto-matching for only that user's account.

  • Subscription catalog: bank descriptors + pricing (2026 researched dataset)subscription_catalog now carries researched bank statement descriptor strings and common nicknames/slang from a 200-service dataset. Migration v0.95 adds four columns (subcategory, starting_monthly_usd, starting_annual_usd, price_notes) and a new subscription_catalog_descriptors table (1,501 rows: 1,069 bank descriptors + 432 slang terms). loadCatalog joins descriptors into each catalog entry at load time. lookupCatalog now checks bank statement descriptors first (score 2000+) before falling back to name/domain fuzzy-match (1000/500); this resolves cases where bank payee strings like "NETFLIX *SUBSCRIPTION" or "AMZN PRIME VIDEO" bore no obvious similarity to the service name. catalogMatchPayload now includes starting_monthly_usd.

  • Improved unmatch flow — choice dialog + bulk deselect — Clicking Unmatch on a linked transaction in the Bill modal now opens a two-option choice dialog. Option 1 ("Unmatch this payment only") shows a confirmation AlertDialog and removes just that transaction. Option 2 ("Review all similar matches") fetches all linked transactions for the bill whose payee normalizes to the same prefix and opens a checklist dialog: each similar match is pre-checked, users can deselect ones to keep, All/None quick-selects are available, and an optional "Remove merchant rule" checkbox appears when a matching merchant rule exists. Confirm shows the count of selected transactions. New backend endpoint POST /api/transactions/unmatch-bulk handles mixed provider_sync (restores balance + soft-deletes payment) and transaction_match entries in a single database transaction.

  • Auto-match review panel — Merchant-rule auto-matches (payment_source = provider_sync) are now surfaced in a collapsible "Auto-matched — review" panel in the Data → Bank Sync section. Each entry shows the payee, date, amount, and the bill it was matched to. An Undo button reverses the match: the payment is soft-deleted, the bill balance is restored (including any interest), and the transaction reverts to unmatched. The panel appears only when there are reviewable items in the last 7 days, disappears when the list is empty, and automatically refreshes after each Sync Now or Backfill. New endpoints: GET /api/payments/recent-auto (fetch the review list), POST /api/payments/:id/undo-auto (reverse one match).

  • Auto-learn merchant rules from manual matches — When a user explicitly confirms a transaction→bill match (via the confirm-match button or the transaction match endpoint with learnMerchant: true), a normalized merchant rule is automatically created so future synced transactions from the same payee auto-match without manual intervention. The derived rule is gated by learnableMerchantFromTransaction() which rejects payee text that is too short (<4 chars) or composed entirely of generic financial tokens (ach, atm, transfer, fee, etc.), preventing a rule like "ACH Payment" from becoming a catch-all. Background auto-matching (via applyMerchantRules) never creates rules — only explicit user actions do. Generic API endpoint tokens are also now collapsed in normalizeMerchant (apostrophes stripped so "Sam's Club" → "sams club").

  • Ambiguous merchant-rule matching protectionapplyMerchantRules now checks whether the most-specific tier of matching merchant rules maps to more than one distinct bill (e.g., two bills both named "Amazon" with matching rules). When ambiguous, the transaction is skipped and left for manual review rather than silently attributing to the wrong bill.

  • Session token hashing — Session tokens are no longer stored in plaintext in the sessions table. The database now stores SHA-256(token); only the cookie retains the raw token. Existing sessions are invalidated on first startup after this version (all users must re-login once). New admin privacy page allows opting into login location recording.

  • Geolocation opt-in privacy setting — Login IP geolocation (city, country, region, ISP) was previously always-on for all logins. It now requires an explicit privacy setting toggle (default off). Admin → Privacy card controls the setting. When disabled, no geolocation requests are made and no location data is stored.

  • TOKEN_ENCRYPTION_KEY env var — Encryption key separation added as a security option. See changed section.

  • Auto-match review panel — Merchant-rule auto-matches (payment_source = provider_sync) are now surfaced in a collapsible "Auto-matched — review" panel in the Data → Bank Sync section. Each entry shows the payee, date, amount, and the bill it was matched to. An Undo button reverses the match: the payment is soft-deleted, the bill balance is restored (including any interest), and the transaction reverts to unmatched. The panel appears only when there are reviewable items in the last 7 days, disappears when the list is empty, and automatically refreshes after each Sync Now or Backfill. New endpoints: GET /api/payments/recent-auto (fetch the review list), POST /api/payments/:id/undo-auto (reverse one match).

🔧 Changed

  • Subscription recommendations narrowed to bank-backed known services — The Recommendations panel now only shows high-confidence (90%+) bank transaction matches that resolve to a known subscription catalog entry. Unknown recurring merchant patterns are no longer mixed into primary recommendations; those can be reviewed separately later without diluting the "known service" signal. Recommendation confidence now separates identity, amount, and cadence evidence instead of relying on a single recurrence score: user-added descriptors and researched bank descriptors score strongest, service name/domain/slang matches score lower, catalog starting monthly/annual pricing is used to judge amount plausibility, and recurring cadence/amount stability add confidence when multiple charges exist. A single exact known bank descriptor with a plausible amount can still appear as a 90%+ recommendation, but weak one-off name/domain matches no longer do. Recommendation cards now expose the evidence to the user with badges and reason chips for descriptor type, price check, recurring cadence, amount range, catalog starting price, account, last seen date, and average amount.

  • Subscription recommendation review details — Recommendation scoring now includes an ambiguity evidence bucket for broad merchants and very short service names such as Amazon, Apple, Google, Walmart, Disney, Microsoft, Target, and Max. Exact known/user bank descriptors remain strong, but weaker broad name/domain/slang matches receive a confidence penalty and surface a "Review" badge with reasons. Recommendation payloads now include the matched transaction previews, and the Subscriptions page has a Details dialog showing identity evidence, price evidence, cadence evidence, ambiguity notes, amount range, catalog pricing, source accounts, and the specific bank transactions behind the recommendation before the user tracks, declines, or links it to an existing bill.

  • Subscription recommendations learn and prefer existing bills — Recommendations now inspect the user's existing bills instead of hiding matches when a bill name already exists. If an existing bill matches the known service or bank merchant, and especially when amount and due day line up, the recommendation's preferred action becomes "Link existing" rather than "Track new"; linking can also backfill the bill's catalog_id when the bill was named correctly but not yet connected to the catalog. New migration v0.97 adds subscription_recommendation_feedback so accepts, declines, existing-bill links, catalog relinks/unlinks, and descriptor additions/removals become per-user learning signals. Future scoring gets a modest user-specific boost or penalty from that history while hard-declined recommendations remain suppressed. Edge-case coverage now pins broad Apple/Amazon/Google one-off purchases, annual known-service charges, exact user descriptors, existing-bill link preference, and feedback-driven confidence changes.

  • Subscription page actions simplified — Recommendation cards now show one clear primary action (Track Subscription or Link Existing Bill), a Details icon, and a compact More menu for secondary actions such as choosing another bill, tracking as new, or dismissing. Tracked subscription rows now use a cleaner Edit + More menu pattern for pause/resume, and dialog/header/search/catalog action buttons use more consistent sizing and icon spacing. The noisy full reason-chip list was removed from the recommendation card surface and remains available in the Details dialog, making the page easier to scan.

  • Subscription cadence sort — The Tracked Subscriptions panel now has a compact Custom/Cadence segmented control. Custom keeps the saved manual order and drag controls; Cadence groups subscriptions by Weekly, Biweekly, Monthly, Quarterly, Yearly, and Other, sorted by next due date within each group. Reordering is automatically disabled while Cadence sort is active so grouped views are never accidentally saved as manual order.

  • Subscription edits update in place — Saving a bill from the Subscriptions page no longer reloads the full subscriptions/recommendations payload. BillModal already returns the saved bill, so the page now updates the edited row, local bill cache, next due date, monthly/yearly totals, paused count, and top subscription type in memory. Modal-internal payment or unmatch actions no longer accidentally close the modal and refresh the page through the same callback.

  • Subscriptions page simplified — Removed the full known-service catalog from the main Subscriptions page to reduce overload. The page now focuses on tracked subscriptions, strict known-service recommendations from bank data, transaction search, and a small "Improve Matching" link to the dedicated Service Catalog. Supporting failures that were previously quiet now surface toasts: loading bills for the link-to-bill dialog, transaction search errors, and catalog load errors.

  • Vite API proxy respects alternate API portsvite.config.mjs now reads API_PORT or PORT when configuring the /api proxy instead of hardcoding port 3000. This lets local development run the Bill Tracker API on another port when 3000 is already occupied.

  • SimpleFIN transaction deduplication stable across disconnect/reconnectprovider_transaction_id was built as simplefin:{data_source_id}:{account_id}:{tx_id}. When a user disconnected (deleting the data source) and reconnected (creating a new data source with a new ID), the ID in the key changed, so nothing matched the old orphaned rows and the full transaction history was duplicated. Changed the key format to simplefin:{account_id}:{tx_id} (no data_source_id) — the SimpleFIN account ID and transaction ID are stable identifiers assigned by the financial institution. The unique dedupe index was changed from (data_source_id, provider_transaction_id) to (user_id, provider_transaction_id) to match the new scope. Migration v0.93 rewrites all existing keys (stripping the numeric data_source_id segment), deduplicates any rows that are now identical after the key change (preserving the linked row over the orphan), and replaces the index atomically.

  • Transaction currency from account, not hardcoded USDnormalizeTransaction hardcoded currency: 'USD' for every imported transaction even though normalizeAccount correctly reads rawAccount.currency. Non-USD users' transactions were always mislabeled. The currency is now read from the account's own currency field; the 'USD' fallback only applies when the account has no currency data.

  • Interest on debt bills charged once per calendar month, not once per paymentcomputeBalanceDelta applied a full month of interest on every call. Making two payments in the same month on a credit-card or loan bill charged interest twice, inflating the tracked balance. computeBalanceDelta now checks bill.interest_accrued_month against the current month (YYYY-MM) and skips the interest component when they match. The month is updated atomically in bills whenever interest accrues (via the new applyBalanceDelta helper, which uses COALESCE to leave the column alone when no interest is charged). interest_delta is now stored on each payment row so the delete/restore/edit paths can correctly reverse only the payment component, not the already-charged interest. Migration v0.93 adds bills.interest_accrued_month TEXT and payments.interest_delta REAL. All payment-writing paths updated: routes/payments.js (create, quick, autopay-confirm, bulk, edit, delete, restore), routes/matches.js, routes/bills.js, services/trackerService.js, services/billMerchantRuleService.js, services/transactionMatchService.js, services/spreadsheetImportService.js. For backward compatibility, payment rows with interest_delta IS NULL (written before this version) fall back to the prior full-reversal behavior on edit/delete.

  • Sync lookback window — single source of truth, accurate UI copy — The SimpleFIN lookback window was described with three different wrong numbers, none of which matched the code: the Data page showed a "90d Backfill" button, a "pulled from the last 90 days" toast, and a 90-day "History window" stat; the Admin → Bank Sync card's "Initial connect & backfill" panel showed a "6 days" badge with body copy reading "60 days" and "60-day hard limit" twice. Actual behavior is a 44-day seed/backfill window (one day under SimpleFIN Bridge's 45-day hard limit). Root cause was duplicated constants: bankSyncService defined its own SEED_SYNC_DAYS = 44 / ROUTINE_SYNC_DAYS = 30 independently of bankSyncConfigService's SYNC_DAYS_EFFECTIVE / SYNC_DAYS_DEFAULT / SYNC_DAYS_MAX, and the UI strings were hardcoded and drifted. bankSyncConfigService is now the single source of truth — it exports the three constants and getBankSyncConfig() returns seed_days (44) and sync_days_max (45) alongside sync_days. bankSyncService imports the shared constants instead of redefining them. The user-facing GET /simplefin/status now returns seed_days, and the admin config endpoint already spreads the full config, so the backfill button label/title/toast, the admin badge and hard-limit copy, the routine-lookback input max/clamp, the at-limit warning, and the validation messages all render the backend values. Stale 90/30 UI fallbacks corrected so no wrong number is ever shown.

  • Encryption key separation — TOKEN_ENCRYPTION_KEY restored — The encryption key was previously auto-generated and stored in the database under _auto_encryption_key, co-located with the ciphertext it protects. Anyone with a database backup or file-level read could decrypt SimpleFIN access URLs, SMTP passwords, TOTP secrets, push notification tokens, and login history geolocation — encryption-at-rest provided no protection against backup theft. TOKEN_ENCRYPTION_KEY env var support is restored: when set, new encryptions use it (prefix e2:); when absent, the DB key is used (prefix v2:). On startup, if TOKEN_ENCRYPTION_KEY is set, all DB-key-encrypted secrets are automatically re-encrypted with the env key in a single transaction — covering data_sources.encrypted_secret, users.totp_secret/totp_recovery_codes/push_url/push_token, settings.notify_smtp_password/oidc_client_secret, and all user_login_history encrypted columns. Migration is idempotent (already-migrated e2: values are skipped). The Admin → Bank Sync card now shows which key source is active: a warning when the DB key is in use, a confirmation when the env key is loaded.

  • Bank balance freshness timestamp on TrackerPage — The bank budget tracking card's pulsing indicator label is renamed from "Live" to "Live Sync", and the card and compact status bar now both show "as of [date, time]" sourced from bank_tracking.last_updated (already returned by the summary API) so users can see how fresh the balance is. CalendarPage already showed this field; TrackerPage now matches.

  • SimpleFIN fetch retries transient failures with backoff — A single network blip or 5xx response previously flipped the connection to status: 'error' immediately. fetchAccountsAndTransactions now retries up to 3 attempts with 1 s / 2 s delays between them. Network errors and timeouts retry unconditionally; 5xx responses retry; 403 (revoked credentials) and other 4xx responses fail immediately since retrying won't help. claimSetupToken is not retried — a setup token is single-use and returns 403 on re-claim.

  • SimpleFIN requests now time out after 30 secondsclaimSetupToken and fetchAccountsAndTransactions used bare fetch with no timeout, so a hung SimpleFIN response would stall the sync indefinitely and hold the worker's running flag, blocking all subsequent auto-sync cycles. Both calls now pass signal: AbortSignal.timeout(30000); a TimeoutError propagates through the existing sanitizeError handler in each function.

  • Manual sync and auto-sync now produce identical match resultsautoMatchForUser (score-based auto-matching) was only called by the background worker after each sync, not by the routes used for manual "Sync Now". runSync in bankSyncService now calls autoMatchForUser directly alongside applyMerchantRules and applySpendingCategoryRules, so every sync path — manual, sync-all, initial connect, and the timer — runs all three matching passes. The redundant autoMatchForUser call removed from bankSyncWorker.

  • Match suggestion rejections now expire correctlymatch_suggestion_rejections has always stored rejected_at, but two queries incorrectly referenced created_at (added by a v0.90 migration that should never have been needed). matchSuggestionService.loadRejections was throwing and falling back to loading all rejections with no time filter, so rejected suggestions were suppressed forever instead of resurfacing after 90 days. cleanupService prune was also throwing and silently catching, so old rejection rows were never deleted. Both queries corrected to use rejected_at; the fallback dead-code in loadRejections removed.

🔒 Security

  • Session tokens hashed in the databasesessions.id previously stored the raw UUID that was set as the session cookie. Any attacker with read access to the database file (backup theft, direct file access) could extract those UUIDs and replay them as valid session cookies. Sessions now store SHA-256(token) as the primary key; the raw token stays only in the cookie and is never written to disk. All session operations in authService hash the cookie value before querying or mutating the table: getSessionUser, logout, rotateSessionId, invalidateOtherSessions, and the two INSERT paths in login/createSession. Migration v0.94 deletes all existing plaintext sessions, forcing a one-time re-login for all users. This matches the pattern already used for session_fingerprint in user_login_history.

  • IP geolocation made opt-in, disabled by defaultrecordLogin called http://ip-api.com/json/{ip} (plain HTTP, no opt-out) on every new-device login, sending the user's IP to a third party without notice. The call is now guarded by a geolocation_enabled admin setting (default: false). When disabled, no outbound request is made and the location_* columns in user_login_history are simply left null. The toggle is exposed in Admin → Privacy. Migration v0.94 seeds the setting at false for all installations including existing ones.

  • Rate limiting on sync/backfill endpointsPOST /:id/sync, POST /sync-all, and POST /:id/backfill had no rate limit despite being able to trigger outbound SimpleFIN requests on every call. A new syncLimiter (10 requests per 15 minutes, keyed by authenticated user ID rather than IP) is applied inline to all three routes. GET routes on the same router are unaffected. The limiter is included in allLimiters so its store is reset alongside the others in tests.

  • WebAuthn / FIDO2 hardware security key 2FA — Migration v0.92 adds webauthn_enabled and webauthn_user_id columns to users, a webauthn_credentials table (per-user, multiple keys supported — stores credential ID, CBOR public key as base64url, sign counter, transports, backup eligibility, friendly name, and AAGUID), and a webauthn_challenges table for short-lived registration, authentication, and login challenges. The new webauthnService.js handles the full lifecycle via @simplewebauthn/server: generating registration options (with excludeCredentials to prevent re-registering existing keys), verifying attestation responses, generating authentication options (passing allowed credentials and transports), verifying assertion responses (updating the sign counter on each use to detect cloned authenticators), and issuing/consuming login challenge tokens. The login flow mirrors TOTP exactly — after password verification succeeds, if webauthn_enabled is set, the server returns requires_webauthn: true alongside a challenge_token (a short-lived login challenge) and webauthn_options (the pre-generated assertion options); the client calls startAuthentication() from @simplewebauthn/browser, and POST /api/auth/webauthn/challenge verifies the assertion and creates a session. Six new endpoints added to routes/auth.js: GET /webauthn/status (enabled flag + credential count), GET /webauthn/credentials (list registered keys with name, AAGUID, backup flags, and timestamps), GET /webauthn/setup (begin registration — returns options + challengeId), POST /webauthn/enable (complete registration — verifies attestation, stores credential, sets webauthn_enabled = 1), DELETE /webauthn/credentials/:credentialId (remove one key — requires password confirmation; auto-disables WebAuthn when last key is removed), POST /webauthn/disable (remove all keys — requires password confirmation). RP ID and origin are configurable via WEBAUTHN_RP_ID and WEBAUTHN_ORIGIN env vars (default to localhost for dev). publicUser() in authService.js now includes webauthn_enabled so the frontend login flow knows to prompt for a security key tap. Expired WebAuthn challenges are pruned in the daily worker alongside expired sessions. OIDC and single-user mode are unaffected. @simplewebauthn/server and @simplewebauthn/browser v13 added to dependencies.

Release Image

Doing my part


v0.36.0

🔧 Changed

  • Bump0.35.10.36.0

  • Login history: encrypted at rest + geolocation + new device alerts + failed attempt tracking + session detection + 10 recordsuser_login_history now stores ip_address and user_agent encrypted with AES-256-GCM (same encryptionService used for SMTP, OIDC, and bank tokens). Migration v0.84 retroactively encrypts any existing plaintext rows and adds four new location columns (location_city, location_country, location_region, location_isp). On each new login, a non-blocking fire-and-forget request to ip-api.com resolves the IP to a city/region/country/ISP and stores it encrypted. Private and loopback IPs are skipped for geolocation. The login-history API decrypts all fields server-side before returning them — only the authenticated user can see their own data. History limit increased from 3 to 10 records. The Profile page now shows city, region, country, and ISP below each login entry. Migration v0.85 adds success (1 = successful login, 0 = failed attempt) and session_fingerprint (SHA-256 of the session ID) columns. Failed login attempts with wrong passwords are now recorded and displayed in the history modal with a red "Failed attempt" badge. The current active session is marked with a "This session" badge by comparing the session cookie fingerprint against stored values. New device logins (device fingerprint not seen in previous 10 logins) trigger a push notification via the user's configured channel (ntfy, Gotify, Discord, or Telegram). The summary card on the Profile page now shows location and always reflects the most recent successful login, not a failed attempt.

  • Login history for single-user mode — In single-user mode the app bypasses the session system entirely, so recordLogin was never called and login history was always empty. Fixed by issuing a bt_single_session presence cookie (httpOnly, 30-day, same security flags as the regular session cookie) on the first request from each new browser. recordLogin fires once per new cookie via setImmediate so it never delays page load. Return visits reuse the cookie and don't create duplicate entries. The login-history route now uses bt_single_session when req.singleUserMode is set so the "This session" fingerprint comparison works correctly without a real session cookie. OIDC logins were also missing the sessionId parameter passed to recordLogin, so "This session" never matched for OIDC sessions; fixed by passing session.sessionId through the OIDC callback.

  • TOTP / Authenticator App 2FA for local login — Migration v0.86 adds totp_enabled, totp_secret (encrypted), and totp_recovery_codes (encrypted) columns to the users table, and a totp_challenges table for short-lived (5-minute) challenge tokens. The new totpService.js handles secret generation, QR code rendering via qrcode, token verification via otplib, and recovery code lifecycle. Setup lives in the Profile page under a new "Two-Factor Authentication" section: scan a QR code with Google Authenticator, Authy, 1Password, Bitwarden, or any TOTP app; enter the 6-digit code to confirm; receive 8 one-time recovery codes (shown once, stored as SHA-256 hashes encrypted at rest). Once enabled, the login flow adds a second step after password verification — the server issues a challenge token instead of creating a session, the client shows a TOTP code input, and the session is only created after the code is verified via POST /api/auth/totp/challenge. Recovery codes can be used in place of the authenticator app. Disabling 2FA requires a valid TOTP code. OIDC and single-user mode are unaffected. otplib and qrcode are included in the Docker image.

  • No Login mode — admin UI redesign — The LoginModeCard in the Admin page is now a clean radio-group selector with two first-class options: Require Login (multi-user, shows the OIDC/local-login settings card below) and No Login — Single User (hides the auth methods card, shows a user picker and an amber security warning). An inline confirmation dialog is shown before enabling No Login mode. The AuthMethodsCard is conditionally hidden when single-user mode is active since OIDC and local-login settings are irrelevant when there is no login screen. The backend lockout validation (which previously blocked saving when all login methods were disabled) now skips entirely when auth_mode === 'single'. The LoginPage no longer flashes the sign-in form in single-user mode — it renders a neutral loading state while the auth-mode check is in-flight and redirects before the form ever appears.

  • Spending page — bank transaction categorization and budgets — Migration v0.87 adds a spending_category_id column to transactions, a spending_category_rules table (merchant → category auto-assignment rules), and a spending_budgets table (per-category monthly budgets). Eight default spending categories (Groceries, Dining, Fuel & Transport, Shopping, Entertainment, Health, Travel, Other) are seeded per user on first migration. A new /spending page appears in the sidebar (between Categories and Snowball) showing: a month navigator; a three-card overview strip (total spending, uncategorized, income received); a category breakdown list where each row shows amount, transaction count, a budget progress bar (red when over), and an inline budget editor; and a paginated transaction list with an inline category picker dropdown per row. Selecting a category in the breakdown filters the transaction list. Categorizing a transaction can optionally save a merchant rule — the same word-boundary matching used for bill rules — which immediately back-fills all existing unmatched transactions from that merchant and auto-categorizes new ones on every future sync. The bank sync worker now calls applySpendingCategoryRules after applyMerchantRules on every sync. The api.js helper layer gained a patch shorthand and get now accepts query-param objects via queryString.

  • Spending page: merchant rules manager and "remember merchant" prompt — The spending category picker (shown on each transaction row) now prompts "Always categorize [payee] as [category]? Save rule / Dismiss" immediately after a category is chosen. The prompt auto-dismisses after 7 seconds. Saving a rule back-fills all existing unmatched transactions from that merchant, marks the category as spending-enabled, and auto-categorizes all future syncs. A collapsible "Merchant Rules" section at the bottom of the spending page lists all saved rules grouped by merchant name with delete buttons and an "Add rule" form. Error handling: all load/delete/add operations have try/catch with toast feedback.

  • Spending categories separated from bill categories — Migration v0.88 adds a spending_enabled INTEGER DEFAULT 0 column to categories. Only categories with spending_enabled = 1 appear in the spending page category picker and breakdown. Migration v0.89 seeds the eight default spending categories (Groceries, Dining, Fuel & Transport, Shopping, Entertainment, Health, Travel, Other) for any user who already had their own bill categories before v0.87 ran (v0.87 only seeded for users with no categories). On the Categories page each category now has a shopping-cart icon button — green means it shows in spending, grey means it doesn't; hover tooltip explains the toggle. Auto-enables a category when a spending merchant rule is saved against it. Empty state on the spending page links to the Categories page when no spending-enabled categories exist.

  • SimpleFIN matching pipeline fixes — Migration v0.90 bundles four corrections to the matching pipeline: (1) normalizeMerchant() now strips & without adding a space — "AT&T" previously normalized to "at t" (two words) while banks report "ATT" → "att" (one word), so AT&T bills never auto-matched; (2) matchSuggestionService.addNameScore() replaced bidirectional .includes() with word-boundary regex matching ((^|\s)TERM(\s|$)) to align with the fix already applied to billMerchantRuleService — the score pipeline fed autoMatchForUser() and could silently create wrong payments at score ≥ 80; (3) merchant rules are now sorted by length descending before matching so the longer (more specific) rule always wins when multiple rules could match a transaction; (4) lateAttributionCandidate() hardcoded a 5-day window ignoring the user's bank_late_attribution_days setting for days 6+. Existing stored merchant rules in bill_merchant_rules and spending_category_rules are re-normalized using the updated function. match_suggestion_rejections gains a created_at column and rejections older than 90 days are now filtered out and pruned in the daily cleanup worker. GET /api/bills/merchant-rules endpoint added — returns all bill merchant rules across all bills grouped by bill name. BillRulesManager component added to the DataPage "Sync & Match" tab showing all rules with merchant name, per-rule auto-late toggle, and delete button.

  • Database performance: composite indexes — Migration v0.91 adds four indexes that were missing on frequently queried columns: idx_categories_user_deleted ON categories(user_id, deleted_at), idx_bills_user_deleted ON bills(user_id, deleted_at), idx_bills_user_active ON bills(user_id, active, deleted_at), and idx_payments_bill_deleted ON payments(bill_id, deleted_at). Without these, every category listing, bill listing, and payment query was a full table scan.

  • Worker health improvements — (1) workers/dailyWorker.js N+1 query fixed: the autopay-marking loop previously ran one SELECT payments WHERE bill_id = ? per bill. Replaced with a single batch query fetching all payments for active bills within a 90-day window, then grouped in memory and filtered per bill's cycle range. (2) statusRuntime.js now persists worker state to the settings table (_worker_last_run_at, _worker_next_run_at, _worker_started_at, _worker_last_error) and seeds in-memory state from those keys on startup. The admin Status page now correctly shows the last run time and next scheduled run after a container restart instead of showing "never / unknown". (3) notificationService.runNotifications() N+1 fixed: replaced per-bill payment query and per-bill-per-recipient hasNotification() calls with two batch queries (one for all payments, one for all notifications sent today), both checked in-memory via a Set. Reduces notification run from O(N + N×M) to O(3) DB calls. (4) The backup status route was reading backup_enabled (a legacy key, always null) instead of backup_schedule_enabled (what the admin UI writes), causing scheduled backups to always show as "Disabled" even when running correctly. Fixed by reading the correct key. (5) Status page timezone bug fixed: SQLite datetime('now') returns "YYYY-MM-DD HH:MM:SS" (no timezone marker); JS Date parses this as local time while next_run_at is a proper ISO-Z string parsed as UTC — mixing baselines made "Next Check" appear before "Last Sync". A toIso() helper appends Z to all SQLite-format timestamps before they leave the server.

  • SpendingPage double-fetch fix — Two useEffect hooks both depended on useCallback function references that were recreated when year/month changed, causing back-to-back duplicate API calls on month navigation. Replaced with a single effect depending on primitive values (year, month, activeCat). Added a cancelled flag so in-flight requests are discarded when the month changes before they resolve.

  • Income breakdown modal on TrackerPage — When SimpleFIN bank tracking is enabled, the green bank balance card on the main Tracker page is now a clickable button. Clicking it opens an "Income Breakdown" modal showing: how the effective starting balance was calculated (raw bank balance → pending deduction → effective balance); all positive unmatched SimpleFIN transactions for the month (paychecks, deposits, etc.) with date, payee, and amount; an eye-off icon to exclude a transaction (marks it ignored — useful for internal transfers); a "Show excluded (N)" toggle to view and restore previously excluded transactions. Error handling: load failures show an error state with a Retry button instead of the misleading "No transactions found" empty state; ignore/restore actions fail cleanly without corrupting the list.

  • Monthly income tracking UI on Summary page — The monthly_income table and PUT /api/summary/income endpoint have existed since early development but were never connected to a UI. A new "Monthly Income" section now appears on the Summary page above Expenses. It shows the current income label and amount in green, and when both income and total expenses are set it shows "After expenses" remainder right-aligned in the same card. An Edit button reveals an inline form with a label field and amount field. Saves via the existing endpoint. Validates non-negative amount, toasts on error.

  • Copy last month's budgets — A "Copy last month" button in the spending page category breakdown header copies all spending budget entries from the prior month into the current month in a single POST /api/spending/budgets/copy request. Non-destructive: existing budgets for the current month are overwritten (they're already visible so the user knows what they're replacing). Updates the UI immediately from the server response. Shows count toast ("3 budgets copied") or info toast if the previous month had no budgets.

  • payments.js SQL fragment renamed for clarityconst LIVE = 'deleted_at IS NULL' was renamed to const SQL_NOT_DELETED and given a 4-line comment explaining why SQL fragment interpolation is safe here, why parameterisation is not applicable to SQL fragments (only values can be bound, not column conditions), and explicitly warning future developers not to replace the pattern with dynamic input.

  • Migration version sync assertion_runMigrationVersions module-level variable is now populated by runMigrations() before its loop runs. reconcileLegacyMigrations() — which runs after runMigrations() on legacy-DB upgrade paths — compares its own version array against the stored list and throws a descriptive error if any version appears in one array but not the other. Catches drift between the two migration arrays at startup rather than silently misconfiguring a legacy schema.

  • Late-attribution coverage extended to single-bill sync and BillModalsyncBillPaymentsFromSimplefin (called by the Sync button in BillModal's Bank Matching Rules section) now runs the same late-attribution detection as the full sync. When a single-bill sync finds a payment that crossed a month boundary, it returns late_attributions in its response. The BillModal sync handler dispatches a tracker:late-attributions DOM event; TrackerPage listens and appends those to the existing queue, so the attribution dialog appears regardless of whether the sync came from the tracker header button or from inside a BillModal. Late attributions from both sources are processed in the same queue.

  • BillMerchantRules preview shows error state instead of silently failing — The debounced preview API call previously swallowed all errors with .catch(() => {}), causing the preview badge to simply disappear on network or server failure. Added previewError state: on failure a red "Error" chip appears in the input, and typing again clears it.

  • Late-attribution prompt for bank-synced payments that just missed month end — When applyMerchantRules auto-matches a transaction and the payment's posted_date falls within 5 days into a new month while the bill's due_day was in the prior month, the payment is flagged as a late-attribution candidate. After "Sync Bank" completes on the TrackerPage, a dialog appears for each candidate: "AT&T payment of $332.97 posted June 1 — should it count for May?" The user can accept (moves paid_date to the last day of the prior month so the tracker shows it as paid that month) or dismiss (keeps the original date). Multiple candidates are queued and shown one at a time. The date-only reclassification goes through a new PATCH /api/payments/:id/attribute-to-month endpoint that is specifically allowed for provider_sync payments (the existing PUT endpoint rejects transaction-linked payments). Amount and bank link are never changed.

  • Encryption key fully app-managed — no env var requiredTOKEN_ENCRYPTION_KEY environment variable support removed entirely. The auto-generated DB key (_auto_encryption_key in the settings table) is now the primary mechanism, not a fallback. The [security] warning that fired on every startup when no env var was set is gone. On first startup a 48-byte cryptographically random key is generated and persisted to the database; subsequent restarts reuse it. All existing encrypted data (SMTP password, OIDC secret, SimpleFIN tokens, push notification tokens) continues to decrypt correctly.

  • TrackerPage crash fixed — activeTotalExpected temporal dead zone — The cashflow block added in an earlier change referenced activeTotalExpected and activePaidTowardDue before their const declarations. JavaScript's temporal dead zone caused Cannot access 'activeTotalExpected' before initialization on every tracker load. Fixed by moving the four active* declarations above the cashflow block that depends on them.

  • Bank merchant rule matching — balance_delta, current_balance, and error handlingbillMerchantRuleService.js was the only payment creation path still missing balance_delta computation and current_balance update after the #49 fix. Both applyMerchantRules (full-user batch matching, called on every sync) and syncBillPaymentsFromSimplefin (single-bill retroactive match) now read the bill fresh before each payment, compute computeBalanceDelta, include balance_delta in the INSERT, and update bills.current_balance. payment_source corrected from 'auto_match' (not in VALID_PAYMENT_SOURCES) to 'provider_sync'. DB migration v0.82 updates all historical auto_match records. Both functions also gained full error handling — txRows queries, the fallback notes query, and db.transaction() blocks are all wrapped with try/catch that logs to console and returns safe zero-match defaults instead of propagating exceptions to the sync worker or API caller.

  • applyMerchantRules returns matched bill names — The function now returns { matched, matched_bills: ['AT&T', 'Netflix'] } instead of just { matched }. Bill name is fetched via JOIN in the rules query. The name set is deduplicated via Map keyed by bill_id (so one bill matched by multiple transactions still appears once). The name list is bubbled through bankSyncService.runSync and the POST /api/data-sources/sync-all endpoint (using a Set to dedupe across multiple SimpleFIN sources). TrackerPage Sync Bank toast now reads "Synced — AT&T, Netflix ✓" instead of "3 payments matched", with a (+N more) suffix when there are more matched bills than bill names to display. Toast duration extended to 5 seconds.

  • BillModal — bank matching Sync button moved, fixed, and made reactive — The old "SimpleFIN payment history" sync button in the Subscriptions tab was removed (it didn't call loadLinkedTransactions() after success and was less discoverable). A new Sync button lives in the Bank Matching Rules section header of the Transactions tab, visible whenever localHasRules is true. A local localHasRules state initialises from sourceBill.has_merchant_rule but flips to true immediately when onRulesChanged fires — so the button appears right after adding the first rule without closing and reopening the modal. After a successful sync, loadLinkedTransactions() is called to refresh the Linked transactions list below, and refetch() updates the parent tracker view.

  • Pin Due — urgent bills float to top of tracker — A "Pin Due" toggle button in the TrackerPage header sorts overdue and due-soon bills to the top of each bucket when enabled. Priority order: missedlatedue_soonupcoming → everything else; ties broken by due_day. The sort runs after filtering but before the bucket split, so each half-month bucket is sorted independently. The button uses variant="default" (solid) when active and variant="outline" when off so the current mode is always visible. Preference persists across sessions via localStorage under tracker_pin_upcoming. Drag reorder is automatically disabled while the toggle is on (reorderEnabled now also requires !pinUpcoming) since the two modes conflict.

  • Bank tracking pending deduction corrected — no double-counting — The pending payments window was subtracting all recent payments regardless of source, including bank-synced ones (payment_source = 'provider_sync'). Since the live bank balance already reflects those outgoing transactions, subtracting them again produced a balance ~$2k lower than reality. Fixed in both trackerService.js and summary.js by adding AND (p.payment_source IS NULL OR p.payment_source != 'provider_sync') to the pending query. Only manually-entered payments and transaction-matched payments are now counted as pending — those are payments the user recorded in the tracker before the bank has seen them. The pending_cleared badge on tracker rows was given the same fix. Additionally, b.name was missing from the SELECT in getOverdueCount, causing the tooltip to show null names — corrected.

  • Bank tracking error handling hardenedbuildBankTracking() in trackerService.js and buildBankTrackingSummary() in summary.js had no try/catch. A DB lock, schema inconsistency, or bad account state would throw an unhandled exception and crash the entire tracker or summary API response for the user. Both functions are now wrapped: buildBankTracking returns { enabled: false } on error (tracker falls back to manual starting amounts gracefully), and buildBankTrackingSummary returns null (summary page omits bank data but still loads). Console error logged in both cases for debugging.

  • Merchant rule word-boundary matching — fixes false positives — The merchant matching logic used simple substring .includes() which caused "suno" (Suno AI music) to match "sunoco" (gas station) because "sunoco" contains "suno" as a substring. All four matching sites replaced with a word-boundary regex check: (^|\s)rule(\s|$) must match within the transaction string. Now "suno" does not match "sunoco" but still correctly matches "suno inc" and similar. DB migration v0.83 adds auto_attribute_late column to bill_merchant_rules.

  • Merchant rule: "Auto-fix month crossing" toggle — Some bills consistently post to the bank 15 days after month end (e.g. AT&T due May 29, posts June 1). Previously this required a manual prompt every cycle. Each merchant rule now has an "Auto-fix" toggle in the Bill Modal Bank Matching Rules section. When on, payments that cross a month boundary are automatically moved to the last day of the prior month on sync — no popup, no manual intervention. The flag is stored as auto_attribute_late in bill_merchant_rules (migration v0.83) and persists. A PATCH /api/bills/:id/merchant-rules/:ruleId/auto-attribute endpoint controls it.

  • Historical payment import dialog — When a merchant rule is added in the Bill Modal, a dialog now opens automatically asking how to handle past transactions: "Import all N payments" (bank as truth), "Choose which ones" (checkbox list showing each transaction's date, amount, and current match status), or "Skip — future only". The "choose" view shows already-handled transactions dimmed at the bottom for context. Backend: GET /api/bills/:id/merchant-rules/candidates returns all matching transactions regardless of current match status; POST /api/bills/:id/merchant-rules/import-historical imports a selected list.

  • Bank tracking status bar and card on TrackerPage — When SimpleFIN bank tracking is enabled, the TrackerPage now shows: (B) a slim colored status bar between the header and the filter panel — pulsing live indicator, account name, balance, pending amount, and projected figure color-coded green/red; (C) the Starting summary card is replaced by a bank-styled card with an emerald top bar, account name label, Landmark icon, pulsing "Live" badge, effective balance as the main number, and projected-after-bills in the hint line. Number sizes unchanged from other summary cards.

  • refetch is not defined in BillModal fixed — Two refetch?.() calls were added to BillModal during an earlier session but refetch is not a prop or variable in that component's scope. JavaScript throws ReferenceError: refetch is not defined before optional chaining can evaluate. Both calls removed — loadLinkedTransactions?.() is sufficient since the parent's onSave callback handles any full tracker refresh.

  • BillMerchantRules suggestion list now uses inline rendering — The suggestions dropdown previously used position: absolute which was clipped by BillModal's overflow-y-auto container. Clicks on suggestions appeared to land on the scroll container behind them. Replaced with inline block rendering (no absolute/fixed positioning) so suggestions are part of the normal document flow and always clickable. onMouseDown + e.preventDefault() keeps the input focused during selection.

  • Bank tracking: projected month-end balance added — The CalendarPage Monthly Money Map (bank mode) now shows a dedicated "Projected Month-End Balance" row below the four metrics. It displays the full equation inline — $5,461 bank $1,737 pending $3,696 remaining bills — so the source of the number is always visible. The row uses a green/red border depending on whether the projection is positive or negative. The TrackerPage Starting card hint also updated from account_name · live balance to account_name · projected $X after bills so the projection is visible without navigating to Calendar.

  • Bank tracking settings and account picker fixed — The loadBankTracking function in BankSyncSection called api.getSettings() but the API method is api.settings(). The wrong name threw TypeError: api.getSettings is not a function, silently caught by the outer catch {}, so neither the toggle state nor the account list ever loaded. Renamed to api.settings(). All 19 financial accounts now appear in the picker and the enabled/account/pending-days settings persist correctly across page loads.

  • Overdue badge: "due today" no longer counts as overdue + tooltip addedgetOverdueCount changed dueDate > todayStr to dueDate >= todayStr so bills due today are not counted as past due — only bills strictly in the past trigger the badge. The function now also returns names: [...] alongside the count. Both the sidebar TrackerMenu (desktop) and NavPill (mobile) wrap the badge in a <Tooltip> that shows "2 past due · Discord Nitro · Camry" on hover, so the number is immediately explained without navigating anywhere. Up to 5 names are shown with a "+N more" overflow line.

  • Column labels larger and lighter in tracker tableTableHead elements in TrackerBucket changed from text-[10px] font-semibold tracking-widest to text-xs font-medium tracking-wider. Size increased from 10 px to 12 px for readability; weight dropped from semibold to medium so the labels don't visually compete with the bold bill name text in each row.

  • Tracker row keyboard navigation — Tracker rows (desktop table view) are now keyboard navigable. Each row has tabIndex={0}, data-tracker-row, aria-rowindex, and an aria-label announcing the bill name, status, and due day. A focus-visible:ring-2 ring-primary/60 ring-inset focus ring appears on keyboard focus only. Key bindings: /j focuses the next row, /k the previous (both cross bucket boundaries via querySelectorAll('[data-tracker-row]')), Enter opens the edit modal, P toggles paid/unpaid (skipped bills ignored, Ctrl+P/Cmd+P passes through to the browser), Esc blurs the row. The onKeyDown handler guards against firing on nested interactive elements with if (e.target !== e.currentTarget) return.

  • Bills list query optimised and merchant rule index addedGET /api/bills replaced a correlated EXISTS(SELECT 1 FROM bill_history_ranges WHERE bill_id = b.id) per bill row with a single LEFT JOIN (SELECT DISTINCT bill_id FROM bill_history_ranges) hr. DB migration v0.81 adds composite index idx_bill_merchant_rules_user_bill ON bill_merchant_rules(user_id, bill_id) — the existing index only covered user_id, making the EXISTS check in GET /api/bills/:id scan by user then filter by bill; the composite index makes it a direct point lookup. Pagination was not added — the UI depends on all bills being loaded at once and personal-scale data volumes don't warrant it.

  • rotateSessionId uses db.transaction() instead of raw SQLrotateSessionId() in authService.js managed its DELETE + INSERT pair with explicit db.prepare('BEGIN').run() / COMMIT / ROLLBACK calls. This is fragile: if the rollback itself throws (e.g. connection in a bad state), the transaction is left open. Replaced with better-sqlite3's db.transaction() wrapper, which commits automatically on success and rolls back automatically on any thrown error with no manual try/catch required.

🔒 Security

  • OIDC client secret encrypted at rest — The OIDC client secret was stored as plaintext in the settings table alongside all other application settings. It is now encrypted using the same AES-256-GCM + HKDF pipeline already in use for SMTP passwords and SimpleFIN tokens. A new getOidcClientSecret() helper in oidcService.js decrypts on read (with a plaintext fallback for legacy values), and the write path calls encryptSecret() before setSetting. DB migration v0.79 encrypts any existing plaintext value on first startup — no manual action required. Env-var-sourced secrets (OIDC_CLIENT_SECRET) are unaffected and bypass the DB path entirely.

  • Admin user routes: integer validation on all ID paramsPUT /api/admin/users/:id/password, /role, /active, /username and DELETE /api/admin/users/:id previously accepted arbitrary strings as the user ID — some routes used raw req.params.id in SQL queries, others called Number() without verifying the result was a positive integer. A shared parseUserId() helper (parseInt + Number.isInteger + > 0) now gates all five handlers, returning 400 Invalid user ID immediately on any non-integer or non-positive input. Backup routes that take filename-style IDs are intentionally unchanged.

Features

  • Bill due notifications — push channels (ntfy / Gotify / Discord / Telegram) — The email notification system (runNotifications()) was already fully built and running in the daily worker at 6 AM. What was missing was push notification support. Four new channels are now wired in: ntfy (HTTP POST with priority/tags headers and optional Bearer auth), Gotify (JSON message with urgency-mapped priority 49), Discord (webhook embed with urgency-matched color and timestamp), and Telegram (Bot API sendMessage with Markdown). Urgency levels (upcoming, soon, today, overdue) map to channel-appropriate priorities and ntfy tags (bell, warning, rotating_light, red_circle). The early-exit guard in runNotifications() was loosened — notifications now fire if either SMTP or any user's push is configured. Each recipient can receive both email and push for the same bill. push_url and push_token are encrypted at rest with the existing AES-256-GCM service. DB migration v0.80 adds five columns to users: notify_push_enabled, push_channel, push_url, push_token, push_chat_id. The Profile page gains a "Push Notifications" card with a master toggle, pill-button channel picker, context-aware inputs (URL, token shown as "✓ saved" rather than pre-filled, Telegram-only chat ID field), and a "Send test" button that fires POST /api/notifications/test-push and surfaces the exact error if the channel is misconfigured.

  • Cash flow projection — A new CashFlowCard on the Calendar page answers "what will I have left after all my bills clear?" — distinct from the existing remaining balance which only reflects what's already been paid. The card shows two panels in the first half of the month (by period end, by month end) and collapses to one panel in the second half since the dates converge. Progress bars are amount-based ($420 of $650 paid) rather than count-based so high-value bills are weighted correctly. When any projection goes negative a prominent red alert banner appears with the shortfall amount and a prompt to review unpaid bills or adjust starting amounts. The "X unpaid →" count is a live link that opens the Tracker pre-filtered to exactly those bills for that period. On TrackerPage the Starting card hint now shows → $1,247 projected by Jun 14 when cashflow data is available, surfacing the projection without leaving the tracker view. When bank tracking is active the projection uses the live effective bank balance as its starting point. Backend: a cashflow block added to the trackerService response containing period and month projections, amount-paid totals, paid/total counts, and an end_label string for the period cutoff date.

  • Sync Bank button on Tracker — A "Sync Bank" button appears in the TrackerPage header toolbar when three conditions are all true: SimpleFIN Bridge is enabled (admin setting), the user has at least one connected SimpleFIN source, and the user has at least one bill merchant matching rule. Clicking it calls POST /api/data-sources/sync-all, which syncs every connected SimpleFIN source in sequence, aggregates the results, and after each source runs applyMerchantRules to auto-match new transactions. The button shows a spinning icon while syncing and toasts a specific result: "3 payments matched", "5 new transactions, no automatic matches", or "no new transactions". The tracker refetches automatically so newly matched payments appear without a page reload. GET /api/data-sources/simplefin/status was extended with two new fields — has_connections and has_merchant_rules — so the single status call drives the button's visibility with no additional requests.

  • Bill bank matching rules — Bills can now be linked to bank transaction patterns so payments import automatically without manual matching. A new "Bank matching rules" section in the Bill Modal (Transactions tab) shows all existing patterns for a bill as removable chips and lets the user add new ones by typing a merchant name or picking from a dropdown of recent unmatched transactions. As the user types, a live preview badge shows how many existing unmatched transactions the pattern would match (debounced, updates as-you-type). If the pattern is already claimed by another bill a conflict warning appears inline with the other bill's name, prompting the user to be more specific. On save the rule is applied retroactively — syncBillPaymentsFromSimplefin runs immediately and a green feedback banner reports how many historical payments were imported (e.g. "3 existing payments imported from your transaction history"). Bills with at least one active rule show a green Bank chip in the bill list with a tooltip. Four new endpoints: GET /api/bills/:id/merchant-rules (list rules + suggestions), GET /api/bills/:id/merchant-rules/preview?merchant=X (match count + conflict check), POST /api/bills/:id/merchant-rules (add + retroactive apply), DELETE /api/bills/:id/merchant-rules/:ruleId (remove).

  • SimpleFIN bank budget tracking — A new opt-in mode replaces manually-entered starting amounts with the live balance of a connected bank account. When enabled from the Bank Sync settings page (Data → Bank Sync → Bank Budget Tracking), the user selects which financial account to track and configures a pending payment window (07 days, default 3). Budget remaining is calculated as: bank balance pending payments unpaid bills this month. Bills already marked paid are not double-counted — the bank balance already reflects them. Payments made within the pending window appear in the tracker with an amber Pending badge, flagging that the bank may not have processed the debit yet. The CalendarPage Monthly Money Map switches to four live metrics (Balance / Pending / Unpaid Bills / After Bills) when bank mode is active. The TrackerPage starting-amounts card shows the account name and "live balance" hint; the manual-edit button is hidden since there is nothing to manually set. Implementation: three new user_settings keys (bank_tracking_enabled, bank_tracking_account_id, bank_tracking_pending_days), a new GET /api/data-sources/accounts/all endpoint for the account picker, buildBankTrackingSummary() in both summary.js and trackerService.js, and pending_cleared flag on tracker rows.

  • 404 page — Unknown routes previously silently redirected to / with no feedback. Replaced both catch-all routes (path="*" inside the auth layout and at the top level) with a dedicated NotFoundPage. The page is standalone (no sidebar), theme-aware, and works for authenticated and unauthenticated users alike. Design features: a glitch counter that cycles each digit of "404" through random numbers before snapping to value (staggered by 180 ms per digit), a CSS mesh gradient background with primary-color radial glows, a 48 px grid overlay faded with a radial mask, a gradient clip-text 404 that scales from 6rem to 14rem via clamp(), and smart CTAs — "Go back" only appears when browser history exists, and the home button adapts to auth state. The bad path is shown inline in a <code> tag so the user knows what they typed.

🐛 Fixed

  • Bills list query optimised and merchant rule index added — The issue requested pagination on GET /api/bills, GET /api/categories, GET /api/tracker, and GET /api/subscriptions. Pagination was not implemented — the entire UI (tracker buckets, snowball list, drag reorder, BillModal) depends on all bills being loaded at once, and a personal bill tracker with 2050 bills has no performance problem at this scale. The genuine fix was in two parts. First, GET /api/bills replaced a correlated EXISTS(SELECT 1 FROM bill_history_ranges WHERE bill_id = b.id) per bill row with a single LEFT JOIN (SELECT DISTINCT bill_id FROM bill_history_ranges) hr — same result, one scan instead of N subqueries. Second, DB migration v0.81 adds a composite index idx_bill_merchant_rules_user_bill ON bill_merchant_rules(user_id, bill_id) — the existing index only covered user_id, so the EXISTS check in GET /api/bills/:id and the snowball debt query had to filter by user then scan for bill_id; the composite index makes it a direct point lookup.

  • Imported payments use correct payment_source = 'file_import' — When issue #49 was fixed (imported payments not updating debt balance), payment_source was set to 'import' — a value not in the canonical VALID_PAYMENT_SOURCES set (manual, file_import, provider_sync). Corrected to 'file_import' in both INSERT paths in spreadsheetImportService.js. Payment history now shows the correct source for imported payments and the value round-trips correctly through the import/export and user DB import paths.

  • Mortgage and housing categories now auto-detected as debtDEBT_LIKE_CLAUSES in routes/snowball.js matched %credit%, %loan%, and %debt% category names but not %mortgage% or %housing%. A real user who created a bill under a "Mortgage" or "Housing" category would never see it on the Snowball page unless they manually toggled snowball_include. Demo data hid the bug because the seed bill has snowball_include pre-set. The frontend's mortgageIncluded warning in SnowballPage.jsx already checked for mortgage/housing in category and bill name — it just never fired because those bills were filtered out before reaching the page. Added both patterns to DEBT_LIKE_CLAUSES; the warning now works as intended, correctly flagging when a mortgage is present so users see the Ramsey Baby Step 2 note about excluding the house.

  • Imported payments now update debt balance — Every payment creation path except spreadsheet import correctly computed balance_delta and updated bills.current_balance. Imported payments were permanently orphaned from debt tracking: the snowball page balance stayed wrong after an Excel import, delete/restore was broken (the restore path checks balance_delta IS NULL and silently skips the reversal), and any debt-related reporting was incorrect. Three paths were fixed. In spreadsheetImportService.js createPaymentFromImport(): added computeBalanceDelta import, fetches the bill fresh on every call (critical for sequential month imports so each payment sees the post-previous-payment balance), includes balance_delta in the INSERT, updates bills.current_balance, and sets payment_source = 'import' (was null). The create_payment action path in the same file had the identical gap and received the same treatment. routes/matches.js manual transaction confirm also had no balance_delta or current_balance update — fixed with the same computeBalanceDelta call inside the existing transaction block.

  • Daily worker cycle range bugs for quarterly and annual billsdailyWorker.js had two bugs affecting non-monthly billing cycles. First, getCycleRange(year, month) was called without a bill argument, always producing the calendar-month range for payment lookups. A quarterly bill paid in January would be invisible to the autopay check in February and March because the worker only searched that calendar month — a payment that existed was treated as missing. Fixed by calling getCycleRange(year, month, bill) per-bill so quarterly bills look at their full 3-month window and annual bills look at the full year. Second, buildTrackerRow() returns null for bills whose cycle does not apply in the current month (quarterly/annual bills in non-due months), and the code immediately accessed row.due_date with no null check. JavaScript's && short-circuit masked the crash for non-autopay bills, but any autopay-enabled quarterly or annual bill in a non-applicable month would throw TypeError: Cannot read properties of null. Fixed with an early continue when getCycleRange returns null and a defensive guard after buildTrackerRow(). The issue description incorrectly stated that resolveDueDate() and getCycleRange() ignore cycle types — both functions already handle quarterly, annual, biweekly, and weekly correctly; the tracker already filters non-applicable bills via .filter(Boolean); no new scheduler was needed.

  • Client snowball projection replaced with server callcomputeLiveProjection() in SnowballPage.jsx was an 86-line client-side reimplementation of snowballService.js. A comment acknowledged the duplication but there was no mechanism to detect drift — a bug fix on the server would silently diverge from the client preview. The function has been deleted. GET /api/snowball/projection now accepts an optional ?extra=N query parameter that overrides the stored extra payment for that request without saving it, giving the client a way to preview an unsaved amount using the authoritative server simulation. The useMemo live projection is replaced with a 220 ms debounced useEffect that calls the endpoint; the existing projectionLoading state and loading indicator fire naturally. Drift between client and server projections is now mechanically impossible.

  • Snowball order PATCH validates all rows before writingPATCH /api/snowball/order previously iterated through the submitted array with a continue on invalid entries, silently skipping bad rows and always returning { success: true }. Any item with a non-integer or negative id/snowball_order now immediately returns 400 with the specific index and value that failed. The transaction only runs after all items pass validation. Response now includes updated count. Soft-deleted bills are also excluded from the UPDATE (deleted_at IS NULL), which simultaneously closes issue #53.

  • isRamseyMode() called once per requestgetDebtBills() previously hid the isRamseyMode() DB query inside itself. Routes that also needed the mode value (or called getDebtBills alongside other isRamseyMode calls) triggered multiple identical queries per request. getDebtBills now accepts an optional pre-fetched ramseyMode parameter; the GET /, GET /projection, and POST /plans routes call isRamseyMode once and pass the result in. PATCH /settings uses the body value directly when ramsey_mode was part of the request, falling back to a DB read only when it wasn't.

  • Month navigation brackets the month name — In TrackerPage the month navigation pill previously showed < Today > — the arrows flanked a "Today" button rather than the current month. The pill now shows < MAY 2026 > with the month and year as a static label between the arrows, and "Today" promoted to a standalone variant="outline" button beside the pill. In CalendarPage the pill already had the correct structure (< MONTH YEAR >) but min-w-40 px-3 (160 px minimum + 24 px of padding) made the label too wide, leaving the arrows visually disconnected from the text. Reduced to min-w-[8rem] px-1 so the arrows bracket the text tightly. Both labels gain tabular-nums (prevents width jitter on month change) and select-none (prevents accidental text selection when clicking arrows quickly).

Release Image

Doing my part

v0.35.1

🔧 Changed

  • Bump0.35.00.35.1

  • Roadmap pulls from Forgejo issues — The Admin Roadmap tab now fetches live open issues from the Forgejo repository instead of parsing FUTURE.md. Issues are grouped into the same priority lanes (CRITICALNICE TO HAVE) using priority:* labels. Each card shows the issue title (priority prefix stripped), a 2-line body preview, all label chips rendered in their actual Forgejo colors, creation time, comment count, and a click-through link to the issue. Results are cached server-side for 5 minutes; a ↻ refresh button bypasses the cache on demand. On fetch failure the last cached result is served with a stale indicator.

  • OIDC login error logging improvedIssuer.discover() failures previously produced a blank log line because the error was an AggregateError (empty .message, real causes in .errors[]). Both the /login and /callback handlers now log the full error, expand err.errors[] entries, and surface err.cause so network-level failures (e.g. ETIMEDOUT, ENOTFOUND) are visible in the server log.

Release Image

Doing my part

v0.35.0

🔧 Changed

  • Bump0.34.30.35.0

🔒 Security

  • TOKEN_ENCRYPTION_KEY deployment guidance — When TOKEN_ENCRYPTION_KEY is not set, encryptionService.js auto-generates a random key and persists it in the user_settings table — placing the key and the ciphertext in the same SQLite file. Anyone with database read access has everything needed to decrypt. The threat is bounded (filesystem access = game over regardless), but is now explicitly surfaced: a one-time console.warn fires on first use of the auto-generated key path, directing operators to set the env var. .env.example gains a TOKEN_ENCRYPTION_KEY section with a generation command and a plain-English explanation of the trade-off.

  • HKDF key derivation with automatic migrationencryptionService.js previously derived the AES-256-GCM key from raw input via SHA-256(ikm), which lacks domain separation and offers no protection if a low-entropy passphrase is supplied. Replaced with HKDF-SHA-256 (RFC 5869) using info label bill-tracker-token-encryption-v1. New ciphertext carries a v2: prefix; decryptSecret uses it to choose the correct derivation path, so legacy and new ciphertext coexist transparently. DB migration v0.78 re-encrypts all existing secrets (data_sources.encrypted_secret and notify_smtp_password) to the v2 format on first startup — no manual action required.

  • CSRF token moved out of readable cookie — The CSRF cookie previously defaulted to httpOnly: false so the SPA could read it from document.cookie. Any XSS vulnerability could steal the token from there and bypass CSRF protection entirely. The cookie is now httpOnly: true by default, removing it from the XSS-accessible cookie surface. The SPA instead fetches the token once from GET /api/auth/csrf-token on startup and stores it in a module-level memory cache; all mutations continue to send it in the x-csrf-token header unchanged. The server-side double-submit validation (header == cookie) is identical. CSRF_HTTP_ONLY=false remains available in .env for compatibility, but is no longer the default.

Release Image

Doing my part

v0.34.3

🔧 Changed

  • Bump0.34.2.10.34.3

  • TrackerPage refactored into focused componentsTrackerPage.jsx was a 2 386-line monolith containing ~13 co-located sub-components. Each has been extracted to its own file in client/components/tracker/:

    • FilterChip, StatusBadge, SummaryCards (TrendIndicator / SummaryCard / TrendCard) — visual primitives
    • EditableCell, PaymentProgress, LowerThisMonthButton, PaymentLedgerDialog, NotesCell — payment sub-components
    • AutopaySuggestionActions, TrackerRow, MobileTrackerRow — bill row (desktop and mobile)
    • TrackerBucket — bucket container
    • Helper functions and constants (rowEffectiveStatus, paymentSummary, ROW_STATUS_CLS, etc.) extracted to client/lib/trackerUtils.js
    • TrackerPage.jsx is now 477 lines (page layout + routing only)
  • TrackerPage filter/nav state is now URL-firstyear, month, search, and all 8 filter flags (autopay, firstBucket, fifteenthBucket, unpaid, overdue, debt, category, cycle) are stored in URL search params instead of local React state. Views are now bookmarkable, shareable, and survive back/forward navigation. Example: /tracker?year=2026&month=5&ov=1&un=1. The search param was previously partially URL-backed; this completes the pattern.


v0.34.2.1

🚀 Features

  • Overview quick-add bill — The Monthly Overview header now includes a plus-button shortcut that opens the existing Add Bill modal and refreshes the tracker after saving.

🔧 Changed

  • Bump0.34.20.34.2.1

🐛 Bug Fixes

  • Async error handling hardened — Five route handlers that called bcrypt.compare() or hashPassword() without a surrounding try/catch now return a clean 500 instead of leaving the promise rejection unhandled. Affected routes: POST /api/auth/change-password, POST /api/admin/users (auth router), POST /api/admin/users (admin router), PUT /api/admin/users/:id/password, POST /api/profile/change-password. A process.on('unhandledRejection') safety-net logger was also added to server.js. All other routes cited in the original bug report already had try/catch and required no changes.

  • SMTP password encrypted at restnotify_smtp_password was stored as plaintext in the settings table, exposing credentials in database backups or direct file access. It is now encrypted with AES-256-GCM via the existing encryptionService (same mechanism as SimpleFIN tokens). The route encrypts on save, the notification service decrypts on read with a legacy plaintext fallback, and migration v0.77 encrypts any existing plaintext password at startup. The masked •••••••• API response is unchanged.

  • User deletion now cleans up audit_log — The DELETE /api/admin/users/:id route was not explicitly deleting rows from audit_log (which has no ON DELETE CASCADE foreign key to users). Deleting a user left their audit trail orphaned in the database, referencing a non-existent user id. The route now explicitly deletes audit_log, import_sessions, and import_history rows for the user before removing the user row; the remaining ~20 user-owned tables are handled by ON DELETE CASCADE.

  • Payment mutations scoped to owner — The PUT, DELETE, and POST /:id/restore handlers in routes/payments.js performed their UPDATE payments and bill balance SELECT queries using only the payment id, without re-asserting ownership in the SQL itself. Ownership was already verified via a JOIN before each mutation (not exploitable in practice), but the SQL provided no independent protection. All three mutation statements now include AND bill_id IN (SELECT id FROM bills WHERE user_id = ? AND deleted_at IS NULL), and the bills balance re-fetch in DELETE and restore now includes AND user_id = ?. The response SELECT at the end of PUT and restore was also updated to use the ownership JOIN consistent with GET /api/payments/:id.

  • JSON body limit made explicitexpress.json() in server.js now declares { limit: '100kb' } explicitly rather than relying on Express's implicit default. No behaviour change; import routes continue to override this per-endpoint (2 MB 10 MB).

  • Silent .catch(() => {}) replaced with console logging — Twelve instances of empty catch handlers across the client codebase swallowed errors with no logging or user feedback: LoginPage.jsx (authMode/session pre-checks), useAuth.jsx (authMode check), SnowballPage.jsx (plan history load), SubscriptionsPage.jsx (bills load on mount and after reorder), BankSyncSection.jsx (bills load for match picker), ImportSpreadsheetSection.jsx (bills and categories load for import controls), TransactionMatchingSection.jsx (categories load), Layout.jsx (SimpleFIN status badge), and ReleaseNotesDialog.jsx (acknowledge-version fire-and-forget). All are background ambient data loads whose fallback silent state is correct for the user experience; they now log console.error('[ComponentName] context message', err) so failures are visible to developers without surfacing disruptive toasts.

  • Monetary aggregation rounding hardened — Floating-point rounding was already applied per-payment in statusService, billsService, and payments.js, but the aggregation layer was unprotected: reduce() sums and subtraction results in trackerService, routes/summary.js, and routes/monthly-starting-amounts.js were returned without rounding, allowing IEEE 754 artifacts (e.g. 12.10 - 0.20 = 12.100000000000001) to leak into API responses. All computed monetary aggregates (total_paid, total_expected, paid_toward_due, remaining, total_remaining, overdue, combined_amount, *_remaining, expense_total, result, etc.) are now passed through Math.round(x * 100) / 100 before being returned. roundMoney is also now exported from statusService so other modules can share the same implementation instead of re-implementing it.

Release Image

Doing my part

v0.34.2

🔧 Changed

  • Bump0.34.1.30.34.2

  • Subscription badge on Tracker — The "S" subscription badge now appears consistently on all bill rows in the Tracker page. The reorder-mode row variant was missing the badge even though it showed the "AP" autopay badge — both badges now render in all row contexts.

  • Data page workflow tabs — Data now groups tools into Sync & Match, Import Data, and Export & History tabs, with remembered collapsible cards and a compact status strip.


v0.34.1.3

🚀 Features

  • Reordering across management pages — Bills, Subscriptions, Categories, and Snowball now expose tracker-style drag/up/down controls. Bill-backed pages persist through sort_order; Categories adds its own persisted sort_order API.

  • Snowball readiness warning — Clicking "Start Snowball Plan" when any readiness checklist items are still incomplete now shows an AlertDialog listing the pending items and asking for confirmation before proceeding.

  • Payoff Simulator — all bills load — Simulator now loads from api.bills() (all active bills) instead of the snowball-only endpoint, so any bill with a current_balance appears in the dropdown regardless of category.

  • Payoff Simulator — Custom mode — Added a "Custom — not in Bill Tracker" option to the dropdown. Selecting it reveals Name (optional) and Balance (required) inputs, letting users simulate any loan or debt without creating a bill. Apply-to-budget and minimum-payment UI are hidden in custom mode.

  • Payoff Simulator — print — Added a Print button (top-right of page header) that triggers window.print(). Print styles isolate the simulator region, hide interactive controls, and inject a summary line showing the simulated parameters.

  • Summary bill ordering — Summary expenses now use the persisted bill order and support tracker-style drag/up/down reordering, while hiding reorder controls from printed/PDF summaries.

  • Unified bill schedule editing — Edit Bill now uses one canonical "Billing Schedule" field instead of separate Billing Cycle and Cycle Type controls.

🔧 Changed

  • Bump0.34.1.20.34.1.3

  • Payoff Simulator — subscriptions excluded — Added explicit !is_subscription guard to the bill filter so subscription bills never appear in the payoff dropdown even if a balance is accidentally set on one.

  • SimpleFIN sync window corrected — Hard limit updated from 90 → 45 days (actual SimpleFIN Bridge cap). Initial seed and backfill use 44 days (1-day buffer). Routine sync default remains 30 days. The admin sync_days setting was previously stored but never read — it now correctly drives routine auto-sync and manual "Sync Now" lookback.

  • Backups status badge fixed — System Status page Backups card previously showed "Enabled" (green) whenever backup_enabled was true, even with no schedule configured. Badge now reflects the scheduler state: "Scheduled" (green) when automatic backups are active, "Manual Only" (amber) when enabled but unscheduled, "Disabled" (amber) when off. Added Schedule and Next Backup rows to the card.

  • SimpleFIN admin card — sync explainer — "Transaction history" field replaced with two clearly labelled blocks: Initial connect & backfill (fixed 44 days, read-only) and Routine sync lookback (editable, 145 days, default 30). Amber warning appears when the routine value reaches the 45-day limit. Persistent info note keeps the hard limit visible at all times.

  • SimpleFIN account monitoring — Turning off tracking for an account now prevents new transaction ingestion for that account and excludes existing transactions from matching, merchant-rule sync, and subscription recommendation/search flows.

  • Snowball extra payment focus — The extra monthly budget input now uses a brighter, professional focus panel with the live monthly amount called out.

  • Snowball drag behavior — Snowball custom ordering now uses the same native drag/drop pattern and visual feedback as the Tracker page.

  • Scheduled backup retention — The database backup scheduler now starts with the server only when enabled in Admin settings and prunes only scheduled backups, keeping the configured default of 2 without deleting manual, imported, or pre-restore backups.

  • Billing schedule migration — Added migration v0.76 to backfill legacy billing-cycle values into cycle_type, normalize cycle_day, and derive the legacy billing_cycle value from the canonical schedule.

  • Subscription recommendations — Possible subscription matches now list the bank account that produced the matching transaction activity.


v0.34.1

🚀 Features

  • Persistent tracker bill ordering — Added sort_order on bills, PUT /api/bills/reorder, and tracker drag/up/down controls so bill order can be changed and remembered.

  • Bill archive endpoint — Added PUT /api/bills/:id/archived to hide or restore bills without deleting them.

  • Subscription catalog matching — Subscription recommendations now use the DB-backed subscription_catalog as a strong matching signal alongside the existing recurrence algorithm. Known services can surface as high-confidence recommendations, with catalog name/type/website carried into the Track flow.

  • Claude.ai catalog seed — Updated the known subscription catalog so Claude.ai/Anthropic transaction descriptors match the Claude Pro subscription entry.

  • Subscription transaction match search — Added /api/subscriptions/transaction-matches for the Subscriptions page. Bank transaction search now annotates known catalog hits, shows "Known: service" badges, and pre-fills new subscriptions from catalog metadata when available.

  • Payoff Simulator page — New /payoff route in sidebar. Select any debt from a dropdown; inputs auto-populate from bill rate, minimum, and expected amount (all editable). Live-updating custom SVG chart with 3 tracks: slate dashed (min-only), indigo dashed (snowball plan), amber solid (simulation). Stats cards show interest saved vs minimum, time saved, and total paid breakdown. "Apply to budget" pushes sim payment back to bill's expected amount with undo support.

  • Snowball plan lifecycle — Snowball page now supports committing to a plan. "Start Snowball Plan" button appears once ≥3 readiness items are checked. Active plan shows a collapsible emerald banner with pulsing status dot, per-debt progress bars, and on-track/ahead/behind indicators computed from the plan's initial snapshot vs. current balances. Actions: Pause · Resume · Complete · Abandon · New Plan (with AlertDialog confirmation).

  • Snowball plan history — Collapsible history panel at the bottom of the Snowball page lists all past plans (completed, abandoned, paused) with status badges, date ranges, and expandable debt snapshot tables showing starting balance, projected payoff, projected interest, and current balance with "Paid off ✓" on cleared debts.

  • snowball_plans table — Migration v0.73 adds persistent plan storage: status, method, extra_payment, started/paused/completed timestamps, and a JSON plan_snapshot of the initial projection and per-debt starting balances. 8 new API endpoints under /api/snowball/plans.

  • Price Change Insights panel — Tracker page now shows a collapsible amber panel when recurring bills have been paid at a different amount than expected for 2+ consecutive months. Per-bill "Update to $X.XX" action (with undo toast) and "Dismiss" (hidden for 30 days). TrendingUp/TrendingDown icons and teal palette for decreases.

  • Drift detection servicedriftService.getDriftReport() computes a rolling median of the last 3 months of payments per bill and compares it against expected_amount. Flags when |delta| ≥ $1 AND |drift%| ≥ threshold.

  • Price-change email digest — Daily worker now calls runDriftNotifications(), sending a single amber-styled digest email per user listing all bills with changed amounts (old → new, Δ%).

  • Drift snooze persistencedrift_snoozed_until column on bills (migration v0.71). POST /api/bills/:id/snooze-drift sets a 30-day snooze server-side.

  • "Notify on price changes" toggle — New notification preference in ProfilePage, backed by notify_amount_change column on users (migration v0.71).

  • Price change sensitivity setting — "Price change sensitivity" % input in SettingsPage Billing Behavior section. Stored as drift_threshold_pct in per-user settings (default 5%, range 125%).

🧹 Roadmap Audit

  • Audited all FUTURE.md items against current codebase:
    • Removed: Architecture: Business Logic Extraction (IS_IMPLEMENTED)
    • Removed: Debt Snowball Readiness Checklist (IS_IMPLEMENTED)
    • Updated status: Keyboard Navigation/Shortcuts → partial (Esc + Cmd+K done, arrow-key grid not)
    • Confirmed not implemented: Projected Cash Flow, Recurring Payment Rules (partial), Calendar Agenda, Filtered Exports, Payment Method Tracking, Unit Tests, Bill Grouping, Form State Management — all remain in FUTURE.md

🛠 Internal

  • Migration hardening — Made late snooze/drift migrations idempotent for fresh databases.
  • Subscription matching tests — Added coverage for known catalog recommendations and catalog-annotated subscription transaction search.

🔧 Changed

  • Bump0.34.00.34.1

v0.34.0

🚀 Features

  • Overdue Command Center — Tracker page now shows a collapsible command center for overdue bills. Per-bill Pay Now, Skip, and Snooze (1/3/7 days). Snoozed bills hidden with a count hint in the header.
  • Sidebar overdue badge — Tracker menu item shows live overdue count on desktop nav pill and mobile drawer.
  • Snooze persistencesnoozed_until column on monthly_bill_state (migration v0.70). Backend validates and persists snooze dates through PUT /bills/:id/monthly-state.
  • Overdue count endpointGET /api/tracker/overdue-count with client-side polling (2-min stale, 5-min poll, tab-only).
  • Instant bill search and filters — Bills and Tracker now include search inputs for quickly finding bills by name, category, notes, or amount. Bills adds category, billing-cycle, autopay, due-bucket, debt, and inactive filters; Tracker adds current-month category, billing-cycle, unpaid, overdue, autopay, due-bucket, and debt filters.
  • Command palette bill lookup — Added a global Ctrl+K command palette for finding bills by name, category, notes, due details, or amount, with quick jumps into Bills or the current Tracker search.
  • Partial payment tracking — Tracker now shows paid-versus-expected progress for each bill, supports multiple payments in the same month, and includes a payment ledger for adding, editing, and reviewing installment payments.
  • Tracker overpayment remaining math — Paying more than a bill's due amount no longer makes Tracker remaining/progress math look reversed. Overpayments now cap paid-toward-due calculations at the amount owed while still showing the extra amount as overpaid.

🔧 Changed

  • CleanupService — Uses BACKUP_DIR import from backupService directly. Added .xlsx export file sweep with 24h cutoff.
  • BackupSchedulercomputeNextRun now exported for external use.
  • Roadmap grid — Sizes from populated priority lanes only. With a single lane (e.g. only LOW items), lane uses full width instead of narrow 5-column slot.
  • Bump0.33.8.70.34.0

v0.33.8.7

🛠 Reverted

  • Compact tracker mode — Removed hasBoth side-by-side buckets, compact prop from Row/Bucket, 2xl:min-w-[700px] table override, 2xl:hidden on Last Month, and narrower Notes/Actions/Due column widths. Restored original single-column layout with min-w-[1120px], Due 10%, Actions 10%, Notes 23%.
  • Wider max-width at 2xl+ — Removed 2xl:max-w-[2000px] from Layout, Sidebar, AdminShell, footer, mobile nav. All back to max-w-[1500px].

Kept

  • "S" badge — Subscription badge shortened to "S" in all four locations.
  • Bucket remaining/Done header — Per-bucket remaining balance and Done label.
  • MkDocs directory added to .gitignore.

v0.33.8.6

🎨 Design

  • Wider max-width at 2xl+ — All layout containers (Sidebar, Layout, AdminShell, footer, mobile nav) now cap at 2000px at ≥1536px instead of 1500px. With lg:px-8, content area grows from ~1436px to ~1936px — each tracker bucket gets ~958px, enough for all 8 columns with Notes clearly readable.

v0.33.8.5

🎨 Design

  • "S" badge (compact) — Subscription badge shortened to "S" in all four locations (desktop tracker, mobile tracker, desktop bills table, mobile bills row), now with matching border style.
  • Tracker bucket side-by-side — When both buckets (1st14th, 15th31st) have bills, they render in a 2-column grid at 2xl+ instead of stacked.
  • Compact bucket modecompact prop narrows table min-width to 700px at 2xl+, hides Last Month column, shrinks Notes (23%→16%) and Actions (10%→8%) columns, Due trimmed (10%→9%).
  • Bucket header: remaining summary — Shows "Remaining" and "Done" labels alongside paid/total/overpaid in bucket headers.

🛠 Internal

  • Removed standalone Remaining summary card from the summary row (redundant with bucket header).
  • Row and Bucket components accept compact = false prop.

v0.33.8.4

🚀 Features

  • 90-day backfill button — New "90d Backfill" button per SimpleFIN connection pulls up to 89 days of transaction history. Available alongside Sync Now; both disable while either is in-flight.
  • Auto-seed on first connect — New connections automatically get an 89-day seed sync on first connect (vs 30-day routine syncs).

🐛 Bug Fixes

  • Status page error logicerrorRow query now adds AND status = 'error'. Erlist advisories stored in last_error on status = 'active' sources no longer show "Error" on the SimpleFIN Sync card.

🎨 Design

  • Status page redesign — Cards now have colored top borders matching their tone, icons in headers, organized into Infrastructure/Services/App Health/Software/Errors sections with section labels and dividers. Health banner with glowing dot indicator. Consistent padding, text sizing, and spacing throughout.

🛠 Internal

  • sinceEpoch() replaced with sinceEpochDays(days) — explicit parameter instead of reading config.
  • runSync() now accepts { days } option; detects first sync (89 days) vs routine (30 days).
  • New backfillDataSource() export — forces 89 days regardless.
  • New POST /api/data-sources/:id/backfill route.
  • New api.backfillDataSource(id) client method.

v0.33.8.3

🚀 Features

  • Subscription badge (indigo)Sub badge in all four locations (desktop tracker, mobile tracker, desktop bills table, mobile bills row), toggleable via Bills page display preferences.
  • SimpleFIN Sync status card — New card in Operations grid on Status page showing connections, accounts, last sync, next check, interval, and any errors.
  • Daily worker now startsdailyWorker.start() was never called; autopay marking, notifications, session pruning, and scheduled cleanup are now active.

🐛 Bug Fixes

  • Tracker overdue countoverdue_count now runs a real SQL query: active monthly bills where due_day < today, no payment this month, and not skipped.
  • Status page accuracy — Header subtitle reflects data.ok ("All systems operational" / "One or more systems need attention"). Application card shows "Degraded" in red when not ok. Worker status pill now checks last_error — a running worker with errors shows "Error" red instead of "Running" green. "Stopped" renamed to "Error" for accuracy.
  • SimpleFIN Recommendations title — Removed redundant "SimpleFIN" prefix from card title on Subscriptions page.

🛠 Internal

  • routes/status.jsbank_sync block returns config, worker state, DB aggregates.

v0.33.8.2

🐛 Bug Fixes

  • Georgia font bleed eliminated — Removed Georgia from --font-serif fallback stack (replaced with ui-serif). Georgia now has exactly one entry point: the GeorgiaDigits @font-face with unicode-range for digits and currency only.
  • --font-mono gets GeorgiaDigits — Added 'GeorgiaDigits' to the mono stack so the snowball extra payment input fields also get Georgia for digit characters.

v0.33.8.1

🚀 Features

  • Subscription catalog v2 — 90 new services across 16 new categories: AI (Suno, Midjourney, Grok, ElevenLabs, Character.ai, Runway, Windsurf, Leonardo.ai), home security (Ring, Nest, SimpliSafe, ADT, Arlo, Wyze, Abode), financial data (SimpleFIN Bridge, Tiller, Monarch, Empower), cloud backup (Backblaze, Carbonite, iDrive), email (Proton Mail, Fastmail, Superhuman, Hey), security/privacy (Bitwarden, Keeper, LastPass, Proton VPN, Mullvad, PIA, Proton Pass, SimpleLogin), identity protection, comics, genealogy, telehealth, website builders, learning platforms, project management, email marketing, cloud compute, media server, homelab/network, and productivity tools.
  • Category corrections — Discord Nitro (newssoftware), Twitch Turbo (newsstreaming), X Premium (newssoftware).
  • GeorgiaDigits font-face — Browser loads Georgia only for digit/currency codepoints via unicode-range. Letters continue with Inter/Roboto. Applied globally via --font-sans and .tracker-number.

🛠 Internal

  • Migration v0.69 — seeds SUBSCRIPTION_CATALOG_V2_ROWS, fixes 3 existing category bugs, skips duplicates.

v0.33.8.0

🚀 Features

  • Advisory non-bill filter system — New advisoryFilterService.js with lazy in-memory cache checks transaction titles against 5,000+ advisory patterns and 83 bill-like override terms. High-confidence matches suppress "Create Bill" in favor of "Probably not a bill · create anyway" text link. Medium confidence mutes the Create Bill button. Lazy-cached on first use, seeded on startup via migration v0.68.

🐛 Bug Fixes

  • BillModal onSave now returns saved billonSave callback receives the saved/updated bill object, enabling downstream actions like auto-matching a transaction after bill creation.
  • Transaction list includes advisory_filter — Each row returns advisory_filter: null | { confidence, category, rationale } from the server.

🛠 Internal

  • Migration v0.68 — seeds advisory_non_bill_filters and advisory_bill_like_overrides from docs/advisory_non_bill_transaction_filters_us_ms_5000.json. Idempotent (skips if already seeded).
  • New tables: advisory_non_bill_filters (pattern, confidence, category, rationale) and advisory_bill_like_overrides (override terms).

v0.33.7.3

🐛 Bug Fixes

  • SimpleFIN transaction table now uses fixed column sizing — Long transaction text no longer pushes action buttons off-screen. Action buttons are compact icon-only with aria-label/title for accessibility. Long matched bill names are truncated.

v0.33.7.2

🚀 Features

  • SimpleFIN payment backfill — The bill edit/subscription modal now shows a "Sync payments" button for bills that have a merchant rule stored (has_merchant_rule === 1). This covers both "Track from recommendation" and "Link to bill" flows. Clicking it calls POST /api/bills/:id/sync-simplefin-payments which scans unmatched negative transactions matching the bill's merchant rules and auto-creates payment_source = 'auto_match' records with the transaction's date and amount.

🐛 Bug Fixes

  • Subscription -> bill flow now creates payments — Both POST /api/subscriptions/recommendations/match-bill and the "Create subscription from recommendation" path now insert auto-match payment records alongside the transaction match update.

🛠 Internal

  • GET /api/bills/:id now returns has_merchant_rule boolean for conditional UI rendering.
  • New helper syncBillPaymentsFromSimplefin() in billMerchantRuleService.js handles merchant extraction, rule fallback from notes, transaction scanning, and payment creation.

v0.28.01

🏆 Major Features

  • Transaction foundation and CSV import — New data_sources, financial_accounts, and transactions tables provide the shared data layer for manual, file-import, and provider-sync workflows. CSV transaction import on the Data page offers upload → preview → column mapping → commit with SHA-256 dedupe and import-history logging.

🚀 Features

  • Manual bill payment history — The Bills edit/detail modal now shows a payment history ledger with paid date, amount, method, notes, and payment source.
  • Bills-side payment management — Users can add, edit, soft-delete, and restore manual payments from the bill modal using the existing payment APIs.
  • Payment source metadata — The existing payments table now carries payment_source and transaction_id metadata so manual records can become the canonical base for later import and sync work without adding a new payment table.
  • Transaction CSV import — The Data page now includes a transaction CSV importer with preview, column mapping, commit counts, duplicate skipping, and import-history logging into the shared transactions table.

🌟 Enhancements

  • Canonical payment ledger — Payment responses now include source metadata while preserving existing REAL-dollar payment amounts, partial-payment status derivation, and Tracker payment behavior.
  • Import controlsDATA_IMPORT_ENABLED=false now disables import preview/apply/commit endpoints, and CSV import is available through both /api/import/csv/* and /api/imports/csv/*.

Release Image

Doing my part

v0.28.0

🏆 Major Features

  • Current 0.28.0 update highlights — Replaced older release-card content with only the latest major v0.28.0 highlights so the popup focuses on the current payment/settings, privacy/login, security, release-note, roadmap, and surface-polish changes.
  • Recoverable deletes for financial records — Bill and category deletes now use confirmation dialogs, move records into a 30-day recovery window instead of immediately destroying history, and show undo actions from the success toast. Payment removal now also requires confirmation and uses the existing soft-delete/restore path with undo.

🚀 Features

  • Bill Health page — Added /health with a backend /api/bills/audit check that flags incomplete bill setups, including missing due days, missing categories, debt bills without minimum payments, autopay bills without reference details, and debt balances without APRs.
  • Login device details — Login history now stores parsed browser, OS, device type, and a short privacy-preserving device fingerprint hash derived from user-agent plus coarse IP prefix. The Profile login history dialog shows the richer device details to help users recognize their own sign-ins.
  • Profile last-login card — The Profile summary now shows the most recent login date, IP address, device type, browser, and OS inline. Clicking the modern last-login card opens the full login history modal with device IDs and recent sign-in details.
  • Public privacy page — Added public /api/privacy and /privacy pages with a modern, scan-friendly policy layout, plus an About page Privacy button. The Profile login-history modal now notes that login device details are shown only to the user and are not shared with admins in the app UI.
  • Release notes image support — Release notes now render Markdown image blocks in a centered responsive frame, and the in-app release notes dialog displays the doingmypart.jpg release image at the end of the v0.28.0 entry.
  • Backend-controlled update card reset — The “Whats new” card now relies only on the backend last_seen_version check. /api/auth/me exposes the active release_notes_version, acknowledging an update stores that version, and any future package-version update will automatically show the card again for users who have not seen it.
  • Instant bill search and filters — Bills and Tracker now include search inputs for quickly finding bills by name, category, notes, or amount. Bills adds category, billing-cycle, autopay, due-bucket, debt, and inactive filters; Tracker adds current-month category, billing-cycle, unpaid, overdue, autopay, due-bucket, and debt filters.
  • Command palette bill lookup — Added a global Ctrl+K command palette for finding bills by name, category, notes, due details, or amount, with quick jumps into Bills or the current Tracker search.
  • Partial payment tracking — Tracker now shows paid-versus-expected progress for each bill, supports multiple payments in the same month, and includes a payment ledger for adding, editing, and reviewing installment payments.
  • One-click lower monthly bill handling — Tracker now shows a Bill was lower shortcut when a payment is below the expected amount. Clicking it sets that months actual amount to the paid total, marking the bill settled for the month without editing the full bill or opening the monthly state dialog.
  • Autopay suggestions and auto-mark paid — Autopay-heavy users can now reduce manual tracking for bills they do not normally think about. Autopay/autodraft bills with autodraft_status = assumed_paid create due-date payment suggestions on Tracker, users can confirm or dismiss each suggestion, and dismissed suggestions are remembered per bill/month. Bills can also opt into the new auto_mark_paid setting to automatically record the expected payment on the due date via the Tracker due-date check. Suggested autopay actions now use a compact modern pill with confirm/dismiss icon controls on desktop and mobile.
  • Bill duplication and templates — Bills can now be duplicated from row actions or the edit modal, created from built-in Utility, Credit Card, Subscription, and Loan templates, and saved as reusable user templates for later bill setup.

🌟 Enhancements

  • Richer demo debt seed — Demo data now includes named credit-card debts such as Discover It, Capital One Quicksilver, and Chase Freedom with balances, APRs, minimum payments, and Snowball inclusion/order already populated.
  • Cleaner category defaults — Default category seeding now includes Food, Beauty, Entertainment, and Pets, and the Categories page hides empty categories by default with a quick inline show-empty toggle.
  • Debt Snowball Ramsey Mode — Snowball now defaults to a strict Dave Ramsey flow: smallest balance first, no drag reordering while Ramsey Mode is on, and mortgage/housing categories no longer auto-enter Baby Step 2 unless manually included. Turning Ramsey Mode off restores custom drag ordering and saved order behavior.
  • Snowball guardrails — The Snowball page now highlights the next win, shows the current attack amount, warns when Custom Order drifts from smallest-balance-first, calls out debts that have a balance but no minimum payment, and includes an inline Ramsey readiness checklist for current bills, starter emergency fund, consumer debts, minimum payments, and extra snowball budget.
  • Roadmap responsiveness — Roadmap lanes now use a five-column layout only on very wide screens, a balanced three-column layout on normal desktop/admin-shell widths, two columns on tablets, and a single column on mobile. Long titles, notes, and file names now wrap or scroll instead of compressing cards.
  • Calmer app surfaces — Darkened the top navigation, shared cards, table surfaces, and page gradients slightly so the interface feels less glowy while keeping the same design language across app, admin, About, Privacy, and Release Notes pages.
  • Tracker period balance — The Tracker summary no longer shows one generic remaining balance. Before the 15th it shows the 1st balance; on and after the 15th it shows the 15th balance, using the matching starting amount and paid total for that period.
  • Calendar money map redesign — Calendar now starts with a monthly money map showing available money, extra income, assigned bills, and after-bills remaining so users coming from budgeting apps can understand the month without hunting across pages. The 1st and 15th available-money amounts are marked directly on the calendar, day details show available-money context, and the sidebar includes a compact Snowball payoff glance with a link to the full Snowball page.

🐛 Bug Fixes

  • Payment input validation — Centralized payment validation now requires positive finite amounts and real YYYY-MM-DD payment dates. Manual payment creation, quick pay, bulk payment creation, payment edits, and bill toggle-paid all reject malformed dates, impossible calendar dates, zero/negative amounts, Infinity, NaN, and partial numeric strings before data reaches SQLite.
  • Tracker overpayment remaining math — Paying more than a bills due amount no longer makes Tracker remaining/progress math look reversed. Overpayments now cap paid-toward-due calculations at the amount owed while still showing the extra amount as overpaid.
  • Recurring autopay status preservation — Confirming suggested autopay payments, quick-paying, or manually recording payments no longer changes bill-level assumed_paid autodraft settings to one-time confirmed, so autopay suggestions and auto-marking continue working in future months.
  • Cycle-aware tracker recurrence — Tracker due dates now respect stored cycle_type and cycle_day values. Weekly and biweekly bills use their selected weekday, quarterly and annual bills only appear in their assigned months, and cycle-aware payment ranges prevent non-monthly bills from being treated as ordinary monthly bills.
  • User-scoped settings/api/settings now stores display and billing preferences in a user_settings table keyed by user_id instead of writing shared global settings. Existing global values are migrated into per-user rows, and tracker/calendar status calculations now use each user's own grace_period_days.
  • CSRF logout-all protection — Removed the unnecessary /api/auth/logout-all CSRF bypass. Logout-all now requires the SPA's normal x-csrf-token header like other authenticated state-changing requests.
  • Password validation alignment — Admin user creation, admin password reset, add-user validation, and first-login password-change validation now require 8 characters in the frontend to match backend password rules.
  • CSRF SPA documentation — Updated CSRF comments and docs to state that this SPA intentionally keeps the CSRF token cookie readable for the double-submit header flow. CSRF_HTTP_ONLY=true is no longer recommended unless token delivery changes.
  • API and tooling cleanup — Removed the duplicate updateBillSnowball client API key. The Vite config now uses an .mjs module file, and npm run check validates backend CommonJS files with node --check while using the Vite build for frontend ESM/JSX.
  • Bills-tab import duplicates — The /data XLSX Bills tab now tracks which matched rows were already handled during the current preview, imports only the remaining rows on repeat clicks, and changes the button to All imported when nothing is left. Matching the same bill/month/amount again is now counted as a skipped duplicate instead of a fresh update.
  • Roadmap route and layout — Added the admin-protected /roadmap route, updated navigation to use it, widened the admin shell, and tightened roadmap breakpoints so priority lanes display cleanly on desktop, tablet, and mobile without runtime matchMedia issues.
  • Bill template hardening — Saved bill templates are now validated before storage, bad template JSON can no longer break the templates endpoint, and duplicated/template-created bills gracefully fall back when a saved category is no longer available.

Release Image

Doing my part

v0.27.04

Added

  • Bills page redesign — Replaced the dense HTML table with a modern card-based layout. Each bill shows name, category badge, autopay/2FA indicators, due day, billing cycle, APR (colour-coded: amber ≥15%, red ≥25%), and current balance inline. Icon action buttons (edit, deactivate/activate, history, delete) are always visible.
  • Column visibility — A "Columns" button in the Bills header opens a persistent display-options panel. Each of the nine fields (category, due day, amount, billing cycle, APR, balance, min payment, autopay badge, 2FA badge) can be individually toggled. Selections are saved to localStorage and restored on every visit.
  • Login history — Every successful sign-in (local and OIDC) is recorded with timestamp, IP address, and browser/OS. Only the last 3 logins per user are kept. The Last Login field on the Profile page is now a clickable link that opens a modal showing the full history with device icons and confidence indicators.
  • Import by bill — The XLSX import page now has a Bills tab alongside the existing Rows tab. It lists every existing bill that has matching rows in the uploaded file, with match counts and a date/amount preview. Clicking Import N immediately applies all rows for that bill — no row-by-row review required.
  • APR projection engineaprService.js provides pure-math utilities: monthlyInterest, monthsToPayoff, totalInterestPaid, amortizationSchedule, and calculateMinimumOnly. The snowball projection now returns three methods (snowball, avalanche, minimum-only) with a comparison block showing months saved and interest saved vs paying minimums only. Each debt result includes an apr_snapshot (monthly interest, principal per payment, interest % of minimum). GET /api/bills/:id/amortization returns a full month-by-month schedule.
  • Live snowball projection panel — The entire projection sidebar (not just the attack card) now updates instantly as you type the extra monthly budget, with no network round-trip.
  • Drag visual feedback — Snowball cards now visually lift off the surface when grabbed (scale up, deep shadow, primary ring). The landing slot dims and shows a ring simultaneously.
  • Authentik logo — The OIDC login button shows the Authentik brand icon when the provider name contains "authentik".
  • About page improvements — Update status (up to date / update available) is now shown to all users, not just admins. The Sign In button is hidden when already signed in. The Back button navigates to the app when authenticated.

Fixed

  • Duplicate route — Removed a duplicate PATCH /api/bills/:id/snowball handler that would have silently overwritten both fields instead of doing a partial update.
  • jsconfig deprecation — Added "ignoreDeprecations": "6.0" to suppress the TypeScript baseUrl deprecation warning.
  • Roadmap page — Chevron icons now correctly rotate when a card is expanded (group-data-[state=open]:rotate-180 replaces the broken group-aria-expanded variant). "Expand/Collapse All" now works by remounting cards with the new default state via a forceKey. The laneForPriority normalisation no longer misclassifies "nice to have" items as "low" priority. The dev log tab no longer leaks stale cancellation flags. About page version now shows immediately from the Vite-injected APP_VERSION constant with a proper loading state for the update check (spinner → Up to date / Update available / Could not check).

v0.27.03

Added

  • APR calculation service — New aprService.js with pure math utilities: monthlyInterest, monthsToPayoff, totalInterestPaid, amortizationSchedule, calculateMinimumOnly, and debtAprSnapshot.
  • Minimum-only projectionGET /api/snowball/projection now returns a third method (minimum_only) showing what happens if each debt is paid independently at its minimum with no snowball rolling. This is the "do nothing extra" baseline.
  • Snowball comparison block — Projection response includes a comparison object with months_saved, years_saved, and interest_saved vs the minimum-only baseline — the headline motivation numbers for the Dave Ramsey method.
  • Per-debt APR snapshot — Every debt in all three projection methods now includes an apr_snapshot block: monthly_interest (dollars accruing this month), principal_per_min_pmt (principal reduction from the minimum payment), interest_pct_of_min (% of minimum that goes to interest), and annual_interest_estimate.
  • Bill amortization endpointGET /api/bills/:id/amortization returns a full month-by-month schedule { month, payment, principal, interest, balance } for a single debt. Optional ?payment=X models higher payments; ?max_months=N caps the response length.

v0.27.02

Added

  • Debt Snowball page — New page built around Dave Ramsey's debt snowball method. Drag-and-drop card ordering via pointer events (touch and mouse), auto-arrange by smallest balance, and a sticky projection sidebar showing per-debt payoff dates and overall debt-free date.
  • Avalanche comparison — Projection sidebar shows the snowball result alongside the avalanche method (highest rate first), including interest saved and months faster or slower.
  • Live payoff preview — Typing the extra monthly budget updates all debt payoff dates and the debt-free date instantly client-side with no network round-trip.
  • Debt Details on Bills — Edit Bill modal has a collapsible "Debt / Credit Details" section with Current Balance (inline-editable on the Snowball page), Minimum Payment, and APR. Bills in Credit Cards, Loans, Mortgage, or Housing categories are auto-detected.
  • Payment → balance sync — Recording a payment on a debt bill reduces its current balance by the principal portion (payment minus one month of accrued interest). Un-marking, deleting, or restoring a payment reverses or re-applies the change exactly.
  • APR colour coding — Interest rates ≥ 15% shown amber, ≥ 25% shown red on the Snowball page.
  • Update check — Backend fetches the latest release from the Forgejo repository (cached 1 hour). Admin status page shows current vs. latest version with a force-refresh "Check Now" button. About page shows update status for all users.
  • Release notes notificationusers.last_seen_version tracks which version each user last acknowledged. First login after an update opens the "What's new" dialog; dismissing it calls POST /auth/acknowledge-version.
  • Snowball exempt — Bills in debt categories can be individually hidden from the Snowball page using the toggle in Edit Bill.

Changed

  • Version source of truthpackage.json is the single version source. Vite injects it into the client bundle at build time; GET /api/version always returns pkg.version regardless of HISTORY.md state.
  • About page — Shows update status for all users. Sign In button hidden when already authenticated. Back button returns to the app when logged in.

Fixed

  • GET /api/version version field — Previously read from HISTORY.md header; now always returns the running package.json version.

v0.26.0

Added

  • Dual-column XLSX import — Spreadsheets with two side-by-side bill tables (bills due ~1st and ~15th) are now both imported. Left half defaults due_day to 1, right half defaults to 15.
  • Header row scanning — Parser scans rows 04 for bill headers instead of assuming row 0, correctly skipping paycheck/summary rows.
  • Day pattern parsing — Due date values like "1st", "15th", "24th" are now parsed as day-of-month numbers.
  • Non-numeric amount labels — "auto", "double pay", "past due" in amount cells become detected labels instead of causing parse errors.

Changed

  • Cell type validation — Allow 's' (shared formula) cell type in XLSX parsing, fixing import failures on some spreadsheet formats.

Security

  • Audit by Private_Hudson — Bounds validation in isBlankRowForHeaderSet, anchored regex for day patterns, label sanitization verified. All checks PASS.

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 brokenhandleTogglePaid() 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 scopedpasswordLimiter 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 alignedCSRF_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 validationGET /api/tracker/upcoming now clamps days to 1365 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 HooksuseTracker(), 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-categoriescycle_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 accessibilityaria-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 validationbill_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 BoundariesErrorBoundary 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 VariablesINIT_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.42bill_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 limitingadminActionLimiter (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 preventionrehype-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-adminadminActionLimiter (30 req/15min per IP) applied to prevent brute-force attempts
  • XSS preventionrehype-sanitize added to ReactMarkdown component in AboutPage.jsx
  • Content redactionroutes/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 months 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 OneCapital One and Discover AustinAustin 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
  • "1st14th" 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 20002100, month 112); 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 (20002100); 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: 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

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.