From 317ef2dfaaa47bd2bb8ac31ae163b2b4121c756f Mon Sep 17 00:00:00 2001 From: SowinskiBraeden Date: Mon, 29 Jun 2026 12:54:10 -0700 Subject: [PATCH] remove legacy system routes --- boker/app.py | 2 - boker/config.py | 5 +- boker/routes/public.py | 176 +-------------- boker/storage.py | 112 ---------- templates/docs.html | 222 ------------------- templates/leaderboard.html | 348 ------------------------------ templates/player_detail.html | 344 ----------------------------- templates/session_detail.html | 285 ------------------------ templates/sessions.html | 54 ----- tests/test_config.py | 8 +- tests/test_phase2_auth_leagues.py | 6 + 11 files changed, 15 insertions(+), 1547 deletions(-) delete mode 100644 templates/docs.html delete mode 100644 templates/leaderboard.html delete mode 100644 templates/player_detail.html delete mode 100644 templates/session_detail.html delete mode 100644 templates/sessions.html diff --git a/boker/app.py b/boker/app.py index 4be0978..2abffef 100644 --- a/boker/app.py +++ b/boker/app.py @@ -16,7 +16,6 @@ from boker.routes.account import account_bp from boker.routes.internal import internal_bp from boker.routes.leagues import leagues_bp from boker.routes.public import public_bp -from boker.storage import ensure_data_file from boker.utils import cents_to_dollars, safe_date_label @@ -40,7 +39,6 @@ def create_app(config_overrides: dict | None = None) -> Flask: if database_url == DEFAULT_DATABASE_URL or database_url.startswith("sqlite:"): raise RuntimeError("Set DATABASE_URL to a production PostgreSQL database before running in production.") - ensure_data_file(app.config["DATA_PATH"]) init_database(app) csrf.init_app(app) limiter.init_app(app) diff --git a/boker/config.py b/boker/config.py index 5a95074..2077e0f 100644 --- a/boker/config.py +++ b/boker/config.py @@ -5,12 +5,10 @@ import os from pathlib import Path BASE_DIR = Path(__file__).resolve().parent.parent -DATA_PATH = BASE_DIR / "data" / "entries.csv" DEFAULT_DATABASE_URL = f"sqlite:///{BASE_DIR / 'data' / 'boker-dev.sqlite3'}" DEFAULT_SECRET_KEY = "change-this-before-deploying" -ELIGIBLE_MIN_SESSIONS = 3 -APP_VERSION = "2.5.31" +APP_VERSION = "2.5.32" def load_local_env(env_path: Path) -> None: @@ -39,7 +37,6 @@ class Config: SESSION_COOKIE_HTTPONLY: bool = True SESSION_COOKIE_SAMESITE: str = "Lax" SESSION_COOKIE_NAME: str = "myboker_org_session" - DATA_PATH: Path = DATA_PATH WTF_CSRF_TIME_LIMIT: int = 3600 APP_BASE_URL: str = os.getenv("APP_BASE_URL", "http://localhost:5000") MAIL_SERVER: str = os.getenv("MAIL_SERVER", "") diff --git a/boker/routes/public.py b/boker/routes/public.py index 137be40..5846658 100644 --- a/boker/routes/public.py +++ b/boker/routes/public.py @@ -4,20 +4,9 @@ from __future__ import annotations from datetime import date from xml.sax.saxutils import escape as xml_escape -from flask import Blueprint, Response, current_app, flash, redirect, render_template, request, url_for +from flask import Blueprint, Response, current_app, render_template, request, url_for from sqlalchemy.exc import SQLAlchemyError -from boker.charts import cumulative_profit_series, player_session_series, session_breakdown_series -from boker.config import ELIGIBLE_MIN_SESSIONS -from boker.services import ( - apply_rank_changes, - build_leaderboard, - build_session_summaries, - session_events, -) -from boker.storage import load_events -from boker.utils import session_label, session_sort_key - public_bp = Blueprint("public", __name__) @@ -110,166 +99,3 @@ def explore(): db.session.rollback() current_app.logger.warning("Explore public league search failed: %s", exc.__class__.__name__) return render_template("explore.html", leagues=leagues, counts=counts, q=q, has_searched=bool(q)) - - -@public_bp.get("/leaderboard") -def leaderboard(): - events = load_events(current_app.config["DATA_PATH"]) - all_sessions = build_session_summaries(events) - ordered_sessions = sorted(all_sessions, key=session_sort_key) - - session_ids = [s.session_id for s in ordered_sessions] - selected_session_id = request.args.get("through_session", "").strip() - mode = request.args.get("mode", "eligible").strip() - if mode not in ("eligible", "all", "recent"): - mode = "eligible" - label = "" - cutoff_index = len(session_ids) - 1 - - if session_ids: - if selected_session_id in session_ids: - cutoff_index = session_ids.index(selected_session_id) - label = session_label(ordered_sessions[cutoff_index]) - else: - label = session_label(ordered_sessions[cutoff_index]) - - filtered_sessions = ordered_sessions[: cutoff_index + 1] - previous_sessions = ordered_sessions[:cutoff_index] - else: - filtered_sessions = [] - previous_sessions = [] - - board = build_leaderboard(filtered_sessions) - previous_board = build_leaderboard(previous_sessions) - board = apply_rank_changes(board, previous_board) - - eligible_count = sum(1 for p in board if p.sessions_played >= ELIGIBLE_MIN_SESSIONS) - all_count = len(board) - recent_sessions_slice = filtered_sessions[-5:] - recent_count = len({ - entry.player_name - for s in recent_sessions_slice - for entry in s.entries - }) - - if mode == "recent": - mode_board = build_leaderboard(recent_sessions_slice) - main_board = mode_board - provisional_board = [] - elif mode == "eligible": - main_board = [p for p in board if p.sessions_played >= ELIGIBLE_MIN_SESSIONS] - provisional_board = [p for p in board if p.sessions_played < ELIGIBLE_MIN_SESSIONS] - else: - main_board = board - provisional_board = [] - - chart_data = cumulative_profit_series(filtered_sessions) - cash_paid_out_cents = sum(s.total_paid_out_cents for s in all_sessions) - - return render_template( - "leaderboard.html", - main_board=main_board, - provisional_board=provisional_board, - mode=mode, - eligible_count=eligible_count, - all_count=all_count, - recent_count=recent_count, - eligible_min_sessions=ELIGIBLE_MIN_SESSIONS, - session_count=len(filtered_sessions), - total_session_count=len(all_sessions), - cash_paid_out_cents=cash_paid_out_cents, - chart_data=chart_data, - available_sessions=all_sessions, - selected_session_id=selected_session_id, - selected_session_label=(label if selected_session_id else "Latest session"), - selected_session_date=( - ordered_sessions[cutoff_index].session_date - if selected_session_id and session_ids - else "" - ), - session_label=session_label, - ) - - -@public_bp.get("/sessions") -def sessions(): - events = load_events(current_app.config["DATA_PATH"]) - all_sessions = build_session_summaries(events) - - return render_template( - "sessions.html", - sessions=all_sessions, - session_label=session_label, - ) - - -@public_bp.get("/sessions/") -def session_detail(session_id: str): - events = load_events(current_app.config["DATA_PATH"]) - all_sessions = build_session_summaries(events) - target_session = next( - (s for s in all_sessions if s.session_id == session_id), - None, - ) - if target_session is None: - flash("That session was not found.", "error") - return redirect(url_for("public.sessions")) - - chronological_sessions = sorted(all_sessions, key=session_sort_key) - chronological_index = next( - index - for index, s in enumerate(chronological_sessions) - if s.session_id == session_id - ) - target_session = chronological_sessions[chronological_index] - session_number = chronological_index + 1 - prev_session = ( - chronological_sessions[chronological_index - 1] - if chronological_index > 0 - else None - ) - next_session = ( - chronological_sessions[chronological_index + 1] - if chronological_index < len(chronological_sessions) - 1 - else None - ) - - return render_template( - "session_detail.html", - session=target_session, - next_session=next_session, - prev_session=prev_session, - session_number=session_number, - raw_events=session_events(events, session_id), - chart_data=session_breakdown_series(target_session), - session_label=session_label, - ) - - -@public_bp.get("/players/") -def player_detail(player_name: str): - events = load_events(current_app.config["DATA_PATH"]) - all_sessions = build_session_summaries(events) - board = build_leaderboard(all_sessions) - player_stats = next( - (player for player in board if player.player_name == player_name), None - ) - if player_stats is None: - flash("That player was not found.", "error") - return redirect(url_for("public.leaderboard")) - - player_rank = next( - ( - index - for index, ranked_player in enumerate(board, start=1) - if ranked_player.player_name == player_name - ), - None, - ) - chart_data = player_session_series(all_sessions, player_name) - return render_template( - "player_detail.html", - player=player_stats, - player_rank=player_rank, - chart_data=chart_data, - ) diff --git a/boker/storage.py b/boker/storage.py index 341aa36..f2d7948 100644 --- a/boker/storage.py +++ b/boker/storage.py @@ -1,10 +1,6 @@ #!/usr/bin/env python3 from __future__ import annotations -import csv -import uuid -from datetime import datetime, timezone -from pathlib import Path from typing import TypedDict CSV_HEADERS = [ @@ -32,111 +28,3 @@ class EventRow(TypedDict): actor: str voided_at: str void_reason: str - - -VALID_EVENT_TYPES = { - "buyin", - "front", - "front_collected", - "front_writeoff", - "cashout", - "paid", - "paid_out", - "rollover_in", - "payout_carry_in", - "rollover_out", - "debt_repayment", - "writeoff", - "note", - "session_open", - "session_close", -} - - -def ensure_data_file(csv_path: Path) -> None: - csv_path.parent.mkdir(parents=True, exist_ok=True) - if csv_path.exists(): - return - - with csv_path.open("w", newline="", encoding="utf-8") as file: - writer = csv.DictWriter(file, fieldnames=CSV_HEADERS) - writer.writeheader() - - -def load_events(csv_path: Path) -> list[EventRow]: - ensure_data_file(csv_path) - - events: list[EventRow] = [] - with csv_path.open("r", newline="", encoding="utf-8") as file: - reader = csv.DictReader(file) - for row in reader: - events.append( - EventRow( - id=row["id"], - created_at=row["created_at"], - session_id=row["session_id"], - session_date=row["session_date"], - player_name=row["player_name"], - event_type=row["event_type"], - amount_cents=int(row["amount_cents"] or 0), - note=row.get("note", ""), - actor=row.get("actor", ""), - voided_at="", - void_reason="", - ) - ) - - events.sort( - key=lambda event: (event["session_date"], event["created_at"], event["id"]) - ) - return events - - -def append_event( - csv_path: Path, - session_id: str, - session_date: str, - player_name: str, - event_type: str, - amount_cents: int, - note: str, - actor: str, -) -> EventRow: - ensure_data_file(csv_path) - - normalized_type = event_type.strip().lower() - if normalized_type not in VALID_EVENT_TYPES: - raise ValueError(f"Unsupported event type: {event_type}") - - if normalized_type in {"note", "session_open", "session_close"}: - amount_cents = 0 - - event = EventRow( - id=str(uuid.uuid4()), - created_at=datetime.now(timezone.utc).isoformat(), - session_id=session_id.strip(), - session_date=session_date.strip(), - player_name=player_name.strip(), - event_type=normalized_type, - amount_cents=amount_cents, - note=note.strip(), - actor=actor.strip(), - ) - - with csv_path.open("a", newline="", encoding="utf-8") as file: - writer = csv.DictWriter(file, fieldnames=CSV_HEADERS) - writer.writerow(event) - - return event - - -def write_events(csv_path: Path, events: list[EventRow]) -> None: - ensure_data_file(csv_path) - - tmp_path = csv_path.with_suffix(f"{csv_path.suffix}.tmp") - with tmp_path.open("w", newline="", encoding="utf-8") as file: - writer = csv.DictWriter(file, fieldnames=CSV_HEADERS) - writer.writeheader() - writer.writerows(events) - - tmp_path.replace(csv_path) diff --git a/templates/docs.html b/templates/docs.html deleted file mode 100644 index d6af72a..0000000 --- a/templates/docs.html +++ /dev/null @@ -1,222 +0,0 @@ -{% extends "base.html" %} -{% block title %}Help · myboker.org{% endblock %} -{% block content %} - -
-

