- OCR pipeline: Tesseract (images) + pdfplumber (PDFs) → AI text prompt → rule-based regex fallback; works with any text model, not just vision models - Scan Receipt toolbar button parses a photo and pre-fills the transaction form; receipt image is automatically attached to the created transaction - AI settings page: provider, API key (AES-256-GCM encrypted), custom URL, model, and per-user debug toggle that gates the OCR/AI debug panel - Fix CSRF cookie secure=False so HTTP deployments work; add 7-day max_age - Fix attachment_refs missing from _to_response (attachments never appeared in UI) - Fix multipart boundary lost when Content-Type was set manually in axios calls - nginx: raise client_max_body_size to 15 MB, add 120s proxy timeout for OCR - Migration 0005: add ai_debug boolean to users table - Update README and CLAUDE.md with AI scanning docs and architecture notes Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
302 lines
12 KiB
Markdown
302 lines
12 KiB
Markdown
# MyMidas
|
||
|
||
A self-hosted, single-user personal finance tracker built for security and completeness. Combines budgeting, investment tracking, and ML-powered predictions in one application — things that no single open-source tool does together.
|
||
|
||
Runs entirely on your own hardware via Docker Compose. Designed for LAN access with VPN for remote, behind an existing reverse proxy (nginx proxy manager, Caddy, etc.).
|
||
|
||
---
|
||
|
||
## Features
|
||
|
||
### Accounts & Transactions
|
||
- Multiple account types: Checking, Savings, Cash ISA, Stocks & Shares ISA, Credit Card, Investment, Pension, Crypto Wallet, Loan, Mortgage, and more
|
||
- Full transaction history with categories, tags, merchant tracking, and notes
|
||
- Transfer detection between accounts
|
||
- Recurring transaction rules (rrule)
|
||
- Receipt and document attachments on transactions (JPEG, PNG, WebP, PDF — up to 10 MB each)
|
||
- **AI receipt scanning** — photograph a receipt to auto-extract merchant, amount, date, and description into a new transaction; receipt is automatically attached
|
||
- CSV import with **auto-detection** for 10 UK bank formats: Monzo, Starling, Revolut, Barclays, Lloyds, NatWest, HSBC, Santander, Nationwide, and generic fallback
|
||
- SHA-256 deduplication prevents re-importing the same transactions
|
||
|
||
### Budgets
|
||
- Monthly, weekly, quarterly, and yearly budget periods
|
||
- Per-category budget tracking with rollover support
|
||
- Visual radial gauges showing spend vs. budget
|
||
- Configurable alert threshold (default 80%)
|
||
|
||
### Investments
|
||
- Portfolio tracking with holdings and buy/sell/dividend/split transactions
|
||
- Live price feeds: stocks and ETFs via **yfinance**, crypto via **CoinGecko** (refreshed every 15 minutes)
|
||
- Hourly FX rates for multi-currency conversion to GBP base
|
||
- TWRR and MWRR performance metrics
|
||
- Capital gains reporting (short/long-term by tax year)
|
||
- OHLCV candlestick charts per asset
|
||
- Allocation treemap across the full portfolio
|
||
|
||
### Reports
|
||
Seven report views:
|
||
1. Net Worth over time (area chart with time slider)
|
||
2. Income vs. Expense (bar chart)
|
||
3. Cash Flow (waterfall)
|
||
4. Category Breakdown (pie with drilldown)
|
||
5. Budget vs. Actual
|
||
6. Investment Performance
|
||
7. Spending Trends
|
||
|
||
Export to CSV and PDF.
|
||
|
||
### ML Predictions
|
||
Four prediction engines, all running locally:
|
||
|
||
| Engine | Algorithm | Output |
|
||
|--------|-----------|--------|
|
||
| **Spending Forecast** | Meta Prophet per category; SARIMA fallback | 90-day forecast with 80%/95% confidence bands |
|
||
| **Net Worth Projection** | Holt-Winters ExponentialSmoothing | 1/3/5-year fan chart, 3 scenarios (conservative/base/optimistic) |
|
||
| **Monte Carlo Portfolio** | Geometric Brownian Motion + Cholesky covariance | 1,000–5,000 simulations, P10–P90 percentile paths |
|
||
| **Budget Forecast** | Velocity-based daily projection + normal CDF | Probability of overspend per category this month |
|
||
|
||
Plus a 30-day cash flow forecast with negative-balance risk flagging.
|
||
|
||
### Themes
|
||
Seven selectable UI themes — pick from the top bar:
|
||
|
||
| Theme | Style |
|
||
|-------|-------|
|
||
| **Obsidian** | Dark slate, blue-grey accents |
|
||
| **Arctic** | Clean light with ice-blue tones |
|
||
| **Midnight** | Deep navy with cyan highlights |
|
||
| **Vault** | Forest green, financial gravitas |
|
||
| **Terminal** | Phosphor green on black, scanline overlay, monospace |
|
||
| **Synthwave** | Neon pink/purple, grid lines, Orbitron headings |
|
||
| **Ledger** | Warm paper, serif typography, ruled lines |
|
||
|
||
---
|
||
|
||
## Security
|
||
|
||
Ten independent security layers:
|
||
|
||
1. **Network isolation** — Postgres and Redis are on an internal Docker bridge, never exposed to the host
|
||
2. **TLS** — handled upstream by your reverse proxy; internal traffic is HTTP (LAN-only)
|
||
3. **Security headers** — CSP, HSTS, X-Frame-Options DENY, X-Content-Type-Options, form-action, Permissions-Policy
|
||
4. **CSRF** — double-submit cookie on all mutating endpoints
|
||
5. **Authentication** — Argon2id password hashing; RS256 JWT (15-min access + 7-day HttpOnly refresh cookie); TOTP (RFC 6238) with backup codes; brute-force lockout (exponential backoff via Redis)
|
||
6. **Rate limiting** — Redis sliding window: 20/min on login (per IP), 10/min on TOTP, 20/min on predictions, 300/min general API
|
||
7. **Field-level encryption** — AES-256-GCM on all PII fields (account names, transaction descriptions, merchant, notes, TOTP secret); IV ‖ ciphertext ‖ tag stored in `bytea`
|
||
8. **Database encryption** — pgcrypto as a second layer on backup dumps
|
||
9. **Row-level security** — PostgreSQL RLS policies enforce user isolation at the DB layer; buggy queries cannot leak another user's data
|
||
10. **Audit log** — every auth event and data mutation is written to an append-only `audit_logs` table; the app role has no UPDATE/DELETE on it
|
||
|
||
**Single-user hardening:** registration is permanently disabled after the first account is created. Re-enable with `ALLOW_REGISTRATION=true` if needed.
|
||
|
||
---
|
||
|
||
## Tech Stack
|
||
|
||
| Layer | Technology |
|
||
|-------|-----------|
|
||
| Backend | Python 3.12, FastAPI, SQLAlchemy 2.0 async |
|
||
| Frontend | React 18, TypeScript, Vite, TanStack Query v5 |
|
||
| Styling | Tailwind CSS v4, shadcn/ui primitives |
|
||
| Charts | Recharts (standard), Plotly (Monte Carlo, candlestick) |
|
||
| Database | PostgreSQL 16 with pgcrypto and RLS |
|
||
| Cache / Sessions | Redis 7 |
|
||
| ML | Prophet, statsmodels, NumPy, SciPy |
|
||
| OCR | Tesseract 5, pdfplumber, pdf2image |
|
||
| Background jobs | APScheduler (in-process) |
|
||
| Containerisation | Docker Compose |
|
||
|
||
---
|
||
|
||
## Getting Started
|
||
|
||
### Prerequisites
|
||
- Docker and Docker Compose
|
||
- An existing reverse proxy (nginx proxy manager, Caddy, Traefik, etc.) pointing at port `8090`
|
||
|
||
### 1. Clone and configure
|
||
|
||
```bash
|
||
git clone https://git.rdx4.com/megaproxy/MyMidas.git
|
||
cd MyMidas
|
||
cp .env.example .env
|
||
```
|
||
|
||
Edit `.env` and fill in every value:
|
||
|
||
```env
|
||
# 32-byte hex key — generate with:
|
||
# python3 -c "import secrets; print(secrets.token_hex(32))"
|
||
ENCRYPTION_KEY=
|
||
|
||
# Strong random passwords
|
||
DB_PASSWORD=
|
||
REDIS_PASSWORD=
|
||
BACKUP_PASSPHRASE=
|
||
|
||
BASE_CURRENCY=GBP
|
||
ENVIRONMENT=production
|
||
ALLOW_REGISTRATION=false # set true only for initial account creation
|
||
```
|
||
|
||
### 2. Generate JWT keys
|
||
|
||
```bash
|
||
mkdir -p secrets
|
||
openssl genrsa -out secrets/jwt_private.pem 4096
|
||
openssl rsa -in secrets/jwt_private.pem -pubout -out secrets/jwt_public.pem
|
||
```
|
||
|
||
### 3. Start
|
||
|
||
```bash
|
||
docker compose up -d --build
|
||
```
|
||
|
||
All four containers start: `backend`, `frontend`, `postgres`, `redis`.
|
||
|
||
### 4. Create your account
|
||
|
||
Temporarily set `ALLOW_REGISTRATION=true` in `.env`, restart the backend, register, then set it back to `false`:
|
||
|
||
```bash
|
||
# In .env: ALLOW_REGISTRATION=true
|
||
docker compose up -d backend
|
||
|
||
# Register via the web UI, then:
|
||
# In .env: ALLOW_REGISTRATION=false
|
||
docker compose up -d backend
|
||
```
|
||
|
||
### 5. Point your reverse proxy
|
||
|
||
Forward your domain to `http://<host>:4000`. The frontend nginx serves the React app and proxies all `/api/` requests to the backend internally.
|
||
|
||
---
|
||
|
||
## AI Receipt Scanning
|
||
|
||
Receipt scanning uses OCR (Tesseract) to extract text from the image first, then optionally passes that text to an AI model to parse it into structured fields. This means **any text-capable LLM works** — you're not limited to vision models.
|
||
|
||
If no AI is configured, or if the AI call fails, a rule-based parser runs on the OCR text as a fallback (finds totals, dates, and merchant names via regex).
|
||
|
||
### Setup
|
||
|
||
Go to **Settings → AI** and fill in:
|
||
|
||
| Field | Description |
|
||
|-------|-------------|
|
||
| Provider | `Anthropic` or `OpenAI-compatible` |
|
||
| API Key | Your key (stored AES-256-GCM encrypted on your server) |
|
||
| Custom API URL | Optional — for Open WebUI, LM Studio, Ollama, etc. |
|
||
| Model | Optional — defaults to `claude-haiku-4-5-20251001` or `gpt-4o-mini` |
|
||
| Debug mode | Shows OCR text and raw AI response in the scan form when enabled |
|
||
|
||
For **Open WebUI**: set the provider to `OpenAI-compatible`, enter `http://your-server:port` as the URL (MyMidas appends `/v1/chat/completions`), and enter the model name exactly as shown in Open WebUI's interface.
|
||
|
||
Use the **Test connection** button to verify your settings before scanning.
|
||
|
||
### Usage
|
||
|
||
Click **Scan Receipt** in the transactions toolbar, select a photo or PDF. The form opens pre-filled with extracted fields — review and save. The receipt image is automatically attached to the created transaction.
|
||
|
||
---
|
||
|
||
## Backups
|
||
|
||
Encrypted backups run automatically every night at 3 AM (GPG AES-256 symmetric encryption). Backups are stored in `./data/backups/` and retained for 30 days.
|
||
|
||
**Via the web UI:** Settings → Backups — trigger a manual backup, download a copy, or restore from any listed backup file.
|
||
|
||
**Manual backup from the command line:**
|
||
|
||
```bash
|
||
docker compose exec backend bash /app/scripts/backup.sh
|
||
```
|
||
|
||
**Manual restore** (stop the backend first to avoid lock contention):
|
||
|
||
```bash
|
||
docker compose stop backend
|
||
|
||
docker compose run --rm backend bash -c '
|
||
export GNUPGHOME=/tmp/.gnupg; mkdir -p $GNUPGHOME && chmod 700 $GNUPGHOME
|
||
PG_URL=${DATABASE_URL/postgresql+asyncpg/postgresql}
|
||
gpg --batch --yes --no-symkey-cache --pinentry-mode loopback \
|
||
--passphrase "$BACKUP_PASSPHRASE" \
|
||
--decrypt $(ls /app/backups/*.sql.gz.gpg | tail -1) \
|
||
| gunzip | psql "$PG_URL"
|
||
'
|
||
|
||
docker compose start backend
|
||
```
|
||
|
||
## Key Rotation
|
||
|
||
To rotate the AES-256-GCM encryption key without data loss:
|
||
|
||
```bash
|
||
# Generate a new key
|
||
NEW_KEY=$(python3 -c "import secrets; print(secrets.token_hex(32))")
|
||
echo "New key: $NEW_KEY"
|
||
|
||
# Dry-run first — validates decryption works, no DB changes
|
||
docker compose exec backend python /app/scripts/rotate_keys.py \
|
||
--old-key "$ENCRYPTION_KEY" --new-key "$NEW_KEY" --dry-run
|
||
|
||
# If dry-run passes, rotate for real (atomic — rolls back on any failure)
|
||
docker compose exec backend python /app/scripts/rotate_keys.py \
|
||
--old-key "$ENCRYPTION_KEY" --new-key "$NEW_KEY"
|
||
|
||
# Update ENCRYPTION_KEY in .env, then restart
|
||
docker compose up -d backend
|
||
```
|
||
|
||
Do not lose your current `ENCRYPTION_KEY` — without it, all encrypted fields are permanently unreadable.
|
||
|
||
---
|
||
|
||
## Environment Variables
|
||
|
||
| Variable | Description | Default |
|
||
|----------|-------------|---------|
|
||
| `ENCRYPTION_KEY` | 32-byte hex key for AES-256-GCM field encryption | **required** |
|
||
| `DB_PASSWORD` | PostgreSQL password | **required** |
|
||
| `REDIS_PASSWORD` | Redis password | **required** |
|
||
| `BACKUP_PASSPHRASE` | GPG symmetric passphrase for backup encryption | **required** |
|
||
| `BASE_CURRENCY` | ISO 4217 base currency | `GBP` |
|
||
| `ENVIRONMENT` | `production` or `development` | `production` |
|
||
| `ALLOW_REGISTRATION` | Enable new account creation | `false` |
|
||
|
||
---
|
||
|
||
## Project Structure
|
||
|
||
```
|
||
MyMidas/
|
||
├── backend/
|
||
│ ├── app/
|
||
│ │ ├── api/v1/ # REST endpoints
|
||
│ │ ├── core/ # Security, encryption, middleware, rate limiting
|
||
│ │ ├── db/ # Models, migrations, repositories
|
||
│ │ ├── ml/ # Prediction engines
|
||
│ │ ├── services/ # Business logic
|
||
│ │ └── workers/ # Scheduled jobs (price sync, FX, snapshots, backups)
|
||
│ └── alembic/ # Database migrations
|
||
├── frontend/
|
||
│ └── src/
|
||
│ ├── api/ # Typed API client
|
||
│ ├── components/ # Shared UI components
|
||
│ ├── pages/ # Route-level page components
|
||
│ └── store/ # Zustand state (auth, UI/theme)
|
||
├── postgres/init/ # PostgreSQL init SQL (extensions, RLS policies)
|
||
├── backend/scripts/ # backup.sh, rotate_keys.py, entrypoint.sh
|
||
├── secrets/ # JWT keys (git-ignored, generate locally)
|
||
└── docker-compose.yml
|
||
```
|
||
|
||
---
|
||
|
||
## Licence
|
||
|
||
Personal use. Not intended for public deployment or multi-user hosting.
|