derekc ca83351e9d Add bottle size, bourbon list modal, and stat improvements
- 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>
2026-03-24 21:53:50 -07:00
2026-03-24 19:11:00 -07:00
2026-03-24 19:11:00 -07:00

🥃 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.
Readme 129 KiB
Languages
HTML 41%
Python 26.6%
JavaScript 20.5%
CSS 11.7%
Dockerfile 0.2%