Documentation

-

How myboker works

-

Everything you need to run a home poker league — sessions, ledgers, leaderboards, and settlement.

-
- -
- - - -
- -
-

Leagues

-

A league is the top-level container for your home game. It holds all your players, sessions, and ledger events in one place. One account can manage multiple leagues — useful if you run separate games (e.g. a Thursday game and a weekend game).

-

When you create a league you give it a name and optional description. A unique public key is automatically generated so you can share a link to it later if you make it public.

-
- Tip: Leagues are long-lived. You don't need to create a new league every season — use sessions to separate individual nights of play. -
-
- -
-

Players

-

Players are the people in your league. They do not need a myboker account — only the league owner or manager needs one. You just add names.

-

Each player has a display name that appears on the leaderboard and in ledger events. You can add an optional note (like a payment handle) to help with settlement later.

-

Archiving a player hides them from the active roster but preserves all their history. You can reactivate them at any time.

-
-
- Active - Shows on the roster and is available to record events against. -
-
- Archived - Hidden from the roster. History and stats are preserved. Reactivate any time. -
-
-
- -
-

Sessions

-

A session is a single night of poker. Sessions are date-stamped and sequenced — if you play twice on the same day, they become S1 and S2 automatically.

-

Sessions have two states: open and closed. While open, you can record buy-ins, cashouts, and other ledger events. When everyone has settled up and cashed out, you close the session. Closing doesn't delete anything — you can always reopen it.

