From 790eb9894e8cd5eff4e61f90260bfc0212416d2d Mon Sep 17 00:00:00 2001 From: NguyenND Date: Mon, 6 Jul 2026 16:37:33 -0400 Subject: [PATCH] July 6 - Update the document to catch up the code --- CLAUDE.md | 63 +++++++++++++++++++++++++++++++++++++++++++------------ 1 file changed, 50 insertions(+), 13 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 1561cf5..1c419ee 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -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/` - `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**: `` 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, `/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/``, sync/confirm, resync, snapshot/``, disconnect | | plaid | /plaid | index, create-link-token, exchange-token, map/``, sync/``, sync/confirm, balance/``, liabilities/``, resync/``, disconnect/``, 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