update readme
This commit is contained in:
1 parent
6da4c3e379
commit
f078034789
1 file changed
+163
-107
@@ -1,132 +1,188 @@
|
|||||||
# Poker Portal
|
# Boker
|
||||||
|
|
||||||
A small Flask app I put together for tracking our low-stakes hold'em nights without having to keep a spreadsheet open all the time.
|
A Flask web app for tracking low-stakes poker nights. Multiple leagues, per-player stats, session history, leaderboards, and a full double-entry ledger — without keeping a spreadsheet open.
|
||||||
|
|
||||||
The idea is pretty simple: public pages for stats and session history, plus a small admin area for recording buy-ins, gross cashout results, actual paid-out cash, and notes. The data sits in a CSV audit log, so everything is append-only and easy to follow later.
|
## Features
|
||||||
|
|
||||||
## What it does
|
- User accounts with registration and login
|
||||||
|
- Create and manage multiple leagues
|
||||||
- all-time leaderboard
|
- Invite league members by email with role-based access (owner / manager / viewer)
|
||||||
- per-session pages
|
- Public or private league visibility
|
||||||
- per-player stat pages
|
- Per-session event ledger (buy-ins, cashouts, fronts, rollovers, payouts)
|
||||||
- admin login for recording events
|
- All-time leaderboard with rank tracking and eligibility thresholds
|
||||||
- open / closed session tracking
|
- Per-player stat pages with session history and charts
|
||||||
- actual cash payout tracking with `paid_out` events
|
- Open / closed session tracking
|
||||||
- debt repayments and write-offs with `debt_repayment` / `writeoff` events
|
- Debt tracking: fronts, repayments, and write-offs
|
||||||
- session and player charts
|
- CSV export of any league ledger
|
||||||
- CSV import / export from the admin page
|
- CSV import for migrating historical data
|
||||||
- append-only `entries.csv` ledger instead of overwriting old rows
|
- Rate limiting and CSRF protection
|
||||||
|
|
||||||
## Stack
|
## Stack
|
||||||
|
|
||||||
Built with a pretty lightweight setup:
|
- Python 3 / Flask
|
||||||
|
- SQLAlchemy + Flask-Migrate (SQLite for local dev, PostgreSQL in production)
|
||||||
- Python
|
- Jinja2 templates
|
||||||
- Flask
|
|
||||||
- Jinja templates
|
|
||||||
- Chart.js
|
- Chart.js
|
||||||
- plain CSS
|
- Plain CSS
|
||||||
- CSV event log for storage
|
|
||||||
|
## Setup
|
||||||
|
|
||||||
|
### 1. Clone and create a virtual environment
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone https://github.com/SowinskiBraeden/boker
|
||||||
|
cd boker
|
||||||
|
python -m venv .venv
|
||||||
|
source .venv/bin/activate
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Install dependencies
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pip install -r requirements.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Configure environment
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cp .env.example .env
|
||||||
|
```
|
||||||
|
|
||||||
|
Open `.env` and set at minimum:
|
||||||
|
|
||||||
|
```env
|
||||||
|
SECRET_KEY=replace-this-with-a-long-random-string
|
||||||
|
```
|
||||||
|
|
||||||
|
The other values can be left as defaults for local development. See the [Environment variables](#environment-variables) section for the full list.
|
||||||
|
|
||||||
|
### 4. Initialize the database
|
||||||
|
|
||||||
|
```bash
|
||||||
|
flask --app app db upgrade
|
||||||
|
```
|
||||||
|
|
||||||
|
This creates `data/boker-dev.sqlite3` and applies all migrations. Run this again whenever you pull new migrations.
|
||||||
|
|
||||||
|
### 5. Run the development server
|
||||||
|
|
||||||
|
```bash
|
||||||
|
flask --app app run
|
||||||
|
```
|
||||||
|
|
||||||
|
Then open `http://127.0.0.1:5000` and register an account.
|
||||||
|
|
||||||
|
## Environment variables
|
||||||
|
|
||||||
|
All variables are read from `.env` at startup. Copy `.env.example` as a starting point.
|
||||||
|
|
||||||
|
| Variable | Required | Default | Description |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `SECRET_KEY` | Yes | `change-this-before-deploying` | Flask session signing key. Use a long random string in production. |
|
||||||
|
| `DATABASE_URL` | No | `sqlite:///data/boker-dev.sqlite3` | SQLAlchemy connection URL. For production use a `postgresql+psycopg://` URL. |
|
||||||
|
| `FLASK_ENV` | No | _(unset)_ | Set to `production` to enable secure cookie flags. |
|
||||||
|
| `APP_BASE_URL` | No | `http://localhost:5000` | Base URL used when generating links in email (invite, password reset). |
|
||||||
|
| `MAIL_SERVER` | No | _(empty)_ | SMTP server hostname. Email features are disabled if left blank. |
|
||||||
|
| `MAIL_PORT` | No | `587` | SMTP port. |
|
||||||
|
| `MAIL_USE_TLS` | No | `true` | Set to `false` to disable STARTTLS. |
|
||||||
|
| `MAIL_USERNAME` | No | _(empty)_ | SMTP username / API key. |
|
||||||
|
| `MAIL_PASSWORD` | No | _(empty)_ | SMTP password / API key secret. |
|
||||||
|
| `MAIL_DEFAULT_SENDER` | No | `noreply@myboker.org` | From address on outgoing mail. |
|
||||||
|
| `FLASK_DEBUG` | No | `0` | Set to `1` to enable the Flask reloader and debugger. |
|
||||||
|
|
||||||
|
## Running tests
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python -m pytest tests/
|
||||||
|
```
|
||||||
|
|
||||||
|
Or with the standard library runner:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python -m unittest discover tests/
|
||||||
|
```
|
||||||
|
|
||||||
|
## Production deployment
|
||||||
|
|
||||||
|
### Database
|
||||||
|
|
||||||
|
Set `DATABASE_URL` to a PostgreSQL connection string:
|
||||||
|
|
||||||
|
```env
|
||||||
|
DATABASE_URL=postgresql+psycopg://user:password@host/dbname
|
||||||
|
```
|
||||||
|
|
||||||
|
`psycopg` (v3) is already in `requirements.txt`.
|
||||||
|
|
||||||
|
After deploying, run migrations:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
flask --app app db upgrade
|
||||||
|
```
|
||||||
|
|
||||||
|
### Environment
|
||||||
|
|
||||||
|
Set `FLASK_ENV=production` to enable:
|
||||||
|
- `Secure` flag on the session cookie
|
||||||
|
- `SameSite=Strict` cookie policy
|
||||||
|
- HTTPS preferred URL scheme
|
||||||
|
|
||||||
|
Generate a strong secret key:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python -c "import secrets; print(secrets.token_hex(32))"
|
||||||
|
```
|
||||||
|
|
||||||
|
### WSGI
|
||||||
|
|
||||||
|
Run with a production WSGI server (gunicorn, uWSGI, etc.) rather than the Flask dev server:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
gunicorn "app:app"
|
||||||
|
```
|
||||||
|
|
||||||
## Ledger model
|
## Ledger model
|
||||||
|
|
||||||
The app treats `data/entries.csv` as the source of truth.
|
Each event in the ledger is an append-only row. Nothing is edited or deleted — corrections are new rows. This keeps the full history readable.
|
||||||
|
|
||||||
Each row is an event, not a final snapshot. Instead of editing an old row, I append another one. That keeps rebuys, corrections, payouts, and session state changes visible in the log instead of hiding them behind edits.
|
### Event types
|
||||||
|
|
||||||
Current event types:
|
| Event | Description |
|
||||||
|
|---|---|
|
||||||
|
| `buyin` | Player buys chips. Counts as poker investment and real cash in. |
|
||||||
|
| `front` | House fronts chips to a player. Counts as poker investment; cash is owed back. |
|
||||||
|
| `cashout` | Gross chip result at end of session. Not cash movement — sets the payout claim. |
|
||||||
|
| `paid_out` | Cash physically paid out to a player. Settles the payout claim. |
|
||||||
|
| `rollover_out` | Player carries winnings into the next session instead of being paid. Settles source session without cash movement. |
|
||||||
|
| `rollover_in` | Carried funds enter play in the destination session. Counts as investment without new cash in. |
|
||||||
|
| `payout_carry_in` | Prior-session credit applied to a later payout. Increases the payout due without counting as investment. |
|
||||||
|
| `debt_repayment` | Player repays a front outside of poker. Counts as real cash in; reduces the receivable. |
|
||||||
|
| `writeoff` | Front is forgiven. Resolves the receivable without cash. |
|
||||||
|
| `note` | Free-text bookkeeping note. No financial effect. |
|
||||||
|
| `session_open` | Marks a session as live. |
|
||||||
|
| `session_close` | Marks a session as closed. |
|
||||||
|
|
||||||
- `buyin`
|
Legacy ledgers may contain `paid`, `front_collected`, or `front_writeoff` — the app reads these as aliases for `paid_out`, `debt_repayment`, and `writeoff`.
|
||||||
- `front`
|
|
||||||
- `debt_repayment`
|
|
||||||
- `writeoff`
|
|
||||||
- `cashout`
|
|
||||||
- `paid_out`
|
|
||||||
- `rollover_in`
|
|
||||||
- `payout_carry_in`
|
|
||||||
- `rollover_out`
|
|
||||||
- `note`
|
|
||||||
- `session_open`
|
|
||||||
- `session_close`
|
|
||||||
|
|
||||||
A few examples:
|
### Accounting summary
|
||||||
|
|
||||||
- another `buyin` for a rebuy
|
- **Poker investment** = `buyin + front + rollover_in`
|
||||||
- another `cashout` if chip counts are corrected later
|
- **Poker net** = `cashout − investment`
|
||||||
- a `paid_out` event when someone is actually settled up
|
- **Real cash in** = `buyin + debt_repayment`
|
||||||
- a `debt_repayment` event when a front is repaid outside poker
|
- **Real cash out** = `paid_out`
|
||||||
- a `writeoff` event when a front will not be collected
|
- **Rollover-out** settles a session without cash leaving the book
|
||||||
- a `note` event for bookkeeping context
|
- **Writeoff** resolves a receivable without cash coming in
|
||||||
- `session_open` / `session_close` to mark whether a game night is still live
|
|
||||||
|
|
||||||
Accounting in the app keeps poker results separate from banker cashflow:
|
|
||||||
|
|
||||||
- poker investment is `buyin + front + rollover_in`
|
|
||||||
- poker net is `cashout - poker investment`
|
|
||||||
- real cash in is `buyin + debt_repayment`
|
|
||||||
- real cash out is `paid_out`
|
|
||||||
- `rollover_out` settles the source session without counting as cash out
|
|
||||||
- `rollover_in` enters play in the destination session without counting as cash in
|
|
||||||
- `payout_carry_in` records prior-session value carried into a later payout; it increases the destination payout due without counting as poker investment or cash in
|
|
||||||
- `writeoff` resolves a receivable without counting as cash in
|
|
||||||
|
|
||||||
Older ledgers may still contain `paid`, `front_collected`, or `front_writeoff`.
|
|
||||||
The app reads those historical names as aliases for `paid_out`,
|
|
||||||
`debt_repayment`, and `writeoff`.
|
|
||||||
|
|
||||||
It is still just a small side project, but I wanted the event model to stay clean enough that the numbers are easy to trust and the history is easy to read back through.
|
|
||||||
|
|
||||||
## CSV format
|
## CSV format
|
||||||
|
|
||||||
Main file:
|
Each league's ledger can be exported and re-imported as CSV.
|
||||||
|
|
||||||
`data/entries.csv`
|
|
||||||
|
|
||||||
Header:
|
Header:
|
||||||
|
|
||||||
```csv
|
```
|
||||||
id,created_at,session_id,session_date,player_name,event_type,amount_cents,note,actor
|
id,created_at,session_id,session_date,player_name,event_type,amount_cents,note,actor
|
||||||
```
|
```
|
||||||
|
|
||||||
Amounts are stored in cents to avoid floating-point issues.
|
Amounts are stored in cents (integer) to avoid floating-point rounding.
|
||||||
|
|
||||||
## Running it locally
|
Import validates headers, event types, session references, and player names before writing anything. Rows with a matching `id` are skipped so re-importing a previous export is safe.
|
||||||
|
|
||||||
```bash
|
|
||||||
python -m venv .venv
|
|
||||||
source .venv/bin/activate
|
|
||||||
pip install -r requirements.txt
|
|
||||||
cp .env.example .env
|
|
||||||
python app.py
|
|
||||||
```
|
|
||||||
|
|
||||||
Then open:
|
|
||||||
|
|
||||||
`http://127.0.0.1:8000`
|
|
||||||
|
|
||||||
## Environment values
|
|
||||||
|
|
||||||
The app reads these from `.env`:
|
|
||||||
|
|
||||||
- `SECRET_KEY`
|
|
||||||
- `ADMIN_USERNAME`
|
|
||||||
- `ADMIN_PASSWORD`
|
|
||||||
|
|
||||||
Example:
|
|
||||||
|
|
||||||
```env
|
|
||||||
SECRET_KEY=change-this
|
|
||||||
ADMIN_USERNAME=admin
|
|
||||||
ADMIN_PASSWORD=change-me
|
|
||||||
```
|
|
||||||
|
|
||||||
## Notes
|
|
||||||
|
|
||||||
A few choices here were deliberate:
|
|
||||||
|
|
||||||
- no database for now
|
|
||||||
- no user accounts, just one admin login
|
|
||||||
- public-facing stats pages, admin-only controls
|
|
||||||
- CSV backup before importing a replacement ledger
|
|
||||||
|
|
||||||
If I ever decide to take it further, the first real upgrade would probably be moving the storage layer to SQLite while keeping the rest of the app roughly the same.
|
|
||||||
Reference in new issue
Block a user