-
-
- Date - The calendar date the game was played. Used to order sessions and group same-day games. -
-
- Label - Optional short name for this session (e.g. "Main table", "Side game"). Shown in lists. -
-
- Notes - Optional freeform context visible in the session list (e.g. "Holiday game", "Marcus's place"). -
-
- Open - Session is live — events can be added and settlement is still in progress. -
-
- Closed - Session is settled. Books are considered final. Can be reopened if needed. -
-
-
- -
-

Ledger events

-

All activity in a session is recorded as ledger events. The ledger is append-only — events are never deleted. This keeps the record clean and auditable, even if you need to correct a mistake (you just add a correcting entry).

-

There are eight event types:

-
-
- buy-in -
- A player buys chips with cash. -

Cash comes into the pot. Records how much the player has invested. Most common event — record one every time someone puts money on the table.

-
-
-
- front -
- The house covers a player's buy-in. -

When a player doesn't have cash handy, the organizer (house) can front them chips. This creates a debt — the player owes that amount back to the house. Track fronts carefully so you know who owes what at the end of the night.

-
-
-
- cashout -
- A player exchanges their chips for their cash value. -

Records the chip count a player walks away with. This is the "you are owed X" event — but it doesn't mean cash has left the house yet. Use paid out to record the actual cash handover.

