July 6 - Update the document to catch up the code
This commit is contained in:
@@ -215,7 +215,26 @@ Self-hosted personal finance web app. Tracks income, expenses, investments. AI a
|
||||
- **Purge** dropdown (7 / 30 / 90 days) via AJAX `POST /logs/purge` — deletes `app_logs` rows older than N days
|
||||
- DB entry count shown in header chip
|
||||
|
||||
### 2.17 Plaid Bank Sync
|
||||
### 2.17 Financial Health Score
|
||||
- Dashboard widget: 0–100 score with letter grade (A/B/C/D/F) + color
|
||||
- Computed by `app/services/health_score_service.py::compute_health_score()`, served via `GET /api/health-score` (AJAX, dashboard.py)
|
||||
- 4 components, 25 points each:
|
||||
- **Savings Rate** — 3-month avg; ≥20% full marks, 10–20% → 18pts, 1–10% → 10pts, ≤0% → 0
|
||||
- **Budget Adherence** — fraction of budgeted categories currently under limit (no budgets set = not penalised)
|
||||
- **Goal Progress** — avg completion % across active (non-completed) goals
|
||||
- **Emergency Fund** — liquid assets vs 3-month expense target, scales linearly
|
||||
- Each component returns a `tip` string when below target; surfaced in UI
|
||||
|
||||
### 2.18 Budget Alerts (Email)
|
||||
- Threshold alerts at 80% and 100% of a category's monthly budget (limit + rollover)
|
||||
- Triggered by `app/services/alert_service.py::check_and_flash_budget_alerts()`, called after committing an expense transaction
|
||||
- **Dedup**: `budgets.alert_sent_80` / `alert_sent_100` boolean flags prevent repeat sends within the same month
|
||||
- **In-app**: Flask `flash()` message (warning/danger) always shown when a threshold is newly crossed
|
||||
- **Email**: HTML email sent only if `budget_alerts_enabled` is on (Settings → Profile) AND SMTP is configured (`SMTP_HOST`, `SMTP_USER`, `SMTP_PASSWORD`, `ALERT_EMAIL` all set)
|
||||
- Settings → Profile: "Email me when a budget category reaches 80% or 100%" checkbox (`users.budget_alerts_enabled`) + "Send test email" button (`POST /settings/test-email`) — shows SMTP-configured status inline
|
||||
- Uses stdlib `smtplib` + `ssl` (STARTTLS), no third-party mail service
|
||||
|
||||
### 2.19 Plaid Bank Sync
|
||||
- Connects 12,000+ US financial institutions via Plaid API
|
||||
- **Plaid page** (`/plaid/`) — accessible via Settings; connect/disconnect/sync/billing
|
||||
- **Link flow**: AJAX `POST /plaid/create-link-token` → open Plaid Link widget (CDN JS) → `onSuccess(public_token)` → AJAX `POST /plaid/exchange-token` → redirect to account mapping
|
||||
@@ -277,6 +296,8 @@ plaid_sync_previews — temporary preview data: item_id (UNIQUE), data_json
|
||||
- `plaid_accounts.cc_*` — credit card billing fields updated by `POST /plaid/liabilities/<item_db_id>`
|
||||
- `app_logs.message` — raw `record.getMessage()` + exception traceback (if any); no pipe-delimited prefix
|
||||
- `users.totp_secret` — base32 TOTP secret (VARCHAR 64); NULL when 2FA disabled
|
||||
- `users.budget_alerts_enabled` — boolean; toggles budget threshold emails (Settings → Profile)
|
||||
- `budgets.alert_sent_80` / `alert_sent_100` — dedup flags so threshold emails/flashes fire once per month per category
|
||||
|
||||
### Migration Scripts
|
||||
```
|
||||
@@ -326,7 +347,7 @@ pfm/ # /home/pfm/web on server
|
||||
│ │
|
||||
│ ├── routes/
|
||||
│ │ ├── auth.py # + TOTP verify/setup/disable routes; rate limits; audit calls
|
||||
│ │ ├── dashboard.py # + savings_rate; Schwab expiry warning; reconcile API; checking/savings/investment totals
|
||||
│ │ ├── dashboard.py # + savings_rate; Schwab expiry warning; reconcile API; checking/savings/investment totals; health-score API
|
||||
│ │ ├── accounts.py # teller_map + schwab_map + plaid_map; skip calc_balance for providers
|
||||
│ │ ├── categories.py
|
||||
│ │ ├── transactions.py # + set-category AJAX; bulk-action AJAX; quick date filter vars; fixed income-form-submits-as-expense bug
|
||||
@@ -335,7 +356,7 @@ pfm/ # /home/pfm/web on server
|
||||
│ │ ├── investments.py # + sync-schwab route; price-history API
|
||||
│ │ ├── reports.py
|
||||
│ │ ├── ai.py
|
||||
│ │ ├── settings.py # + audit_log route; audit_purge route; audit calls on password change
|
||||
│ │ ├── settings.py # + audit_log route; audit_purge route; audit calls on password change; MIME magic-byte check on receipt upload; test-email route
|
||||
│ │ ├── teller.py # balance uses ledger/available correctly
|
||||
│ │ ├── schwab.py # OAuth, mapping, sync, snapshot; audit calls; fallback type 'other'
|
||||
│ │ ├── plaid.py # Link flow, exchange, map, sync preview/confirm, balance, liabilities, resync, disconnect
|
||||
@@ -346,11 +367,13 @@ pfm/ # /home/pfm/web on server
|
||||
│ │ ├── account_service.py
|
||||
│ │ ├── ai_service.py
|
||||
│ │ ├── budget_service.py
|
||||
│ │ ├── export_service.py
|
||||
│ │ ├── export_service.py # CSV/Excel exports stream via generator + yield_per(500)
|
||||
│ │ ├── fx_service.py
|
||||
│ │ ├── goal_service.py
|
||||
│ │ ├── import_service.py
|
||||
│ │ ├── investment_service.py # get_portfolio_summary returns account_groups
|
||||
│ │ ├── health_score_service.py # compute_health_score() — savings/budgets/goals/emergency fund → 0-100 score
|
||||
│ │ ├── alert_service.py # budget threshold alerts (flash + SMTP email), dedup via alert_sent_80/100
|
||||
│ │ ├── ocr_service.py
|
||||
│ │ ├── recurring_service.py
|
||||
│ │ ├── report_service.py
|
||||
@@ -527,6 +550,8 @@ sentry-sdk[flask]==2.7.0
|
||||
## 12. UI/UX
|
||||
|
||||
- **Sidebar**: collapsible (desktop state saved in localStorage), mobile overlay; Teller Sync and Schwab Sync **removed** — accessible via Settings only
|
||||
- **Mobile responsive**: sidebar goes off-canvas with a dimmed overlay under 769px (topbar toggle button opens/closes it); topbar and main content collapse to full width; tables scroll horizontally via `.table-wrap` / `.pcard.p-0` wrapper classes + `.pfm-table` min-widths, with `.d-mob-none` hiding low-priority columns first; under 576px, button labels hide to icon-only (`.btn-label`) and chart/chat heights are capped (base.html)
|
||||
- **Dark mode**: toggle button in topbar (moon/sun icon); persisted via `localStorage['pfm_dark']`; applied pre-paint via a `data-pfm-dark` attribute to avoid flash-of-light-mode; CSS variable overrides plus targeted `[style*="..."]` overrides for hardcoded inline colors in templates (base.html)
|
||||
- **Charts**: Chart.js 4.x (CDN)
|
||||
- **Forms**: WTForms + Bootstrap 5.3
|
||||
- **Icons**: Bootstrap Icons 1.11
|
||||
@@ -535,6 +560,7 @@ sentry-sdk[flask]==2.7.0
|
||||
- **CSS**: All inline in templates (no build step)
|
||||
- **CSRF meta tag**: `<meta name="csrf-token">` in `base.html` for JS fetch calls
|
||||
- **Back buttons**: all Settings sub-pages have `← Settings` (or `← Recurring` for the recurring form) in `{% block topbar_actions %}`
|
||||
- **Keyboard shortcuts**: `n`/`i` new expense/income, `/` focus search, `g h`/`g t`/`g a` navigation chords, `?` shows cheatsheet modal (base.html)
|
||||
|
||||
---
|
||||
|
||||
@@ -603,6 +629,13 @@ PLAID_ENV=sandbox
|
||||
SENTRY_DSN= # leave blank to disable Sentry
|
||||
SESSION_IDLE_MINUTES=60 # session idle timeout in minutes
|
||||
RATELIMIT_STORAGE_URI=redis://localhost:6379 # use Redis to share rate limits across Gunicorn workers
|
||||
# Budget alert emails (optional — leave blank to disable)
|
||||
SMTP_HOST=
|
||||
SMTP_PORT=587
|
||||
SMTP_USER=
|
||||
SMTP_PASSWORD=
|
||||
ALERT_EMAIL= # recipient address for budget alert emails
|
||||
APP_URL=https://pfm.ngodanguyen.tech # used to build links in alert emails
|
||||
```
|
||||
|
||||
---
|
||||
@@ -613,7 +646,7 @@ RATELIMIT_STORAGE_URI=redis://localhost:6379 # use Redis to share rate limits a
|
||||
|-----------|--------|------------|
|
||||
| health | (none) | /health (public, no auth) |
|
||||
| auth | /auth | login, logout, totp/verify, totp/setup, totp/disable |
|
||||
| dashboard | / | index, api/fx-history, api/fx-refresh, api/reconcile |
|
||||
| dashboard | / | index, api/fx-history, api/fx-refresh, api/reconcile, api/health-score |
|
||||
| accounts | /accounts | CRUD, adjust |
|
||||
| categories | /categories | CRUD |
|
||||
| transactions | /transactions | index, new, edit, delete, transfer, ocr, ocr-file, `<id>/set-category`, bulk-action |
|
||||
@@ -622,7 +655,7 @@ RATELIMIT_STORAGE_URI=redis://localhost:6379 # use Redis to share rate limits a
|
||||
| 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 |
|
||||
| ai | /ai | index, stream (SSE), history, generate-insight |
|
||||
| settings | /settings | index, profile, password, audit, audit/purge, recurring, import, recalc-balances, upload_receipt, delete_receipt, view_receipt |
|
||||
| settings | /settings | index, profile, password, test-email, audit, audit/purge, recurring, import, recalc-balances, upload_receipt, delete_receipt, view_receipt |
|
||||
| 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 |
|
||||
| plaid | /plaid | index, create-link-token, exchange-token, map/`<id>`, sync/`<id>`, sync/confirm, balance/`<pa_id>`, liabilities/`<id>`, resync/`<id>`, disconnect/`<id>`, webhook, update-webhook |
|
||||
@@ -683,22 +716,26 @@ RATELIMIT_STORAGE_URI=redis://localhost:6379 # use Redis to share rate limits a
|
||||
## 19. To-Do / Roadmap
|
||||
|
||||
### High priority
|
||||
- [ ] **Mobile responsiveness pass** — sidebar auto-collapses on mobile; tables scroll horizontally *(base.html mobile CSS improved; further work needed)*
|
||||
- [ ] **Budget alerts** — email/Twilio SMS when category spending hits 80% / 100%
|
||||
*(none currently open)*
|
||||
|
||||
### Medium priority
|
||||
- [ ] **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
|
||||
- [ ] **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
|
||||
*(none currently open)*
|
||||
|
||||
### Low priority / future
|
||||
- [ ] iOS companion app
|
||||
- [ ] Shared household mode (2 users, row-level isolation)
|
||||
- [ ] Bank statement PDF: table-extraction fallback (pdfplumber tables API) before Groq call
|
||||
- [ ] Dark mode toggle
|
||||
- [ ] Minor: bank-import column-mapping step (`.map-row` grid, fixed `160px 1fr`) feels cramped under ~360px viewport width — cosmetic only, not broken
|
||||
|
||||
### Completed (removed from backlog)
|
||||
- [x] **Mobile responsiveness pass** — sidebar off-canvas + overlay under 769px, topbar collapse, table horizontal scroll via `.table-wrap`/`.pcard.p-0` wrappers, `.d-mob-none` column hiding, icon-only buttons + capped chart/chat heights under 576px (base.html)
|
||||
- [x] **Dark mode toggle** — full implementation in base.html: toggle button, localStorage persistence, dark CSS variables, override rules for hardcoded inline colors
|
||||
- [x] **Budget alerts** — flash + email (SMTP) at 80%/100% of category budget, dedup flags, "Send test email" button ([2.18](#218-budget-alerts-email))
|
||||
- [x] **Financial Health Score** — 0–100 score/grade from savings rate, budget adherence, goal progress, emergency fund ([2.17](#217-financial-health-score))
|
||||
- [x] **Receipt MIME validation** — magic-byte sniffing in `settings.py`, rejects mismatched/renamed files
|
||||
- [x] **OCR ownership check** — re-extract requires filename to exist in `receipts` table
|
||||
- [x] **PDF export memory** — CSV/Excel exports now stream via generator + `yield_per(500)`
|
||||
- [x] **Bank import progress** — chunked import with live per-row progress bar
|
||||
- [x] **Pagination info** — "Page X of Y" added to transactions, AI history, accounts/payments, audit log
|
||||
- [x] **Schwab IRA account type** — IRA/ROTH_IRA/401K/BROKERAGE types added to ACCOUNT_TYPE_MAP
|
||||
- [x] **Investment price history chart** — 1W/1M/3M/6M/1Y chart on investment detail page
|
||||
|
||||
Reference in New Issue
Block a user