From 82de135186cf1dc9772b2672b3778d07ee288291 Mon Sep 17 00:00:00 2001 From: null Date: Mon, 18 May 2026 09:44:16 -0500 Subject: [PATCH] push --- client/components/BillModal.jsx | 36 ++- client/pages/CalendarPage.jsx | 129 ++++++-- client/pages/DataPage.jsx | 177 +++++++++- docs/Engineering_Reference_Manual.md | 465 +++++++++++++++------------ package.json | 2 +- 5 files changed, 568 insertions(+), 241 deletions(-) diff --git a/client/components/BillModal.jsx b/client/components/BillModal.jsx index a275078..3b2e1f8 100644 --- a/client/components/BillModal.jsx +++ b/client/components/BillModal.jsx @@ -67,6 +67,26 @@ function isTransactionLinkedPayment(payment) { return payment?.payment_source === 'transaction_match' || payment?.transaction_id != null; } +function paymentSourceLabel(source) { + const labels = { + manual: 'Manual', + file_import: 'File import', + provider_sync: 'Sync', + transaction_match: 'Transaction', + }; + return labels[source] || source || 'Manual'; +} + +function paymentSourceTone(source) { + const tones = { + manual: 'border-emerald-500/25 bg-emerald-500/10 text-emerald-600 dark:text-emerald-400', + file_import: 'border-sky-500/25 bg-sky-500/10 text-sky-600 dark:text-sky-400', + provider_sync: 'border-violet-500/25 bg-violet-500/10 text-violet-600 dark:text-violet-400', + transaction_match: 'border-primary/25 bg-primary/10 text-primary', + }; + return tones[source] || tones.manual; +} + function isDebtCat(categories, catId) { if (!catId || catId === CAT_NONE) return false; const cat = categories.find(c => String(c.id) === catId); @@ -886,8 +906,11 @@ export default function BillModal({ bill, initialBill, categories, onClose, onSa

{fmt(payment.amount)}

- - {payment.payment_source || 'manual'} + + {paymentSourceLabel(payment.payment_source)}

@@ -926,9 +949,14 @@ export default function BillModal({ bill, initialBill, categories, onClose, onSa

Linked transactions

{linkedTransactions.length} confirmed matches

- + 0 + ? 'border-primary/20 bg-primary/5 text-primary' + : 'border-border/60 bg-muted/30 text-muted-foreground', + )}> - Matched + {linkedTransactions.length} diff --git a/client/pages/CalendarPage.jsx b/client/pages/CalendarPage.jsx index c555b99..c9e1e04 100644 --- a/client/pages/CalendarPage.jsx +++ b/client/pages/CalendarPage.jsx @@ -8,7 +8,9 @@ import { CircleDollarSign, PiggyBank, RefreshCw, + Target, TrendingDown, + Trophy, WalletCards, } from 'lucide-react'; import { toast } from 'sonner'; @@ -297,50 +299,117 @@ function CalendarGrid({ data, selectedDate, onSelectDay, moneyMarkers }) { function DebtPayoffGlance({ projection }) { const snowball = projection?.snowball; const comparison = projection?.comparison; - const nextDebt = snowball?.debts?.find(debt => Number(debt.balance) > 0) || snowball?.debts?.[0]; + const targetDebt = snowball?.debts?.[0] || null; + const targetMonths = Number(targetDebt?.months || 0); + const monthsSaved = comparison?.months_saved; return ( - + -
- - Debt Payoff +
+
+ + Snowball Target +
+ + Focus +
- Quick snowball projection. Full controls stay on Snowball. + Current payoff focus, with the final debt-free date close by. {snowball?.months_to_freedom ? (
-
-

Projected payoff

-

{snowball.payoff_display}

-

{snowball.months_to_freedom} months remaining

-
-
-
-

Interest

-

{fmt(snowball.total_interest_paid)}

+ {targetDebt && ( +
+
+ + + +
+

+ Target debt +

+

{targetDebt.name}

+

+ Clears {targetDebt.payoff_display || 'on the current plan'} +

+
+
+ +
+
+

Target runway

+

+ {targetMonths ? `${targetMonths} mo` : '—'} +

+
+
+

Debt-free

+

{snowball.payoff_display}

+
+
-
-

Saved

-

{comparison ? `${comparison.months_saved} mo` : '—'}

-
-
- {nextDebt && ( -

- Next focus: {nextDebt.name} -

)} -
) : ( -
-

- Add debt balances and minimum payments to see a payoff date here. -

-
diff --git a/client/pages/DataPage.jsx b/client/pages/DataPage.jsx index db69f1c..7e43ff8 100644 --- a/client/pages/DataPage.jsx +++ b/client/pages/DataPage.jsx @@ -387,6 +387,123 @@ function transactionTitle(tx) { return tx?.payee || tx?.description || tx?.memo || 'Transaction'; } +function matchScoreTone(score) { + const value = Number(score) || 0; + if (value >= 80) return 'border-emerald-500/30 bg-emerald-500/10 text-emerald-600 dark:text-emerald-400'; + if (value >= 55) return 'border-sky-500/30 bg-sky-500/10 text-sky-600 dark:text-sky-400'; + return 'border-amber-500/30 bg-amber-500/10 text-amber-600 dark:text-amber-400'; +} + +function SuggestedMatchesPanel({ suggestions, loading, actionId, onAccept, onReject }) { + return ( +
+
+
+ + + +
+

Suggested matches

+

{loading ? 'Checking transactions' : `${suggestions.length} ready for review`}

+
+
+
+ + {loading ? ( +
+ + Finding likely bill matches... +
+ ) : suggestions.length === 0 ? ( +
+ No suggested matches right now. +
+ ) : ( +
+ {suggestions.map(suggestion => { + const tx = suggestion.transaction || {}; + const bill = suggestion.bill || {}; + const acceptBusy = actionId === `suggestion-match:${suggestion.id}`; + const rejectBusy = actionId === `suggestion-reject:${suggestion.id}`; + const busy = acceptBusy || rejectBusy; + + return ( +
+
+
+
+ + {suggestion.score} + +

{transactionTitle(tx)}

+
+

+ {transactionDate(tx)} · {tx.source_label || tx.source_type_label || 'Transaction'} +

+
+

+ {formatTransactionAmount(tx.amount, tx.currency)} +

+
+ +
+ +
+

{bill.name || `Bill ${suggestion.billId}`}

+

+ Expected ${Number(bill.expected_amount || 0).toFixed(2)} +

+
+
+ + {suggestion.reasons?.length > 0 && ( +
+ {suggestion.reasons.slice(0, 4).map(reason => ( + + {reason} + + ))} +
+ )} + +
+ + +
+
+ ); + })} +
+ )} +
+ ); +} + function MatchBillDialog({ open, onOpenChange, transaction, bills, onConfirm, loading }) { const [query, setQuery] = useState(''); const [selectedBillId, setSelectedBillId] = useState(''); @@ -504,9 +621,11 @@ function MatchBillDialog({ open, onOpenChange, transaction, bills, onConfirm, lo function TransactionMatchingSection({ refreshKey }) { const [transactions, setTransactions] = useState([]); + const [suggestions, setSuggestions] = useState([]); const [bills, setBills] = useState([]); const [filter, setFilter] = useState('open'); const [loading, setLoading] = useState(true); + const [suggestionsLoading, setSuggestionsLoading] = useState(true); const [billsLoading, setBillsLoading] = useState(true); const [actionId, setActionId] = useState(null); const [matchOpen, setMatchOpen] = useState(false); @@ -527,6 +646,23 @@ function TransactionMatchingSection({ refreshKey }) { } }; + const loadSuggestions = async () => { + setSuggestionsLoading(true); + try { + const data = await api.matchSuggestions({ limit: 8 }); + setSuggestions(data || []); + } catch (err) { + toast.error(err.message || 'Failed to load match suggestions.'); + setSuggestions([]); + } finally { + setSuggestionsLoading(false); + } + }; + + const refreshTransactionWorkbench = async () => { + await Promise.all([loadTransactions(), loadSuggestions()]); + }; + const loadBills = async () => { setBillsLoading(true); try { @@ -541,6 +677,7 @@ function TransactionMatchingSection({ refreshKey }) { useEffect(() => { loadBills(); }, []); useEffect(() => { loadTransactions(); }, [filter, refreshKey]); + useEffect(() => { loadSuggestions(); }, [refreshKey]); const openMatchDialog = (tx) => { setMatchTransaction(tx); @@ -561,7 +698,7 @@ function TransactionMatchingSection({ refreshKey }) { await api.unignoreTransaction(tx.id); toast.success('Transaction restored.'); } - await loadTransactions(); + await refreshTransactionWorkbench(); } catch (err) { toast.error(err.message || 'Transaction action failed.'); } finally { @@ -577,7 +714,7 @@ function TransactionMatchingSection({ refreshKey }) { toast.success('Transaction matched to bill.'); setMatchOpen(false); setMatchTransaction(null); - await loadTransactions(); + await refreshTransactionWorkbench(); } catch (err) { toast.error(err.message || 'Transaction match failed.'); } finally { @@ -585,6 +722,32 @@ function TransactionMatchingSection({ refreshKey }) { } }; + const acceptSuggestion = async (suggestion) => { + setActionId(`suggestion-match:${suggestion.id}`); + try { + await api.matchTransaction(suggestion.transactionId, suggestion.billId); + toast.success('Suggested match confirmed.'); + await refreshTransactionWorkbench(); + } catch (err) { + toast.error(err.message || 'Suggested match failed.'); + } finally { + setActionId(null); + } + }; + + const rejectSuggestion = async (suggestion) => { + setActionId(`suggestion-reject:${suggestion.id}`); + try { + await api.rejectMatchSuggestion(suggestion.id); + toast.success('Suggestion rejected.'); + await loadSuggestions(); + } catch (err) { + toast.error(err.message || 'Suggestion could not be rejected.'); + } finally { + setActionId(null); + } + }; + return ( ))}
-
+ +
{loading ? (
Loading transactions…
diff --git a/docs/Engineering_Reference_Manual.md b/docs/Engineering_Reference_Manual.md index 808b738..f4a4271 100644 --- a/docs/Engineering_Reference_Manual.md +++ b/docs/Engineering_Reference_Manual.md @@ -2,7 +2,7 @@ **Status:** Current code reference **Last Updated:** 2026-05-16 -**Version:** 0.28.01 +**Version:** 0.28.1 **Primary stack:** Node.js + Express, React + Vite, Tailwind CSS + shadcn/ui, Sonner, SQLite via `better-sqlite3` This manual reflects the current application code in `server.js`, `routes/`, `services/`, `middleware/`, `db/`, `client/`, `package.json`, `Dockerfile`, and `docker-compose.yml`. It is written as a current-state reference, not a changelog. @@ -33,8 +33,24 @@ Runtime flow: ## 2. Project Layout - `server.js` — Express entry point and route mounting. -- `routes/` — HTTP API handlers. -- `services/` — auth, OIDC, backup, cleanup, notification, import, status, audit, transaction, CSV import business logic. +- `routes/` — HTTP API handlers: + - `auth.js` — login, logout, password change, OIDC callback. + - `bills.js` — bills CRUD, auto-mark paid, history. + - `payments.js` — payments CRUD, status matching, snowball handling. + - `categories.js` — category CRUD, tree support. + - `tracker.js` — monthly tracker data, bucket resolution, cycle handling. + - `summary.js` — summary stats, starting amounts. + - `analytics.js` — expense reports, category breakdown. + - `settings.js` — user settings, admin config, notification settings. + - `notifications.js` — notification management. + - `profile.js` — user profile, demo data. + - `import.js` — CSV, Excel, user-SQLite import workflows. + - `export.js` — CSV, Excel, SQLite export. + - `status.js` — system status, health. + - `dataSources.js` — new — data sources CRUD with sync status. + - `transactions.js` — new — transaction CRUD, match/ignore/commit actions. + - `matches.js` — new — match suggestions, rejection tracking. +- `services/` — business logic modules. - `middleware/` — auth guards, CSRF, rate limits, security headers, error formatting. - `db/schema.sql` — base SQLite schema. - `db/database.js` — DB connection, migrations, defaults, settings, rollback support. @@ -779,6 +795,9 @@ Mounted under `/api/import`; auth: user/admin tracker access; import limiter app - Body: `{import_session_id, mapping, options?}`. - Response: `{imported, skipped, failed, details}`. +- `GET /import/history` + - Response: current user's import history. + ### 5.18 Match Suggestions Mounted under `/api/matches`; auth: user/admin tracker access. @@ -827,9 +846,154 @@ Mounted under `/api/export`; auth: user/admin tracker access; export limiter app - Public. - Response: package version and raw history text, or error if unavailable. ---- +### 5.22 Services -## 6. Database Reference +Key service modules: + +- **`paymentValidation.js`** — Payment input validation with `PAYMENT_SOURCES = ['manual', 'file_import', 'provider_sync', 'transaction_match']` enum, `validatePaymentSource()`, and `validatePaymentInput()`. + +- **`csvTransactionImportService.js`** — CSV parsing, field mapping, SHA-256 deduplication, import session management with preview/commit workflow. + +- **`transactionService.js`** — Transaction helpers: `ensureManualDataSource()`, `decorateDataSource()`, `decorateTransaction()`. + +- **`transactionMatchService.js`** — Match/unmatch transactions to bills: `matchTransactionToBill()`, `unmatchTransaction()`, `ignoreTransaction()`, `unignoreTransaction()`. + +- **`matchSuggestionService.js`** — Match suggestion discovery: `listMatchSuggestions()`, `rejectMatchSuggestion()`, `suggestionCounts()`. + +- **`snowballService.js`** — Debt snowball/avalanche calculations, Ramsey mode support, order updates. + +- **`dataSourcesService.js`** — Data source CRUD with `ensureManualDataSource()` for user-scoped manual sources. + +- **`monthlyStartingAmountsService.js`** — Starting cash bucket tracking: first/fifteenth/other bucket amounts, payments, remaining values. + +- **`auditService.js`** — Audit logging via `logAudit()`; lazy-loaded in `database.js` to avoid circular dependency. + +- **`emailService.js`** — Email dispatch with SMTP configuration, templating, retry logic. + +- **`exportService.js`** — Export helpers: CSV, XLSX, SQLite user DB export with metadata. + +- **`csvTransactionImportService.js`** — CSV parsing, field mapping, SHA-256 deduplication, import session management with preview/commit workflow. + +- **`trackerService.js`** — Tracker calculations, cycle detection (weekly/biweekly/quarterly/annual), debt snowball support. + +- **`statusService.js`** — Health status, cycle validation, autopay simulation, budget projections. + +- **`analyticsService.js`** — Analytics queries: spending, categories, bills, filters. + +- **`notificationService.js`** — Bill notifications: due_3d, due_1d, due_today, overdue. + +- **`authService.js`** — Auth helpers: login, JWT, password hashing, session management, OIDC integration. + +- **`userService.js`** — User CRUD, profile updates, demo data seeding, role changes. + +- **`settingsService.js`** — Settings CRUD, allowed keys validation, SMTP/billing/export settings. + +- **`backupService.js`** — SQLite backup, retention, schedule. + +- **`cronService.js`** — Scheduled tasks: backup, cleanup, auto-mark paid, cycle updates. + +### 5.23 Import and Sync Workflow + +Data ingestion follows a layered architecture: + +1. **Data Sources** (`data_sources` table) + - `manual`: User-created source (one per user, type='manual', provider='manual') + - `file_import`: CSV/XLSX imports (provider='csv_transactions', 'spreadsheet') + - `provider_sync`: External institution sync (e.g., 'plaid', 'mint') + - Fields: `type`, `provider`, `name`, `status` ('active', 'inactive', 'error'), `config_json`, `encrypted_secret`, `last_sync_at`, `last_error` + +2. **Financial Accounts** (`financial_accounts` table) + - Linked to `data_sources` via `data_source_id` + - One data source can have many accounts + - Fields: `provider_account_id`, `name`, `org_name`, `account_type`, `currency`, `balance`, `available_balance`, `raw_data` + +3. **Transactions** (`transactions` table) + - Linked to `data_sources` and `financial_accounts` + - Source type: `manual`, `file_import`, `provider_sync` + - Match states: `unmatched`, `matched`, `ignored` + - Optional `provider_transaction_id` for deduplication + - Fields: `amount` (cents), `transaction_type`, `posted_date`, `transacted_at`, `description`, `payee`, `memo`, `category`, `raw_data`, `matched_bill_id`, `match_status`, `ignored` + +4. **CSV Import Flow** + - User uploads CSV → `/api/import/csv/preview` + - Preview parses headers, suggests field mapping + - `/api/import/csv/commit` writes to `transactions` with `source_type='file_import'` + - Import history tracked in `import_history` with counts + +5. **Transaction Matching** + - Manual transactions (`source_type='manual'`) can be matched to bills + - Match suggestions discovered via `matchSuggestionService` + - Users can reject suggestions to avoid重复 suggestions + +6. **Provider Sync** (future) + - External sync jobs write to `data_sources` with `type='provider_sync'` + - Financial accounts created per institution account + - Transactions imported from provider + - Match suggestions offered for unmatched transactions + +7. **Payments** (bills → payments) + - Payments link to transactions via `transaction_id` for auto-draft + - `payment_source` indicates origin: `manual`, `file_import`, `provider_sync`, `transaction_match` + - Balance delta tracked for debt payoff + +8. **Import Sessions** (`import_sessions` table) + - Temporary storage for CSV/XLSX previews + - 1-hour TTL, auto-cleaned + - Fields: `preview_json`, `expires_at` + +9. **Import History** (`import_history` table) + - Audit trail of all imports + - Fields: `imported_at`, `source_filename`, `file_type`, `rows_parsed/created/updated/skipped/errored`, `options_json`, `summary_json` + +### 5.24 Validation and Services + +Key validation and service patterns: + +- **`paymentValidation.js`** + - `PAYMENT_SOURCES = ['manual', 'file_import', 'provider_sync', 'transaction_match']` enum + - `validatePaymentSource(value)` returns error if not in list + - `validatePaymentInput(body)` validates amount, paid_date, bill ownership, payment_source + - Invalid source returns 400 with `VALIDATION_ERROR` code + +- **CSV Import Service** + - Field suggestion via header analysis + - SHA-256 hash for deduplication + - Import session management with preview/commit split + - Error collection per row with detailed messages + +- **Transaction Service** + - `ensureManualDataSource(db, userId)` creates one manual data source per user + - `decorateTransaction(row)` adds `source_label`, `source_type_label` + - `decorateDataSource(row)` adds `source_label`, `source_type_label`, `account_count`, `transaction_count` + +- **Snowball Service** + - Computes snowball/avalanche orderings + - Ramsey mode: minimum payments only vs. full extra payment + - `snowball_include` bills sorted by `snowball_order` + - `snowball_exempt` bills excluded from ordering + +- **Match Suggestion Service** + - `listMatchSuggestions(userId, transactionId, billId, limit, offset)` + - `rejectMatchSuggestion(userId, transactionId, billId)` + - `suggestionCounts(userId)` + - Rejects stored to prevent repeated suggestions + +- **Monthly Starting Amounts Service** + - Bucketed amounts: first_amount, fifteenth_amount, other_amount + - Payment tracking from each bucket + - Remaining values computed on read + +- **Tracker Service** + - Cycle type detection: monthly, weekly, biweekly, quarterly, annually + - Cycle day mapping for non-standard cycles + - Auto-mark paid logic for autopay bills + +- **Status Service** + - Cycle validation warnings + - Autopay simulation + - Budget projections + +### 6. Database Reference SQLite uses WAL mode and foreign keys. Base schema is in `db/schema.sql`; `db/database.js` applies migrations to reach the current schema. @@ -973,202 +1137,6 @@ SQLite uses WAL mode and foreign keys. Base schema is in `db/schema.sql`; `db/da - `end_year INTEGER` - `end_month INTEGER` - `label TEXT` -- `created_at TEXT DEFAULT datetime('now')` -- `updated_at TEXT DEFAULT datetime('now')` - -#### `data_sources` - -- `id INTEGER PRIMARY KEY AUTOINCREMENT` -- `user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE` -- `type TEXT NOT NULL` (`manual`, `file_import`, `provider_sync`) -- `provider TEXT` -- `name TEXT NOT NULL` -- `status TEXT NOT NULL DEFAULT 'active'` (`active`, `inactive`, `error`) -- `config_json TEXT` -- `encrypted_secret TEXT` -- `last_sync_at TEXT` -- `last_error TEXT` -- `created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP` -- `updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP` -- Unique partial index: `(user_id, type, provider)` WHERE `type='manual' AND provider='manual'` - -#### `financial_accounts` - -- `id INTEGER PRIMARY KEY` -- `user_id INTEGER REFERENCES users(id) ON DELETE CASCADE` -- `data_source_id INTEGER REFERENCES data_sources(id) ON DELETE SET NULL` -- `provider_account_id TEXT NOT NULL` -- `name TEXT` -- `org_name TEXT` -- `account_type TEXT` -- `currency TEXT` -- `balance REAL` -- `available_balance REAL` -- `raw_data TEXT` -- `created_at TEXT DEFAULT datetime('now')` -- `updated_at TEXT DEFAULT datetime('now')` -- Unique on `(data_source_id, provider_account_id)` - -#### `transactions` - -- `id INTEGER PRIMARY KEY` -- `user_id INTEGER REFERENCES users(id) ON DELETE CASCADE` -- `data_source_id INTEGER REFERENCES data_sources(id) ON DELETE SET NULL` -- `account_id INTEGER REFERENCES financial_accounts(id) ON DELETE SET NULL` -- `provider_transaction_id TEXT` -- `source_type TEXT NOT NULL` (`manual`, `file_import`, `provider_sync`) -- `transaction_type TEXT` -- `posted_date TEXT` -- `transacted_at TEXT` -- `amount INTEGER NOT NULL` (cents) -- `currency TEXT` -- `description TEXT` -- `payee TEXT` -- `memo TEXT` -- `category TEXT` -- `raw_data TEXT` -- `matched_bill_id INTEGER REFERENCES bills(id) ON DELETE SET NULL` -- `match_status TEXT` (`unmatched`, `matched`, `ignored`) -- `ignored INTEGER NOT NULL DEFAULT 0` -- `created_at TEXT DEFAULT datetime('now')` -- `updated_at TEXT DEFAULT datetime('now')` -- Unique partial index on `(data_source_id, provider_transaction_id)` WHERE `provider_transaction_id IS NOT NULL` -- Indexes on `(user_id, COALESCE(posted_date, transacted_at, created_at))`, `(user_id, match_status, ignored)`, `account_id`, `matched_bill_id` - -#### `import_sessions` - -- `id TEXT PRIMARY KEY` -- `user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE` -- `created_at TEXT NOT NULL` -- `expires_at TEXT NOT NULL` -- `preview_json TEXT NOT NULL` - -#### `import_history` - -- `id INTEGER PRIMARY KEY` -- `user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE` -- `imported_at TEXT NOT NULL` -- `source_filename TEXT` -- `file_type TEXT DEFAULT 'csv_transactions'` -- `rows_parsed INTEGER DEFAULT 0` -- `rows_created INTEGER DEFAULT 0` -- `rows_updated INTEGER DEFAULT 0` -- `rows_skipped INTEGER DEFAULT 0` -- `rows_errored INTEGER DEFAULT 0` -- `options_json TEXT` -- `summary_json TEXT` -- `created_at TEXT DEFAULT datetime('now')` - -#### `autopay_suggestion_dismissals` - -- `id INTEGER PRIMARY KEY AUTOINCREMENT` -- `user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE` -- `bill_id INTEGER NOT NULL REFERENCES bills(id) ON DELETE CASCADE` -- `year INTEGER NOT NULL CHECK(year BETWEEN 2000 AND 2100)` -- `month INTEGER NOT NULL CHECK(month BETWEEN 1 AND 12)` -- `dismissed_at TEXT NOT NULL DEFAULT (datetime('now'))` -- Unique: `(user_id, bill_id, year, month)` - -#### `bill_templates` - -- `id INTEGER PRIMARY KEY AUTOINCREMENT` -- `user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE` -- `name TEXT NOT NULL` -- `data TEXT NOT NULL` -- `created_at TEXT DEFAULT (datetime('now'))` -- `updated_at TEXT DEFAULT (datetime('now'))` -- Unique: `(user_id, name)` - -#### `match_suggestion_rejections` - -- `id INTEGER PRIMARY KEY AUTOINCREMENT` -- `user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE` -- `transaction_id INTEGER NOT NULL REFERENCES transactions(id) ON DELETE CASCADE` -- `bill_id INTEGER NOT NULL REFERENCES bills(id) ON DELETE CASCADE` -- `rejected_at TEXT NOT NULL DEFAULT (datetime('now'))` -- Unique: `(user_id, transaction_id, bill_id)` - -#### `user_login_history` - -- `id INTEGER PRIMARY KEY AUTOINCREMENT` -- `user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE` -- `logged_in_at TEXT NOT NULL DEFAULT (datetime('now'))` -- `ip_address TEXT` -- `user_agent TEXT` -- `browser TEXT` -- `os TEXT` -- `device_type TEXT` -- `device_fingerprint TEXT` - -#### `settings` - -- `key TEXT PRIMARY KEY` -- `value TEXT NOT NULL` -- `updated_at TEXT DEFAULT datetime('now')` - -Used for app settings, auth mode, OIDC settings, SMTP settings, backup schedule, cleanup settings, and worker state. - -#### `notifications` - -- `id INTEGER PRIMARY KEY` -- `bill_id INTEGER NOT NULL REFERENCES bills(id) ON DELETE CASCADE` -- `user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE` -- `year INTEGER NOT NULL` -- `month INTEGER NOT NULL` -- `type TEXT NOT NULL` (`due_3d`, `due_1d`, `due_today`, `overdue`) -- `sent_date TEXT NOT NULL DEFAULT date('now')` -- Unique: `(bill_id, user_id, year, month, type, sent_date)` - -#### `import_sessions` - -- `id TEXT PRIMARY KEY` -- `user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE` -- `created_at TEXT NOT NULL` -- `expires_at TEXT NOT NULL` -- `preview_json TEXT NOT NULL` - -#### `import_history` - -- `id INTEGER PRIMARY KEY` -- `user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE` -- `imported_at TEXT NOT NULL` -- `source_filename TEXT` -- `file_type TEXT DEFAULT 'xlsx'` -- `sheet_name TEXT` -- `rows_parsed INTEGER DEFAULT 0` -- `rows_created INTEGER DEFAULT 0` -- `rows_updated INTEGER DEFAULT 0` -- `rows_skipped INTEGER DEFAULT 0` -- `rows_ambiguous INTEGER DEFAULT 0` -- `rows_errored INTEGER DEFAULT 0` -- `options_json TEXT` -- `summary_json TEXT` - -#### `oidc_states` - -- `id TEXT PRIMARY KEY` -- `nonce TEXT NOT NULL` -- `code_verifier TEXT NOT NULL` -- `redirect_to TEXT` -- `created_at TEXT NOT NULL` -- `expires_at TEXT NOT NULL` - -#### `audit_log` - -- `id INTEGER PRIMARY KEY` -- `user_id INTEGER` -- `action TEXT NOT NULL` -- `entity_type TEXT` -- `entity_id INTEGER` -- `details_json TEXT` -- `ip_address TEXT` -- `user_agent TEXT` -- `created_at TEXT DEFAULT datetime('now')` - -#### `schema_migrations` - -- `id INTEGER PRIMARY KEY` -- `version TEXT NOT NULL UNIQUE` - `description TEXT NOT NULL` - `applied_at TEXT NOT NULL DEFAULT datetime('now')` @@ -1254,6 +1222,71 @@ Important indexes include: - `idx_notifications_lookup(bill_id, user_id, year, month)` - `idx_import_sessions_user(user_id)`, `idx_import_sessions_expires(expires_at)` - `idx_import_history_user(user_id)`, `idx_import_history_imported_at(imported_at)` + +#### `import_sessions` + +- `id TEXT PRIMARY KEY` +- `user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE` +- `created_at TEXT NOT NULL` +- `expires_at TEXT NOT NULL` +- `preview_json TEXT NOT NULL` + +#### `import_history` + +- `id INTEGER PRIMARY KEY AUTOINCREMENT` +- `user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE` +- `imported_at TEXT NOT NULL` +- `source_filename TEXT` +- `file_type TEXT DEFAULT 'csv_transactions'` +- `rows_parsed INTEGER DEFAULT 0` +- `rows_created INTEGER DEFAULT 0` +- `rows_updated INTEGER DEFAULT 0` +- `rows_skipped INTEGER DEFAULT 0` +- `rows_errored INTEGER DEFAULT 0` +- `options_json TEXT` +- `summary_json TEXT` +- `created_at TEXT DEFAULT (datetime('now'))` + +#### `autopay_suggestion_dismissals` + +- `id INTEGER PRIMARY KEY AUTOINCREMENT` +- `user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE` +- `bill_id INTEGER NOT NULL REFERENCES bills(id) ON DELETE CASCADE` +- `year INTEGER NOT NULL CHECK(year BETWEEN 2000 AND 2100)` +- `month INTEGER NOT NULL CHECK(month BETWEEN 1 AND 12)` +- `dismissed_at TEXT NOT NULL DEFAULT (datetime('now'))` +- Unique: `(user_id, bill_id, year, month)` + +#### `bill_templates` + +- `id INTEGER PRIMARY KEY AUTOINCREMENT` +- `user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE` +- `name TEXT NOT NULL` +- `data TEXT NOT NULL` +- `created_at TEXT DEFAULT (datetime('now'))` +- `updated_at TEXT DEFAULT (datetime('now'))` +- Unique: `(user_id, name)` + +#### `match_suggestion_rejections` + +- `id INTEGER PRIMARY KEY AUTOINCREMENT` +- `user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE` +- `transaction_id INTEGER NOT NULL REFERENCES transactions(id) ON DELETE CASCADE` +- `bill_id INTEGER NOT NULL REFERENCES bills(id) ON DELETE CASCADE` +- `rejected_at TEXT NOT NULL DEFAULT (datetime('now'))` +- Unique: `(user_id, transaction_id, bill_id)` + +#### `user_login_history` + +- `id INTEGER PRIMARY KEY AUTOINCREMENT` +- `user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE` +- `logged_in_at TEXT NOT NULL DEFAULT (datetime('now'))` +- `ip_address TEXT` +- `user_agent TEXT` +- `browser TEXT` +- `os TEXT` +- `device_type TEXT` +- `device_fingerprint TEXT` - `idx_oidc_states_expires(expires_at)` - `idx_bill_history_ranges_bill(bill_id)` - `idx_audit_log_user(user_id, created_at)`, `idx_audit_log_action(action, created_at)` @@ -1428,9 +1461,35 @@ Routes: - `_fetch(method, path, body)` calls `/api${path}` with JSON headers and `credentials: include`. - Mutating methods read `bt_csrf_token` cookie and send `x-csrf-token`. - Non-OK responses throw `Error` with `status`, `data`, `details`, and `code`. -- Exposes grouped functions for auth, admin, notifications, profile, tracker, calendar, summary, bills, payments, categories, settings, analytics, status, version/about, import, and export. +- Exposes grouped functions for auth, admin, notifications, profile, tracker, calendar, summary, bills, payments, categories, settings, analytics, status, version/about, import, export, data-sources, transactions, and matches. - File download/upload endpoints use raw `fetch` because responses/bodies are blobs or octet streams. +#### Client API v0.28.1 additions + +- `api.dataSources(type, status)` → `/api/data-sources` +- `api.transactions(filters)` → `/api/transactions` +- `api.transactions.create(payload)` → `/api/transactions/manual` +- `api.transactions.update(id, payload)` → `/api/transactions/:id` +- `api.transactions.delete(id)` → `/api/transactions/:id` +- `api.transactions.match(id, {billId})` → `/api/transactions/:id/match` +- `api.transactions.unmatch(id)` → `/api/transactions/:id/unmatch` +- `api.transactions.ignore(id)` → `/api/transactions/:id/ignore` +- `api.transactions.unignore(id)` → `/api/transactions/:id/unignore` +- `api.csvImport.preview(file, options)` → `/api/import/csv/preview` +- `api.csvImport.commit(importSessionId, mapping, options)` → `/api/import/csv/commit` +- `api.import.history()` → `/api/import/history` +- `api.matchSuggestions.list(filters)` → `/api/matches/suggestions` +- `api.matchSuggestions.reject(id)` → `/api/matches/:id/reject` +- `api.snowball.get()` → `/api/snowball` +- `api.snowball.settings.get()` → `/api/snowball/settings` +- `api.snowball.settings.patch(payload)` → `/api/snowball/settings` +- `api.snowball.projection.get()` → `/api/snowball/projection` +- `api.snowball.order.patch(bills)` → `/api/snowball/order` +- `api.monthlyStartingAmounts.get(year, month)` → `/api/monthly-starting-amounts` +- `api.monthlyStartingAmounts.update(payload)` → `/api/monthly-starting-amounts` + +### Auth state + ### Auth state `client/hooks/useAuth.jsx`: @@ -1459,7 +1518,7 @@ These use TanStack Query keys and cache server data for common pages. - `CategoriesPage.jsx` — category list/create/update/delete and related bill info. - `AnalyticsPage.jsx` — analytics summary filters and charts. - `SettingsPage.jsx` — user/app settings and demo data seed. -- `DataPage.jsx` — export, spreadsheet import, user DB import, import history. +- `DataPage.jsx` — export, spreadsheet import, user DB import, import history, CSV transaction import with preview and commit flow (`ImportTransactionCsvSection`). - `ProfilePage.jsx` — display name, notification preferences, password change, export/import-history links. - `AdminPage.jsx` — users, auth mode/OIDC, backups, cleanup, notifications, migrations, admin settings. - `StatusPage.jsx` — admin system status. diff --git a/package.json b/package.json index 7148406..b840f4e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "bill-tracker", - "version": "0.28.01", + "version": "0.28.1", "description": "Monthly bill tracking system", "main": "server.js", "scripts": {