ca83351e9dc6d6e5387bf4152de9b6a2cff14294
- Add bottle_size field to User model and UserResponse/UserUpdate schemas - Settings modal includes bottle size input (shots capacity) - Community bottles and My Bottle page show fill bar based on bottle size - Community bottle cards are clickable — opens searchable bourbon list modal - Add total_shots_added stat to replace duplicate net volume on dashboard - Reorder dashboard stats: Bourbons Added, Total Poured In, Shots Remaining, Est. Proof - Theme-matched custom scrollbar (amber on dark) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
🥃 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 |
Admin
| Method | Path | Auth | Description |
|---|---|---|---|
| GET | /api/admin/users |
Admin | List all users |
| POST | /api/admin/users |
Admin | Create a user |
| 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 |
| 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 |
Authenticated routes expect Authorization: Bearer <token> header.
Entry Schema
{
"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
cp .env.example .env
# Edit .env — set real passwords and a secure SECRET_KEY
Generate a secure secret key:
python3 -c "import secrets; print(secrets.token_hex(32))"
Run
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
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 |
ADMIN_USERNAME |
Admin account email (seeded on every start) | — |
ADMIN_PASSWORD |
Admin account password (re-synced on every start) | — |
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
Description
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.
Languages
HTML
41%
Python
26.6%
JavaScript
20.5%
CSS
11.7%
Dockerfile
0.2%