Add a persistent hand history log to the Texas Hold'em dealer tool

Give the dealer a running record of resolved pots — hand number, total
pot, who got paid what, and the time — instead of losing that
information the moment the pot wizard closes. It's the kind of audit
trail a real dealer needs when a chip count gets disputed after the
fact.

- state.js tracks state.handNumber and state.handHistory (capped at 200
  entries) and persists both through the existing localStorage save/load
  path, with backfill defaults for tables saved before this change.
- potWizard.js now hands its final payouts back to the caller instead of
  discarding them once "Start Next Hand" is clicked.
- main.js records a history entry (skipping hands where nobody actually
  won anything) before advancing to the next hand, and wires up a "Clear
  history" control that also resets the hand counter.
- A new sidebar panel lists history entries newest-first; the header
  shows a running "Hand #N" badge next to the existing subtitle.

Verified via Playwright MCP: seated two players, resolved a pot, and
confirmed the header's hand counter incremented, the history panel
listed the payout, the entry survived a full page reload (localStorage
persistence), and Clear History reset both the log and the counter. No
console errors during the flow.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01C3NiiUDimoKDJ8GjC3KL3x
This commit is contained in:
2026-08-31 17:15:23 -06:00
co-authored by Claude Sonnet 5
parent 105a74bc51
commit 68e263f325
6 changed files with 156 additions and 2 deletions
@@ -63,7 +63,8 @@ document.addEventListener("DOMContentLoaded", () => {
});
document.getElementById("resolvePotBtn").addEventListener("click", () => {
PotWizard.open(state, () => {
PotWizard.open(state, payouts => {
if (payouts.length > 0) HoldemState.recordHand(state, payouts);
HoldemState.startNextHand(state);
hooks.save(state);
Render.renderAll();
@@ -74,4 +75,11 @@ document.addEventListener("DOMContentLoaded", () => {
document.getElementById("potWizardOverlay").addEventListener("click", e => {
if (e.target.id === "potWizardOverlay") PotWizard.close();
});
document.getElementById("clearHistoryBtn").addEventListener("click", () => {
if (!confirm("Clear the hand history and reset the hand counter to 1? This can't be undone.")) return;
HoldemState.clearHistory(state);
hooks.save(state);
Render.renderAll();
});
});
@@ -175,8 +175,12 @@ const PotWizard = {
applyBtn.className = "holdemBtn holdemBtnAccent";
applyBtn.textContent = "Start Next Hand";
applyBtn.addEventListener("click", () => {
const payouts = Array.from(this.totals.entries()).map(([id, amount]) => ({
label: this.playerLabelById.get(id),
amount,
}));
this.close();
this.onFinished();
this.onFinished(payouts);
});
actions.appendChild(applyBtn);
@@ -30,6 +30,7 @@ const Render = {
this.updatePotBadge();
this.renderSetupFields();
this.renderTableHint();
this.renderHandHistory();
},
renderSetupFields() {
@@ -349,6 +350,39 @@ const Render = {
list.appendChild(row);
});
},
// ---- Hand counter + history ----
formatHandTime(timestamp) {
try {
return new Date(timestamp).toLocaleTimeString(undefined, { hour: "2-digit", minute: "2-digit" });
} catch {
return "";
}
},
renderHandHistory() {
document.getElementById("handCounterBadge").textContent = `Hand #${this.state.handNumber || 1}`;
const list = document.getElementById("handHistoryList");
const emptyHint = document.getElementById("handHistoryEmptyHint");
const history = this.state.handHistory || [];
list.innerHTML = "";
emptyHint.hidden = history.length > 0;
const template = document.getElementById("handHistoryRowTemplate");
history.forEach(entry => {
const row = template.content.firstElementChild.cloneNode(true);
row.querySelector(".handHistoryHandNumber").textContent = `Hand #${entry.handNumber}`;
row.querySelector(".handHistoryPot").textContent = Money.format(entry.pot);
row.querySelector(".handHistoryTime").textContent = this.formatHandTime(entry.timestamp);
const payoutsEl = row.querySelector(".handHistoryPayouts");
payoutsEl.textContent = entry.payouts.map(p => `${p.label} ${Money.format(p.amount)}`).join(", ");
list.appendChild(row);
});
},
};
const Money = {
@@ -29,6 +29,8 @@ const HoldemState = {
};
},
MAX_HISTORY_ENTRIES: 200,
createDefault() {
const seatCount = this.DEFAULT_SEAT_COUNT;
return {
@@ -40,6 +42,8 @@ const HoldemState = {
chipLegend: this.DEFAULT_CHIP_LEGEND.map(c => ({ ...c })),
seats: Array.from({ length: seatCount }, () => this.makePlayer()),
notes: "",
handNumber: 1,
handHistory: [],
};
},
@@ -49,6 +53,8 @@ const HoldemState = {
if (!raw) return this.createDefault();
const parsed = JSON.parse(raw);
if (!parsed || !Array.isArray(parsed.seats)) return this.createDefault();
if (typeof parsed.handNumber !== "number" || parsed.handNumber < 1) parsed.handNumber = 1;
if (!Array.isArray(parsed.handHistory)) parsed.handHistory = [];
return parsed;
} catch {
return this.createDefault();
@@ -137,6 +143,31 @@ const HoldemState = {
p.amountOverride = null;
});
},
// Appends a resolved-pot record to the running session log a dealer can point to
// for chip-count disputes, then advances the hand counter. `payouts` is
// [{ label, amount }, ...] — who got paid what out of this hand's pot(s).
recordHand(state, payouts) {
if (typeof state.handNumber !== "number") state.handNumber = 1;
if (!Array.isArray(state.handHistory)) state.handHistory = [];
const pot = payouts.reduce((sum, p) => sum + p.amount, 0);
state.handHistory.unshift({
handNumber: state.handNumber,
timestamp: Date.now(),
pot,
payouts,
});
if (state.handHistory.length > this.MAX_HISTORY_ENTRIES) {
state.handHistory.length = this.MAX_HISTORY_ENTRIES;
}
state.handNumber += 1;
},
clearHistory(state) {
state.handHistory = [];
state.handNumber = 1;
},
};
if (typeof module !== "undefined" && module.exports) {