-
-
-
- paid out -
- Cash physically leaves the house to the player. -

Closes the loop on a cashout. Once a player has been paid out, their balance goes to $0. Record this separately from cashout because you may pay people out at different times (end of night, next day via transfer, etc.).

-
-
-
- rollover in -
- Chips carried in from a previous session. -

If a player didn't cash out at the end of a session and instead carries their chip stack forward to the next game, record a rollover in at the start of the new session.

-
-
-
- rollover out -
- Chips carried out to a future session. -

Recorded when a player leaves a session without cashing out, intending to carry their stack forward. Paired with a rollover in at the next session.

-
-
-
- debt repayment -
- A player repays an outstanding front. -

When a player who was fronted chips pays the house back (cash, transfer, etc.), record a debt repayment. This reduces their outstanding balance with the house.

-
-
-
- write-off -
- The house forgives a debt. -

If a front won't be collected (player left, debt forgiven, etc.), write it off. The debt is cleared from the open balance without cash changing hands. This is irreversible — add a note explaining why.

-
-
-
-
- -
-

Settlement

-

The ledger tracks two types of open balances at all times:

-
-
- Due to players - Cash the house owes to players who have cashed out but haven't been paid yet. Pay these out to clear the balance. -
-
- Due to house - Cash players owe to the house from unpaid fronts. Collect debt repayments or write these off to clear the balance. -
-
- Cash in - Total real cash that has come into the pot across all sessions — buy-ins and repaid fronts only, not fronts themselves. -
-
- Cash paid out - Total real cash that has left the house to players. Ideally this approaches cash in over time. -
-
-
- Fronts vs buy-ins: A front doesn't add to "cash in" because no cash actually entered the pot — the house covered it. Cash in only counts real money on the table. -
-
- -
-

