Update README with current features, API, and data model

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-24 21:55:32 -07:00
parent ca83351e9d
commit 1ac5a191be

148
README.md
View File

@@ -1,6 +1,18 @@
# 🥃 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.
A self-hosted infinity bottle tracker. Log every bourbon you add to your blend, track what you've consumed, and watch your bottle's story unfold over time. Community bottles are visible to anyone who visits the site — click a card to browse the full bourbon list inside that bottle.
Built with FastAPI, vanilla JS, and MySQL — fully containerized with Docker Compose. No framework dependencies on the frontend.
## Features
- **Infinity bottle tracking** — log `add` entries (bourbon name, proof, shots) and `remove` entries (shots consumed)
- **Live stats** — bourbons added, total shots poured in, shots remaining, estimated weighted proof
- **Bottle fill bar** — visual level indicator based on your configured bottle size
- **Community page** — public leaderboard of all bottles; click any card to see a searchable, alphabetical bourbon list
- **Settings modal** — update display name, bottle size, timezone, and password from any page via the gear icon
- **Admin panel** — list all users, reset passwords, disable/enable accounts, delete users, and impersonate any user for debugging
- **JWT auth** — Bearer token auth with impersonation support (admin tokens carry an `admin_id` claim)
## Stack
@@ -9,8 +21,8 @@ A personal bourbon tracking web app. Log pours, track your collection, and view
| 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]` |
| Reverse Proxy | Nginx |
| Auth | JWT via `python-jose`, passwords hashed with `passlib[bcrypt==4.0.1]` |
| Container | Docker Compose |
## Project Structure
@@ -19,55 +31,82 @@ A personal bourbon tracking web app. Log pours, track your collection, and view
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
│ │ ├── main.py # FastAPI app entry point, lifespan (DB init + admin seed)
│ │ ├── config.py # Pydantic settings (reads from .env)
│ │ ├── database.py # Async engine, session factory, Base, init_db()
│ │ ├── dependencies.py # get_db, get_current_user, get_current_admin
│ │ ├── 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
│ │ │ ├── users.py # GET/PUT /api/users/me, PUT /api/users/me/password
│ │ │ ├── entries.py # CRUD /api/entries, GET /api/entries/stats
│ │ │ ── public.py # GET /api/public/stats (unauthenticated)
│ │ │ ── public.py # GET /api/public/stats (unauthenticated)
│ │ │ └── admin.py # Admin user management + impersonation
│ │ ├── schemas/
│ │ │ ├── user.py # Pydantic schemas for user/auth
│ │ │ └── entry.py # Pydantic schemas for entries/stats
│ │ └── utils/
│ │ └── security.py # JWT creation, password hashing/verification
│ │ └── security.py # JWT creation/decode, password hashing
│ ├── Dockerfile
│ └── requirements.txt
├── frontend/
│ ├── index.html # Landing / public stats page
│ ├── index.html # Landing page + public community bottles
│ ├── login.html
│ ├── register.html
│ ├── dashboard.html # Authenticated home with stats
│ ├── log.html # Log a new entry
│ ├── profile.html # User profile / settings
│ ├── css/style.css
│ ├── dashboard.html # My Bottle — stats, fill bar, entry log
│ ├── log.html # Log add or remove entries
│ ├── profile.html # Profile settings (display name, timezone, password)
│ ├── admin.html # Admin user management panel
│ ├── css/style.css # Bourbon-themed dark UI
│ └── js/
│ ├── api.js # Fetch wrapper, base URL, token handling
── auth.js # Login/register/logout logic
│ ├── api.js # Fetch wrapper, token injection, escHtml, admin API
── auth.js # Auth state, nav rendering, settings modal
│ └── admin.js # Admin page logic (user table, modals, impersonation)
├── nginx/
│ └── default.conf # Serves frontend, proxies /api/ to backend:8000
├── docker-compose.yml
└── .env.example
```
## Pages
| Page | Auth | Description |
|---|---|---|
| `/index.html` | No | Landing page with community bottle cards; click to browse bourbons |
| `/login.html` | No | Login form |
| `/register.html` | No | Registration form |
| `/dashboard.html` | Yes | My Bottle — stats, fill bar, full entry log with delete |
| `/log.html` | Yes | Log an add or remove entry |
| `/profile.html` | Yes | Edit display name, timezone, bottle size, password |
| `/admin.html` | Admin | User management — reset PW, disable, impersonate, delete |
## Navigation
The top-right nav area shows (when logged in):
- **Display name** — non-clickable label
- **Admin** button — only visible to admin accounts, links to `/admin.html`
- **⚙ Settings** button — opens a modal to update display name, bottle size, timezone, and password
- **Logout** button
When impersonating a user, the nav shows **"Viewing as [name]"** and a **Return to Admin** button.
## API Endpoints
### Auth
| Method | Path | Auth | Description |
|---|---|---|---|
| POST | `/api/auth/register` | No | Register, returns JWT |
| POST | `/api/auth/register` | No | Register new account, 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 |
| PUT | `/api/users/me` | Yes | Update display name, timezone, bottle size |
| PUT | `/api/users/me/password` | Yes | Change password |
### Entries
| Method | Path | Auth | Description |
@@ -80,7 +119,7 @@ bourbonacci/
### Public
| Method | Path | Auth | Description |
|---|---|---|---|
| GET | `/api/public/stats` | No | Aggregated stats for all users |
| GET | `/api/public/stats` | No | Stats + bourbon list for all users |
### Admin
| Method | Path | Auth | Description |
@@ -90,9 +129,9 @@ bourbonacci/
| POST | `/api/admin/users/{id}/reset-password` | Admin | Force-reset a user's password |
| POST | `/api/admin/users/{id}/disable` | Admin | Disable a user account |
| POST | `/api/admin/users/{id}/enable` | Admin | Re-enable a user account |
| DELETE | `/api/admin/users/{id}` | Admin | Hard-delete a user |
| DELETE | `/api/admin/users/{id}` | Admin | Hard-delete a user and all their data |
| POST | `/api/admin/users/{id}/impersonate` | Admin | Get a token scoped as that user |
| POST | `/api/admin/unimpersonate` | Admin | Swap back to the admin token |
| POST | `/api/admin/unimpersonate` | Admin | Exchange impersonation token back for admin token |
Authenticated routes expect `Authorization: Bearer <token>` header.
@@ -109,7 +148,18 @@ Authenticated routes expect `Authorization: Bearer <token>` header.
}
```
Stats return `current_total_shots` (adds minus removes) and `estimated_proof` (weighted average across add entries).
### Stats Response
```json
{
"total_add_entries": 12,
"total_shots_added": 18.5,
"current_total_shots": 14.25,
"estimated_proof": 95.3
}
```
`current_total_shots` = total added total removed. `estimated_proof` = weighted average proof across all add entries by shot count.
## Getting Started
@@ -126,7 +176,7 @@ cp .env.example .env
Generate a secure secret key:
```bash
python3 -c "import secrets; print(secrets.token_hex(32))"
python3 -c "import secrets; print(secrets.token_hex(64))"
```
### Run
@@ -135,9 +185,9 @@ python3 -c "import secrets; print(secrets.token_hex(32))"
docker compose up -d
```
The app will be available at `http://localhost:8057`.
The app will be available at `http://localhost` (or whichever port is mapped in `docker-compose.yml`).
The backend waits for MySQL to pass its healthcheck before starting. Tables are created automatically on first boot via SQLAlchemy's `init_db()`.
The backend waits for MySQL to pass its healthcheck before starting. Tables are created automatically on first boot via SQLAlchemy's `create_all`. The admin account defined in `.env` is seeded/re-synced on every container start.
### Stop
@@ -149,22 +199,42 @@ 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` |
| `ADMIN_USERNAME` | Admin account email (seeded on every start) | — |
| `ADMIN_PASSWORD` | Admin account password (re-synced on every start) | — |
| Variable | Description |
|---|---|
| `MYSQL_ROOT_PASSWORD` | MySQL root password |
| `MYSQL_DATABASE` | Database name (default: `bourbonacci`) |
| `MYSQL_USER` | App DB user |
| `MYSQL_PASSWORD` | App DB password |
| `DATABASE_URL` | SQLAlchemy async DSN (`mysql+aiomysql://user:pass@db:3306/dbname`) |
| `SECRET_KEY` | JWT signing secret keep long and random |
| `ACCESS_TOKEN_EXPIRE_MINUTES` | JWT TTL in minutes (default: `480`) |
| `ADMIN_USERNAME` | Admin account email, seeded on every container start |
| `ADMIN_PASSWORD` | Admin account password, re-synced on every container start |
## Data Model
**users**
- `id`, `email` (unique), `password_hash`, `display_name`, `timezone`, `created_at`
| Column | Type | Notes |
|---|---|---|
| `id` | int PK | |
| `email` | varchar(255) unique | |
| `password_hash` | varchar(255) | bcrypt |
| `display_name` | varchar(100) nullable | |
| `timezone` | varchar(50) | default `UTC` |
| `bottle_size` | float nullable | user-configured bottle capacity in shots |
| `is_admin` | bool | default false |
| `is_disabled` | bool | default false |
| `created_at` | datetime | server default |
**entries**
- `id`, `user_id` (FK), `entry_type` (`add`/`remove`), `date`, `bourbon_name`, `proof`, `amount_shots`, `notes`, `created_at`
| Column | Type | Notes |
|---|---|---|
| `id` | int PK | |
| `user_id` | int FK → users | cascade delete |
| `entry_type` | enum | `add` or `remove` |
| `date` | date | user-supplied |
| `bourbon_name` | varchar nullable | required for `add` entries |
| `proof` | float nullable | |
| `amount_shots` | float | default 1.0 |
| `notes` | text nullable | |
| `created_at` | datetime | server default |