06/03 Update documents
This commit is contained in:
@@ -7,7 +7,7 @@
|
|||||||
|
|
||||||
## 1. Project Overview
|
## 1. Project Overview
|
||||||
|
|
||||||
Self-hosted personal finance web app. Tracks income, expenses, investments. AI assistant powered by **Groq API** (free tier, fast inference, no local hardware). Receipt OCR via Groq vision model. Bank account sync via Teller API (mTLS). Bank statement import (CSV, OFX/QFX, PDF). Everything runs on Ubuntu server behind Nginx + Certbot SSL.
|
Self-hosted personal finance web app. Tracks income, expenses, investments. AI assistant powered by **Groq API** (free tier, fast inference, no local hardware). Receipt OCR via Groq vision model. Bank account sync via Teller API (mTLS) and Schwab Developer API (OAuth 2.0). Bank statement import (CSV, OFX/QFX, PDF). Everything runs on Ubuntu server behind Nginx + Certbot SSL.
|
||||||
|
|
||||||
**Status: All 7 phases complete + all post-MVP features implemented.**
|
**Status: All 7 phases complete + all post-MVP features implemented.**
|
||||||
|
|
||||||
@@ -27,6 +27,7 @@ Self-hosted personal finance web app. Tracks income, expenses, investments. AI a
|
|||||||
- Source label shown (yfinance / exchangerate-api)
|
- Source label shown (yfinance / exchangerate-api)
|
||||||
- Stale indicator if rate > 1 day old
|
- Stale indicator if rate > 1 day old
|
||||||
- Period selector: This Month / Last Month / Custom date range
|
- Period selector: This Month / Last Month / Custom date range
|
||||||
|
- Credit card accounts display "owed" balance (positive Amount Owed) not raw negative
|
||||||
|
|
||||||
### 2.2 Transactions
|
### 2.2 Transactions
|
||||||
- Income + Expense entry with Income/Expense tabs
|
- Income + Expense entry with Income/Expense tabs
|
||||||
@@ -36,14 +37,24 @@ Self-hosted personal finance web app. Tracks income, expenses, investments. AI a
|
|||||||
- Receipt upload (PNG/JPG/WEBP/GIF/PDF, max 10MB)
|
- Receipt upload (PNG/JPG/WEBP/GIF/PDF, max 10MB)
|
||||||
- **AI Receipt OCR** — drag-drop receipt image → Groq vision extracts amount/date/merchant/category → auto-fills form
|
- **AI Receipt OCR** — drag-drop receipt image → Groq vision extracts amount/date/merchant/category → auto-fills form
|
||||||
- Re-extract from already-uploaded receipt (edit mode)
|
- Re-extract from already-uploaded receipt (edit mode)
|
||||||
|
- **Inline category change** — Category column is a `<select>` dropdown; change fires AJAX `POST /transactions/<id>/set-category` with no page reload
|
||||||
- Export to CSV / Excel
|
- Export to CSV / Excel
|
||||||
- Edit form: receipt sub-forms are outside `#txnForm` to prevent nested-form bug
|
- Edit form: receipt sub-forms are outside `#txnForm` to prevent nested-form bug
|
||||||
|
|
||||||
### 2.3 Accounts
|
### 2.3 Accounts
|
||||||
- Types: checking, savings, cash, credit_card, crypto, investment, other
|
- Types: checking, savings, cash, credit_card, crypto, investment, other
|
||||||
- Balance auto-calculated from all transactions (not manually entered)
|
- Balance source of truth:
|
||||||
- Color + icon picker
|
- **Teller-linked accounts**: balance comes from Teller API (live refresh or after sync); `calc_balance` is NOT called on page load for these
|
||||||
- Soft delete
|
- **Schwab-linked accounts**: balance comes from Schwab snapshot sync; `calc_balance` is NOT called on page load for these
|
||||||
|
- **Unlinked accounts**: balance auto-calculated from all transactions via `calc_balance`
|
||||||
|
- **Teller badge** (blue) shown on account cards linked to Teller
|
||||||
|
- **Schwab badge** (green) shown on account cards linked to Schwab
|
||||||
|
- Per-account action buttons for provider-linked accounts:
|
||||||
|
- **Teller**: Refresh (live balance AJAX), Sync (transaction preview), Reset (90-day resync)
|
||||||
|
- **Schwab**: Balance & Positions (snapshot sync POST), Transactions (preview link)
|
||||||
|
- Color + icon picker; soft delete
|
||||||
|
- Credit cards show "Amount Owed" (positive) and "This Month" charges
|
||||||
|
- Opening balance field on account creation (negative for credit cards = starting debt)
|
||||||
|
|
||||||
### 2.4 Categories
|
### 2.4 Categories
|
||||||
- Expense + Income categories with color/icon
|
- Expense + Income categories with color/icon
|
||||||
@@ -72,8 +83,12 @@ Self-hosted personal finance web app. Tracks income, expenses, investments. AI a
|
|||||||
- yfinance price auto-fetch (daily 4PM weekdays via cron)
|
- yfinance price auto-fetch (daily 4PM weekdays via cron)
|
||||||
- Manual price refresh button (portfolio page)
|
- Manual price refresh button (portfolio page)
|
||||||
- Live ticker check on add form
|
- Live ticker check on add form
|
||||||
- Doughnut allocation chart
|
- Doughnut allocation chart + per-asset-type breakdown
|
||||||
- P&L per holding + portfolio total
|
- P&L per holding + portfolio total
|
||||||
|
- **"Sync Schwab"** button (topbar POST) — runs snapshot sync for all mapped Schwab accounts, redirects back to investments page
|
||||||
|
- **Per-account sections** — when investments span multiple accounts (e.g. Individual + Roth IRA), holdings are grouped into one card per account, each showing account name, total value, and holdings table. Allocation sidebar also shows "By Account" breakdown
|
||||||
|
- `investments.account_id` FK — each Schwab-synced holding is stamped with its source account; same ticker in different accounts (AAPL in Individual vs Roth IRA) stays as separate rows
|
||||||
|
- Holdings table uses a Jinja2 `{% macro %}` (reused across single and multi-account views)
|
||||||
|
|
||||||
### 2.8 AI Financial Assistant
|
### 2.8 AI Financial Assistant
|
||||||
- Chat UI with SSE streaming (Groq API, word-by-word response)
|
- Chat UI with SSE streaming (Groq API, word-by-word response)
|
||||||
@@ -126,61 +141,64 @@ Self-hosted personal finance web app. Tracks income, expenses, investments. AI a
|
|||||||
|
|
||||||
### 2.13 Teller Bank Sync
|
### 2.13 Teller Bank Sync
|
||||||
- Connects US bank accounts via Teller API (mTLS + HTTP Basic Auth)
|
- Connects US bank accounts via Teller API (mTLS + HTTP Basic Auth)
|
||||||
- Enrollment via Teller Connect modal (JavaScript widget)
|
- **Bank Connections page** (`/teller/`) — connect/disconnect only; no sync buttons here
|
||||||
- Account mapping: each Teller account → PFM account (or auto-create new)
|
- Shows: institution name, connected date, last synced, account list with linked PFM account names
|
||||||
- Account ID validated against DB before saving (security fix)
|
- "Map Account" button for unmapped accounts
|
||||||
- Transaction sync: preview → confirm → import
|
- **Accounts page** — all Teller action buttons live here per account card (see §2.3)
|
||||||
- Duplicate detection using Teller transaction ID (stored as `Teller:<id>` in notes)
|
- Transaction sync: preview → confirm → import; after import, live balance fetched from Teller API (not recalculated from transactions)
|
||||||
- Balance refresh (live from Teller API)
|
- Duplicate detection using Teller transaction ID (`Teller:<id>` in notes)
|
||||||
|
- Balance refresh: uses `ledger` field for credit cards (amount owed), `available` for bank accounts
|
||||||
|
- Credit card ledger forced negative (our debt convention: `-abs(ledger)`)
|
||||||
|
- Auto-categorize: keyword match on description (`auto_categorize` from `bank_import_service`) first; Teller category field as fallback
|
||||||
|
- Income/expense type: positive Teller `amount` = income (credit), negative = expense (debit)
|
||||||
- Webhook: `transactions.processed` event with HMAC-SHA256 signature + 5-min replay protection
|
- Webhook: `transactions.processed` event with HMAC-SHA256 signature + 5-min replay protection
|
||||||
- Disconnect enrollment
|
|
||||||
- Config: `TELLER_APP_ID`, `TELLER_ENV`, `TELLER_CERT_PATH`, `TELLER_KEY_PATH`, `TELLER_WEBHOOK_SECRET`
|
- Config: `TELLER_APP_ID`, `TELLER_ENV`, `TELLER_CERT_PATH`, `TELLER_KEY_PATH`, `TELLER_WEBHOOK_SECRET`
|
||||||
- Models: `teller_enrollments`, `teller_accounts` (2 new tables)
|
- Models: `teller_enrollments`, `teller_accounts` (2 tables)
|
||||||
|
|
||||||
### 2.14 Bank Statement Import
|
### 2.14 Schwab Bank Sync
|
||||||
|
- Connects Schwab brokerage/IRA accounts via Schwab Developer API (OAuth 2.0)
|
||||||
|
- **OAuth flow**: `GET /schwab/connect` → redirect to Schwab with PKCE state → `GET /schwab/callback` → exchange code → store tokens
|
||||||
|
- State parameter included in auth URL (fix for "OAuth state mismatch" error)
|
||||||
|
- `SCHWAB_REDIRECT_URI` must match exactly what's registered in Schwab developer portal
|
||||||
|
- **Account hashes**: Schwab requires `hashValue` (encrypted account number) in all API paths
|
||||||
|
- On connect: calls `GET /trader/v1/accounts/accountNumbers` to get `{accountNumber → hashValue}` map
|
||||||
|
- `account_hash` stored in DB is always the `hashValue`, never the raw account number
|
||||||
|
- On reconnect: existing `SchwabAccount` records updated in-place (by hash, raw number, or masked display) to preserve `pfm_account_id` mapping
|
||||||
|
- **Account mapping**: each Schwab account → PFM account (or create new); stored in `schwab_accounts`
|
||||||
|
- **Transaction sync**: preview → confirm → import; uses same duplicate-skipping as other providers
|
||||||
|
- After import: live balance fetched from Schwab API (not recalculated from transactions)
|
||||||
|
- **Balance + position snapshot** (`POST /schwab/snapshot/<id>` or "Balance & Positions" button):
|
||||||
|
- Fetches `GET /trader/v1/accounts/{hash}?fields=positions`
|
||||||
|
- Updates linked PFM account balance from `currentBalances.liquidationValue`
|
||||||
|
- Upserts `Investment` records for each long position, matched on `(ticker, account_id)`
|
||||||
|
- Asset type mapping: EQUITY→stock, ETF→etf, MUTUAL_FUND→etf, FIXED_INCOME→bond, CASH_EQUIVALENT→cash, unknown→other
|
||||||
|
- Position zero-quantity and empty-symbol positions skipped; `null` positions array guarded
|
||||||
|
- **Investments sync**: topbar "Sync Schwab" button on investments page → `POST /investments/sync-schwab` → runs snapshot for all mapped accounts (ignores stale `connection_id` — always uses active connection)
|
||||||
|
- Token auto-refresh: access token expires 30 min; refreshed automatically before API calls
|
||||||
|
- Config: `SCHWAB_CLIENT_ID`, `SCHWAB_CLIENT_SECRET`, `SCHWAB_REDIRECT_URI`
|
||||||
|
- Models: `schwab_connections`, `schwab_accounts` (2 new tables)
|
||||||
|
|
||||||
|
### 2.15 Bank Statement Import
|
||||||
- Sidebar link: "Import Statement" under Money section
|
- Sidebar link: "Import Statement" under Money section
|
||||||
- Supported formats:
|
- Supported formats: PDF, OFX/QFX, Chase/BofA/Citi/Capital One/Discover/Amex/USAA/Wells Fargo CSV, Generic CSV, Custom column mapping
|
||||||
- **PDF** — pdfplumber text extraction + Groq LLM parsing (digital PDFs only; not scanned)
|
|
||||||
- **OFX / QFX** — both SGML and XML variants; handles all TRNTYPE codes
|
|
||||||
- **Chase** CSV — `Transaction Date, Description, Amount`
|
|
||||||
- **Bank of America** CSV — `Posted Date, Payee, Amount`
|
|
||||||
- **Citi** CSV — `Date, Description, Debit, Credit`
|
|
||||||
- **Capital One** CSV — `Transaction Date, Description, Debit, Credit`
|
|
||||||
- **Discover** CSV — `Trans. Date, Description, Amount` (positive = expense)
|
|
||||||
- **Amex** CSV — `Date, Description, Amount`
|
|
||||||
- **USAA** CSV — `Date, Description, Original Description, Amount`
|
|
||||||
- **Wells Fargo** CSV — `Date, Amount, Description`
|
|
||||||
- **Generic CSV** — heuristic column detection
|
|
||||||
- **Custom mapping** — UI to map columns when auto-detect fails
|
|
||||||
- Auto-categorizes using 200+ keyword rules across 14 categories
|
- Auto-categorizes using 200+ keyword rules across 14 categories
|
||||||
- Preview table: per-row checkboxes, editable category dropdowns
|
- Preview table: per-row checkboxes, editable category dropdowns
|
||||||
- Duplicate detection: OFX FITID (stored as `import:<id>` in notes) or date+amount+description+account
|
- Duplicate detection: OFX FITID (`import:<id>` in notes) or date+amount+description+account
|
||||||
- PDF notes: 30 K char limit per upload; scanned PDFs rejected with clear error
|
- AJAX-based: no page reloads
|
||||||
- AJAX-based: no page reloads, no session storage for rows
|
|
||||||
- File input lives outside drop zone (prevents overlay-blocking other controls)
|
|
||||||
|
|
||||||
### 2.15 System Logs Viewer
|
### 2.16 System Logs Viewer
|
||||||
- Sidebar link: "System Logs" (`bi-terminal`) in footer section
|
|
||||||
- Log file: `logs/app.log` (rotating, 10 MB, 5 backups)
|
- Log file: `logs/app.log` (rotating, 10 MB, 5 backups)
|
||||||
- Format: `YYYY-MM-DD HH:MM:SS|LEVEL|module.name|message` (pipe-delimited for parsing)
|
- Format: `YYYY-MM-DD HH:MM:SS|LEVEL|module.name|message`
|
||||||
- Viewer at `/logs/`:
|
- Viewer at `/logs/`: colour-coded pills, free-text search, module filter, auto-refresh, clear, download
|
||||||
- Colour-coded level pills: ERROR / WARNING / INFO / DEBUG
|
|
||||||
- Free-text search + module filter + row limit (100/200/500/1000)
|
|
||||||
- Auto-refresh every 5 s (toggle with green pulse indicator)
|
|
||||||
- Clear log file button (POST with CSRF)
|
|
||||||
- Download raw log file
|
|
||||||
- Logging wired to Gunicorn via `gunicorn.error` handlers; also writes to stderr
|
|
||||||
- `app.*` namespace loggers all inherit from `logging.getLogger('app')` at INFO level
|
|
||||||
- All Teller API calls log: status, URL, and response body on error
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 3. Database Schema (MySQL)
|
## 3. Database Schema (MySQL)
|
||||||
|
|
||||||
### All 16 Tables
|
### All 18 Tables
|
||||||
```
|
```
|
||||||
users — single user, hashed password, currency/timezone prefs
|
users — single user, hashed password, currency/timezone prefs
|
||||||
accounts — bank/wallet accounts (balance auto-calc from txns)
|
accounts — bank/wallet accounts (balance managed per provider rules)
|
||||||
categories — expense/income categories with color/icon
|
categories — expense/income categories with color/icon
|
||||||
transactions — income/expense/transfer, receipt_id, recurring_rule_id
|
transactions — income/expense/transfer, receipt_id, recurring_rule_id
|
||||||
receipts — receipt file metadata (filename, size, mime_type)
|
receipts — receipt file metadata (filename, size, mime_type)
|
||||||
@@ -188,21 +206,28 @@ recurring_rules — templates: frequency, next_run, start/end date
|
|||||||
budgets — monthly limits per category, rollover support
|
budgets — monthly limits per category, rollover support
|
||||||
goals — savings goals with target amount/date
|
goals — savings goals with target amount/date
|
||||||
goal_contributions — individual deposits toward each goal
|
goal_contributions — individual deposits toward each goal
|
||||||
investments — holdings: ticker, shares, avg_cost_basis, current_price
|
investments — holdings: ticker, shares, avg_cost_basis, current_price, account_id
|
||||||
investment_transactions — buy/sell/dividend/split log
|
investment_transactions — buy/sell/dividend/split log
|
||||||
net_worth_snapshots — monthly snapshots: assets, liabilities, net_worth (JSON)
|
net_worth_snapshots — monthly snapshots: assets, liabilities, net_worth (JSON)
|
||||||
ai_insights — stored AI responses: daily_summary / chat_response
|
ai_insights — stored AI responses: daily_summary / chat_response
|
||||||
fx_rates — daily USD/VND rate cache (date UNIQUE, source)
|
fx_rates — daily USD/VND rate cache (date UNIQUE, source)
|
||||||
teller_enrollments — Teller enrollment: enrollment_id, access_token, institution_name
|
teller_enrollments — Teller enrollment: enrollment_id, access_token, institution_name
|
||||||
teller_accounts — Teller account ↔ PFM account mapping, last_sync_date
|
teller_accounts — Teller account ↔ PFM account mapping, last_sync_date
|
||||||
|
schwab_connections — Schwab OAuth tokens: access_token, refresh_token, token_expires_at
|
||||||
|
schwab_accounts — Schwab account ↔ PFM account mapping, account_hash (hashValue)
|
||||||
```
|
```
|
||||||
|
|
||||||
### Key Column Notes
|
### Key Column Notes
|
||||||
- `transactions.balance` — NOT stored; calculated on-demand via `account_service.calc_balance()`
|
- `accounts.balance` — set by `calc_balance()` for unlinked accounts; set directly by Teller/Schwab sync for provider-linked accounts; never overwritten on page load for provider accounts
|
||||||
- `investments.shares` / `avg_cost_basis` — recalculated from `investment_transactions` (FIFO)
|
- `investments.account_id` — nullable FK to `accounts.id`; NULL for manually-added holdings, set to PFM account ID for Schwab-synced holdings; enables per-account grouping on portfolio page
|
||||||
- `goals.current_amount` — updated on each contribution add/delete
|
- `investments.shares` / `avg_cost_basis` — recalculated from `investment_transactions` (FIFO) for manual holdings; overwritten directly by Schwab snapshot for synced holdings
|
||||||
- `net_worth_snapshots.account_balances` — JSON snapshot of each account balance at time of snapshot
|
- `transactions.notes` — used to store import source IDs: `Teller:<id>`, `Schwab:<activityId>`, or `import:<fitid>`
|
||||||
- `transactions.notes` — used to store import source IDs: `Teller:<id>` or `import:<fitid>`
|
- `schwab_accounts.account_hash` — Schwab `hashValue` (encrypted account number), required in all API paths
|
||||||
|
|
||||||
|
### Migration Scripts
|
||||||
|
```
|
||||||
|
scripts/add_investment_account.py — adds investments.account_id column (run once after deploy)
|
||||||
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -210,21 +235,20 @@ teller_accounts — Teller account ↔ PFM account mapping, last_sync_da
|
|||||||
|
|
||||||
```
|
```
|
||||||
pfm/ # /home/pfm/web on server
|
pfm/ # /home/pfm/web on server
|
||||||
├── wsgi.py # Gunicorn entry — sys.path fix included
|
├── wsgi.py
|
||||||
├── requirements.txt
|
├── requirements.txt
|
||||||
├── .env # Not committed
|
├── .env # Not committed
|
||||||
├── .env.example
|
├── .env.example
|
||||||
├── .gitignore
|
├── .gitignore
|
||||||
├── logs/
|
├── logs/
|
||||||
│ └── app.log # Rotating application log (created at runtime)
|
│ └── app.log
|
||||||
│
|
│
|
||||||
├── app/
|
├── app/
|
||||||
│ ├── __init__.py # Flask app factory; _setup_logging(); all blueprints registered
|
│ ├── __init__.py
|
||||||
│ ├── config.py # Dev/Prod configs; LOG_FILE_PATH; TELLER_* vars
|
│ ├── config.py # SCHWAB_* vars added
|
||||||
│ ├── extensions.py # db, login_manager, migrate, csrf
|
│ ├── extensions.py
|
||||||
│ │
|
│ │
|
||||||
│ ├── models/
|
│ ├── models/
|
||||||
│ │ ├── __init__.py
|
|
||||||
│ │ ├── user.py
|
│ │ ├── user.py
|
||||||
│ │ ├── account.py
|
│ │ ├── account.py
|
||||||
│ │ ├── category.py
|
│ │ ├── category.py
|
||||||
@@ -233,27 +257,29 @@ pfm/ # /home/pfm/web on server
|
|||||||
│ │ ├── recurring_rule.py
|
│ │ ├── recurring_rule.py
|
||||||
│ │ ├── budget.py
|
│ │ ├── budget.py
|
||||||
│ │ ├── goal.py
|
│ │ ├── goal.py
|
||||||
│ │ ├── investment.py
|
│ │ ├── investment.py # + account_id FK, account relationship
|
||||||
│ │ ├── net_worth_snapshot.py
|
│ │ ├── net_worth_snapshot.py
|
||||||
│ │ ├── ai_insight.py
|
│ │ ├── ai_insight.py
|
||||||
│ │ ├── fx_rate.py
|
│ │ ├── fx_rate.py
|
||||||
│ │ └── teller_enrollment.py # TellerEnrollment + TellerAccount models
|
│ │ ├── teller_enrollment.py # TellerEnrollment + TellerAccount
|
||||||
|
│ │ └── schwab_connection.py # SchwabConnection + SchwabAccount (NEW)
|
||||||
│ │
|
│ │
|
||||||
│ ├── routes/
|
│ ├── routes/
|
||||||
│ │ ├── auth.py
|
│ │ ├── auth.py
|
||||||
│ │ ├── dashboard.py
|
│ │ ├── dashboard.py # credit card "owed" display; no recalc_all
|
||||||
│ │ ├── accounts.py
|
│ │ ├── accounts.py # teller_map + schwab_map; skip calc_balance for providers
|
||||||
│ │ ├── categories.py
|
│ │ ├── categories.py
|
||||||
│ │ ├── transactions.py # /transactions/ocr, /ocr-file; safe filter int parsing
|
│ │ ├── transactions.py # + set-category AJAX endpoint
|
||||||
│ │ ├── budgets.py
|
│ │ ├── budgets.py
|
||||||
│ │ ├── goals.py
|
│ │ ├── goals.py
|
||||||
│ │ ├── investments.py
|
│ │ ├── investments.py # + sync-schwab route
|
||||||
│ │ ├── reports.py
|
│ │ ├── reports.py
|
||||||
│ │ ├── ai.py
|
│ │ ├── ai.py
|
||||||
│ │ ├── settings.py # view_receipt: os.path.basename() path-traversal fix
|
│ │ ├── settings.py
|
||||||
│ │ ├── teller.py # Teller sync, webhook, account mapping (validated IDs)
|
│ │ ├── teller.py # balance uses ledger/available correctly; full_resync honours next param
|
||||||
│ │ ├── bank_import.py # /bank-import/; parse (AJAX); import (AJAX)
|
│ │ ├── schwab.py # OAuth, mapping, sync, snapshot (NEW)
|
||||||
│ │ └── logs.py # /logs/; /logs/api; /logs/clear; /logs/download
|
│ │ ├── bank_import.py
|
||||||
|
│ │ └── logs.py
|
||||||
│ │
|
│ │
|
||||||
│ ├── services/
|
│ ├── services/
|
||||||
│ │ ├── account_service.py
|
│ │ ├── account_service.py
|
||||||
@@ -262,49 +288,37 @@ pfm/ # /home/pfm/web on server
|
|||||||
│ │ ├── export_service.py
|
│ │ ├── export_service.py
|
||||||
│ │ ├── fx_service.py
|
│ │ ├── fx_service.py
|
||||||
│ │ ├── goal_service.py
|
│ │ ├── goal_service.py
|
||||||
│ │ ├── import_service.py # duplicate check now scoped by account_id
|
│ │ ├── import_service.py
|
||||||
│ │ ├── investment_service.py
|
│ │ ├── investment_service.py # get_portfolio_summary returns account_groups
|
||||||
│ │ ├── ocr_service.py
|
│ │ ├── ocr_service.py
|
||||||
│ │ ├── recurring_service.py # 90-day catchup cap prevents runaway loops
|
│ │ ├── recurring_service.py
|
||||||
│ │ ├── report_service.py
|
│ │ ├── report_service.py
|
||||||
│ │ ├── teller_service.py # mTLS session; full response-body logging on errors
|
│ │ ├── teller_service.py # auto_categorize; correct sign convention; live balance after sync
|
||||||
│ │ └── bank_import_service.py # PDF+OFX+CSV parsing; Groq PDF parsing; auto-categorize
|
│ │ ├── schwab_service.py # OAuth, account hash, snapshot sync, position upsert (NEW)
|
||||||
|
│ │ └── bank_import_service.py
|
||||||
│ │
|
│ │
|
||||||
│ ├── templates/
|
│ ├── templates/
|
||||||
│ │ ├── base.html # Sidebar (all active states); csrf-token meta tag
|
│ │ ├── base.html
|
||||||
│ │ ├── auth/login.html
|
│ │ ├── dashboard/index.html # credit card owed display
|
||||||
│ │ ├── dashboard/index.html
|
│ │ ├── accounts/index.html # Teller/Schwab badges + action buttons
|
||||||
│ │ ├── accounts/
|
│ │ ├── transactions/index.html # inline category <select> + AJAX
|
||||||
│ │ ├── categories/
|
│ │ ├── investments/index.html # per-account sections; holdings_table macro; Sync Schwab btn
|
||||||
│ │ ├── transactions/ # form.html: receipt forms outside #txnForm (nested-form fix)
|
│ │ ├── teller/index.html # connect/disconnect only (sync buttons removed)
|
||||||
│ │ ├── budgets/
|
│ │ ├── schwab/ # index.html, map_accounts.html, preview.html (NEW)
|
||||||
│ │ ├── goals/
|
│ │ └── ... (other templates unchanged)
|
||||||
│ │ ├── investments/
|
|
||||||
│ │ ├── reports/
|
|
||||||
│ │ ├── ai/
|
|
||||||
│ │ ├── settings/
|
|
||||||
│ │ ├── teller/ # index.html, map_accounts.html, preview.html
|
|
||||||
│ │ ├── bank_import/ # index.html (drag-drop; AJAX parse+import; column mapper)
|
|
||||||
│ │ └── logs/ # index.html (live viewer; filters; auto-refresh)
|
|
||||||
│ │
|
|
||||||
│ ├── static/
|
|
||||||
│ │ ├── css/ # (empty — all CSS inline in templates)
|
|
||||||
│ │ ├── js/ # (empty — all JS inline in templates)
|
|
||||||
│ │ └── img/
|
|
||||||
│ │
|
│ │
|
||||||
│ └── utils/
|
│ └── utils/
|
||||||
│ ├── formatters.py
|
│ ├── formatters.py
|
||||||
│ └── decorators.py
|
│ └── decorators.py
|
||||||
│
|
│
|
||||||
├── migrations/
|
|
||||||
│
|
|
||||||
├── scripts/
|
├── scripts/
|
||||||
│ ├── init_db.py
|
│ ├── init_db.py
|
||||||
│ ├── process_recurring.py
|
│ ├── process_recurring.py
|
||||||
│ ├── fetch_fx_rate.py
|
│ ├── fetch_fx_rate.py
|
||||||
│ ├── fetch_prices.py
|
│ ├── fetch_prices.py
|
||||||
│ ├── daily_snapshot.py
|
│ ├── daily_snapshot.py
|
||||||
│ └── daily_ai_insight.py
|
│ ├── daily_ai_insight.py
|
||||||
|
│ └── add_investment_account.py # NEW — adds investments.account_id column
|
||||||
│
|
│
|
||||||
└── tests/
|
└── tests/
|
||||||
```
|
```
|
||||||
@@ -330,7 +344,7 @@ apscheduler==3.10.4
|
|||||||
requests==2.32.3
|
requests==2.32.3
|
||||||
cryptography==44.0.2
|
cryptography==44.0.2
|
||||||
python-dateutil==2.9.0
|
python-dateutil==2.9.0
|
||||||
pdfplumber==0.11.4 # PDF text extraction for bank statement import
|
pdfplumber==0.11.4
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -351,9 +365,8 @@ pdfplumber==0.11.4 # PDF text extraction for bank statement import
|
|||||||
|
|
||||||
### Bank Statement PDF Parsing
|
### Bank Statement PDF Parsing
|
||||||
- Text extracted by `pdfplumber` then sent to `llama-3.3-70b-versatile`
|
- Text extracted by `pdfplumber` then sent to `llama-3.3-70b-versatile`
|
||||||
- Prompt requests JSON array of `{date, description, amount, transaction_type}`
|
- Max 30 K chars sent per request
|
||||||
- Max 30 K chars sent per request (~6 months of typical statements)
|
- Scanned PDFs rejected with clear error message
|
||||||
- Scanned PDFs (no text layer) are rejected with a clear error message
|
|
||||||
|
|
||||||
### Free Tier Limits
|
### Free Tier Limits
|
||||||
| Metric | Limit |
|
| Metric | Limit |
|
||||||
@@ -371,64 +384,82 @@ pdfplumber==0.11.4 # PDF text extraction for bank statement import
|
|||||||
- Endpoints: `GET /accounts`, `GET /accounts/:id/balances`, `GET /accounts/:id/transactions`
|
- Endpoints: `GET /accounts`, `GET /accounts/:id/balances`, `GET /accounts/:id/transactions`
|
||||||
- All API errors logged with status code + full response body
|
- All API errors logged with status code + full response body
|
||||||
- Webhook: HMAC-SHA256 `Teller-Signature` header; 5-minute replay window
|
- Webhook: HMAC-SHA256 `Teller-Signature` header; 5-minute replay window
|
||||||
|
- **Balance convention**: credit cards use `ledger` (amount owed, stored as negative); bank accounts use `available`
|
||||||
|
- **After transaction sync**: live balance re-fetched from Teller API instead of computing from transactions
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 8. Bank Statement Import
|
## 8. Schwab Developer API
|
||||||
|
|
||||||
|
- OAuth 2.0: `SCHWAB_AUTH_URL` + `SCHWAB_TOKEN_URL`
|
||||||
|
- State parameter sent in auth URL (CSRF protection)
|
||||||
|
- Account identification: `hashValue` from `/trader/v1/accounts/accountNumbers` (NOT raw account number)
|
||||||
|
- Token refresh: access tokens expire 30 min; auto-refreshed via `_ensure_fresh(connection)`
|
||||||
|
- Endpoints:
|
||||||
|
- `GET /trader/v1/accounts/accountNumbers` → `{accountNumber: hashValue}` map
|
||||||
|
- `GET /trader/v1/accounts?fields=positions` → accounts list with balances + positions
|
||||||
|
- `GET /trader/v1/accounts/{hash}?fields=positions` → single account
|
||||||
|
- `GET /trader/v1/accounts/{hash}/transactions?startDate&endDate` → transactions
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. Bank Statement Import
|
||||||
|
|
||||||
- Route: `/bank-import/` (blueprint `bank_import_bp`)
|
- Route: `/bank-import/` (blueprint `bank_import_bp`)
|
||||||
- Parse: `POST /bank-import/parse` (AJAX, multipart with `X-CSRFToken` header)
|
- Parse: `POST /bank-import/parse` (AJAX, multipart with `X-CSRFToken` header)
|
||||||
- Import: `POST /bank-import/import` (AJAX, JSON with `X-CSRFToken` header)
|
- Import: `POST /bank-import/import` (AJAX, JSON with `X-CSRFToken` header)
|
||||||
- File input is hidden and outside the drop zone (`fileInput.click()` on drop zone click)
|
- Duplicate detection: OFX `import:<FITID>` in notes; CSV/PDF: date+amount+type+description scoped to account_id
|
||||||
- Duplicate detection:
|
|
||||||
- OFX: match on `import:<FITID>` in notes
|
|
||||||
- CSV/PDF: match on date + amount + type + description scoped to same account_id
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 9. Logging System
|
## 10. Account Balance Rules
|
||||||
|
|
||||||
|
| Account type | Balance source | When updated |
|
||||||
|
|---|---|---|
|
||||||
|
| Unlinked (no provider) | `calc_balance()` from transactions | After every txn add/edit/delete; on accounts page load |
|
||||||
|
| Teller-linked | Teller API `available` (bank) or `ledger` (credit card) | After Teller sync; when Refresh button clicked |
|
||||||
|
| Schwab-linked | Schwab API `liquidationValue` | After Schwab sync; when Balance & Positions clicked |
|
||||||
|
|
||||||
|
**Key rule**: accounts page load calls `calc_balance` ONLY for accounts NOT in `teller_map` or `schwab_map`. Dashboard does NOT call `calc_balance` (reads stored values).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 11. Logging System
|
||||||
|
|
||||||
- Config key: `LOG_FILE_PATH` (default: `<project-root>/logs/app.log`)
|
- Config key: `LOG_FILE_PATH` (default: `<project-root>/logs/app.log`)
|
||||||
- Handler: `RotatingFileHandler` — 10 MB per file, 5 backups
|
- Handler: `RotatingFileHandler` — 10 MB per file, 5 backups
|
||||||
- Format: `YYYY-MM-DD HH:MM:SS|LEVEL|module.name|message`
|
- Format: `YYYY-MM-DD HH:MM:SS|LEVEL|module.name|message`
|
||||||
- Namespace: `logging.getLogger('app')` at INFO; `propagate=False`
|
- Namespace: `logging.getLogger('app')` at INFO; `propagate=False`
|
||||||
- Also writes to stderr (Gunicorn captures it)
|
- Schwab snapshot sync logs: number of positions returned, per-position symbol/type/qty/mapped-type
|
||||||
- Viewer: `/logs/` — real-time filtered display, per-level counts, auto-refresh, clear, download
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 10. UI/UX
|
## 12. UI/UX
|
||||||
|
|
||||||
- **Sidebar**: collapsible (desktop state saved in localStorage), mobile overlay
|
- **Sidebar**: collapsible (desktop state saved in localStorage), mobile overlay
|
||||||
- Active states: `{% if request.blueprint == '...' %}active{% endif %}`
|
|
||||||
- Links: Dashboard · Transactions · Add Income · Add Expense · Accounts · Import Statement · Budgets · Goals · Investments · Reports · AI Assistant · Categories · System Logs · Settings · Logout
|
|
||||||
- **Charts**: Chart.js 4.x (CDN)
|
- **Charts**: Chart.js 4.x (CDN)
|
||||||
- **Forms**: WTForms + Bootstrap 5.3
|
- **Forms**: WTForms + Bootstrap 5.3
|
||||||
- **Icons**: Bootstrap Icons 1.11
|
- **Icons**: Bootstrap Icons 1.11
|
||||||
- **Fonts**: DM Sans + DM Mono (Google Fonts CDN)
|
- **Fonts**: DM Sans + DM Mono (Google Fonts CDN)
|
||||||
- **Color scheme**: `#0f172a` sidebar, `#f1f5f9` body, `#10b981` income, `#ef4444` expense, `#3b82f6` invest
|
- **Color scheme**: `#0f172a` sidebar, `#f1f5f9` body, `#10b981` income, `#ef4444` expense, `#3b82f6` invest
|
||||||
- **CSS**: All inline in templates (no build step)
|
- **CSS**: All inline in templates (no build step)
|
||||||
- **SSE**: AI chat stream + FX refresh
|
|
||||||
- **CSRF meta tag**: `<meta name="csrf-token">` in `base.html` for JS fetch calls
|
- **CSRF meta tag**: `<meta name="csrf-token">` in `base.html` for JS fetch calls
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 11. Authentication & Security
|
## 13. Authentication & Security
|
||||||
|
|
||||||
- Single-user, Flask-Login, session-based
|
- Single-user, Flask-Login, session-based
|
||||||
- Hashed password (Werkzeug `generate_password_hash`)
|
- Hashed password (Werkzeug `generate_password_hash`)
|
||||||
- `SESSION_COOKIE_SECURE=True` in production
|
- `SESSION_COOKIE_SECURE=True`, `SESSION_COOKIE_HTTPONLY=True`, `SESSION_COOKIE_SAMESITE='Lax'`
|
||||||
- `SESSION_COOKIE_HTTPONLY=True`, `SESSION_COOKIE_SAMESITE='Lax'`
|
|
||||||
- CSRF protection on all forms (Flask-WTF); meta tag in base.html for AJAX
|
- CSRF protection on all forms (Flask-WTF); meta tag in base.html for AJAX
|
||||||
- SQLAlchemy ORM (no raw SQL)
|
- SQLAlchemy ORM (no raw SQL)
|
||||||
- Receipt file path: `os.path.basename()` in both upload AND view_receipt (path-traversal fix)
|
- Schwab OAuth state parameter validated on callback (CSRF protection)
|
||||||
- Teller account IDs validated against DB before mapping
|
- `next` redirect params validated to start with `/` (no open redirect)
|
||||||
- Transaction filter params safely cast with try/except (no crash on bad int input)
|
|
||||||
- Groq receives anonymised transaction summaries (no account/personal names)
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 12. Scheduled Jobs
|
## 14. Scheduled Jobs
|
||||||
|
|
||||||
| Job | Schedule | Script | Notes |
|
| Job | Schedule | Script | Notes |
|
||||||
|-----|----------|--------|-------|
|
|-----|----------|--------|-------|
|
||||||
@@ -441,7 +472,7 @@ pdfplumber==0.11.4 # PDF text extraction for bank statement import
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 13. Environment Variables (`.env`)
|
## 15. Environment Variables (`.env`)
|
||||||
|
|
||||||
```
|
```
|
||||||
SECRET_KEY=your-secret-key
|
SECRET_KEY=your-secret-key
|
||||||
@@ -462,44 +493,50 @@ TELLER_ENV=development
|
|||||||
TELLER_CERT_PATH=/home/pfm/teller/certificate.pem
|
TELLER_CERT_PATH=/home/pfm/teller/certificate.pem
|
||||||
TELLER_KEY_PATH=/home/pfm/teller/private_key.pem
|
TELLER_KEY_PATH=/home/pfm/teller/private_key.pem
|
||||||
TELLER_WEBHOOK_SECRET=your-webhook-secret
|
TELLER_WEBHOOK_SECRET=your-webhook-secret
|
||||||
|
# Schwab
|
||||||
|
SCHWAB_CLIENT_ID=your-schwab-client-id
|
||||||
|
SCHWAB_CLIENT_SECRET=your-schwab-client-secret
|
||||||
|
SCHWAB_REDIRECT_URI=https://pfm.ngodanguyen.tech/schwab/callback
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 14. Blueprints Registered (13 total)
|
## 16. Blueprints Registered (15 total)
|
||||||
|
|
||||||
| Blueprint | Prefix | Key routes |
|
| Blueprint | Prefix | Key routes |
|
||||||
|-----------|--------|------------|
|
|-----------|--------|------------|
|
||||||
| auth | /auth | login, logout |
|
| auth | /auth | login, logout |
|
||||||
| dashboard | / | index, api/fx-history, api/fx-refresh |
|
| dashboard | / | index, api/fx-history, api/fx-refresh |
|
||||||
| accounts | /accounts | CRUD |
|
| accounts | /accounts | CRUD, adjust |
|
||||||
| categories | /categories | CRUD |
|
| categories | /categories | CRUD |
|
||||||
| transactions | /transactions | index, new, edit, delete, transfer, ocr, ocr-file |
|
| transactions | /transactions | index, new, edit, delete, transfer, ocr, ocr-file, `<id>/set-category` |
|
||||||
| budgets | /budgets | index, new, edit, delete, copy |
|
| budgets | /budgets | index, new, edit, delete, copy |
|
||||||
| goals | /goals | index, new, edit, delete, contribute, contributions |
|
| goals | /goals | index, new, edit, delete, contribute, contributions |
|
||||||
| investments | /investments | index, new, detail, edit, delete, add_transaction, refresh-prices, api/price |
|
| investments | /investments | index, new, detail, edit, delete, add_transaction, refresh-prices, sync-schwab, api/price, api/daychange, api/price-history |
|
||||||
| reports | /reports | monthly, quarterly, yearly, tax, export/csv\|excel\|pdf, snapshot |
|
| reports | /reports | monthly, quarterly, yearly, tax, export/csv\|excel\|pdf, snapshot |
|
||||||
| ai | /ai | index, stream (SSE), history, generate-insight |
|
| ai | /ai | index, stream (SSE), history, generate-insight |
|
||||||
| settings | /settings | index, profile, password, recurring, import, upload_receipt, delete_receipt, view_receipt |
|
| settings | /settings | index, profile, password, recurring, import, recalc-balances, upload_receipt, delete_receipt, view_receipt |
|
||||||
| teller | /teller | callback, map, index, sync, sync/confirm, sync/all, balance, disconnect, webhook |
|
| teller | /teller | callback, map, index, sync, sync/confirm, sync/all, balance, balance/all, resync, disconnect, webhook |
|
||||||
|
| schwab | /schwab | connect, callback, index, map, sync/`<id>`, sync/confirm, resync, snapshot/`<id>`, disconnect |
|
||||||
| bank_import | /bank-import | index, parse (AJAX), import (AJAX) |
|
| bank_import | /bank-import | index, parse (AJAX), import (AJAX) |
|
||||||
| logs | /logs | index, api (AJAX), clear (AJAX), download |
|
| logs | /logs | index, api (AJAX), clear (AJAX), download |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 15. Known Issues / Notes
|
## 17. Known Issues / Notes
|
||||||
|
|
||||||
- `wsgi.py` has `sys.path.insert(0, ...)` — required for Gunicorn at `/home/pfm/web/`
|
- `wsgi.py` has `sys.path.insert(0, ...)` — required for Gunicorn at `/home/pfm/web/`
|
||||||
- FX rate widget: `open.er-api.com` may return stale values; yfinance is the reliable primary
|
- FX rate widget: `open.er-api.com` may return stale values; yfinance is the reliable primary
|
||||||
- WeasyPrint PDF: requires `libpango*` system libs on server
|
- WeasyPrint PDF: requires `libpango*` system libs on server
|
||||||
- Bank statement PDF import: scanned/image PDFs have no text layer; must use digital download
|
- Bank statement PDF import: scanned/image PDFs have no text layer; must use digital download
|
||||||
- Bank statement PDF import: large PDFs (>30 K chars) are truncated; split into shorter date ranges
|
- Schwab: `investments.account_id` column requires migration — run `scripts/add_investment_account.py` once after deploy
|
||||||
- pdfplumber must be installed: `pip install pdfplumber==0.11.4`
|
- Schwab: after first connect, run "Balance & Positions" to populate investments; then re-sync if holdings were already added manually (they will be updated to link to the account)
|
||||||
- Teller: development environment only; requires cert/key from Teller Dashboard
|
- Teller: development environment only; requires cert/key from Teller Dashboard
|
||||||
|
- MySQL does not support `NULLS LAST`; use `func.isnull(column)` for null-last ordering
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 16. Security Fixes Applied (session log)
|
## 18. Security Fixes Applied (session log)
|
||||||
|
|
||||||
| Date | Fix | File |
|
| Date | Fix | File |
|
||||||
|------|-----|------|
|
|------|-----|------|
|
||||||
@@ -511,23 +548,27 @@ TELLER_WEBHOOK_SECRET=your-webhook-secret
|
|||||||
| 2026-06 | CSRF token added to bank import AJAX parse request | bank_import/index.html |
|
| 2026-06 | CSRF token added to bank import AJAX parse request | bank_import/index.html |
|
||||||
| 2026-06 | Receipt sub-forms moved outside `#txnForm` (nested-form bug) | transactions/form.html |
|
| 2026-06 | Receipt sub-forms moved outside `#txnForm` (nested-form bug) | transactions/form.html |
|
||||||
| 2026-06 | Drop zone file input moved outside overlay (blocked account select) | bank_import/index.html |
|
| 2026-06 | Drop zone file input moved outside overlay (blocked account select) | bank_import/index.html |
|
||||||
|
| 2026-06 | Teller balance refresh uses `ledger` for credit cards (not `available`) | teller.py |
|
||||||
|
| 2026-06 | Schwab OAuth `state` param added to auth URL (state mismatch fix) | schwab_service.py |
|
||||||
|
| 2026-06 | Schwab uses `hashValue` (not raw account number) in API paths | schwab.py, schwab_service.py |
|
||||||
|
| 2026-06 | `next` redirect params validated to start with `/` (no open redirect) | teller.py, schwab.py |
|
||||||
|
| 2026-06 | Teller income/expense type corrected (positive = income) | teller_service.py |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 17. To-Do / Roadmap
|
## 19. To-Do / Roadmap
|
||||||
|
|
||||||
### High priority
|
### High priority
|
||||||
- [ ] **Mobile responsiveness pass** — sidebar auto-collapses on mobile; tables scroll horizontally
|
- [ ] **Mobile responsiveness pass** — sidebar auto-collapses on mobile; tables scroll horizontally
|
||||||
- [ ] **Empty-state messages** — transactions, budgets, goals, investments pages when no data
|
|
||||||
- [ ] **Budget alerts** — email/Twilio SMS when category spending hits 80% / 100%
|
- [ ] **Budget alerts** — email/Twilio SMS when category spending hits 80% / 100%
|
||||||
|
|
||||||
### Medium priority
|
### Medium priority
|
||||||
- [ ] **Pagination info** — show "Page X of Y" on AI history and other paginated pages
|
- [ ] **Pagination info** — show "Page X of Y" on AI history and other paginated pages
|
||||||
- [ ] **PDF export memory** — stream CSV/Excel exports for users with large transaction history
|
- [ ] **PDF export memory** — stream CSV/Excel exports for users with large transaction history
|
||||||
- [ ] **Receipt MIME validation** — validate file magic bytes server-side, not just extension
|
- [ ] **Receipt MIME validation** — validate file magic bytes server-side, not just extension
|
||||||
- [ ] **Teller multi-account sync** — sync all mapped accounts in sequence (currently syncs first only)
|
|
||||||
- [ ] **OCR ownership check** — verify re-extracted filename belongs to current user's transaction
|
- [ ] **OCR ownership check** — verify re-extracted filename belongs to current user's transaction
|
||||||
- [ ] **Bank import progress** — show per-row import progress for large statement files
|
- [ ] **Bank import progress** — show per-row import progress for large statement files
|
||||||
|
- [ ] **Schwab IRA account type** — map Schwab IRA account type to `investment` in ACCOUNT_TYPE_MAP
|
||||||
|
|
||||||
### Low priority / future
|
### Low priority / future
|
||||||
- [ ] iOS companion app
|
- [ ] iOS companion app
|
||||||
@@ -535,3 +576,4 @@ TELLER_WEBHOOK_SECRET=your-webhook-secret
|
|||||||
- [ ] Bank statement PDF: table-extraction fallback (pdfplumber tables API) before Groq call
|
- [ ] Bank statement PDF: table-extraction fallback (pdfplumber tables API) before Groq call
|
||||||
- [ ] Investment price history chart per holding
|
- [ ] Investment price history chart per holding
|
||||||
- [ ] Dark mode toggle
|
- [ ] Dark mode toggle
|
||||||
|
- [ ] Schwab auto-sync on schedule (currently manual only)
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# PFM — Personal Finance Manager
|
# PFM — Personal Finance Manager
|
||||||
|
|
||||||
A self-hosted personal finance web application. Track income, expenses, investments, budgets, and savings goals. AI-powered financial assistant and receipt OCR built in.
|
A self-hosted personal finance web application. Track income, expenses, investments, budgets, and savings goals. AI-powered financial assistant and receipt OCR built in. Sync bank accounts via Teller or Schwab.
|
||||||
|
|
||||||
**Live at:** https://pfm.ngodanguyen.tech
|
**Live at:** https://pfm.ngodanguyen.tech
|
||||||
|
|
||||||
@@ -23,8 +23,10 @@ A self-hosted personal finance web application. Track income, expenses, investme
|
|||||||
13. [Recurring Transactions](#recurring-transactions)
|
13. [Recurring Transactions](#recurring-transactions)
|
||||||
14. [CSV Import](#csv-import)
|
14. [CSV Import](#csv-import)
|
||||||
15. [Receipt OCR](#receipt-ocr)
|
15. [Receipt OCR](#receipt-ocr)
|
||||||
16. [USD → VND Rate Widget](#usd--vnd-rate-widget)
|
16. [Teller Bank Sync](#teller-bank-sync)
|
||||||
17. [Keyboard Shortcuts & Tips](#keyboard-shortcuts--tips)
|
17. [Schwab Bank Sync](#schwab-bank-sync)
|
||||||
|
18. [USD → VND Rate Widget](#usd--vnd-rate-widget)
|
||||||
|
19. [Keyboard Shortcuts & Tips](#keyboard-shortcuts--tips)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -33,15 +35,17 @@ A self-hosted personal finance web application. Track income, expenses, investme
|
|||||||
| Module | What it does |
|
| Module | What it does |
|
||||||
|--------|-------------|
|
|--------|-------------|
|
||||||
| **Dashboard** | Net worth, cash flow chart, budget gauges, AI insight, USD/VND rate |
|
| **Dashboard** | Net worth, cash flow chart, budget gauges, AI insight, USD/VND rate |
|
||||||
| **Transactions** | Income + expense entry, transfer, filter, search, receipt upload |
|
| **Transactions** | Income + expense entry, transfer, filter, search, inline category change, receipt upload |
|
||||||
| **Accounts** | Multiple bank/cash/credit accounts, auto-calculated balances |
|
| **Accounts** | Multiple bank/cash/credit accounts, auto-calculated or provider-synced balances |
|
||||||
| **Categories** | Custom expense + income categories with color and icon |
|
| **Categories** | Custom expense + income categories with color and icon |
|
||||||
| **Budgets** | Monthly spending limits per category with progress tracking |
|
| **Budgets** | Monthly spending limits per category with progress tracking |
|
||||||
| **Goals** | Savings goals with contribution tracking and projected completion |
|
| **Goals** | Savings goals with contribution tracking and projected completion |
|
||||||
| **Investments** | Stock/ETF/crypto/real estate portfolio with live price fetch |
|
| **Investments** | Stock/ETF/crypto/real estate portfolio, per-account view (Individual vs Roth IRA), live price fetch |
|
||||||
| **AI Assistant** | Chat with your finances via Groq (free), daily auto-insight |
|
| **AI Assistant** | Chat with your finances via Groq (free), daily auto-insight |
|
||||||
| **Receipt OCR** | Drag-drop a receipt image — AI extracts and fills the form |
|
| **Receipt OCR** | Drag-drop a receipt image — AI extracts and fills the form |
|
||||||
| **Reports** | Monthly/quarterly/yearly summaries, net worth history, PDF/CSV/Excel export |
|
| **Reports** | Monthly/quarterly/yearly summaries, net worth history, PDF/CSV/Excel export |
|
||||||
|
| **Teller** | Live US bank account sync (mTLS), balance refresh, transaction import |
|
||||||
|
| **Schwab** | Schwab brokerage sync — balance, stock/ETF positions, transaction import |
|
||||||
| **Settings** | Profile, currency, recurring rules, CSV import, password |
|
| **Settings** | Profile, currency, recurring rules, CSV import, password |
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -56,14 +60,14 @@ A self-hosted personal finance web application. Track income, expenses, investme
|
|||||||
|
|
||||||
### Recommended Setup Order
|
### Recommended Setup Order
|
||||||
|
|
||||||
1. **Add accounts** — add your bank accounts, cash wallet, and credit cards first (Settings sidebar → Accounts)
|
1. **Add accounts** — add your bank accounts, cash wallet, and credit cards first
|
||||||
2. **Review categories** — default categories are already seeded; add custom ones if needed
|
2. **Review categories** — default categories are already seeded; add custom ones if needed
|
||||||
3. **Set your currency** — go to Settings → Profile and set your app currency (USD, VND, etc.)
|
3. **Set your currency** — go to Settings → Profile and set your app currency
|
||||||
4. **Add transactions** — start entering income and expenses
|
4. **Connect bank sync** — connect Teller (US banks) or Schwab for automatic data
|
||||||
5. **Set budgets** — once you have categories, set monthly limits
|
5. **Add transactions** — start entering income and expenses (or import via bank sync)
|
||||||
6. **Create goals** — add savings goals and start contributing
|
6. **Set budgets** — once you have categories, set monthly limits
|
||||||
7. **Add investments** — track your portfolio holdings
|
7. **Create goals** — add savings goals and start contributing
|
||||||
8. **Set recurring rules** — automate repeating income/expenses
|
8. **Add investments** — manually or via Schwab sync
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -72,129 +76,80 @@ A self-hosted personal finance web application. Track income, expenses, investme
|
|||||||
The dashboard is the home screen — accessible from the sidebar or by clicking the PFM logo.
|
The dashboard is the home screen — accessible from the sidebar or by clicking the PFM logo.
|
||||||
|
|
||||||
### Period Selector
|
### Period Selector
|
||||||
Three buttons in the top-right of the topbar:
|
Three buttons in the top-right: **This Month**, **Last Month**, **Custom** (date range picker).
|
||||||
- **This Month** — income/expenses for the current calendar month
|
|
||||||
- **Last Month** — previous calendar month
|
|
||||||
- **Custom** — opens a date picker modal; choose any start and end date
|
|
||||||
|
|
||||||
The period affects the summary cards and recent transactions feed. Charts always show fixed windows (6 months for cash flow, etc.).
|
|
||||||
|
|
||||||
### Summary Cards
|
### Summary Cards
|
||||||
Four cards across the top:
|
- **Income** — total income for the selected period
|
||||||
- **Income** — total income for the selected period (green)
|
- **Expenses** — total expenses for the selected period
|
||||||
- **Expenses** — total expenses for the selected period (red)
|
- **Net Cash Flow** — income minus expenses
|
||||||
- **Net Cash Flow** — income minus expenses; green if positive, red if negative
|
|
||||||
- **Net Worth** — total assets minus liabilities across all accounts
|
- **Net Worth** — total assets minus liabilities across all accounts
|
||||||
|
|
||||||
### Cash Flow Chart
|
### Cash Flow Chart
|
||||||
Bar chart showing the last 6 months of income (green bars) vs expenses (red bars). Hover over bars to see exact amounts.
|
Bar chart showing the last 6 months of income (green) vs expenses (red).
|
||||||
|
|
||||||
### Top Spending
|
|
||||||
Right of the cash flow chart — shows your top 5 expense categories for the period with horizontal progress bars. Widest bar = highest spending.
|
|
||||||
|
|
||||||
### USD → VND Widget
|
|
||||||
Dark card below the top spending section. Shows today's USD to VND exchange rate for reference. Click the widget to reveal a 30-day trend chart. Click the ↻ button to force-refresh the rate without reloading the page. See [USD → VND Rate Widget](#usd--vnd-rate-widget) for details.
|
|
||||||
|
|
||||||
### AI Daily Insight
|
|
||||||
Dark card above the accounts section. Shows a 3–5 sentence AI-generated summary of your finances for the day. Generated automatically at midnight. Click "Open AI →" to go to the full chat interface.
|
|
||||||
|
|
||||||
### Accounts Panel
|
### Accounts Panel
|
||||||
Left of the recent transactions — lists all active accounts with their current balance. Green = positive, red = negative (credit card debt). Click "+" to add a new account.
|
Lists all active accounts with current balances. Credit cards show the **Amount Owed** (positive number) instead of a raw negative balance. Green = positive balance, red = amount owed.
|
||||||
|
|
||||||
### Recent Transactions
|
### AI Daily Insight
|
||||||
Last 8 income and expense transactions. Click "View all" to go to the full transaction list.
|
Auto-generated summary of your finances. Click "Open AI →" for the full chat interface.
|
||||||
|
|
||||||
### Quick Add Buttons
|
### USD → VND Widget
|
||||||
Two buttons in the page header:
|
Reference-only exchange rate. Click ↻ to refresh. Click the widget to show a 30-day chart.
|
||||||
- **+Income** (green) — opens new income form
|
|
||||||
- **+Expense** (red) — opens new expense form
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Transactions
|
## Transactions
|
||||||
|
|
||||||
### Viewing Transactions
|
### Viewing Transactions
|
||||||
|
Navigate via **Transactions** in the sidebar. Two tabs: **Expenses** and **Income**.
|
||||||
Navigate via **Transactions** in the sidebar. Two tabs at the top:
|
|
||||||
- **Expenses** — shows expense transactions with a count badge
|
|
||||||
- **Income** — shows income transactions with a count badge
|
|
||||||
|
|
||||||
Transactions are ordered newest first, paginated at 30 per page.
|
|
||||||
|
|
||||||
### Filtering
|
### Filtering
|
||||||
|
Search bar, category dropdown, account dropdown, date range. Click ✕ to clear all filters.
|
||||||
A filter bar appears below the tabs:
|
|
||||||
- **Search** — matches against the description field (case-insensitive)
|
|
||||||
- **Category** — filter by a single category
|
|
||||||
- **Account** — filter by a single account
|
|
||||||
- **From / To** — date range filter
|
|
||||||
- Click the magnifying glass button to apply filters
|
|
||||||
- Click the ✕ button to clear all filters and reset to defaults
|
|
||||||
|
|
||||||
### Adding a Transaction
|
### Adding a Transaction
|
||||||
|
Use the sidebar links (Add Income / Add Expense), the dashboard quick-add buttons, or the topbar buttons on the transaction list.
|
||||||
|
|
||||||
**From the sidebar:** click "Add Income" or "Add Expense"
|
### Inline Category Change
|
||||||
**From the dashboard:** use the +Income / +Expense buttons
|
On any transaction row, click the **category dropdown** directly in the table to change the category without opening the edit form. The change saves automatically.
|
||||||
**From the transaction list:** use the topbar buttons
|
|
||||||
|
|
||||||
#### New Expense / Income Form Fields
|
### Editing and Deleting
|
||||||
|
Click **Edit** to open the full form (same as adding, plus receipt management). Click **Del** to delete permanently.
|
||||||
|
|
||||||
| Field | Required | Notes |
|
### Transfers
|
||||||
|-------|----------|-------|
|
Click **Transfer** in the topbar. Creates a single transfer between two accounts. Transfers are excluded from income/expense totals.
|
||||||
| Description | Yes | What the transaction was for |
|
|
||||||
| Amount | Yes | Positive number only |
|
|
||||||
| Date | Yes | Defaults to today |
|
|
||||||
| Account | Yes | Which account to debit/credit |
|
|
||||||
| Category | No | Helps with budgets and reports |
|
|
||||||
| Notes | No | Free text, up to 500 characters |
|
|
||||||
|
|
||||||
#### AI Receipt Scanner (on new expense form)
|
|
||||||
A purple panel sits above the form. Drop a receipt image onto it (or click to browse). The AI extracts the amount, date, merchant name, and category and fills the form fields automatically. Review the filled values before saving — highlighted fields (green flash) show what was auto-filled. See [Receipt OCR](#receipt-ocr) for details.
|
|
||||||
|
|
||||||
### Editing a Transaction
|
|
||||||
|
|
||||||
Click **Edit** on any transaction row. Same form as adding, with an additional receipt section:
|
|
||||||
- If a receipt is attached: shows filename with a **Re-extract** button (re-runs OCR on the stored file) and a **Remove** button
|
|
||||||
- If no receipt: shows an upload field; selecting an image file auto-triggers OCR
|
|
||||||
|
|
||||||
### Deleting a Transaction
|
|
||||||
|
|
||||||
Click **Del** on any transaction row. A confirmation prompt appears. Deletion is permanent and account balances are recalculated immediately.
|
|
||||||
|
|
||||||
### Transfers Between Accounts
|
|
||||||
|
|
||||||
Click **Transfer** in the topbar. Select source account, destination account, amount, and date. This creates a single transfer transaction that debits the source and credits the destination. Transfers do not appear on the Income or Expense tabs — they are excluded from income/expense totals.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Accounts
|
## Accounts
|
||||||
|
|
||||||
### Adding an Account
|
### Account Types
|
||||||
|
checking / savings / cash / credit_card / crypto / investment / other
|
||||||
|
|
||||||
Settings sidebar → **Accounts** → **New Account** (or from dashboard Accounts panel → "+").
|
### Balance Sources
|
||||||
|
- **Manually managed accounts** — balance is computed from all transactions (income − expenses − transfers out + transfers in). Updates automatically after every transaction.
|
||||||
|
- **Teller-linked accounts** — balance comes from Teller's live API. Shown with a blue **Teller** badge. Use the action buttons on the account card.
|
||||||
|
- **Schwab-linked accounts** — balance comes from Schwab's snapshot sync. Shown with a green **Schwab** badge. Use the action buttons on the account card.
|
||||||
|
|
||||||
| Field | Notes |
|
### Teller Account Actions (on account card)
|
||||||
|-------|-------|
|
| Button | What it does |
|
||||||
| Account Name | e.g. "Chase Checking", "Cash Wallet", "Visa Card" |
|
|--------|-------------|
|
||||||
| Type | checking / savings / cash / credit_card / crypto / investment / other |
|
| **Refresh** | Pulls the live balance from Teller API (AJAX, no page reload) |
|
||||||
| Color | Click a color swatch — used for visual identification |
|
| **Sync** | Opens transaction preview — import new transactions from this account |
|
||||||
| Icon | Click an icon swatch — appears throughout the app |
|
| **Reset** | Clears the sync cursor — next Sync will re-fetch the full 90-day history |
|
||||||
| Notes | Optional description |
|
|
||||||
|
|
||||||
### Account Balances
|
### Schwab Account Actions (on account card)
|
||||||
|
| Button | What it does |
|
||||||
|
|--------|-------------|
|
||||||
|
| **Balance & Positions** | Fetches live balance + investment holdings from Schwab, updates immediately |
|
||||||
|
| **Transactions** | Opens transaction import preview for this account |
|
||||||
|
|
||||||
Balances are **automatically calculated** from all transactions linked to that account — you do not enter a balance manually. When you add a transaction, the account balance updates instantly.
|
### Credit Cards
|
||||||
|
Credit cards show two values:
|
||||||
|
- **Amount Owed** — how much you currently owe (positive number in red)
|
||||||
|
- **This Month** — expenses charged to the card this calendar month
|
||||||
|
|
||||||
**Formula:**
|
### Opening Balance
|
||||||
```
|
When creating a new account, enter the current balance in the **Opening Balance** field. For credit cards, enter a **negative number** to indicate debt (e.g. `-500` if you owe $500).
|
||||||
Balance = sum(income) - sum(expenses) - sum(transfers_out) + sum(transfers_in)
|
|
||||||
```
|
|
||||||
|
|
||||||
For a brand-new account with no transactions, balance is 0. To set a starting balance, add an income transaction dated your desired start date with the description "Opening balance."
|
|
||||||
|
|
||||||
### Removing an Account
|
|
||||||
|
|
||||||
Click the three-dot menu (⋯) on an account card → **Remove**. This is a soft delete — the account is hidden but its transactions remain in the database and still affect totals. You cannot permanently delete an account with transactions.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -202,73 +157,22 @@ Click the three-dot menu (⋯) on an account card → **Remove**. This is a soft
|
|||||||
|
|
||||||
Navigate via the sidebar footer → **Categories**.
|
Navigate via the sidebar footer → **Categories**.
|
||||||
|
|
||||||
Two panels: **Expense Categories** (left) and **Income Categories** (right).
|
**Default Expense (14):** Housing, Food & Dining, Transport, Utilities, Health, Entertainment, Shopping, Education, Insurance, Personal Care, Travel, Subscriptions, Gifts, Other
|
||||||
|
|
||||||
### Default Categories
|
**Default Income (7):** Salary, Freelance, Business, Investment, Rental, Gift Received, Other Income
|
||||||
|
|
||||||
**Expense (14):** Housing, Food & Dining, Transport, Utilities, Health, Entertainment, Shopping, Education, Insurance, Personal Care, Travel, Subscriptions, Gifts, Other
|
System categories (grey badge) cannot be deleted but color/icon can be changed. Custom categories can be fully edited and deleted.
|
||||||
|
|
||||||
**Income (7):** Salary, Freelance, Business, Investment, Rental, Gift Received, Other Income
|
|
||||||
|
|
||||||
System categories (marked with a grey "system" badge) cannot be deleted but can be edited (color and icon only — name is protected).
|
|
||||||
|
|
||||||
### Adding a Category
|
|
||||||
|
|
||||||
Click **+Expense** or **+Income** in the topbar. Set a name, type, color (click a swatch), and icon (click an icon swatch).
|
|
||||||
|
|
||||||
**Category type options:**
|
|
||||||
- **Expense** — appears only in expense transaction dropdown
|
|
||||||
- **Income** — appears only in income transaction dropdown
|
|
||||||
- **Both** — appears in both dropdowns
|
|
||||||
|
|
||||||
### Editing a Category
|
|
||||||
|
|
||||||
Click **Edit** next to any category. Color and icon can always be changed. Name can only be changed for non-system categories.
|
|
||||||
|
|
||||||
### Deleting a Category
|
|
||||||
|
|
||||||
Click **Del** next to a non-system category. Categories with existing transactions cannot be hard-deleted — they are deactivated (hidden) instead. A warning message explains this.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Budgets
|
## Budgets
|
||||||
|
|
||||||
Navigate via **Budgets** in the sidebar.
|
Navigate via **Budgets** in the sidebar. Use ◀ ▶ to navigate months.
|
||||||
|
|
||||||
### Month Navigation
|
- Set monthly limits per expense category
|
||||||
|
- Progress bars: green (< 80%) → amber (80–99%) → red (100%+)
|
||||||
Use the **◀** and **▶** arrows to move between months. The current month is the default.
|
- Rollover toggle: unspent budget carries forward to next month
|
||||||
|
- **Copy from previous month** — duplicate last month's budgets in one click
|
||||||
### Setting a Budget
|
|
||||||
|
|
||||||
Click **Add Budget** in the topbar (or "Set Budget" next to an unbudgeted category).
|
|
||||||
|
|
||||||
| Field | Notes |
|
|
||||||
|-------|-------|
|
|
||||||
| Category | Expense categories only |
|
|
||||||
| Monthly Limit | Maximum spend for this category this month |
|
|
||||||
| Roll over unused amount | If checked, unspent budget carries forward to next month |
|
|
||||||
|
|
||||||
### Budget Progress Bars
|
|
||||||
|
|
||||||
Each category row shows:
|
|
||||||
- Category name and icon
|
|
||||||
- Amount spent (colored by status)
|
|
||||||
- Budget limit
|
|
||||||
- Progress bar: green (< 80%) → amber (80–99%) → red (100%+)
|
|
||||||
- Remaining amount (or over-budget amount in red)
|
|
||||||
|
|
||||||
### Unbudgeted Spending
|
|
||||||
|
|
||||||
Categories with spending but no budget appear at the bottom of the table with a yellow "no budget" badge and a "Set Budget" button.
|
|
||||||
|
|
||||||
### Copy from Previous Month
|
|
||||||
|
|
||||||
Click **Copy from YYYY-MM** button (top right or empty state) to duplicate all budget entries from the previous month. Existing budgets for the current month are not overwritten — only missing ones are created.
|
|
||||||
|
|
||||||
### Editing / Deleting a Budget
|
|
||||||
|
|
||||||
Click **Edit** or **Del** on any budget row. Deleting a budget does not delete the transactions — it only removes the limit.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -276,49 +180,10 @@ Click **Edit** or **Del** on any budget row. Deleting a budget does not delete t
|
|||||||
|
|
||||||
Navigate via **Goals** in the sidebar.
|
Navigate via **Goals** in the sidebar.
|
||||||
|
|
||||||
### Emergency Fund Tracker
|
- Create goals with target amount, target date, color, and icon
|
||||||
|
- Add contributions with dates and notes
|
||||||
A yellow card at the top (appears once you have 3+ months of expense data). Shows:
|
- Auto-completes when target is reached
|
||||||
- Average monthly expense (last 3 months)
|
- **Emergency Fund Tracker** — shows how many months your liquid assets cover at current spending
|
||||||
- Liquid assets (checking + savings + cash accounts combined)
|
|
||||||
- 3-month target and 6-month target with progress bars
|
|
||||||
- Months covered: how long your liquid assets would last at current spending
|
|
||||||
|
|
||||||
### Creating a Goal
|
|
||||||
|
|
||||||
Click **New Goal** in the topbar.
|
|
||||||
|
|
||||||
| Field | Notes |
|
|
||||||
|-------|-------|
|
|
||||||
| Goal Name | e.g. "Vacation Fund", "Emergency Fund", "New Laptop" |
|
|
||||||
| Target Amount | How much you want to save |
|
|
||||||
| Target Date | Optional deadline |
|
|
||||||
| Linked Account | Optional — associates the goal with a specific account |
|
|
||||||
| Description | Optional notes |
|
|
||||||
| Color | Visual identifier (circle swatches) |
|
|
||||||
| Icon | Visual identifier (icon swatches) |
|
|
||||||
|
|
||||||
### Goal Cards
|
|
||||||
|
|
||||||
Each active goal shows:
|
|
||||||
- Progress bar (fills with goal color)
|
|
||||||
- Current amount saved / target amount
|
|
||||||
- Completion percentage
|
|
||||||
- Projected completion date (calculated from average monthly contribution history — only shown after 2+ contributions)
|
|
||||||
- Description (if set)
|
|
||||||
- **Add Contribution** button (styled in goal color)
|
|
||||||
|
|
||||||
### Adding a Contribution
|
|
||||||
|
|
||||||
Click **Add Contribution** on a goal card or from the three-dot menu. Enter amount, date, and optional notes. When the total reaches the target, the goal auto-completes and moves to the "Completed Goals" section.
|
|
||||||
|
|
||||||
### Contribution History
|
|
||||||
|
|
||||||
Three-dot menu → **History** — shows all contributions with dates and amounts, plus the projected completion date. Individual contributions can be deleted (recalculates goal total and un-completes if needed).
|
|
||||||
|
|
||||||
### Completed Goals
|
|
||||||
|
|
||||||
Shown at the bottom of the page in a compact table. Last 5 completed goals displayed.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -327,62 +192,25 @@ Shown at the bottom of the page in a compact table. Last 5 completed goals displ
|
|||||||
Navigate via **Investments** in the sidebar.
|
Navigate via **Investments** in the sidebar.
|
||||||
|
|
||||||
### Portfolio Overview
|
### Portfolio Overview
|
||||||
|
Summary cards: Total Value, Total Cost, Unrealized P&L, Return %. Allocation doughnut chart by asset type.
|
||||||
|
|
||||||
Summary cards at the top:
|
### Per-Account View
|
||||||
- **Total Value** — current market value of all holdings
|
If you have investments in multiple accounts (e.g. Schwab Individual + Schwab Roth IRA), the holdings are displayed in **separate sections**, one per account. Each section shows the account name, total account value, and the holdings table for that account. The allocation chart sidebar also shows a "By Account" balance breakdown.
|
||||||
- **Total Cost** — total amount invested (cost basis)
|
|
||||||
- **Unrealized P&L** — gain or loss vs cost basis
|
|
||||||
- **Return** — percentage return
|
|
||||||
|
|
||||||
**Allocation chart** — doughnut chart showing portfolio split by asset type (stock, ETF, crypto, real estate, bond, cash, other). Each slice is color-coded. Click a row in the legend for exact values.
|
### Syncing from Schwab
|
||||||
|
Click **Sync Schwab** in the topbar to pull the latest holdings and balances for all mapped Schwab accounts. After syncing, each holding is associated with its source account so Individual and Roth IRA positions stay separate even if they hold the same tickers.
|
||||||
|
|
||||||
**Holdings table** — all active holdings. Click any row to go to the detail page.
|
### Adding a Holding Manually
|
||||||
|
Click **Add Holding** in the topbar. After saving, go to the detail page to record buy transactions.
|
||||||
|
|
||||||
### Adding a Holding
|
### Ticker Format
|
||||||
|
- US Stocks: `AAPL`, `MSFT`
|
||||||
Click **Add Holding** in the topbar.
|
- ETFs: `VOO`, `QQQ`
|
||||||
|
- Crypto: `BTC-USD`, `ETH-USD`
|
||||||
| Field | Notes |
|
- Other markets: Yahoo Finance suffix (e.g. `VIC.VN`)
|
||||||
|-------|-------|
|
|
||||||
| Asset Name | Full name, e.g. "Apple Inc.", "Bitcoin" |
|
|
||||||
| Asset Type | stock / ETF / crypto / real_estate / bond / cash / other |
|
|
||||||
| Ticker Symbol | Yahoo Finance format: AAPL, BTC-USD, ETH-USD, VNM. Click **Check** to verify |
|
|
||||||
|
|
||||||
After saving, you are taken to the detail page to record your first buy transaction.
|
|
||||||
|
|
||||||
### Ticker Format (Yahoo Finance)
|
|
||||||
- US Stocks: `AAPL`, `MSFT`, `GOOGL`
|
|
||||||
- ETFs: `VOO`, `QQQ`, `VTI`
|
|
||||||
- Crypto: `BTC-USD`, `ETH-USD`, `BNB-USD`
|
|
||||||
- Vietnamese stocks: `VNM` (VanEck Vietnam ETF on NYSE)
|
|
||||||
- Other markets: use Yahoo Finance suffix, e.g. `VIC.VN` for Vingroup on HOSE
|
|
||||||
|
|
||||||
### Holding Detail Page
|
|
||||||
|
|
||||||
Shows the holding summary (shares, avg cost, current price, market value, P&L) and transaction history. Current price timestamp is shown below the price.
|
|
||||||
|
|
||||||
### Recording a Transaction
|
|
||||||
|
|
||||||
On the detail page, click **Add Transaction** in the topbar.
|
|
||||||
|
|
||||||
| Transaction Type | What it does |
|
|
||||||
|-----------------|-------------|
|
|
||||||
| **Buy** | Adds shares, increases cost basis |
|
|
||||||
| **Sell** | Reduces shares, adjusts cost basis (FIFO) |
|
|
||||||
| **Dividend** | Records a dividend payment (does not change shares) |
|
|
||||||
| **Split** | Adds shares without changing cost (stock split) |
|
|
||||||
|
|
||||||
The **↓ button** next to the price field fetches the current live price from Yahoo Finance and fills it in. The "Estimated Total" preview updates as you type shares and price.
|
|
||||||
|
|
||||||
After saving, shares and average cost basis are automatically recalculated from the full transaction history.
|
|
||||||
|
|
||||||
### Refreshing Prices
|
### Refreshing Prices
|
||||||
|
Click **Refresh Prices** in the topbar. Prices also auto-update daily at 4PM (weekdays).
|
||||||
Click **Refresh Prices** in the topbar on the portfolio page to fetch current prices for all holdings with ticker symbols. Prices are also automatically updated daily at 4PM (weekdays) via a background job.
|
|
||||||
|
|
||||||
### Removing a Holding
|
|
||||||
|
|
||||||
On the detail page, click **Remove** (top right of the holding card). This is a soft delete — the holding is hidden but transaction history is preserved.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -390,52 +218,15 @@ On the detail page, click **Remove** (top right of the holding card). This is a
|
|||||||
|
|
||||||
Navigate via **AI Assistant** in the sidebar.
|
Navigate via **AI Assistant** in the sidebar.
|
||||||
|
|
||||||
### Chat Interface
|
The AI has access to the last 90 days of transactions, budget status, goals, investments, and net worth. No personal names or identifying details are sent to Groq — only aggregated financial figures.
|
||||||
|
|
||||||
Type a question in the text box at the bottom and press **Enter** (or click the send button). The response streams word-by-word in real time.
|
- Type and press **Enter** to send; **Shift+Enter** for new line
|
||||||
|
- Eight quick-question suggestion buttons on the right
|
||||||
**Shift+Enter** adds a new line without sending.
|
- **Daily Insight** — auto-generated summary at midnight; click **Generate Now** to create on demand
|
||||||
|
- **Chat History** button — all past responses with timestamps
|
||||||
The AI has access to:
|
|
||||||
- Last 90 days of transactions (description, category, amount, date)
|
|
||||||
- Current month income/expense totals and budget status
|
|
||||||
- Active savings goals with progress
|
|
||||||
- Investment portfolio summary
|
|
||||||
- Net worth
|
|
||||||
|
|
||||||
No personal names, account names, or identifying details are sent to Groq — only aggregated financial figures.
|
|
||||||
|
|
||||||
### Suggested Questions
|
|
||||||
|
|
||||||
Eight suggestion buttons appear on the right panel. Click any to fill the chat input:
|
|
||||||
- "Where did I overspend this month?"
|
|
||||||
- "How is my budget looking?"
|
|
||||||
- "Am I on track for my goals?"
|
|
||||||
- "What's my biggest expense category?"
|
|
||||||
- "Summarize my finances"
|
|
||||||
- "How can I save more?"
|
|
||||||
- "What's my net worth trend?"
|
|
||||||
- "Review my investments"
|
|
||||||
|
|
||||||
### Daily Insight
|
|
||||||
|
|
||||||
A dark card on the right shows today's auto-generated insight (3–5 sentences covering spending, budget alerts, and a tip). Generated automatically at midnight. If not yet generated for today, click **Generate Now**. Click **Regenerate** to get a fresh one.
|
|
||||||
|
|
||||||
### Chat History
|
|
||||||
|
|
||||||
Click **History** in the topbar. Shows all past chat responses and daily summaries with timestamps and token counts. Paginated at 20 per page.
|
|
||||||
|
|
||||||
### AI Model
|
### AI Model
|
||||||
|
Change in Settings → Profile → AI Model: `llama-3.3-70b-versatile` (best quality) or `llama-3.1-8b-instant` (faster).
|
||||||
The model can be changed in Settings → Profile → AI Model:
|
|
||||||
- `llama-3.3-70b-versatile` — best quality, slightly slower
|
|
||||||
- `llama-3.1-8b-instant` — faster, slightly less detailed
|
|
||||||
|
|
||||||
### Error States
|
|
||||||
|
|
||||||
- **"AI assistant is not configured"** — GROQ_API_KEY is missing from .env
|
|
||||||
- **"Rate limit reached"** — too many requests; wait a moment and retry
|
|
||||||
- **"Invalid Groq API key"** — check the key at console.groq.com
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -443,107 +234,32 @@ The model can be changed in Settings → Profile → AI Model:
|
|||||||
|
|
||||||
Navigate via **Reports** in the sidebar.
|
Navigate via **Reports** in the sidebar.
|
||||||
|
|
||||||
### Report Types
|
Four tabs: **Monthly**, **Quarterly**, **Yearly**, **Tax Year**.
|
||||||
|
|
||||||
Four tabs at the top:
|
Export buttons in the topbar: **CSV** (plain text), **Excel** (color-coded, formatted), **PDF** (printable report).
|
||||||
|
|
||||||
**Monthly** — income, expenses, net, savings rate for a single month. Bar chart + expense doughnut.
|
**Snapshot Now** — saves today's net worth to the history chart.
|
||||||
|
|
||||||
**Quarterly** — same metrics for a quarter (Q1–Q4), plus monthly breakdown bars within the quarter.
|
|
||||||
|
|
||||||
**Yearly** — full year summary with monthly breakdown, average monthly income/expense.
|
|
||||||
|
|
||||||
**Tax Year** — income by source and expenses by category for a full calendar year, plus a full list of all income transactions for the year. Useful for tax preparation.
|
|
||||||
|
|
||||||
### Selecting a Period
|
|
||||||
|
|
||||||
Use the year/month/quarter dropdowns next to the period tabs and click **Go**.
|
|
||||||
|
|
||||||
### Charts
|
|
||||||
|
|
||||||
**Period chart** — bar chart of income vs expenses for the selected period (or monthly breakdown for quarterly/yearly).
|
|
||||||
|
|
||||||
**Expense breakdown** — doughnut chart of expenses by category. Hover for exact amounts. Top 5 categories listed below with amounts.
|
|
||||||
|
|
||||||
**Net worth history** — line chart of net worth over time (requires monthly snapshots to be saved). Dashed line shows total assets. Click **Snapshot Now** to save today's values.
|
|
||||||
|
|
||||||
**Category spending trends** — line chart of top 6 expense categories over the last 6 months. Useful for spotting trends.
|
|
||||||
|
|
||||||
### Exporting
|
|
||||||
|
|
||||||
Three export buttons in the topbar:
|
|
||||||
|
|
||||||
| Button | Format | Contents |
|
|
||||||
|--------|--------|----------|
|
|
||||||
| **CSV** | `.csv` | All transactions for the selected period, plain text |
|
|
||||||
| **Excel** | `.xlsx` | Color-coded rows (green=income, red=expense), formatted amounts, totals row |
|
|
||||||
| **PDF** | `.pdf` | Clean printable report with summary cards and expense breakdown table |
|
|
||||||
|
|
||||||
Exports use the period selected in the current report view (year + month/quarter).
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Settings
|
## Settings
|
||||||
|
|
||||||
Navigate via the gear icon at the bottom of the sidebar, or Settings → Profile.
|
Navigate via the gear icon at the bottom of the sidebar.
|
||||||
|
|
||||||
### Profile
|
| Section | What you can change |
|
||||||
|
|---------|-------------------|
|
||||||
| Setting | Notes |
|
| Profile | Display name, email, timezone, currency, AI model |
|
||||||
|---------|-------|
|
| Password | Current password + new password (min 6 chars) |
|
||||||
| Display Name | Shown in the topbar |
|
| Recurring | Create/edit/pause recurring transaction rules |
|
||||||
| Email | For reference only (no email features yet) |
|
| Import | Upload CSV to bulk-import transactions |
|
||||||
| Timezone | Used for scheduled jobs |
|
|
||||||
| Currency | App-wide currency code (USD, VND, EUR, etc.) |
|
|
||||||
| Currency Symbol | Auto-set when currency is changed |
|
|
||||||
| AI Model | Groq model for chat and daily insights |
|
|
||||||
|
|
||||||
Click **Save Profile** to apply. Currency and symbol changes take effect immediately throughout the app.
|
|
||||||
|
|
||||||
### Changing Password
|
|
||||||
|
|
||||||
Settings → Password. Requires current password + new password (min 6 characters) + confirmation.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Recurring Transactions
|
## Recurring Transactions
|
||||||
|
|
||||||
Navigate via Settings → **Recurring** (also accessible from Settings landing page).
|
Navigate via Settings → **Recurring**.
|
||||||
|
|
||||||
### What It Does
|
Rules auto-create transactions on a schedule. Frequencies: daily, weekly, biweekly, monthly, quarterly, yearly. **Run Now** button processes all overdue rules immediately.
|
||||||
|
|
||||||
Recurring rules automatically create transactions on a schedule. At 6AM daily, the system checks all active rules and creates any transactions that are due (including any missed occurrences if the server was down).
|
|
||||||
|
|
||||||
### Creating a Rule
|
|
||||||
|
|
||||||
Click **New Rule**.
|
|
||||||
|
|
||||||
| Field | Notes |
|
|
||||||
|-------|-------|
|
|
||||||
| Name | Label for the rule, e.g. "Monthly Rent" |
|
|
||||||
| Type | income or expense |
|
|
||||||
| Description | Used as the transaction description |
|
|
||||||
| Amount | Fixed amount each occurrence |
|
|
||||||
| Frequency | daily / weekly / biweekly / monthly / quarterly / yearly |
|
|
||||||
| Account | Which account to debit/credit |
|
|
||||||
| Category | Optional |
|
|
||||||
| Start Date | First occurrence date |
|
|
||||||
| End Date | Optional — leave blank for no end date |
|
|
||||||
|
|
||||||
### Managing Rules
|
|
||||||
|
|
||||||
The rules table shows all rules with their next run date, frequency, and status. Available actions:
|
|
||||||
- **Edit** — change any field (does not affect already-created transactions)
|
|
||||||
- **Pause / Enable** — temporarily disable without deleting
|
|
||||||
- **×** — delete the rule (already-created transactions are preserved)
|
|
||||||
|
|
||||||
### Run Now
|
|
||||||
|
|
||||||
Click **Run Now** in the topbar to immediately process all due rules. Useful after creating a new rule that has a past start date, or after the server was offline.
|
|
||||||
|
|
||||||
### Upcoming Preview
|
|
||||||
|
|
||||||
Right panel shows all upcoming recurring transactions for the next 30 days in date order, with amounts and type badges.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -551,140 +267,104 @@ Right panel shows all upcoming recurring transactions for the next 30 days in da
|
|||||||
|
|
||||||
Navigate via Settings → **Import**.
|
Navigate via Settings → **Import**.
|
||||||
|
|
||||||
### CSV Format
|
**Required columns:** `date`, `type` (income/expense), `description`, `amount`
|
||||||
|
|
||||||
```
|
**Optional:** `category`, `account`, `notes`
|
||||||
date,type,description,category,account,amount,notes
|
|
||||||
2025-01-15,expense,Groceries,Food & Dining,Checking,85.50,Weekly shop
|
|
||||||
2025-01-16,income,Salary,Salary,Checking,3000.00,
|
|
||||||
```
|
|
||||||
|
|
||||||
**Required columns:** `date`, `type`, `description`, `amount`
|
Process: upload → preview (with warnings for unmatched categories/accounts) → confirm.
|
||||||
|
|
||||||
**Optional columns:** `category`, `account`, `notes`
|
|
||||||
|
|
||||||
**Accepted date formats:** `YYYY-MM-DD`, `MM/DD/YYYY`, `DD/MM/YYYY`
|
|
||||||
|
|
||||||
**Type values:** must be exactly `income` or `expense` (lowercase)
|
|
||||||
|
|
||||||
### Import Process
|
|
||||||
|
|
||||||
1. Choose your CSV file
|
|
||||||
2. Select a **Default Account** — used when the account column is missing or the name doesn't match any of your accounts
|
|
||||||
3. Check **Skip duplicate transactions** (recommended) — skips rows where date + description + amount + type exactly match an existing transaction
|
|
||||||
4. Click **Preview Import**
|
|
||||||
5. Review the preview table:
|
|
||||||
- ⚠ yellow warning on category = category name not found (will be uncategorised)
|
|
||||||
- ⚠ yellow warning on account = account not found (will use default account or be unlinked)
|
|
||||||
6. Click **Confirm Import** to save all rows
|
|
||||||
|
|
||||||
Account and category matching is case-insensitive. If your CSV has `food & dining` it will match the `Food & Dining` system category.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Receipt OCR
|
## Receipt OCR
|
||||||
|
|
||||||
Receipt OCR uses the Groq `llama-4-scout` vision model to extract transaction data from a photo of a receipt.
|
On the new expense form: drag-drop or click the purple panel to upload a receipt image (JPG/PNG/GIF/WEBP, max 10MB). The AI extracts amount, date, merchant, and category and fills the form. Always review before saving.
|
||||||
|
|
||||||
### On New Expense Form
|
On the edit form: click **Re-extract** to re-run OCR on an already-attached receipt.
|
||||||
|
|
||||||
A purple panel with a dashed border appears above the transaction form:
|
---
|
||||||
|
|
||||||
1. **Drag and drop** a receipt image onto the panel, OR click the panel to browse for a file
|
## Teller Bank Sync
|
||||||
2. Accepted formats: JPG, PNG, GIF, WEBP (max 10MB — PDF not supported for OCR)
|
|
||||||
3. The panel shows a spinning animation while scanning
|
|
||||||
4. On success: the panel turns green and form fields flash green to show what was filled:
|
|
||||||
- **Description** ← merchant name (e.g. "McDonald's")
|
|
||||||
- **Amount** ← total from receipt
|
|
||||||
- **Date** ← date on receipt (falls back to today if not found)
|
|
||||||
- **Category** ← AI's best guess matched to your system categories
|
|
||||||
- **Notes** ← brief description
|
|
||||||
5. Review all fields before clicking Save — OCR is not perfect, especially on low-quality photos
|
|
||||||
6. You can drop another receipt to re-scan and overwrite the filled values
|
|
||||||
|
|
||||||
### On Edit Transaction Form
|
Teller connects US bank accounts using a secure mTLS connection.
|
||||||
|
|
||||||
**If a receipt is already attached:**
|
### Connecting
|
||||||
- A purple **Re-extract** button appears next to the receipt filename
|
1. Go to **Bank Connections** in the sidebar
|
||||||
- Click it to re-run OCR on the stored file and update the form fields
|
2. Click **Connect a Bank** — the Teller Connect modal opens
|
||||||
|
3. Select your bank and log in
|
||||||
|
4. After connecting, go to the **Map Accounts** page to link each Teller account to a PFM account
|
||||||
|
|
||||||
**If no receipt is attached:**
|
### Syncing Transactions
|
||||||
- An upload field appears
|
On the **Accounts** page, find your Teller-linked account (blue **Teller** badge) and click **Sync**. Review the transaction preview and confirm to import.
|
||||||
- Selecting a JPG/PNG/WEBP file automatically triggers OCR and opens the scanner panel
|
|
||||||
|
|
||||||
### Tips for Best OCR Results
|
### Refreshing Balance
|
||||||
|
Click **Refresh** on a Teller account card to pull the live balance from your bank. For checking/savings this shows the available balance; for credit cards it shows the amount owed.
|
||||||
|
|
||||||
- Use a well-lit, in-focus photo
|
### Disconnecting
|
||||||
- Make sure the total amount is clearly visible
|
Go to **Bank Connections** → **Disconnect** next to the institution. Imported transactions are kept.
|
||||||
- Flatten crumpled receipts before photographing
|
|
||||||
- Portrait orientation works better than landscape
|
|
||||||
- Higher resolution = better accuracy
|
|
||||||
- If OCR misreads the amount, correct it manually — amounts are the most important field
|
|
||||||
|
|
||||||
### What OCR Cannot Do
|
---
|
||||||
|
|
||||||
- Read multi-page receipts (only the uploaded image is processed)
|
## Schwab Bank Sync
|
||||||
- Handle PDF receipts (PDF format is excluded from OCR; it can still be uploaded as an attachment)
|
|
||||||
- Guarantee 100% accuracy — always review extracted values before saving
|
Schwab integration uses OAuth 2.0 to sync brokerage and IRA accounts.
|
||||||
|
|
||||||
|
### Connecting
|
||||||
|
1. Set `SCHWAB_CLIENT_ID`, `SCHWAB_CLIENT_SECRET`, `SCHWAB_REDIRECT_URI` in `.env`
|
||||||
|
2. Register the redirect URI in the Schwab developer portal
|
||||||
|
3. Go to **Schwab** in the sidebar → **Connect Schwab Account**
|
||||||
|
4. Authorize on Schwab's login page
|
||||||
|
5. On the **Map Accounts** page, link each Schwab account to a PFM account (or create new ones)
|
||||||
|
|
||||||
|
### Syncing Balance & Investment Positions
|
||||||
|
On the **Accounts** page, find your Schwab-linked account (green **Schwab** badge) and click **Balance & Positions**. This:
|
||||||
|
- Updates the account's balance to Schwab's total portfolio value
|
||||||
|
- Imports all stock/ETF/bond/fund holdings into the Investments section
|
||||||
|
- Associates each holding with the specific account (Individual vs Roth IRA stay separate)
|
||||||
|
|
||||||
|
You can also click **Sync Schwab** in the Investments page topbar to update all Schwab accounts at once.
|
||||||
|
|
||||||
|
### Syncing Transactions
|
||||||
|
Click **Transactions** on a Schwab account card. Review the preview and confirm to import.
|
||||||
|
|
||||||
|
### Per-Account Investment View
|
||||||
|
After syncing both Individual and Roth IRA accounts, the Investments page shows separate sections for each account — even if both hold the same ticker (e.g. AAPL appears in both sections independently).
|
||||||
|
|
||||||
|
### Disconnecting
|
||||||
|
Go to **Schwab** in the sidebar → **Disconnect**. Imported transactions and investment holdings are kept.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## USD → VND Rate Widget
|
## USD → VND Rate Widget
|
||||||
|
|
||||||
The dark widget on the dashboard shows the current USD to VND exchange rate. This is **for reference only** — it does not affect any transactions or calculations in the app.
|
Reference-only exchange rate on the dashboard. Does not affect any app calculations.
|
||||||
|
|
||||||
### How the Rate is Fetched
|
- Click ↻ to force-refresh without reloading the page
|
||||||
|
- Click the widget to show a 30-day trend chart
|
||||||
1. **Primary:** Yahoo Finance forex (`USDVND=X` via yfinance) — most reliable
|
- ⚠ indicator appears if the cached rate is more than a day old
|
||||||
2. **Fallback:** ExchangeRate API (`open.er-api.com`) — free, no key required
|
|
||||||
3. **Stale fallback:** Last known rate from database — shown with a ⚠ indicator
|
|
||||||
|
|
||||||
The rate is cached once per day. The daily cron job at 8AM always force-fetches a fresh rate.
|
|
||||||
|
|
||||||
### Refreshing Manually
|
|
||||||
|
|
||||||
Click the **↻** button (top-right of the widget) to force-fetch a fresh rate without reloading the page. The rate, date, and source label update in-place.
|
|
||||||
|
|
||||||
### 30-Day History Chart
|
|
||||||
|
|
||||||
Click anywhere on the widget (except the ↻ button) to toggle a compact line chart showing the rate trend over the last 30 days.
|
|
||||||
|
|
||||||
### Stale Indicator
|
|
||||||
|
|
||||||
If the cached rate is from a previous day and all live sources fail, a ⚠ symbol appears next to the date. This typically means the server has no internet access or the APIs are temporarily unavailable.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Keyboard Shortcuts & Tips
|
## Keyboard Shortcuts & Tips
|
||||||
|
|
||||||
### Navigation
|
### Navigation
|
||||||
- The sidebar collapses on desktop — click the ☰ button in the topbar to toggle. State is remembered across page loads.
|
- Sidebar collapses on desktop — click ☰ to toggle (state remembered)
|
||||||
- On mobile, the sidebar slides in as an overlay — tap anywhere outside to close it.
|
- On mobile, tap outside the sidebar to close it
|
||||||
|
|
||||||
### Forms
|
### Transactions
|
||||||
- On the AI chat input: **Enter** sends the message, **Shift+Enter** adds a new line
|
- **Enter** sends in AI chat; **Shift+Enter** adds a new line
|
||||||
- On transaction forms: the date field defaults to today — change it if entering a past transaction
|
- Change category directly in the transactions table — click the category dropdown on any row
|
||||||
- On the investment transaction form: the **↓** button next to price fetches the current live price
|
- Date defaults to today; change it for past transactions
|
||||||
|
|
||||||
### Transaction Filters
|
|
||||||
- Filters persist within a tab session but reset when you switch tabs (Income ↔ Expense)
|
|
||||||
- Use the ✕ button to clear all filters at once
|
|
||||||
- Date range filter: both From and To are optional — leave one blank to filter from/to open-ended
|
|
||||||
|
|
||||||
### Budgets
|
|
||||||
- The budget list for an empty month shows a "Copy from previous month" button — use this at the start of each month instead of re-entering all budgets
|
|
||||||
- Rollover amounts appear as a blue "+rollover" badge — hover to see the exact amount
|
|
||||||
|
|
||||||
### Investments
|
### Investments
|
||||||
- Click any row in the holdings table to go to the detail page
|
- Click any row in the holdings table to go to the detail page
|
||||||
- Ticker symbols are case-insensitive on entry (auto-uppercased on save)
|
- **↓** button on the transaction price field fetches the current live price
|
||||||
- The **Check** button on the ticker field verifies the ticker and shows the current price before you save
|
- Ticker symbols are auto-uppercased on save
|
||||||
|
- **Sync Schwab** (topbar) syncs all Schwab accounts at once
|
||||||
|
|
||||||
|
### Budgets
|
||||||
|
- **Copy from previous month** button at the start of each month saves re-entering all budgets
|
||||||
|
- Rollover amounts appear as a blue "+rollover" badge
|
||||||
|
|
||||||
### Reports
|
### Reports
|
||||||
- The "Snapshot Now" button on the Reports page saves today's net worth to the history chart — do this manually if you want more data points than the monthly automatic snapshots
|
- **Snapshot Now** button saves today's net worth to the history chart
|
||||||
- CSV and Excel exports use the period currently selected in the report view
|
- CSV/Excel exports use the period currently selected in the report view
|
||||||
|
|
||||||
### AI Assistant
|
|
||||||
- The AI does not have memory between sessions — each conversation starts fresh
|
|
||||||
- For best results, ask specific questions: "How much did I spend on food in March?" rather than "How am I doing?"
|
|
||||||
- The context includes the last 90 days of transactions — questions about older data may not be accurate
|
|
||||||
|
|||||||
Reference in New Issue
Block a user