Leaderboard & stats

-

The leaderboard ranks players by their all-time performance. Stats are computed from ledger events, not manually entered.

-
-
- Net - Total cash out minus total cash in across all sessions. Positive means up, negative means down. -
-
- ROI - Net divided by total invested, expressed as a percentage. Compares performance independent of how much someone plays. -
-
- Win rate - Percentage of sessions where the player finished with a positive net. Ties (exactly break even) count as a loss. -
-
- Sessions played - Number of sessions with at least one ledger event for this player. -
-
- Biggest win - The single session where the player netted the most profit. -
-
- Biggest loss - The single session where the player lost the most money. -
-
-
- -
-

Public vs private

-

Each league has a visibility setting you can change in league settings.

-
-
- Private - Only members (owners and managers) can see this league. The league won't appear on the Explore page. This is the default. -
-
- Public - Anyone with the link — or who finds it on the Explore page — can view the leaderboard and session history. No account required to view. Members still need an account to manage it. -
-
-
- Note: Making a league public shares leaderboard rankings and session results, but not private financial details like individual debt balances or account emails. -
-
- -
-
- -{% endblock %} diff --git a/templates/leaderboard.html b/templates/leaderboard.html deleted file mode 100644 index 19fa8ce..0000000 --- a/templates/leaderboard.html +++ /dev/null @@ -1,348 +0,0 @@ -{% extends "base.html" %} -{% block title %}Leaderboard · myboker.org{% endblock %} -{% block content %} - -
-
-

All-time results

-

Leaderboard

-

{{ session_count }} session{{ 's' if session_count != 1 else '' }}{% if selected_session_date %} through {{ selected_session_label }}{% endif %}

-
-
-
- Cash paid out -
{{ cash_paid_out_cents | money }}
-
-
- Sessions -
{{ session_count }}{% if total_session_count != session_count %} / {{ total_session_count }}{% endif %}
-
-
- Players -
{{ all_count }}
-
-
-
- -
-
-
- Trend -

Cumulative profit

-
- -
-
-
- -
-
-
- -
-
-
- - - -
-

- {% if mode == 'eligible' %} - {{ eligible_min_sessions }}+ sessions required. Keeps one-off guests from skewing standings. - {% elif mode == 'all' %} - Everyone who's sat down — rankings can swing on tiny samples. - {% else %} - Form guide — aggregated over the last 5 sessions only. - {% endif %} -

-
-
-
- - - {% if selected_session_id %} - Reset - {% endif %} -
-
-
- -
- - - - - - - - - - - - - - - - {% for player in main_board %} - {% set idx = loop.index %} - - - - - - - - - - - - {% else %} - - - - {% endfor %} - -
- # - - -
- #{{ idx }} - - {{ player.player_name }} - - {% if player.rank_change > 0 %} - ▲{{ player.rank_change }} - {% elif player.rank_change < 0 %} - ▼{{ player.rank_change | abs }} - {% else %} - - {% endif %} - - {{ '+' if player.total_net_cents > 0 else '' }}{{ player.total_net_cents | money }} - {{ "%.1f"|format(player.win_pct) }}%{{ "%.1f"|format(player.roi_pct) }}%{{ player.avg_win_cents | money }}{{ player.avg_loss_cents | money }}{{ player.sessions_played }}
- No players yet. -
-
- -{% if mode == 'eligible' and provisional_board %} -
-
- Provisional · fewer than {{ eligible_min_sessions }} sessions -
- -
-{% endif %} - - - - - -{% endblock %} diff --git a/templates/player_detail.html b/templates/player_detail.html deleted file mode 100644 index eba5360..0000000 --- a/templates/player_detail.html +++ /dev/null @@ -1,344 +0,0 @@ -{% extends "base.html" %} -{% block title %}{{ player.player_name }} · myboker.org{% endblock %} -{% block content %} - -
-
- {% if player_rank %} -

