Compare commits

..

2 Commits

Author SHA1 Message Date
Jonathan 8982f76d15 Change frontend port to 3001 and fix REACT_APP_API_URL as build arg
REACT_APP_API_URL is baked into the JS bundle at build time, so it
must be passed as a build arg, not a runtime environment variable.
Port changed from 3000 to 3001 to avoid conflict with Gitea.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-24 16:54:49 +01:00
Jonathan 22c36eff67 Update README with full project documentation
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-24 16:32:11 +01:00
2 changed files with 127 additions and 5 deletions
+122 -1
View File
@@ -1 +1,122 @@
Welcome the only Portfolio you'll ever need
# BTC Portfolio Tracker
A full-stack Bitcoin portfolio tracker with live EUR pricing, purchase history, and interactive charts.
---
## Features
- **Live BTC price** — fetched from CoinGecko in EUR
- **Purchase tracking** — log BTC buys with amount (EUR) and price per BTC
- **Portfolio stats** — total invested, current value, profit/loss
- **Interactive charts** — portfolio value over time and 1-year BTC price history
- **Edit & delete** — manage purchases with inline editing
- **JWT authentication** — secure per-user portfolios
---
## Tech Stack
| Layer | Technology |
|----------|-------------------------------------|
| Frontend | React 18, Chart.js, dark theme |
| Backend | FastAPI, SQLAlchemy, SQLite |
| Auth | JWT (HS256, 24h expiry) + bcrypt |
| Pricing | CoinGecko API (EUR) |
| Deploy | Docker + Docker Compose |
---
## Getting Started
### Docker (recommended)
```bash
git clone <repo-url>
cd btc-portfolio
docker-compose up
```
- Frontend: [http://localhost:3000](http://localhost:3000)
- Backend API: [http://localhost:8000](http://localhost:8000)
### Local Development
**Backend**
```bash
cd btc-portfolio/backend
pip install -r requirements.txt
uvicorn app.main:app --reload
```
**Frontend**
```bash
cd btc-portfolio/frontend
npm install
npm start
```
---
## Project Structure
```
btc-portfolio/
├── backend/
│ └── app/
│ ├── main.py # FastAPI app + CORS
│ ├── models.py # User & Purchase ORM models
│ ├── auth.py # JWT + bcrypt
│ ├── dependencies.py # Auth dependency injection
│ ├── routes/
│ │ ├── users.py # POST /register, POST /login
│ │ ├── purchases.py # CRUD /purchases
│ │ ├── stats.py # GET /stats
│ │ └── history.py # GET /history (365-day BTC prices)
│ └── services/
│ └── btc.py # CoinGecko integration
└── frontend/
└── src/
├── App.js # Routing
├── pages/
│ └── Dashboard.js # Main view
└── components/
├── AddPurchase.js # Purchase form
├── PurchaseList.js # Purchase table (edit/delete)
├── PortfolioChart.js # Invested vs current value
└── BTCHistoryChart.js # 1-year BTC price history
```
---
## API Endpoints
| Method | Endpoint | Description | Auth |
|--------|-------------------|------------------------------|------|
| POST | `/register` | Create account | No |
| POST | `/login` | Get JWT token | No |
| GET | `/purchases` | List user purchases | Yes |
| POST | `/purchases` | Add a purchase | Yes |
| PUT | `/purchases/{id}` | Edit a purchase | Yes |
| DELETE | `/purchases/{id}` | Delete a purchase | Yes |
| GET | `/stats` | Portfolio stats (P&L) | Yes |
| GET | `/history` | 365-day BTC price history | Yes |
---
## Database
SQLite, stored at `/app/data/btc_portfolio.db` (persisted via Docker volume).
| Table | Columns |
|-------------|------------------------------------------------------|
| `users` | id, username (unique), password (bcrypt hash) |
| `purchases` | id, amount_eur, price_eur, created_at, user_id (FK) |
---
## Notes
- The `SECRET_KEY` in `auth.py` is hardcoded — use an environment variable in production.
- CoinGecko requests are unauthenticated; failures return `0.0` gracefully.
- CORS is restricted to `localhost:3000` by default.
+5 -4
View File
@@ -10,11 +10,12 @@ services:
restart: unless-stopped
frontend:
build: ./frontend
build:
context: ./frontend
args:
- REACT_APP_API_URL=http://localhost:8000
ports:
- "3000:3000"
environment:
- REACT_APP_API_URL=http://localhost:8000
- "3001:3000"
depends_on:
- backend
restart: unless-stopped