Update README with full project documentation
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
156
README.md
156
README.md
@@ -1,2 +1,156 @@
|
|||||||
# bourbonacci
|
# Bourbonacci
|
||||||
|
|
||||||
|
A personal bourbon tracking web app. Log pours, track your collection, and view stats across users. Built with FastAPI, vanilla JS, and MySQL — fully containerized with Docker Compose.
|
||||||
|
|
||||||
|
## Stack
|
||||||
|
|
||||||
|
| Layer | Technology |
|
||||||
|
|---|---|
|
||||||
|
| Backend | FastAPI (Python 3.12), async SQLAlchemy 2.0, Uvicorn |
|
||||||
|
| Database | MySQL 8 |
|
||||||
|
| Frontend | Vanilla HTML/CSS/JS (no framework) |
|
||||||
|
| Reverse Proxy | Nginx (Alpine) |
|
||||||
|
| Auth | JWT via `python-jose`, passwords hashed with `passlib[bcrypt]` |
|
||||||
|
| Container | Docker Compose |
|
||||||
|
|
||||||
|
## Project Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
bourbonacci/
|
||||||
|
├── backend/
|
||||||
|
│ ├── app/
|
||||||
|
│ │ ├── main.py # FastAPI app entry point, lifespan (DB init)
|
||||||
|
│ │ ├── config.py # Pydantic settings
|
||||||
|
│ │ ├── database.py # Async engine, session factory, Base
|
||||||
|
│ │ ├── dependencies.py # get_db, get_current_user
|
||||||
|
│ │ ├── models/
|
||||||
|
│ │ │ ├── user.py # User ORM model
|
||||||
|
│ │ │ └── entry.py # Entry ORM model (add/remove types)
|
||||||
|
│ │ ├── routers/
|
||||||
|
│ │ │ ├── auth.py # POST /api/auth/register, /api/auth/login
|
||||||
|
│ │ │ ├── users.py # GET/PATCH /api/users/me
|
||||||
|
│ │ │ ├── entries.py # CRUD /api/entries, GET /api/entries/stats
|
||||||
|
│ │ │ └── public.py # GET /api/public/stats (unauthenticated)
|
||||||
|
│ │ ├── schemas/
|
||||||
|
│ │ │ ├── user.py # Pydantic schemas for user/auth
|
||||||
|
│ │ │ └── entry.py # Pydantic schemas for entries/stats
|
||||||
|
│ │ └── utils/
|
||||||
|
│ │ └── security.py # JWT creation, password hashing/verification
|
||||||
|
│ ├── Dockerfile
|
||||||
|
│ └── requirements.txt
|
||||||
|
├── frontend/
|
||||||
|
│ ├── index.html # Landing / public stats page
|
||||||
|
│ ├── login.html
|
||||||
|
│ ├── register.html
|
||||||
|
│ ├── dashboard.html # Authenticated home with stats
|
||||||
|
│ ├── log.html # Log a new entry
|
||||||
|
│ ├── profile.html # User profile / settings
|
||||||
|
│ ├── css/style.css
|
||||||
|
│ └── js/
|
||||||
|
│ ├── api.js # Fetch wrapper, base URL, token handling
|
||||||
|
│ └── auth.js # Login/register/logout logic
|
||||||
|
├── nginx/
|
||||||
|
│ └── default.conf # Serves frontend, proxies /api/ to backend:8000
|
||||||
|
├── docker-compose.yml
|
||||||
|
└── .env.example
|
||||||
|
```
|
||||||
|
|
||||||
|
## API Endpoints
|
||||||
|
|
||||||
|
### Auth
|
||||||
|
| Method | Path | Auth | Description |
|
||||||
|
|---|---|---|---|
|
||||||
|
| POST | `/api/auth/register` | No | Register, returns JWT |
|
||||||
|
| POST | `/api/auth/login` | No | Login, returns JWT |
|
||||||
|
|
||||||
|
### Users
|
||||||
|
| Method | Path | Auth | Description |
|
||||||
|
|---|---|---|---|
|
||||||
|
| GET | `/api/users/me` | Yes | Get current user profile |
|
||||||
|
| PATCH | `/api/users/me` | Yes | Update display name / timezone |
|
||||||
|
|
||||||
|
### Entries
|
||||||
|
| Method | Path | Auth | Description |
|
||||||
|
|---|---|---|---|
|
||||||
|
| GET | `/api/entries` | Yes | List all entries for current user |
|
||||||
|
| POST | `/api/entries` | Yes | Create an entry (`add` or `remove`) |
|
||||||
|
| DELETE | `/api/entries/{id}` | Yes | Delete an entry |
|
||||||
|
| GET | `/api/entries/stats` | Yes | Aggregated stats for current user |
|
||||||
|
|
||||||
|
### Public
|
||||||
|
| Method | Path | Auth | Description |
|
||||||
|
|---|---|---|---|
|
||||||
|
| GET | `/api/public/stats` | No | Aggregated stats for all users |
|
||||||
|
|
||||||
|
Authenticated routes expect `Authorization: Bearer <token>` header.
|
||||||
|
|
||||||
|
### Entry Schema
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"entry_type": "add" | "remove",
|
||||||
|
"date": "YYYY-MM-DD",
|
||||||
|
"bourbon_name": "string (required for add)",
|
||||||
|
"proof": 90.0,
|
||||||
|
"amount_shots": 1.0,
|
||||||
|
"notes": "optional string"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Stats return `current_total_shots` (adds minus removes) and `estimated_proof` (weighted average across add entries).
|
||||||
|
|
||||||
|
## Getting Started
|
||||||
|
|
||||||
|
### Prerequisites
|
||||||
|
|
||||||
|
- Docker + Docker Compose
|
||||||
|
|
||||||
|
### Setup
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cp .env.example .env
|
||||||
|
# Edit .env — set real passwords and a secure SECRET_KEY
|
||||||
|
```
|
||||||
|
|
||||||
|
Generate a secure secret key:
|
||||||
|
```bash
|
||||||
|
python3 -c "import secrets; print(secrets.token_hex(32))"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Run
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
The app will be available at `http://localhost:8057`.
|
||||||
|
|
||||||
|
The backend waits for MySQL to pass its healthcheck before starting. Tables are created automatically on first boot via SQLAlchemy's `init_db()`.
|
||||||
|
|
||||||
|
### Stop
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker compose down
|
||||||
|
# To also remove the database volume:
|
||||||
|
docker compose down -v
|
||||||
|
```
|
||||||
|
|
||||||
|
## Environment Variables
|
||||||
|
|
||||||
|
| Variable | Description | Default |
|
||||||
|
|---|---|---|
|
||||||
|
| `MYSQL_ROOT_PASSWORD` | MySQL root password | — |
|
||||||
|
| `MYSQL_DATABASE` | Database name | `bourbonacci` |
|
||||||
|
| `MYSQL_USER` | App DB user | `bourbonacci` |
|
||||||
|
| `MYSQL_PASSWORD` | App DB password | — |
|
||||||
|
| `DATABASE_URL` | SQLAlchemy async DSN | `mysql+aiomysql://...` |
|
||||||
|
| `SECRET_KEY` | JWT signing secret (keep long & random) | — |
|
||||||
|
| `ACCESS_TOKEN_EXPIRE_MINUTES` | JWT TTL in minutes | `480` |
|
||||||
|
|
||||||
|
## Data Model
|
||||||
|
|
||||||
|
**users**
|
||||||
|
- `id`, `email` (unique), `password_hash`, `display_name`, `timezone`, `created_at`
|
||||||
|
|
||||||
|
**entries**
|
||||||
|
- `id`, `user_id` (FK), `entry_type` (`add`/`remove`), `date`, `bourbon_name`, `proof`, `amount_shots`, `notes`, `created_at`
|
||||||
|
|||||||
Reference in New Issue
Block a user