Rank #{{ player_rank }}

- {% endif %} -

{{ player.player_name }}

-

{{ player.sessions_played }} session{{ 's' if player.sessions_played != 1 else '' }} · {{ "%.1f"|format(player.win_pct) }}% win rate

-
-
- -
-
- Total net -
- {{ '+' if player.total_net_cents > 0 else '' }}{{ player.total_net_cents | money }} -
-
-
- Win rate -
{{ "%.1f"|format(player.win_pct) }}%
-
{{ player.winning_sessions }}W · {{ player.losing_sessions }}L · {{ player.break_even_sessions }}E
-
-
- ROI -
{{ "%.1f"|format(player.roi_pct) }}%
-
-
- Sessions -
{{ player.sessions_played }}
-
-
- Current W/L -
- {{ player.current_win_streak }}W - {{ player.current_loss_streak }}L -
-
Longest {{ player.longest_win_streak }}W · {{ player.longest_loss_streak }}L
-
-
- Best / worst -
- {{ player.biggest_win_cents | money }} - {{ player.biggest_loss_cents | money }} -
-
-
- Total invested -
{{ player.total_invested_cents | money }}
-
-
- Gross cashouts -
{{ player.total_cash_out_cents | money }}
-
-
- - -
-
- Trend -

Cumulative profit

-
-
-
- -
-
-
- - -
-
-
- Session by session -

Results

-
-
-
- -
-
-
-
-
- Breakdown -

Result mix

-
-
-
- {% set result_total = player.winning_sessions + player.losing_sessions + player.break_even_sessions %} -
- {% if player.winning_sessions > 0 %} - - {% endif %} - {% if player.break_even_sessions > 0 %} - - {% endif %} - {% if player.losing_sessions > 0 %} - - {% endif %} -
-
-
- Wins - {{ player.winning_sessions }}{{ "%.1f"|format((player.winning_sessions / result_total * 100) if result_total else 0) }}% -
-
- Even - {{ player.break_even_sessions }}{{ "%.1f"|format((player.break_even_sessions / result_total * 100) if result_total else 0) }}% -
-
- Losses - {{ player.losing_sessions }}{{ "%.1f"|format((player.losing_sessions / result_total * 100) if result_total else 0) }}% -
-
- {% if result_total > 0 %} -
±$1.00 counts as even
- {% endif %} -
-
-
-
- - -
- - -
-
-
-
- - Poker performance -
-

Game results

-
-
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Total Net - {{ '+' if player.total_net_cents > 0 else '' }}{{ player.total_net_cents | money }} -
Win %{{ "%.1f"|format(player.win_pct) }}%
Record - {{ player.winning_sessions }}W - · - {{ player.losing_sessions }}L - · - {{ player.break_even_sessions }}E -
ROI{{ "%.1f"|format(player.roi_pct) }}%
Average Win{{ player.avg_win_cents | money }}
Average Loss{{ player.avg_loss_cents | money }}
Best / Worst - {{ player.biggest_win_cents | money }} - / - {{ player.biggest_loss_cents | money }} -
Win / Loss Streak - {{ player.current_win_streak }}W - · - {{ player.current_loss_streak }}L -
Longest Win / Loss - {{ player.longest_win_streak }}W · {{ player.longest_loss_streak }}L -
-
- - -
-
-
-
- - Poker basis + settlement -
-

Investment and settlement

