remove legacy system routes

This commit is contained in:
SowinskiBraeden committed 2026-06-29 12:54:10 -07:00
1 parent 737d6d7561
commit 317ef2dfaa
11 files changed
+15 -1547

No files matched your search

-222
View File
@@ -1,222 +0,0 @@
{% extends "base.html" %}
{% block title %}Help · myboker.org{% endblock %}
{% block content %}
<div class="hero" style="padding-bottom:12px;">
<p class="hero__kicker">Documentation</p>
<h1 class="hero__title">How myboker works</h1>
<p class="hero__sub">Everything you need to run a home poker league — sessions, ledgers, leaderboards, and settlement.</p>
</div>
<div class="docs-grid">
<nav class="docs-nav">
<p class="kicker">Contents</p>
<a href="#leagues">Leagues</a>
<a href="#players">Players</a>
<a href="#sessions">Sessions</a>
<a href="#events">Ledger events</a>
<a href="#settlement">Settlement</a>
<a href="#leaderboard">Leaderboard &amp; stats</a>
<a href="#visibility">Public vs private</a>
</nav>
<div class="docs-body">
<section class="docs-section" id="leagues">
<h2 class="docs-h2">Leagues</h2>
<p>A <strong>league</strong> 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).</p>
<p>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.</p>
<div class="docs-callout">
<strong>Tip:</strong> Leagues are long-lived. You don't need to create a new league every season — use <em>sessions</em> to separate individual nights of play.
</div>
</section>
<section class="docs-section" id="players">
<h2 class="docs-h2">Players</h2>
<p>Players are the people in your league. They <strong>do not need a myboker account</strong> — only the league owner or manager needs one. You just add names.</p>
<p>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.</p>
<p><strong>Archiving</strong> a player hides them from the active roster but preserves all their history. You can reactivate them at any time.</p>
<div class="docs-term-grid">
<div class="docs-term">
<strong>Active</strong>
<span>Shows on the roster and is available to record events against.</span>
</div>
<div class="docs-term">
<strong>Archived</strong>
<span>Hidden from the roster. History and stats are preserved. Reactivate any time.</span>
</div>
</div>
</section>
<section class="docs-section" id="sessions">
<h2 class="docs-h2">Sessions</h2>
<p>A <strong>session</strong> 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.</p>
<p>Sessions have two states: <strong>open</strong> and <strong>closed</strong>. 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.</p>
<div class="docs-term-grid">
<div class="docs-term">
<strong>Date</strong>
<span>The calendar date the game was played. Used to order sessions and group same-day games.</span>
</div>
<div class="docs-term">
<strong>Label</strong>
<span>Optional short name for this session (e.g. "Main table", "Side game"). Shown in lists.</span>
</div>
<div class="docs-term">
<strong>Notes</strong>
<span>Optional freeform context visible in the session list (e.g. "Holiday game", "Marcus's place").</span>
</div>
<div class="docs-term">
<strong>Open</strong>
<span>Session is live — events can be added and settlement is still in progress.</span>
</div>
<div class="docs-term">
<strong>Closed</strong>
<span>Session is settled. Books are considered final. Can be reopened if needed.</span>
</div>
</div>
</section>
<section class="docs-section" id="events">
<h2 class="docs-h2">Ledger events</h2>
<p>All activity in a session is recorded as <strong>ledger events</strong>. The ledger is <strong>append-only</strong> — 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).</p>
<p>There are eight event types:</p>
<div class="docs-events">
<div class="docs-event">
<span class="pill pill-buyin">buy-in</span>
<div>
<strong>A player buys chips with cash.</strong>
<p>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.</p>
</div>
</div>
<div class="docs-event">
<span class="pill pill-front">front</span>
<div>
<strong>The house covers a player's buy-in.</strong>
<p>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.</p>
</div>
</div>
<div class="docs-event">
<span class="pill pill-cashout">cashout</span>
<div>
<strong>A player exchanges their chips for their cash value.</strong>
<p>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 <em>paid out</em> to record the actual cash handover.</p>
</div>
</div>
<div class="docs-event">
<span class="pill pill-paid_out">paid out</span>
<div>
<strong>Cash physically leaves the house to the player.</strong>
<p>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.).</p>
</div>
</div>
<div class="docs-event">
<span class="pill pill-rollover_in">rollover in</span>
<div>
<strong>Chips carried in from a previous session.</strong>
<p>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.</p>
</div>
</div>
<div class="docs-event">
<span class="pill pill-rollover_out">rollover out</span>
<div>
<strong>Chips carried out to a future session.</strong>
<p>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.</p>
</div>
</div>
<div class="docs-event">
<span class="pill pill-debt_repayment">debt repayment</span>
<div>
<strong>A player repays an outstanding front.</strong>
<p>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.</p>
</div>
</div>
<div class="docs-event">
<span class="pill pill-writeoff">write-off</span>
<div>
<strong>The house forgives a debt.</strong>
<p>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.</p>
</div>
</div>
</div>
</section>
<section class="docs-section" id="settlement">
<h2 class="docs-h2">Settlement</h2>
<p>The ledger tracks two types of open balances at all times:</p>
<div class="docs-term-grid">
<div class="docs-term">
<strong>Due to players</strong>
<span>Cash the house owes to players who have cashed out but haven't been paid yet. Pay these out to clear the balance.</span>
</div>
<div class="docs-term">
<strong>Due to house</strong>
<span>Cash players owe to the house from unpaid fronts. Collect debt repayments or write these off to clear the balance.</span>
</div>
<div class="docs-term">
<strong>Cash in</strong>
<span>Total real cash that has come into the pot across all sessions — buy-ins and repaid fronts only, not fronts themselves.</span>
</div>
<div class="docs-term">
<strong>Cash paid out</strong>
<span>Total real cash that has left the house to players. Ideally this approaches cash in over time.</span>
</div>
</div>
<div class="docs-callout">
<strong>Fronts vs buy-ins:</strong> 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.
</div>
</section>
<section class="docs-section" id="leaderboard">
<h2 class="docs-h2">Leaderboard &amp; stats</h2>
<p>The leaderboard ranks players by their all-time performance. Stats are computed from ledger events, not manually entered.</p>
<div class="docs-term-grid">
<div class="docs-term">
<strong>Net</strong>
<span>Total cash out minus total cash in across all sessions. Positive means up, negative means down.</span>
</div>
<div class="docs-term">
<strong>ROI</strong>
<span>Net divided by total invested, expressed as a percentage. Compares performance independent of how much someone plays.</span>
</div>
<div class="docs-term">
<strong>Win rate</strong>
<span>Percentage of sessions where the player finished with a positive net. Ties (exactly break even) count as a loss.</span>
</div>
<div class="docs-term">
<strong>Sessions played</strong>
<span>Number of sessions with at least one ledger event for this player.</span>
</div>
<div class="docs-term">
<strong>Biggest win</strong>
<span>The single session where the player netted the most profit.</span>
</div>
<div class="docs-term">
<strong>Biggest loss</strong>
<span>The single session where the player lost the most money.</span>
</div>
</div>
</section>
<section class="docs-section" id="visibility">
<h2 class="docs-h2">Public vs private</h2>
<p>Each league has a <strong>visibility</strong> setting you can change in league settings.</p>
<div class="docs-term-grid">
<div class="docs-term">
<strong>Private</strong>
<span>Only members (owners and managers) can see this league. The league won't appear on the Explore page. This is the default.</span>
</div>
<div class="docs-term">
<strong>Public</strong>
<span>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.</span>
</div>
</div>
<div class="docs-callout">
<strong>Note:</strong> Making a league public shares leaderboard rankings and session results, but not private financial details like individual debt balances or account emails.
</div>
</section>
</div>
</div>
{% endblock %}
-348
View File
@@ -1,348 +0,0 @@
{% extends "base.html" %}
{% block title %}Leaderboard · myboker.org{% endblock %}
{% block content %}
<div class="hero" style="display:flex;flex-wrap:wrap;gap:var(--s6);align-items:flex-start;justify-content:space-between;">
<div>
<p class="hero__kicker">All-time results</p>
<h1 class="hero__title">Leaderboard</h1>
<p class="hero__sub">{{ session_count }} session{{ 's' if session_count != 1 else '' }}{% if selected_session_date %} through {{ selected_session_label }}{% endif %}</p>
</div>
<div class="leaderboard-hero-stats">
<div class="stat">
<span class="kicker">Cash paid out</span>
<div class="stat__val" style="color:var(--accent)">{{ cash_paid_out_cents | money }}</div>
</div>
<div class="stat">
<span class="kicker">Sessions</span>
<div class="stat__val" style="color:var(--text-strong)">{{ session_count }}{% if total_session_count != session_count %}<span style="color:var(--faintest);font-size:16px"> / {{ total_session_count }}</span>{% endif %}</div>
</div>
<div class="stat">
<span class="kicker">Players</span>
<div class="stat__val" style="color:var(--text-strong)">{{ all_count }}</div>
</div>
</div>
</div>
<div class="panel stack" style="gap:0;margin-bottom:20px;">
<div class="panel__head">
<div>
<span class="kicker">Trend</span>
<h2 class="panel__title">Cumulative profit</h2>
</div>
<button class="btn btn--ghost btn--sm" type="button" id="expandLeaderboardChart">Expand</button>
</div>
<div class="chart-wrap">
<div class="chart-frame chart-frame--tall">
<canvas id="leaderboardChart"></canvas>
</div>
</div>
</div>
<div class="toolbar">
<div class="toolbar__left">
<div class="segmented">
<button class="seg {{ 'is-on' if mode=='eligible' }}" data-mode="eligible">
Eligible <span class="seg__count">{{ eligible_count }}</span>
</button>
<button class="seg {{ 'is-on' if mode=='all' }}" data-mode="all">
All players <span class="seg__count">{{ all_count }}</span>
</button>
<button class="seg {{ 'is-on' if mode=='recent' }}" data-mode="recent">
Recent <span class="seg__count">5 sess</span>
</button>
</div>
<p class="toolbar__hint">
{% 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 %}
</p>
</div>
<div class="toolbar__right">
<form class="session-filter-form" method="get">
<input type="hidden" name="mode" value="{{ mode }}">
<select class="session-filter-select" name="through_session" onchange="this.form.submit()">
<option value="">Latest session</option>
{% for s in available_sessions %}
<option value="{{ s.session_id }}" {{ 'selected' if selected_session_id == s.session_id else '' }}>{{ session_label(s) }}</option>
{% endfor %}
</select>
{% if selected_session_id %}
<a class="btn btn--ghost btn--sm" href="{{ url_for('public.leaderboard', mode=mode) }}">Reset</a>
{% endif %}
</form>
</div>
</div>
<div class="panel tbl-scroll" style="margin-bottom:20px;">
<table class="tbl" id="leaderboardTable" data-sort-key="net" data-sort-direction="desc">
<thead>
<tr>
<th class="col-rank" style="width:52px;">
<span style="font-family:var(--font-mono);font-size:10.5px;letter-spacing:.10em;text-transform:uppercase;color:var(--muted);">#</span>
</th>
<th class="col-player">
<button type="button" class="sort-btn" data-sort-key="player" data-sort-type="string" data-initial-direction="asc">Player <span class="sort-arrow"></span></button>
</th>
<th class="col-rank-delta"><button type="button" class="sort-btn" data-sort-key="rankdelta" data-sort-type="number" data-initial-direction="desc">Δ Rank <span class="sort-arrow"></span></button></th>
<th><button type="button" class="sort-btn" data-sort-key="net" data-sort-type="number" data-initial-direction="desc">Net <span class="sort-arrow"></span></button></th>
<th><button type="button" class="sort-btn" data-sort-key="winpct" data-sort-type="number" data-initial-direction="desc">Win% <span class="sort-arrow"></span></button></th>
<th><button type="button" class="sort-btn" data-sort-key="roi" data-sort-type="number" data-initial-direction="desc">ROI <span class="sort-arrow"></span></button></th>
<th><button type="button" class="sort-btn" data-sort-key="avgwin" data-sort-type="number" data-initial-direction="desc">Avg Win <span class="sort-arrow"></span></button></th>
<th><button type="button" class="sort-btn" data-sort-key="avgloss" data-sort-type="number" data-initial-direction="desc">Avg Loss <span class="sort-arrow"></span></button></th>
<th><button type="button" class="sort-btn" data-sort-key="sessions" data-sort-type="number" data-initial-direction="desc">Sessions <span class="sort-arrow"></span></button></th>
</tr>
</thead>
<tbody>
{% for player in main_board %}
{% set idx = loop.index %}
<tr data-player-row="true" data-original-rank="{{ idx }}"
onclick="window.location='{{ url_for('public.player_detail', player_name=player.player_name) }}'">
<td class="col-rank" data-sort-value="{{ idx }}">
<span class="rank-num {{ 'rank-1' if idx==1 else 'rank-2' if idx==2 else 'rank-3' if idx==3 else '' }}" data-rank-cell>#{{ idx }}</span>
</td>
<td class="col-player" data-sort-value="{{ player.player_name|lower }}">
<a href="{{ url_for('public.player_detail', player_name=player.player_name) }}">{{ player.player_name }}</a>
</td>
<td class="col-rank-delta" data-sort-value="{{ player.rank_change }}">
{% if player.rank_change > 0 %}
<span class="rank-delta rank-delta--up">▲{{ player.rank_change }}</span>
{% elif player.rank_change < 0 %}
<span class="rank-delta rank-delta--down">▼{{ player.rank_change | abs }}</span>
{% else %}
<span class="rank-delta rank-delta--flat"></span>
{% endif %}
</td>
<td class="num-net {{ 'num-pos' if player.total_net_cents > 0 else 'num-neg' if player.total_net_cents < 0 else 'num-muted' }}"
data-sort-value="{{ player.total_net_cents }}">
{{ '+' if player.total_net_cents > 0 else '' }}{{ player.total_net_cents | money }}
</td>
<td data-sort-value="{{ '%.4f'|format(player.win_pct) }}">{{ "%.1f"|format(player.win_pct) }}%</td>
<td class="{{ 'num-pos' if player.roi_pct > 0 else 'num-neg' if player.roi_pct < 0 else '' }}"
data-sort-value="{{ '%.4f'|format(player.roi_pct) }}">{{ "%.1f"|format(player.roi_pct) }}%</td>
<td class="num-pos" data-sort-value="{{ player.avg_win_cents }}">{{ player.avg_win_cents | money }}</td>
<td class="num-neg" data-sort-value="{{ player.avg_loss_cents }}">{{ player.avg_loss_cents | money }}</td>
<td data-sort-value="{{ player.sessions_played }}">{{ player.sessions_played }}</td>
</tr>
{% else %}
<tr>
<td colspan="9" style="text-align:left;padding:28px;color:var(--muted);">
No players yet.
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% if mode == 'eligible' and provisional_board %}
<div style="margin-bottom:20px;">
<div class="toolbar" style="margin-top:0;margin-bottom:12px;">
<span class="kicker" style="margin:0;">Provisional · fewer than {{ eligible_min_sessions }} sessions</span>
</div>
<div class="provisional-grid">
{% for player in provisional_board %}
<a class="provisional-card" href="{{ url_for('public.player_detail', player_name=player.player_name) }}"
style="display:block;text-decoration:none;">
<p class="provisional-card__name">{{ player.player_name }}</p>
<div class="provisional-card__meta">
<span>{{ player.sessions_played }} sess</span>
<span class="{{ 'num-pos' if player.total_net_cents > 0 else 'num-neg' if player.total_net_cents < 0 else '' }}"
style="font-family:var(--font-mono);font-size:11px;font-weight:600;">{{ player.total_net_cents | money }}</span>
</div>
</a>
{% endfor %}
</div>
</div>
{% endif %}
<!-- Expand modal -->
<div id="chartModal" style="display:none;position:fixed;inset:0;z-index:80;align-items:center;justify-content:center;">
<div style="position:absolute;inset:0;background:rgba(8,10,12,.82);backdrop-filter:blur(6px);" data-close-modal></div>
<div style="position:relative;z-index:1;width:min(1280px,calc(100vw - 48px));margin:24px auto;padding:22px;background:var(--surface);border:1px solid var(--border-strong);border-radius:var(--r-lg);box-shadow:0 24px 64px rgba(0,0,0,.4);">
<div style="display:flex;align-items:center;justify-content:space-between;gap:12px;margin-bottom:18px;">
<h2 style="margin:0;">Cumulative profit</h2>
<button class="btn btn--ghost btn--sm" data-close-modal>Close</button>
</div>
<div style="height:min(70vh,780px);position:relative;">
<canvas id="leaderboardChartExpanded"></canvas>
</div>
</div>
</div>
<script>
const CHART_PALETTE = ['#9b8cf0','#6fc093','#e0b15c','#cf6f86','#8f93c2','#7cb9e0','#f4a261','#a78bb0','#5cb8a0','#e58f3a'];
const leaderboardChartData = {{ chart_data | tojson }};
const rootStyles = getComputedStyle(document.documentElement);
const textMuted = rootStyles.getPropertyValue('--text-muted').trim();
const lineColor = rootStyles.getPropertyValue('--line').trim();
leaderboardChartData.datasets.forEach((ds, i) => {
ds.borderColor = CHART_PALETTE[i % CHART_PALETTE.length];
ds.backgroundColor = CHART_PALETTE[i % CHART_PALETTE.length];
});
function buildChartConfig(data, compact) {
return {
type: 'line',
data: {
labels: data.labels,
datasets: data.datasets.map(ds => ({
...ds,
pointRadius: compact ? 0 : 2,
pointHoverRadius: 5,
borderWidth: compact ? 2 : 2.5,
tension: 0.28,
fill: false,
})),
},
options: {
responsive: true,
maintainAspectRatio: false,
interaction: { mode: 'nearest', intersect: false },
plugins: {
legend: {
display: true,
position: 'bottom',
labels: { color: textMuted, usePointStyle: true, boxWidth: 8, boxHeight: 8, padding: 14 },
},
tooltip: {
callbacks: {
label(ctx) {
const v = ctx.parsed.y;
return v == null ? `${ctx.dataset.label}: —` : `${ctx.dataset.label}: $${Number(v).toFixed(2)}`;
}
}
}
},
scales: {
x: {
ticks: { color: textMuted, maxRotation: 0, autoSkip: true, maxTicksLimit: compact ? 6 : 10 },
grid: { color: lineColor, display: !compact },
},
y: {
ticks: {
color: textMuted,
maxTicksLimit: compact ? 8 : 10,
callback(v) { return (v < 0 ? '-' : '') + '$' + Math.abs(Number(v)).toFixed(0); }
},
grid: { color: lineColor },
}
}
}
};
}
const ctx = document.getElementById('leaderboardChart');
let mainChart = null;
if (ctx && leaderboardChartData.labels.length > 0) {
const filtered = leaderboardChartData.datasets.filter(ds => (ds.sessions_played || 0) > 1);
const chartData = {
labels: leaderboardChartData.labels,
datasets: filtered.length ? filtered : leaderboardChartData.datasets,
};
mainChart = new Chart(ctx, buildChartConfig(chartData, true));
}
const modal = document.getElementById('chartModal');
const expandBtn = document.getElementById('expandLeaderboardChart');
let expandedChart = null;
expandBtn?.addEventListener('click', () => {
if (!leaderboardChartData.labels.length) return;
modal.style.display = 'flex';
document.body.style.overflow = 'hidden';
if (expandedChart) expandedChart.destroy();
expandedChart = new Chart(
document.getElementById('leaderboardChartExpanded'),
buildChartConfig(leaderboardChartData, false)
);
});
document.querySelectorAll('[data-close-modal]').forEach(el => {
el.addEventListener('click', () => {
modal.style.display = 'none';
document.body.style.overflow = '';
if (expandedChart) { expandedChart.destroy(); expandedChart = null; }
});
});
document.addEventListener('keydown', e => {
if (e.key === 'Escape' && modal.style.display === 'flex') {
modal.style.display = 'none';
document.body.style.overflow = '';
if (expandedChart) { expandedChart.destroy(); expandedChart = null; }
}
});
// Segmented mode switcher
document.querySelectorAll('[data-mode]').forEach(btn => {
btn.addEventListener('click', () => {
const url = new URL(window.location.href);
url.searchParams.set('mode', btn.dataset.mode);
window.location.href = url.toString();
});
});
// Table sort
const tbl = document.getElementById('leaderboardTable');
const sortBtns = tbl ? Array.from(tbl.querySelectorAll('.sort-btn[data-sort-key]')) : [];
const colMap = { rank: 0, player: 1, rankdelta: 2, net: 3, winpct: 4, roi: 5, avgwin: 6, avgloss: 7, sessions: 8 };
function setActiveSort(key, dir) {
sortBtns.forEach(b => {
const active = b.dataset.sortKey === key;
b.classList.toggle('is-active', active);
const arrow = b.querySelector('.sort-arrow');
if (arrow) arrow.textContent = active ? (dir === 'asc' ? '↑' : '↓') : '↕';
});
}
function sortTable(key, sortType, forcedDir) {
if (!tbl) return;
const tbody = tbl.tBodies[0];
const rows = Array.from(tbody.querySelectorAll('tr[data-player-row]'));
const curKey = tbl.dataset.sortKey;
const curDir = tbl.dataset.sortDirection || 'desc';
let dir = forcedDir;
if (!dir) {
if (curKey === key) dir = curDir === 'desc' ? 'asc' : 'desc';
else {
const btn = sortBtns.find(b => b.dataset.sortKey === key);
dir = btn?.dataset.initialDirection || 'desc';
}
}
const mult = dir === 'asc' ? 1 : -1;
const ci = colMap[key] ?? 0;
rows.sort((a, b) => {
const av = a.children[ci]?.dataset.sortValue ?? '';
const bv = b.children[ci]?.dataset.sortValue ?? '';
if (sortType === 'number') {
const n = (Number(av) - Number(bv)) * mult;
if (n !== 0) return n;
} else {
const s = av.localeCompare(bv) * mult;
if (s !== 0) return s;
}
return Number(a.dataset.originalRank) - Number(b.dataset.originalRank);
});
rows.forEach(r => tbody.appendChild(r));
tbl.dataset.sortKey = key;
tbl.dataset.sortDirection = dir;
rows.forEach((row, i) => {
const rc = row.querySelector('[data-rank-cell]');
if (rc) rc.textContent = `#${i + 1}`;
});
setActiveSort(key, dir);
}
sortBtns.forEach(b => {
b.addEventListener('click', () => sortTable(b.dataset.sortKey, b.dataset.sortType));
});
setActiveSort('net', 'desc');
</script>
{% endblock %}
-344
View File
@@ -1,344 +0,0 @@
{% extends "base.html" %}
{% block title %}{{ player.player_name }} · myboker.org{% endblock %}
{% block content %}
<div class="hero">
<div>
{% if player_rank %}
<p class="player-rank">Rank #{{ player_rank }}</p>
{% endif %}
<h1 class="hero__title">{{ player.player_name }}</h1>
<p class="hero__sub">{{ player.sessions_played }} session{{ 's' if player.sessions_played != 1 else '' }} · {{ "%.1f"|format(player.win_pct) }}% win rate</p>
</div>
</div>
<div class="player-stat-grid" style="margin-bottom:20px;">
<div class="stat">
<span class="kicker">Total net</span>
<div class="stat__val {{ 'num-pos' if player.total_net_cents > 0 else 'num-neg' if player.total_net_cents < 0 else 'num-muted' }}">
{{ '+' if player.total_net_cents > 0 else '' }}{{ player.total_net_cents | money }}
</div>
</div>
<div class="stat">
<span class="kicker">Win rate</span>
<div class="stat__val" style="color:var(--text-strong)">{{ "%.1f"|format(player.win_pct) }}%</div>
<div class="stat__sub">{{ player.winning_sessions }}W · {{ player.losing_sessions }}L · {{ player.break_even_sessions }}E</div>
</div>
<div class="stat">
<span class="kicker">ROI</span>
<div class="stat__val {{ 'num-pos' if player.roi_pct > 0 else 'num-neg' if player.roi_pct < 0 else 'num-muted' }}">{{ "%.1f"|format(player.roi_pct) }}%</div>
</div>
<div class="stat">
<span class="kicker">Sessions</span>
<div class="stat__val" style="color:var(--text-strong)">{{ player.sessions_played }}</div>
</div>
<div class="stat">
<span class="kicker">Current W/L</span>
<div class="stat-pair">
<span class="stat-pair__val" style="color:var(--pos)">{{ player.current_win_streak }}W</span>
<span class="stat-pair__val" style="color:var(--neg)">{{ player.current_loss_streak }}L</span>
</div>
<div class="stat__sub">Longest {{ player.longest_win_streak }}W · {{ player.longest_loss_streak }}L</div>
</div>
<div class="stat">
<span class="kicker">Best / worst</span>
<div class="stat-pair">
<span class="stat-pair__val" style="color:var(--pos)">{{ player.biggest_win_cents | money }}</span>
<span class="stat-pair__val" style="color:var(--neg)">{{ player.biggest_loss_cents | money }}</span>
</div>
</div>
<div class="stat">
<span class="kicker">Total invested</span>
<div class="stat__val" style="color:var(--num)">{{ player.total_invested_cents | money }}</div>
</div>
<div class="stat">
<span class="kicker">Gross cashouts</span>
<div class="stat__val" style="color:var(--num)">{{ player.total_cash_out_cents | money }}</div>
</div>
</div>
<!-- Cumulative profit chart -->
<div class="panel" style="margin-bottom:20px;">
<div class="panel__head">
<span class="kicker" style="margin:0;">Trend</span>
<h2 class="panel__title">Cumulative profit</h2>
</div>
<div class="chart-wrap">
<div class="chart-frame chart-frame--tall">
<canvas id="playerCumulativeChart"></canvas>
</div>
</div>
</div>
<!-- Charts row: session results + result mix -->
<div class="split" style="margin-bottom:20px;">
<div class="panel col-main">
<div class="panel__head">
<span class="kicker" style="margin:0;">Session by session</span>
<h2 class="panel__title">Results</h2>
</div>
<div class="chart-wrap">
<div class="chart-frame">
<canvas id="playerSessionChart"></canvas>
</div>
</div>
</div>
<div class="panel col-side">
<div class="panel__head">
<span class="kicker" style="margin:0;">Breakdown</span>
<h2 class="panel__title">Result mix</h2>
</div>
<div class="chart-wrap">
<div class="result-mix" aria-label="Player result mix">
{% set result_total = player.winning_sessions + player.losing_sessions + player.break_even_sessions %}
<div class="result-mix__bar">
{% if player.winning_sessions > 0 %}
<span class="result-mix__seg result-mix__seg--win" style="flex-grow:{{ player.winning_sessions }}"></span>
{% endif %}
{% if player.break_even_sessions > 0 %}
<span class="result-mix__seg result-mix__seg--even" style="flex-grow:{{ player.break_even_sessions }}"></span>
{% endif %}
{% if player.losing_sessions > 0 %}
<span class="result-mix__seg result-mix__seg--loss" style="flex-grow:{{ player.losing_sessions }}"></span>
{% endif %}
</div>
<div class="result-mix__legend">
<div class="result-mix__legend-row">
<span><i class="result-mix__key result-mix__key--win"></i>Wins</span>
<strong><b>{{ player.winning_sessions }}</b><em>{{ "%.1f"|format((player.winning_sessions / result_total * 100) if result_total else 0) }}%</em></strong>
</div>
<div class="result-mix__legend-row">
<span><i class="result-mix__key result-mix__key--even"></i>Even</span>
<strong><b>{{ player.break_even_sessions }}</b><em>{{ "%.1f"|format((player.break_even_sessions / result_total * 100) if result_total else 0) }}%</em></strong>
</div>
<div class="result-mix__legend-row">
<span><i class="result-mix__key result-mix__key--loss"></i>Losses</span>
<strong><b>{{ player.losing_sessions }}</b><em>{{ "%.1f"|format((player.losing_sessions / result_total * 100) if result_total else 0) }}%</em></strong>
</div>
</div>
{% if result_total > 0 %}
<div class="result-mix__note">±$1.00 counts as even</div>
{% endif %}
</div>
</div>
</div>
</div>
<!-- Perf vs Cash two-panel split -->
<div class="split" style="margin-bottom:20px;">
<!-- Poker performance -->
<div class="panel col-main">
<div class="panel__head">
<div>
<div class="section-marker">
<span class="section-marker__dot section-marker__dot--accent"></span>
Poker performance
</div>
<h2 class="panel__title">Game results</h2>
</div>
</div>
<table style="width:100%;border-collapse:collapse;">
<tbody>
<tr>
<td style="padding:11px 20px;color:var(--muted);font-size:13.5px;border-top:1px solid var(--divider);">Total Net</td>
<td style="padding:11px 20px;text-align:right;font-family:var(--font-mono);font-variant-numeric:tabular-nums;border-top:1px solid var(--divider);"
class="{{ 'num-pos' if player.total_net_cents > 0 else 'num-neg' if player.total_net_cents < 0 else '' }}">
{{ '+' if player.total_net_cents > 0 else '' }}{{ player.total_net_cents | money }}
</td>
</tr>
<tr>
<td style="padding:11px 20px;color:var(--muted);font-size:13.5px;border-top:1px solid var(--divider);">Win %</td>
<td style="padding:11px 20px;text-align:right;font-family:var(--font-mono);border-top:1px solid var(--divider);color:var(--num);">{{ "%.1f"|format(player.win_pct) }}%</td>
</tr>
<tr>
<td style="padding:11px 20px;color:var(--muted);font-size:13.5px;border-top:1px solid var(--divider);">Record</td>
<td style="padding:11px 20px;text-align:right;font-family:var(--font-mono);border-top:1px solid var(--divider);color:var(--num);">
<span style="color:var(--pos)">{{ player.winning_sessions }}W</span>
<span style="color:var(--faintest);margin:0 4px;">·</span>
<span style="color:var(--neg)">{{ player.losing_sessions }}L</span>
<span style="color:var(--faintest);margin:0 4px;">·</span>
<span>{{ player.break_even_sessions }}E</span>
</td>
</tr>
<tr>
<td style="padding:11px 20px;color:var(--muted);font-size:13.5px;border-top:1px solid var(--divider);">ROI</td>
<td style="padding:11px 20px;text-align:right;font-family:var(--font-mono);border-top:1px solid var(--divider);"
class="{{ 'num-pos' if player.roi_pct > 0 else 'num-neg' if player.roi_pct < 0 else '' }}">{{ "%.1f"|format(player.roi_pct) }}%</td>
</tr>
<tr>
<td style="padding:11px 20px;color:var(--muted);font-size:13.5px;border-top:1px solid var(--divider);">Average Win</td>
<td style="padding:11px 20px;text-align:right;font-family:var(--font-mono);border-top:1px solid var(--divider);color:var(--pos);">{{ player.avg_win_cents | money }}</td>
</tr>
<tr>
<td style="padding:11px 20px;color:var(--muted);font-size:13.5px;border-top:1px solid var(--divider);">Average Loss</td>
<td style="padding:11px 20px;text-align:right;font-family:var(--font-mono);border-top:1px solid var(--divider);color:var(--neg);">{{ player.avg_loss_cents | money }}</td>
</tr>
<tr>
<td style="padding:11px 20px;color:var(--muted);font-size:13.5px;border-top:1px solid var(--divider);">Best / Worst</td>
<td style="padding:11px 20px;text-align:right;font-family:var(--font-mono);border-top:1px solid var(--divider);color:var(--num);">
<span style="color:var(--pos)">{{ player.biggest_win_cents | money }}</span>
<span style="color:var(--faintest);margin:0 4px;">/</span>
<span style="color:var(--neg)">{{ player.biggest_loss_cents | money }}</span>
</td>
</tr>
<tr>
<td style="padding:11px 20px;color:var(--muted);font-size:13.5px;border-top:1px solid var(--divider);">Win / Loss Streak</td>
<td style="padding:11px 20px;text-align:right;font-family:var(--font-mono);border-top:1px solid var(--divider);color:var(--num);">
<span style="color:var(--pos)">{{ player.current_win_streak }}W</span>
<span style="color:var(--faintest);margin:0 4px;">·</span>
<span style="color:var(--neg)">{{ player.current_loss_streak }}L</span>
</td>
</tr>
<tr>
<td style="padding:11px 20px;color:var(--muted);font-size:13.5px;border-top:1px solid var(--divider);">Longest Win / Loss</td>
<td style="padding:11px 20px;text-align:right;font-family:var(--font-mono);border-top:1px solid var(--divider);color:var(--num);">
{{ player.longest_win_streak }}W · {{ player.longest_loss_streak }}L
</td>
</tr>
</tbody>
</table>
</div>
<!-- Poker inputs and settlement. Fronts/rollovers are part of poker basis,
while paid/collected rows are real cash movement. -->
<div class="panel col-side">
<div class="panel__head">
<div>
<div class="section-marker">
<span class="section-marker__dot section-marker__dot--rolled"></span>
Poker basis + settlement
</div>
<h2 class="panel__title">Investment and settlement</h2>
</div>
</div>
<table style="width:100%;border-collapse:collapse;">
<tbody>
<tr>
<td style="padding:11px 20px;color:var(--muted);font-size:13.5px;border-top:1px solid var(--divider);">Buy-ins paid</td>
<td style="padding:11px 20px;text-align:right;font-family:var(--font-mono);border-top:1px solid var(--divider);color:var(--num);">{{ player.total_buy_in_cents | money }}</td>
</tr>
<tr>
<td style="padding:11px 20px;color:var(--muted);font-size:13.5px;border-top:1px solid var(--divider);">Fronted</td>
<td style="padding:11px 20px;text-align:right;font-family:var(--font-mono);border-top:1px solid var(--divider);color:var(--warn);">{{ player.total_front_cents | money }}</td>
</tr>
<tr>
<td style="padding:11px 20px;color:var(--muted);font-size:13.5px;border-top:1px solid var(--divider);">Rolled in</td>
<td style="padding:11px 20px;text-align:right;font-family:var(--font-mono);border-top:1px solid var(--divider);color:var(--rolled);">{{ player.total_rollover_in_cents | money }}</td>
</tr>
{% if player.total_payout_carry_in_cents > 0 %}
<tr>
<td style="padding:11px 20px;color:var(--muted);font-size:13.5px;border-top:1px solid var(--divider);">Payout carry-in</td>
<td style="padding:11px 20px;text-align:right;font-family:var(--font-mono);border-top:1px solid var(--divider);color:var(--rolled);">{{ player.total_payout_carry_in_cents | money }}</td>
</tr>
{% endif %}
<tr>
<td style="padding:11px 20px;color:var(--muted);font-size:13.5px;border-top:1px solid var(--divider);">Total invested</td>
<td style="padding:11px 20px;text-align:right;font-family:var(--font-mono);border-top:1px solid var(--divider);color:var(--num);">{{ player.total_invested_cents | money }}</td>
</tr>
<tr>
<td style="padding:11px 20px;color:var(--muted);font-size:13.5px;border-top:1px solid var(--divider);">Gross cashout result</td>
<td style="padding:11px 20px;text-align:right;font-family:var(--font-mono);border-top:1px solid var(--divider);color:var(--num);">{{ player.total_cash_out_cents | money }}</td>
</tr>
<tr>
<td style="padding:11px 20px;color:var(--muted);font-size:13.5px;border-top:1px solid var(--divider);">Gross player claim</td>
<td style="padding:11px 20px;text-align:right;font-family:var(--font-mono);border-top:1px solid var(--divider);color:var(--num);">{{ player.total_gross_payout_cents | money }}</td>
</tr>
<tr>
<td style="padding:11px 20px;color:var(--muted);font-size:13.5px;border-top:1px solid var(--divider);">Rolled out</td>
<td style="padding:11px 20px;text-align:right;font-family:var(--font-mono);border-top:1px solid var(--divider);color:var(--rolled);">{{ player.total_rollover_out_cents | money }}</td>
</tr>
<tr>
<td style="padding:11px 20px;color:var(--muted);font-size:13.5px;border-top:1px solid var(--divider);">Cash paid out</td>
<td style="padding:11px 20px;text-align:right;font-family:var(--font-mono);border-top:1px solid var(--divider);color:var(--pos);">{{ player.total_paid_out_cents | money }}</td>
</tr>
{% if player.total_debt_repayment_cents > 0 %}
<tr>
<td style="padding:11px 20px;color:var(--muted);font-size:13.5px;border-top:1px solid var(--divider);">Cash debt repaid</td>
<td style="padding:11px 20px;text-align:right;font-family:var(--font-mono);border-top:1px solid var(--divider);color:var(--pos);">{{ player.total_debt_repayment_cents | money }}</td>
</tr>
{% endif %}
{% if player.current_due_to_player_cents > 0 %}
<tr>
<td style="padding:11px 20px;color:var(--muted);font-size:13.5px;border-top:1px solid var(--divider);">House owes player</td>
<td style="padding:11px 20px;text-align:right;font-family:var(--font-mono);border-top:1px solid var(--divider);color:var(--neg);">{{ player.current_due_to_player_cents | money }}</td>
</tr>
{% endif %}
{% if player.current_due_to_house_cents > 0 %}
<tr>
<td style="padding:11px 20px;color:var(--muted);font-size:13.5px;border-top:1px solid var(--divider);">Owes house</td>
<td style="padding:11px 20px;text-align:right;font-family:var(--font-mono);border-top:1px solid var(--divider);color:var(--owes);">{{ player.current_due_to_house_cents | money }}</td>
</tr>
{% endif %}
{% if player.total_writeoff_cents > 0 %}
<tr>
<td style="padding:11px 20px;color:var(--muted);font-size:13.5px;border-top:1px solid var(--divider);">Written off</td>
<td style="padding:11px 20px;text-align:right;font-family:var(--font-mono);border-top:1px solid var(--divider);color:var(--neg);">{{ player.total_writeoff_cents | money }}</td>
</tr>
{% endif %}
</tbody>
</table>
</div>
</div>
<script>
const playerChartData = {{ chart_data | tojson }};
const rootStyles = getComputedStyle(document.documentElement);
const textMuted = rootStyles.getPropertyValue('--text-muted').trim();
const lineColor = rootStyles.getPropertyValue('--line').trim();
const sharedOpts = {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: { position: 'bottom', labels: { color: textMuted, usePointStyle: true, boxWidth: 9, boxHeight: 9, padding: 16 } },
},
scales: {
x: { ticks: { color: textMuted, maxRotation: 0 }, grid: { color: lineColor } },
y: { ticks: { color: textMuted }, grid: { color: lineColor } },
},
};
const cumulativeCtx = document.getElementById('playerCumulativeChart');
if (cumulativeCtx) {
new Chart(cumulativeCtx, {
type: 'line',
data: {
labels: playerChartData.labels,
datasets: [{
label: 'Cumulative Profit',
data: playerChartData.cumulative_values,
borderColor: playerChartData.color,
backgroundColor: playerChartData.color + '38',
borderWidth: 2.5,
pointRadius: 3,
pointHoverRadius: 5,
tension: 0.22,
fill: true,
}]
},
options: sharedOpts,
});
}
const sessionCtx = document.getElementById('playerSessionChart');
if (sessionCtx) {
new Chart(sessionCtx, {
type: 'bar',
data: {
labels: playerChartData.labels,
datasets: [{
label: 'Session Net',
data: playerChartData.net_values,
backgroundColor: playerChartData.net_colors,
borderColor: playerChartData.net_colors,
borderRadius: 6,
borderSkipped: false,
}]
},
options: sharedOpts,
});
}
</script>
{% endblock %}
-285
View File
@@ -1,285 +0,0 @@
{% extends "base.html" %}
{% block title %}{{ session_label(session) }} · myboker.org{% endblock %}
{% block page_class %}page--session{% endblock %}
{% block content %}
<div class="breadcrumb">
<a href="{{ url_for('public.sessions') }}">Sessions</a>
<span class="breadcrumb__sep">/</span>
<span>{{ session_label(session) }}</span>
<span style="flex:1"></span>
{% if prev_session %}
<a class="btn btn--ghost btn--sm" href="{{ url_for('public.session_detail', session_id=prev_session.session_id) }}">← Prev</a>
{% endif %}
{% if next_session %}
<a class="btn btn--ghost btn--sm" href="{{ url_for('public.session_detail', session_id=next_session.session_id) }}">Next →</a>
{% endif %}
</div>
<div class="hero" style="padding-top:0;">
<p class="hero__kicker session-number">
Session #{{ "%03d"|format(session_number) }}
</p>
<h1 class="hero__title">{{ session_label(session) }}</h1>
<p class="hero__sub">{{ session.entries|length }} player{{ 's' if session.entries|length != 1 else '' }} · <span class="status-badge status-{{ session.status }}">{{ session.status }}</span></p>
</div>
<!-- Recon cards -->
<div class="recon-grid" style="margin-bottom:20px;">
<div class="panel">
<div class="recon-card-head">
<span class="kicker kicker--dot kicker--dot-accent">Banker cash flow</span>
</div>
<div class="recon-metric-grid">
<div class="recon-cell">
<span class="kicker" style="font-size:10px;">Actual cash in</span>
<div class="recon-val recon-val--num">{{ session.total_real_cash_in_cents | money }}</div>
</div>
<div class="recon-cell">
<span class="kicker" style="font-size:10px;">Gross player claim</span>
<div class="recon-val recon-val--num">{{ session.total_gross_payout_cents | money }}</div>
</div>
<div class="recon-cell">
<span class="kicker" style="font-size:10px;">Cash paid out</span>
<div class="recon-val recon-val--pos">{{ session.total_real_cash_out_cents | money }}</div>
</div>
<div class="recon-cell">
<span class="kicker" style="font-size:10px;">Rolled out</span>
<div class="recon-val" style="color:var(--rolled)">{{ session.total_rollover_out_cents | money }}</div>
</div>
</div>
</div>
<div class="panel">
<div class="recon-card-head">
<span class="kicker kicker--dot kicker--dot-warn">Open items</span>
</div>
<div class="recon-metric-grid">
<div class="recon-cell">
<span class="kicker" style="font-size:10px;">House owes players</span>
<div class="recon-val {{ 'recon-val--neg' if session.total_current_due_to_player_cents > 0 else 'recon-val--pos' }}">
{{ session.total_current_due_to_player_cents | money }}
</div>
</div>
<div class="recon-cell">
<span class="kicker" style="font-size:10px;">Players owe house</span>
<div class="recon-val {{ 'recon-val--warn' if session.total_current_due_to_house_cents > 0 else '' }}">
{{ session.total_current_due_to_house_cents | money }}
</div>
</div>
<div class="recon-cell">
<span class="kicker" style="font-size:10px;">Cash debt repaid</span>
<div class="recon-val" style="color:var(--pos)">{{ session.total_debt_repayment_cents | money }}</div>
</div>
<div class="recon-cell">
<span class="kicker" style="font-size:10px;">Written off</span>
<div class="recon-val" style="color:var(--neg)">{{ session.total_writeoff_cents | money }}</div>
</div>
</div>
</div>
</div>
<!-- Player totals table -->
<div class="panel" style="margin-bottom:20px;">
<div class="panel__head">
<div>
<span class="kicker" style="margin:0;">Results</span>
<h2 class="panel__title">Player totals</h2>
</div>
<div class="segmented" id="viewToggle">
<button class="seg is-on" data-view="compact">Compact</button>
<button class="seg" data-view="full">Full ledger</button>
</div>
</div>
<div class="tbl-scroll">
<table class="tbl tbl--session" id="sessionTable">
<thead>
<tr>
<th class="col-player">Player</th>
<th>Invested</th>
<th>Gross cashout</th>
<th>Net</th>
<th>Paid out</th>
<th>Open settlement</th>
<th>Settlement status</th>
<th class="col-acct">Front</th>
<th class="col-acct">Rolled in</th>
<th class="col-acct">Payout carry-in</th>
<th class="col-acct">Gross player claim</th>
<th class="col-acct">Rolled out</th>
<th class="col-acct">Notes</th>
</tr>
</thead>
<tbody>
{% for entry in session.entries %}
<tr onclick="window.location='{{ url_for('public.player_detail', player_name=entry.player_name) }}'">
<td class="col-player">
<a href="{{ url_for('public.player_detail', player_name=entry.player_name) }}">{{ entry.player_name }}</a>
</td>
<td>{{ entry.invested_cents | money }}</td>
<td>{{ entry.cash_out_cents | money }}</td>
<td class="num-net {{ 'num-pos' if entry.net_cents > 0 else 'num-neg' if entry.net_cents < 0 else 'num-muted' }}">
{{ '+' if entry.net_cents > 0 else '' }}{{ entry.net_cents | money }}
</td>
<td style="color:var(--pos)">{{ entry.paid_out_cents | money }}</td>
<td class="{{ 'num-neg' if entry.current_due_to_player_cents > 0 else '' }}">
{% if entry.current_due_to_house_cents > 0 %}
<span style="color:var(--owes)">owes {{ entry.current_due_to_house_cents | money }}</span>
{% elif entry.current_due_to_player_cents > 0 %}
<span style="color:var(--neg)">due {{ entry.current_due_to_player_cents | money }}</span>
{% else %}
{{ entry.current_due_to_player_cents | money }}
{% endif %}
</td>
<td>
<span class="badge badge--{{ entry.payout_status }}">{{ entry.payout_status }}</span>
</td>
<td class="col-acct" style="color:var(--warn)">{{ entry.front_cents | money }}</td>
<td class="col-acct" style="color:var(--rolled)">{{ entry.rollover_in_cents | money }}</td>
<td class="col-acct" style="color:var(--rolled)">{{ entry.payout_carry_in_cents | money }}</td>
<td class="col-acct">
{% if entry.gross_due_to_house_cents > 0 %}—{% else %}{{ entry.gross_payout_cents | money }}{% endif %}
</td>
<td class="col-acct" style="color:var(--rolled)">{{ entry.rollover_out_cents | money }}</td>
<td class="col-acct" style="text-align:left;font-family:var(--font-ui);">
{% if entry.notes %}
<ul class="note-list">{% for n in entry.notes %}<li>{{ n }}</li>{% endfor %}</ul>
{% else %}
<span style="color:var(--faintest-2)"></span>
{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
<!-- Bottom row: net bars + snapshot + audit feed -->
<div class="split">
<div class="panel col-main">
<div class="panel__head">
<span class="kicker" style="margin:0;">Breakdown</span>
<h2 class="panel__title">Net by player</h2>
</div>
<div class="chart-wrap">
<div class="session-net-bars" id="sessionBreakdownChart" aria-label="Net by player"></div>
</div>
</div>
<div class="panel col-side">
<div class="panel__head">
<span class="kicker" style="margin:0;">Quick read</span>
<h2 class="panel__title">Session snapshot</h2>
</div>
{% set winners = session.entries | selectattr('net_cents', 'gt', 0) | list %}
{% set losers = session.entries | selectattr('net_cents', 'lt', 0) | list %}
<div class="snapshot-grid">
<div class="snapshot-cell">
<span class="kicker" style="font-size:10px;">Biggest winner</span>
<div class="snapshot-val" style="color:var(--pos)">
{% if winners %}{{ (winners | sort(attribute='net_cents', reverse=True) | first).net_cents | money }}{% else %}—{% endif %}
</div>
{% if winners %}<div style="font-size:12px;color:var(--muted);margin-top:4px;">{{ winners | sort(attribute='net_cents', reverse=True) | first | attr('player_name') }}</div>{% endif %}
</div>
<div class="snapshot-cell">
<span class="kicker" style="font-size:10px;">Biggest loser</span>
<div class="snapshot-val" style="color:var(--neg)">
{% if losers %}{{ (losers | sort(attribute='net_cents') | first).net_cents | money }}{% else %}—{% endif %}
</div>
{% if losers %}<div style="font-size:12px;color:var(--muted);margin-top:4px;">{{ losers | sort(attribute='net_cents') | first | attr('player_name') }}</div>{% endif %}
</div>
<div class="snapshot-cell">
<span class="kicker" style="font-size:10px;">Gross cashout result</span>
<div class="snapshot-val">{{ session.total_cash_out_cents | money }}</div>
</div>
<div class="snapshot-cell">
<span class="kicker" style="font-size:10px;">Open items</span>
<div class="snapshot-val {{ 'recon-val--neg' if (session.total_current_due_to_player_cents + session.total_current_due_to_house_cents) > 0 else '' }}">
{{ (session.total_current_due_to_player_cents + session.total_current_due_to_house_cents) | money }}
</div>
</div>
</div>
</div>
</div>
<!-- Audit feed -->
<div class="panel" style="margin-top:20px;">
<div class="panel__head">
<div>
<span class="kicker" style="margin:0;">Audit trail</span>
<h2 class="panel__title">Raw ledger events</h2>
</div>
<span class="badge badge--closed" style="font-size:9.5px;letter-spacing:.08em;">append-only</span>
</div>
<div class="event-feed feed feed--inline" style="max-height:none;">
{% for event in raw_events | reverse %}
<div class="event-card event-card--inline">
<div class="event-card-top">
<strong>{{ event.player_name or "Session" }}</strong>
<span class="pill pill-{{ event.event_type }}">{{ event.event_type | replace('_', ' ') }}</span>
{% if event.note %}<span class="event-note">{{ event.note }}</span>{% endif %}
</div>
<p class="event-amount">{{ event.amount_cents | money }}</p>
<p class="tiny-text" style="white-space:nowrap;">{{ event.created_at }}</p>
</div>
{% else %}
<p class="muted-text" style="padding:20px;">No events recorded.</p>
{% endfor %}
</div>
</div>
<script>
const sessionChartData = {{ chart_data | tojson }};
const breakdownCtx = document.getElementById('sessionBreakdownChart');
if (breakdownCtx) {
const values = sessionChartData.net_values || [];
const labels = sessionChartData.labels || [];
const colors = sessionChartData.net_colors || [];
const maxAbs = Math.max(...values.map(value => Math.abs(value)), 1);
const moneyFormatter = new Intl.NumberFormat('en-US', {
style: 'currency',
currency: 'USD',
});
const escapeHtml = value => String(value).replace(/[&<>"']/g, char => ({
'&': '&amp;',
'<': '&lt;',
'>': '&gt;',
'"': '&quot;',
"'": '&#39;',
}[char]));
breakdownCtx.innerHTML = labels.map((label, index) => {
const value = Number(values[index] || 0);
const width = Math.min(Math.abs(value) / maxAbs * 50, 50);
const sideClass = value < 0 ? 'session-net-bars__bar--neg' : 'session-net-bars__bar--pos';
const amount = `${value > 0 ? '+' : ''}${moneyFormatter.format(value)}`;
const color = colors[index] || (value < 0 ? '#e0758a' : value > 0 ? '#6fc093' : '#84828e');
return `
<div class="session-net-bars__row">
<div class="session-net-bars__name">${escapeHtml(label)}</div>
<div class="session-net-bars__track">
<span class="session-net-bars__bar ${sideClass}" style="width:${width}%;background:${color};"></span>
</div>
<div class="session-net-bars__amount">${amount}</div>
</div>
`;
}).join('');
}
// Compact / Full toggle
const sessionTable = document.getElementById('sessionTable');
document.querySelectorAll('#viewToggle .seg').forEach(btn => {
btn.addEventListener('click', () => {
document.querySelectorAll('#viewToggle .seg').forEach(b => b.classList.remove('is-on'));
btn.classList.add('is-on');
if (btn.dataset.view === 'full') {
sessionTable.classList.add('is-full');
} else {
sessionTable.classList.remove('is-full');
}
});
});
</script>
{% endblock %}
-54
View File
@@ -1,54 +0,0 @@
{% extends "base.html" %}
{% block title %}Sessions · myboker.org{% endblock %}
{% block content %}
<div class="hero">
<p class="hero__kicker">Session history</p>
<h1 class="hero__title">Sessions</h1>
<p class="hero__sub">Browse every recorded game night and inspect table totals.</p>
</div>
<div class="panel">
<div class="panel__head">
<div>
<span class="kicker" style="margin:0;">Archive</span>
<h2 class="panel__title">All sessions</h2>
</div>
</div>
<div class="tbl-scroll">
<table class="tbl" style="min-width:600px;">
<thead>
<tr>
<th class="col-left">Date</th>
<th>Status</th>
<th>Players</th>
<th>Invested</th>
<th>Gross cashout</th>
<th>Table net</th>
</tr>
</thead>
<tbody>
{% for s in sessions %}
<tr onclick="window.location='{{ url_for('public.session_detail', session_id=s.session_id) }}'">
<td class="col-left">
<a href="{{ url_for('public.session_detail', session_id=s.session_id) }}">{{ session_label(s) }}</a>
</td>
<td><span class="badge badge--{{ s.status }}">{{ s.status }}</span></td>
<td>{{ s.entries|length }}</td>
<td>{{ s.total_invested_cents | money }}</td>
<td>{{ s.total_cash_out_cents | money }}</td>
<td class="{{ 'num-pos' if s.total_net_cents > 0 else 'num-neg' if s.total_net_cents < 0 else '' }}">
{{ s.total_net_cents | money }}
</td>
</tr>
{% else %}
<tr>
<td colspan="6" style="text-align:left;padding:28px;color:var(--muted);">No sessions yet.</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
{% endblock %}