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
|
||||
|
||||
- all-time leaderboard
|
||||
- per-session pages
|
||||
- per-player stat pages
|
||||
- admin login for recording events
|
||||
- open / closed session tracking
|
||||
- actual cash payout tracking with `paid_out` events
|
||||
- debt repayments and write-offs with `debt_repayment` / `writeoff` events
|
||||
- session and player charts
|
||||
- CSV import / export from the admin page
|
||||
- append-only `entries.csv` ledger instead of overwriting old rows
|
||||
- User accounts with registration and login
|
||||
- Create and manage multiple leagues
|
||||
- Invite league members by email with role-based access (owner / manager / viewer)
|
||||
- Public or private league visibility
|
||||
- Per-session event ledger (buy-ins, cashouts, fronts, rollovers, payouts)
|
||||
- All-time leaderboard with rank tracking and eligibility thresholds
|
||||
- Per-player stat pages with session history and charts
|
||||
- Open / closed session tracking
|
||||
- Debt tracking: fronts, repayments, and write-offs
|
||||
- CSV export of any league ledger
|
||||
- CSV import for migrating historical data
|
||||
- Rate limiting and CSRF protection
|
||||
|
||||
## Stack
|
||||
|
||||
Built with a pretty lightweight setup:
|
||||
|
||||
- Python
|
||||
- Flask
|
||||
- Jinja templates
|
||||
- Python 3 / Flask
|
||||
- SQLAlchemy + Flask-Migrate (SQLite for local dev, PostgreSQL in production)
|
||||
- Jinja2 templates
|
||||
- Chart.js
|
||||
- plain CSS
|
||||
- CSV event log for storage
|
||||
- Plain CSS
|
||||
|
||||
## 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
|
||||
|
||||
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`
|
||||
- `front`
|
||||
- `debt_repayment`
|
||||
- `writeoff`
|
||||
- `cashout`
|
||||
- `paid_out`
|
||||
- `rollover_in`
|
||||
- `payout_carry_in`
|
||||
- `rollover_out`
|
||||
- `note`
|
||||
- `session_open`
|
||||
- `session_close`
|
||||
Legacy ledgers may contain `paid`, `front_collected`, or `front_writeoff` — the app reads these as aliases for `paid_out`, `debt_repayment`, and `writeoff`.
|
||||
|
||||
A few examples:
|
||||
### Accounting summary
|
||||
|
||||
- another `buyin` for a rebuy
|
||||
- another `cashout` if chip counts are corrected later
|
||||
- a `paid_out` event when someone is actually settled up
|
||||
- a `debt_repayment` event when a front is repaid outside poker
|
||||
- a `writeoff` event when a front will not be collected
|
||||
- a `note` event for bookkeeping context
|
||||
- `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.
|
||||
- **Poker investment** = `buyin + front + rollover_in`
|
||||
- **Poker net** = `cashout − investment`
|
||||
- **Real cash in** = `buyin + debt_repayment`
|
||||
- **Real cash out** = `paid_out`
|
||||
- **Rollover-out** settles a session without cash leaving the book
|
||||
- **Writeoff** resolves a receivable without cash coming in
|
||||
|
||||
## CSV format
|
||||
|
||||
Main file:
|
||||
|
||||
`data/entries.csv`
|
||||
Each league's ledger can be exported and re-imported as CSV.
|
||||
|
||||
Header:
|
||||
|
||||
```csv
|
||||
```
|
||||
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
|
||||
|
||||
```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.
|
||||
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.
|
||||
Reference in new issue
Block a user