-
-
- - - - - - - - - - - - - - - {% if player.total_payout_carry_in_cents > 0 %} - - - - - {% endif %} - - - - - - - - - - - - - - - - - - - - - {% if player.total_debt_repayment_cents > 0 %} - - - - - {% endif %} - {% if player.current_due_to_player_cents > 0 %} - - - - - {% endif %} - {% if player.current_due_to_house_cents > 0 %} - - - - - {% endif %} - {% if player.total_writeoff_cents > 0 %} - - - - - {% endif %} - -
Buy-ins paid{{ player.total_buy_in_cents | money }}
Fronted{{ player.total_front_cents | money }}
Rolled in{{ player.total_rollover_in_cents | money }}
Payout carry-in{{ player.total_payout_carry_in_cents | money }}
Total invested{{ player.total_invested_cents | money }}
Gross cashout result{{ player.total_cash_out_cents | money }}
Gross player claim{{ player.total_gross_payout_cents | money }}
Rolled out{{ player.total_rollover_out_cents | money }}
Cash paid out{{ player.total_paid_out_cents | money }}
Cash debt repaid{{ player.total_debt_repayment_cents | money }}
House owes player{{ player.current_due_to_player_cents | money }}
Owes house{{ player.current_due_to_house_cents | money }}
Written off{{ player.total_writeoff_cents | money }}
-
-
- - -{% endblock %} diff --git a/templates/session_detail.html b/templates/session_detail.html deleted file mode 100644 index 7b6f1bd..0000000 --- a/templates/session_detail.html +++ /dev/null @@ -1,285 +0,0 @@ -{% extends "base.html" %} -{% block title %}{{ session_label(session) }} · myboker.org{% endblock %} -{% block page_class %}page--session{% endblock %} -{% block content %} - - - -
-

- Session #{{ "%03d"|format(session_number) }} -

-

{{ session_label(session) }}

-

{{ session.entries|length }} player{{ 's' if session.entries|length != 1 else '' }} · {{ session.status }}

-
- - -
-
-
- Banker cash flow -
-
-
- Actual cash in -
{{ session.total_real_cash_in_cents | money }}
-
-
- Gross player claim -
{{ session.total_gross_payout_cents | money }}
-
-
- Cash paid out -
{{ session.total_real_cash_out_cents | money }}
-
-
- Rolled out -
{{ session.total_rollover_out_cents | money }}
-
-
-
-
-
- Open items -
-
-
- House owes players -
- {{ session.total_current_due_to_player_cents | money }} -
-
-
- Players owe house -
- {{ session.total_current_due_to_house_cents | money }} -
-
-
- Cash debt repaid -
{{ session.total_debt_repayment_cents | money }}
-
-
- Written off -
{{ session.total_writeoff_cents | money }}
-
-
-
-
- - -
-
-
- Results -

Player totals

-
-
- - -
-
-
- - - - - - - - - - - - - - - - - - - - {% for entry in session.entries %} - - - - - - - - - - - - - - - - {% endfor %} - -
PlayerInvestedGross cashoutNetPaid outOpen settlementSettlement statusFrontRolled inPayout carry-inGross player claimRolled outNotes
- {{ entry.player_name }} - {{ entry.invested_cents | money }}{{ entry.cash_out_cents | money }} - {{ '+' if entry.net_cents > 0 else '' }}{{ entry.net_cents | money }} - {{ entry.paid_out_cents | money }} - {% if entry.current_due_to_house_cents > 0 %} - owes {{ entry.current_due_to_house_cents | money }} - {% elif entry.current_due_to_player_cents > 0 %} - due {{ entry.current_due_to_player_cents | money }} - {% else %} - {{ entry.current_due_to_player_cents | money }} - {% endif %} - - {{ entry.payout_status }} - {{ entry.front_cents | money }}{{ entry.rollover_in_cents | money }}{{ entry.payout_carry_in_cents | money }} - {% if entry.gross_due_to_house_cents > 0 %}—{% else %}{{ entry.gross_payout_cents | money }}{% endif %} - {{ entry.rollover_out_cents | money }} - {% if entry.notes %} -
    {% for n in entry.notes %}
  • {{ n }}
  • {% endfor %}
- {% else %} - - {% endif %} -
-
-
- - -
-
-
- Breakdown -

Net by player

-
-
-
-
-
- -
-
- Quick read -

Session snapshot

-
- {% set winners = session.entries | selectattr('net_cents', 'gt', 0) | list %} - {% set losers = session.entries | selectattr('net_cents', 'lt', 0) | list %} -
-
- Biggest winner -
- {% if winners %}{{ (winners | sort(attribute='net_cents', reverse=True) | first).net_cents | money }}{% else %}—{% endif %} -
- {% if winners %}
{{ winners | sort(attribute='net_cents', reverse=True) | first | attr('player_name') }}
{% endif %} -
-
- Biggest loser -
- {% if losers %}{{ (losers | sort(attribute='net_cents') | first).net_cents | money }}{% else %}—{% endif %} -
- {% if losers %}
{{ losers | sort(attribute='net_cents') | first | attr('player_name') }}
{% endif %} -
-
- Gross cashout result -
{{ session.total_cash_out_cents | money }}
-
-
- Open items -
- {{ (session.total_current_due_to_player_cents + session.total_current_due_to_house_cents) | money }} -
-
-
-
-
- - -
-
-
- Audit trail -

Raw ledger events

-
- append-only -
-
- {% for event in raw_events | reverse %} -
-
- {{ event.player_name or "Session" }} - {{ event.event_type | replace('_', ' ') }} - {% if event.note %}{{ event.note }}{% endif %} -
-

{{ event.amount_cents | money }}

-

{{ event.created_at }}

-
- {% else %} -

No events recorded.

- {% endfor %} -
-
- - -{% endblock %} diff --git a/templates/sessions.html b/templates/sessions.html deleted file mode 100644 index 1fa76f9..0000000 --- a/templates/sessions.html +++ /dev/null @@ -1,54 +0,0 @@ -{% extends "base.html" %} -{% block title %}Sessions · myboker.org{% endblock %} -{% block content %} - -
-

Session history

-

Sessions

-

Browse every recorded game night and inspect table totals.

-
- -
-
-
- Archive -

All sessions

-
-
-
- - - - - - - - - - - - - {% for s in sessions %} - - - - - - - - - {% else %} - - - - {% endfor %} - -
DateStatusPlayersInvestedGross cashoutTable net
- {{ session_label(s) }} - {{ s.status }}{{ s.entries|length }}{{ s.total_invested_cents | money }}{{ s.total_cash_out_cents | money }} - {{ s.total_net_cents | money }} -
No sessions yet.
-
-
- -{% endblock %} diff --git a/tests/test_config.py b/tests/test_config.py index d6c8027..962a001 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -3,6 +3,7 @@ import unittest from unittest.mock import patch from app import create_app +from boker.config import DEFAULT_DATABASE_URL class ProductionConfigTests(unittest.TestCase): @@ -13,7 +14,12 @@ class ProductionConfigTests(unittest.TestCase): clear=False, ): with self.assertRaisesRegex(RuntimeError, "DATABASE_URL"): - create_app({"SECRET_KEY": "test-production-secret"}) + create_app( + { + "SECRET_KEY": "test-production-secret", + "SQLALCHEMY_DATABASE_URI": DEFAULT_DATABASE_URL, + } + ) def test_production_accepts_postgresql_database_url(self): with patch.dict( diff --git a/tests/test_phase2_auth_leagues.py b/tests/test_phase2_auth_leagues.py index 3b2fde1..1529dbf 100644 --- a/tests/test_phase2_auth_leagues.py +++ b/tests/test_phase2_auth_leagues.py @@ -121,6 +121,12 @@ class Phase2AuthLeagueRouteTests(unittest.TestCase): response = self.client.get("/admin/login") self.assertEqual(response.status_code, 404) + def test_legacy_csv_public_routes_are_not_registered(self): + for path in ("/leaderboard", "/sessions", "/sessions/legacy-session", "/players/Legacy"): + with self.subTest(path=path): + response = self.client.get(path) + self.assertEqual(response.status_code, 404) + def test_duplicate_league_slug_is_allowed_because_route_uses_public_key(self): owner = self.create_verified_user() self.login_as(owner)