feat(web): iPhone PWA fixes (M1) + warm RTO redesign (M2) #3
+1
-1
@@ -44,7 +44,7 @@ _CASH_TOOLS = _BASE + _LOOKUPS + (
|
|||||||
"start_session", "add_buyin", "log_stack", "log_hand", "record_hand",
|
"start_session", "add_buyin", "log_stack", "log_hand", "record_hand",
|
||||||
"add_read", "analyze_spot", "session_stats", "session_state", "end_session",
|
"add_read", "analyze_spot", "session_stats", "session_state", "end_session",
|
||||||
"generate_recap", "scar_note", "confidence_bank", "alligator_blood", "reset_ritual",
|
"generate_recap", "scar_note", "confidence_bank", "alligator_blood", "reset_ritual",
|
||||||
"undo_last",
|
"undo_last", "update_session",
|
||||||
)
|
)
|
||||||
|
|
||||||
# Talk mode also gets start_session as the *entry point*: opening a session from a
|
# Talk mode also gets start_session as the *entry point*: opening a session from a
|
||||||
|
|||||||
+48
-1
@@ -341,6 +341,50 @@ def _resolve(session_id: int | None) -> int | None:
|
|||||||
return live["id"] if live else None
|
return live["id"] if live else None
|
||||||
|
|
||||||
|
|
||||||
|
def review_session_id() -> int | None:
|
||||||
|
"""The session to attach reflective entries to: the live one if any, else the
|
||||||
|
most-recent real session (closed). Lets rituals/notes land while reviewing after
|
||||||
|
you've racked up. Excludes the standing 'Hand Reviews' bucket."""
|
||||||
|
live = live_session()
|
||||||
|
if live:
|
||||||
|
return live["id"]
|
||||||
|
r = _c().execute(
|
||||||
|
"SELECT id FROM poker_sessions WHERE status != 'review' ORDER BY id DESC LIMIT 1"
|
||||||
|
).fetchone()
|
||||||
|
return int(r["id"]) if r else None
|
||||||
|
|
||||||
|
|
||||||
|
_EDITABLE = ("venue", "stakes", "game", "format", "buy_in_total", "cash_out",
|
||||||
|
"mantra", "mood")
|
||||||
|
|
||||||
|
|
||||||
|
def update_session(session_id: int, **fields) -> dict | None:
|
||||||
|
"""Edit session details (during or after play). Only known columns are touched;
|
||||||
|
net is recomputed when buy-in/cash-out change and both are known."""
|
||||||
|
s = get_session(session_id)
|
||||||
|
if not s:
|
||||||
|
return None
|
||||||
|
sets, vals = [], []
|
||||||
|
for k, v in fields.items():
|
||||||
|
if k in _EDITABLE and v is not None:
|
||||||
|
sets.append(f"{k} = ?")
|
||||||
|
vals.append(float(v) if k in ("buy_in_total", "cash_out") else v)
|
||||||
|
if sets:
|
||||||
|
conn = _c()
|
||||||
|
with conn:
|
||||||
|
conn.execute(f"UPDATE poker_sessions SET {', '.join(sets)} WHERE id = ?",
|
||||||
|
(*vals, session_id))
|
||||||
|
s = get_session(session_id)
|
||||||
|
# keep net consistent if the money fields changed and both are present
|
||||||
|
if s.get("cash_out") is not None and s.get("buy_in_total") is not None:
|
||||||
|
net = float(s["cash_out"]) - float(s["buy_in_total"])
|
||||||
|
if net != s.get("net"):
|
||||||
|
with conn:
|
||||||
|
conn.execute("UPDATE poker_sessions SET net = ? WHERE id = ?", (net, session_id))
|
||||||
|
s = get_session(session_id)
|
||||||
|
return s
|
||||||
|
|
||||||
|
|
||||||
def add_buyin(amount: float, session_id: int | None = None) -> float:
|
def add_buyin(amount: float, session_id: int | None = None) -> float:
|
||||||
"""Add a buy-in/rebuy to a session. Returns the new total in."""
|
"""Add a buy-in/rebuy to a session. Returns the new total in."""
|
||||||
sid = _resolve(session_id)
|
sid = _resolve(session_id)
|
||||||
@@ -1090,7 +1134,10 @@ def hud(session_id: int | None = None) -> dict | None:
|
|||||||
"id": sid, "venue": s.get("venue"), "stakes": s.get("stakes"),
|
"id": sid, "venue": s.get("venue"), "stakes": s.get("stakes"),
|
||||||
"game": s.get("game"), "format": s.get("format"),
|
"game": s.get("game"), "format": s.get("format"),
|
||||||
"status": s.get("status"), "started_at": s.get("started_at"),
|
"status": s.get("status"), "started_at": s.get("started_at"),
|
||||||
"buy_in_total": s.get("buy_in_total"),
|
"ended_at": s.get("ended_at"), "hours": s.get("hours"),
|
||||||
|
"buy_in_total": s.get("buy_in_total"), "cash_out": s.get("cash_out"),
|
||||||
|
"net": s.get("net"), "mantra": s.get("mantra"), "mood": s.get("mood"),
|
||||||
|
"is_live": s.get("status") == "live", "has_recap": bool(s.get("recap_md")),
|
||||||
},
|
},
|
||||||
"stack": {
|
"stack": {
|
||||||
"current": state["current"], "buy_in": state["buy_in"], "net": state["net"],
|
"current": state["current"], "buy_in": state["buy_in"], "net": state["net"],
|
||||||
|
|||||||
+41
-12
@@ -117,6 +117,21 @@ def _log_stack(args: dict, ctx: dict) -> str:
|
|||||||
return f"Stack ${amount:g} logged" + (f" (net {net:+.0f})." if net is not None else ".")
|
return f"Stack ${amount:g} logged" + (f" (net {net:+.0f})." if net is not None else ".")
|
||||||
|
|
||||||
|
|
||||||
|
def _update_session(args: dict, ctx: dict) -> str:
|
||||||
|
sid = poker.review_session_id()
|
||||||
|
if sid is None:
|
||||||
|
return "No session to edit yet."
|
||||||
|
fields = {k: args.get(k) for k in ("venue", "stakes", "game", "format",
|
||||||
|
"buy_in_total", "cash_out", "mantra", "mood") if args.get(k) not in (None, "")}
|
||||||
|
if not fields:
|
||||||
|
return "Tell me what to change (venue, stakes, game, buy-in, etc.)."
|
||||||
|
s = poker.update_session(sid, **fields)
|
||||||
|
if not s:
|
||||||
|
return "Couldn't find that session."
|
||||||
|
changed = ", ".join(f"{k}={v}" for k, v in fields.items())
|
||||||
|
return f"Session #{sid} updated — {changed}."
|
||||||
|
|
||||||
|
|
||||||
def _undo_last(args: dict, ctx: dict) -> str:
|
def _undo_last(args: dict, ctx: dict) -> str:
|
||||||
what = (args.get("what") or "").strip().lower()
|
what = (args.get("what") or "").strip().lower()
|
||||||
aliases = {"hands": "hand", "stacks": "stack", "reads": "read",
|
aliases = {"hands": "hand", "stacks": "stack", "reads": "read",
|
||||||
@@ -143,11 +158,11 @@ def _scar_note(args: dict, ctx: dict) -> str:
|
|||||||
cls = (args.get("classification") or "").strip().lower() or None
|
cls = (args.get("classification") or "").strip().lower() or None
|
||||||
if cls and cls not in ("punt", "cooler", "standard"):
|
if cls and cls not in ("punt", "cooler", "standard"):
|
||||||
cls = None
|
cls = None
|
||||||
try:
|
sid = poker.review_session_id() # live, or the most-recent session (post-game review)
|
||||||
|
if sid is None:
|
||||||
|
return "No session yet — start one and I'll keep the scar notes."
|
||||||
poker.log_ritual("scar", content=content, classification=cls,
|
poker.log_ritual("scar", content=content, classification=cls,
|
||||||
hand_id=args.get("hand_id"))
|
hand_id=args.get("hand_id"), session_id=sid)
|
||||||
except ValueError:
|
|
||||||
return "No live session — start one and I'll keep the scar notes."
|
|
||||||
return f"Scar note logged{f' ({cls})' if cls else ''}."
|
return f"Scar note logged{f' ({cls})' if cls else ''}."
|
||||||
|
|
||||||
|
|
||||||
@@ -155,10 +170,10 @@ def _confidence_bank(args: dict, ctx: dict) -> str:
|
|||||||
content = (args.get("content") or "").strip()
|
content = (args.get("content") or "").strip()
|
||||||
if not content:
|
if not content:
|
||||||
return "Nothing to bank — tell me the good process."
|
return "Nothing to bank — tell me the good process."
|
||||||
try:
|
sid = poker.review_session_id()
|
||||||
poker.log_ritual("confidence", content=content, hand_id=args.get("hand_id"))
|
if sid is None:
|
||||||
except ValueError:
|
return "No session yet — start one and I'll run the confidence bank."
|
||||||
return "No live session — start one and I'll run the confidence bank."
|
poker.log_ritual("confidence", content=content, hand_id=args.get("hand_id"), session_id=sid)
|
||||||
return "Banked. 💰"
|
return "Banked. 💰"
|
||||||
|
|
||||||
|
|
||||||
@@ -174,10 +189,10 @@ def _alligator_blood(args: dict, ctx: dict) -> str:
|
|||||||
|
|
||||||
def _reset_ritual(args: dict, ctx: dict) -> str:
|
def _reset_ritual(args: dict, ctx: dict) -> str:
|
||||||
content = (args.get("content") or "").strip() or None
|
content = (args.get("content") or "").strip() or None
|
||||||
try:
|
sid = poker.review_session_id()
|
||||||
poker.log_ritual("reset", content=content)
|
if sid is None:
|
||||||
except ValueError:
|
return "No session to reset."
|
||||||
return "No live session to reset."
|
poker.log_ritual("reset", content=content, session_id=sid)
|
||||||
return "Reset logged. Clean slate — this is a new session in your head."
|
return "Reset logged. Clean slate — this is a new session in your head."
|
||||||
|
|
||||||
|
|
||||||
@@ -389,6 +404,20 @@ TOOLS.update({
|
|||||||
"add_buyin": {"handler": _add_buyin, "spec": _f(
|
"add_buyin": {"handler": _add_buyin, "spec": _f(
|
||||||
"add_buyin", "Record a rebuy / additional buy-in in the live session.",
|
"add_buyin", "Record a rebuy / additional buy-in in the live session.",
|
||||||
{"amount": {**_N, "description": "Amount added"}}, ["amount"])},
|
{"amount": {**_N, "description": "Amount added"}}, ["amount"])},
|
||||||
|
"update_session": {"handler": _update_session, "spec": _f(
|
||||||
|
"update_session",
|
||||||
|
"Edit details of the current/most-recent session — during or after play. Use "
|
||||||
|
"when Brian corrects something ('change the stakes to 2/5', 'venue was actually "
|
||||||
|
"Bellagio', 'I bought in for 600', 'cashed out 1240'). Only pass fields that change.",
|
||||||
|
{"venue": {**_S, "description": "Casino/room"},
|
||||||
|
"stakes": {**_S, "description": "e.g. '1/3', '2/5'"},
|
||||||
|
"game": {**_S, "description": "NLH, PLO, ..."},
|
||||||
|
"format": {**_S, "description": "cash | tournament"},
|
||||||
|
"buy_in_total": {**_N, "description": "Total bought in"},
|
||||||
|
"cash_out": {**_N, "description": "Final cashout (recomputes net)"},
|
||||||
|
"mantra": {**_S, "description": "Pre-session focus/anchor"},
|
||||||
|
"mood": {**_S, "description": "Mental-game note"}},
|
||||||
|
[])},
|
||||||
"undo_last": {"handler": _undo_last, "spec": _f(
|
"undo_last": {"handler": _undo_last, "spec": _f(
|
||||||
"undo_last",
|
"undo_last",
|
||||||
"Undo/delete the most recent logged entry in the live session when Brian says "
|
"Undo/delete the most recent logged entry in the live session when Brian says "
|
||||||
|
|||||||
+11
-3
@@ -108,11 +108,19 @@ def create_app() -> FastAPI:
|
|||||||
return FileResponse(str(_STATIC / "session.html"))
|
return FileResponse(str(_STATIC / "session.html"))
|
||||||
|
|
||||||
@app.get("/session/data")
|
@app.get("/session/data")
|
||||||
async def session_hud_data() -> dict:
|
async def session_hud_data(id: int | None = None) -> dict:
|
||||||
"""The current live session's HUD bundle (or {session: None} if none open)."""
|
"""HUD bundle for the live session, or a specific past session via ?id=."""
|
||||||
bundle = await asyncio.to_thread(poker.hud)
|
bundle = await asyncio.to_thread(poker.hud, id)
|
||||||
return bundle or {"session": None}
|
return bundle or {"session": None}
|
||||||
|
|
||||||
|
@app.patch("/session/{session_id}")
|
||||||
|
async def session_update(session_id: int, request: Request) -> dict:
|
||||||
|
"""Edit a session's details (venue/stakes/game/buy-in/cash-out/…)."""
|
||||||
|
body = await request.json()
|
||||||
|
s = await asyncio.to_thread(lambda: poker.update_session(session_id, **body))
|
||||||
|
logbus.log("info", "session edited", id=session_id, fields=list(body))
|
||||||
|
return {"ok": s is not None, "session": s}
|
||||||
|
|
||||||
@app.delete("/session/entry/{kind}/{entry_id}")
|
@app.delete("/session/entry/{kind}/{entry_id}")
|
||||||
async def delete_entry(kind: str, entry_id: int) -> dict:
|
async def delete_entry(kind: str, entry_id: int) -> dict:
|
||||||
"""Delete one HUD entry (hand | stack | read | ritual) by id."""
|
"""Delete one HUD entry (hand | stack | read | ritual) by id."""
|
||||||
|
|||||||
@@ -85,7 +85,7 @@
|
|||||||
const date=(s.started_at||'').slice(0,10);
|
const date=(s.started_at||'').slice(0,10);
|
||||||
const meta=[date,s.venue,`${s.hands} hand${s.hands===1?'':'s'}`,
|
const meta=[date,s.venue,`${s.hands} hand${s.hands===1?'':'s'}`,
|
||||||
s.hours?`${(+s.hours).toFixed(1)}h`:''].filter(Boolean).join(' · ');
|
s.hours?`${(+s.hours).toFixed(1)}h`:''].filter(Boolean).join(' · ');
|
||||||
const href=s.has_recap?`/recap/${s.id}`:`/session`;
|
const href=`/session?id=${s.id}`; // read-only HUD detail for any session
|
||||||
const net=s.net!=null?money(s.net):(s.status==='live'?'live':'—');
|
const net=s.net!=null?money(s.net):(s.status==='live'?'live':'—');
|
||||||
return `<div class="row">
|
return `<div class="row">
|
||||||
<a class="body" href="${href}">
|
<a class="body" href="${href}">
|
||||||
|
|||||||
@@ -84,6 +84,21 @@
|
|||||||
.del-x { flex: none; background: none; border: none; color: var(--fade); font-size: 1.15rem;
|
.del-x { flex: none; background: none; border: none; color: var(--fade); font-size: 1.15rem;
|
||||||
line-height: 1; padding: 2px 6px; cursor: pointer; -webkit-tap-highlight-color: transparent; }
|
line-height: 1; padding: 2px 6px; cursor: pointer; -webkit-tap-highlight-color: transparent; }
|
||||||
.del-x:active { color: var(--low); }
|
.del-x:active { color: var(--low); }
|
||||||
|
/* session edit form */
|
||||||
|
.edit-btn { margin-left: auto; background: #241400; border: 1px solid var(--border); color: var(--accent);
|
||||||
|
border-radius: 8px; padding: 5px 10px; font-size: .8rem; cursor: pointer; -webkit-tap-highlight-color: transparent; }
|
||||||
|
.mantra { color: var(--mid); font-style: italic; font-size: .9rem; margin-top: 10px; }
|
||||||
|
.edit-form { grid-template-columns: 1fr 1fr; gap: 10px; margin-top: 14px; }
|
||||||
|
.edit-form label { display: flex; flex-direction: column; gap: 4px; font-size: .68rem;
|
||||||
|
color: var(--fade); text-transform: uppercase; letter-spacing: .4px; }
|
||||||
|
.edit-form label.wide { grid-column: 1 / -1; }
|
||||||
|
.edit-form input { background: var(--bg-line); border: 1px solid var(--border); border-radius: 8px;
|
||||||
|
padding: 8px 10px; color: var(--text); font-size: 16px; }
|
||||||
|
.edit-form input:focus { outline: none; border-color: var(--accent); }
|
||||||
|
.edit-actions { grid-column: 1 / -1; display: flex; gap: 8px; justify-content: flex-end; }
|
||||||
|
.edit-actions button { background: var(--bg-line); border: 1px solid var(--border); color: var(--text);
|
||||||
|
border-radius: 8px; padding: 8px 16px; cursor: pointer; }
|
||||||
|
.edit-actions button.save { background: var(--accent); color: #0a0a0a; border-color: var(--accent); font-weight: 600; }
|
||||||
.empty { color: var(--fade); font-size: .92rem; }
|
.empty { color: var(--fade); font-size: .92rem; }
|
||||||
.err { color: var(--low); text-align: center; padding: 30px; }
|
.err { color: var(--low); text-align: center; padding: 30px; }
|
||||||
.big-empty { text-align: center; padding: 50px 20px; color: var(--fade); }
|
.big-empty { text-align: center; padding: 50px 20px; color: var(--fade); }
|
||||||
@@ -108,6 +123,8 @@
|
|||||||
const root = document.getElementById('root');
|
const root = document.getElementById('root');
|
||||||
const dot = document.getElementById('dot');
|
const dot = document.getElementById('dot');
|
||||||
const updatedEl = document.getElementById('updated');
|
const updatedEl = document.getElementById('updated');
|
||||||
|
const SID = new URLSearchParams(location.search).get('id'); // past-session view when set
|
||||||
|
let curSession = null; // the session object currently rendered (for the edit form)
|
||||||
|
|
||||||
function esc(s){ const d=document.createElement('div'); d.textContent = s==null?'':String(s); return d.innerHTML; }
|
function esc(s){ const d=document.createElement('div'); d.textContent = s==null?'':String(s); return d.innerHTML; }
|
||||||
function money(v){ if (v == null) return '—'; const n = Number(v); return (n<0?'-$':'$') + Math.abs(n).toLocaleString(); }
|
function money(v){ if (v == null) return '—'; const n = Number(v); return (n<0?'-$':'$') + Math.abs(n).toLocaleString(); }
|
||||||
@@ -127,6 +144,16 @@
|
|||||||
const h = Math.floor(s/3600), m = Math.round((s%3600)/60);
|
const h = Math.floor(s/3600), m = Math.round((s%3600)/60);
|
||||||
return h ? `${h}h ${m}m` : `${m}m`;
|
return h ? `${h}h ${m}m` : `${m}m`;
|
||||||
}
|
}
|
||||||
|
// For a live session: time since start. For a closed one: actual played duration.
|
||||||
|
function clock(sess){
|
||||||
|
if(sess.is_live) return elapsed(sess.started_at);
|
||||||
|
if(sess.hours != null) return (+sess.hours).toFixed(1) + 'h';
|
||||||
|
if(sess.started_at && sess.ended_at){
|
||||||
|
const s = Math.max(0,(new Date(sess.ended_at)-new Date(sess.started_at))/1000);
|
||||||
|
const h=Math.floor(s/3600), m=Math.round((s%3600)/60); return h?`${h}h ${m}m`:`${m}m`;
|
||||||
|
}
|
||||||
|
return '—';
|
||||||
|
}
|
||||||
|
|
||||||
// Tiny inline sparkline of the stack-over-time series.
|
// Tiny inline sparkline of the stack-over-time series.
|
||||||
function sparkline(series){
|
function sparkline(series){
|
||||||
@@ -148,6 +175,28 @@
|
|||||||
|
|
||||||
function netClass(v){ return v == null ? 'flat' : v > 0 ? 'up' : v < 0 ? 'down' : 'flat'; }
|
function netClass(v){ return v == null ? 'flat' : v > 0 ? 'up' : v < 0 ? 'down' : 'flat'; }
|
||||||
|
|
||||||
|
function toggleEdit(){
|
||||||
|
const f = document.getElementById('editForm');
|
||||||
|
if(f) f.style.display = (f.style.display === 'none' || !f.style.display) ? 'grid' : 'none';
|
||||||
|
}
|
||||||
|
async function saveEdit(){
|
||||||
|
if(!curSession) return;
|
||||||
|
const body = {};
|
||||||
|
for(const k of ['venue','stakes','game','format','buy_in_total','cash_out','mantra','mood']){
|
||||||
|
const el = document.getElementById('ed_'+k);
|
||||||
|
if(!el) continue;
|
||||||
|
let v = el.value.trim();
|
||||||
|
if(v === '') continue;
|
||||||
|
body[k] = (k==='buy_in_total'||k==='cash_out') ? Number(v) : v;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const r = await fetch('/session/' + curSession.id, {
|
||||||
|
method:'PATCH', headers:{'Content-Type':'application/json'}, body: JSON.stringify(body) });
|
||||||
|
if(!r.ok) throw new Error('HTTP '+r.status);
|
||||||
|
toggleEdit(); refresh();
|
||||||
|
} catch(e){ alert('Save failed: '+e.message); }
|
||||||
|
}
|
||||||
|
|
||||||
// Delete one logged entry (hand | ritual | read | stack), then refresh.
|
// Delete one logged entry (hand | ritual | read | stack), then refresh.
|
||||||
async function del(kind, id){
|
async function del(kind, id){
|
||||||
if(!confirm('Delete this entry?')) return;
|
if(!confirm('Delete this entry?')) return;
|
||||||
@@ -168,6 +217,7 @@
|
|||||||
updatedEl.textContent = '';
|
updatedEl.textContent = '';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
curSession = s;
|
||||||
const stack = data.stack || {};
|
const stack = data.stack || {};
|
||||||
const hands = data.hands || [];
|
const hands = data.hands || [];
|
||||||
const villains = data.villains || [];
|
const villains = data.villains || [];
|
||||||
@@ -190,14 +240,29 @@
|
|||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="sess-top">
|
<div class="sess-top">
|
||||||
<span class="sess-title">${esc(title)}</span>
|
<span class="sess-title">${esc(title)}</span>
|
||||||
<span class="sess-sub">${esc(s.venue || 'unknown room')}${s.status && s.status!=='live' ? ' · '+esc(s.status) : ''}</span>
|
<span class="sess-sub">${esc(s.venue || 'unknown room')}${!s.is_live && s.status ? ' · '+esc(s.status) : ''}</span>
|
||||||
|
<button class="edit-btn" onclick="toggleEdit()" title="Edit session details">✎ Edit</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="chips">
|
<div class="chips">
|
||||||
<span class="chip">⏱ <b>${elapsed(s.started_at)}</b></span>
|
<span class="chip">⏱ <b>${clock(s)}</b></span>
|
||||||
<span class="chip">in <b>${money(s.buy_in_total)}</b></span>
|
<span class="chip">in <b>${money(s.buy_in_total)}</b></span>
|
||||||
|
${!s.is_live && s.net != null ? `<span class="chip">net <b class="${netClass(s.net)}" style="font-weight:700">${signed(s.net)}</b></span>` : ''}
|
||||||
<span class="chip">${esc(s.format || 'cash')}</span>
|
<span class="chip">${esc(s.format || 'cash')}</span>
|
||||||
<span class="chip"><b>${hands.length}</b> hands</span>
|
<span class="chip"><b>${hands.length}</b> hands</span>
|
||||||
${resets.length ? `<span class="chip">🔄 <b>${resets.length}</b> reset${resets.length>1?'s':''}</span>` : ''}
|
${resets.length ? `<span class="chip">🔄 <b>${resets.length}</b> reset${resets.length>1?'s':''}</span>` : ''}
|
||||||
|
${s.has_recap ? `<a class="chip" style="color:var(--accent);text-decoration:none" href="/recap/${s.id}">📝 recap</a>` : ''}
|
||||||
|
</div>
|
||||||
|
${s.mantra ? `<div class="mantra">“${esc(s.mantra)}”</div>` : ''}
|
||||||
|
<div id="editForm" class="edit-form" style="display:none">
|
||||||
|
<label>Venue<input id="ed_venue" value="${esc(s.venue||'')}"></label>
|
||||||
|
<label>Stakes<input id="ed_stakes" value="${esc(s.stakes||'')}"></label>
|
||||||
|
<label>Game<input id="ed_game" value="${esc(s.game||'')}"></label>
|
||||||
|
<label>Format<input id="ed_format" value="${esc(s.format||'')}"></label>
|
||||||
|
<label>Buy-in $<input id="ed_buy_in_total" type="number" value="${s.buy_in_total??''}"></label>
|
||||||
|
<label>Cash-out $<input id="ed_cash_out" type="number" value="${s.cash_out??''}"></label>
|
||||||
|
<label class="wide">Mantra<input id="ed_mantra" value="${esc(s.mantra||'')}"></label>
|
||||||
|
<label class="wide">Mood<input id="ed_mood" value="${esc(s.mood||'')}"></label>
|
||||||
|
<div class="edit-actions"><button onclick="saveEdit()" class="save">Save</button><button onclick="toggleEdit()">Cancel</button></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -272,8 +337,11 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function refresh(){
|
async function refresh(){
|
||||||
|
// don't clobber the edit form mid-edit on a poll tick
|
||||||
|
const ef = document.getElementById('editForm');
|
||||||
|
if (ef && ef.style.display === 'grid') return;
|
||||||
try {
|
try {
|
||||||
const r = await fetch('/session/data', { cache: 'no-store' });
|
const r = await fetch('/session/data' + (SID ? ('?id=' + encodeURIComponent(SID)) : ''), { cache: 'no-store' });
|
||||||
const data = await r.json();
|
const data = await r.json();
|
||||||
data._fetched = new Date().toISOString();
|
data._fetched = new Date().toISOString();
|
||||||
dot.classList.add('pulse'); setTimeout(() => dot.classList.remove('pulse'), 400);
|
dot.classList.add('pulse'); setTimeout(() => dot.classList.remove('pulse'), 400);
|
||||||
@@ -284,7 +352,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
refresh();
|
refresh();
|
||||||
setInterval(refresh, 5000);
|
if (!SID) setInterval(refresh, 5000); // live HUD polls; a past session is static
|
||||||
document.addEventListener('visibilitychange', () => { if (!document.hidden) refresh(); });
|
document.addEventListener('visibilitychange', () => { if (!document.hidden) refresh(); });
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
+49
-6
@@ -179,9 +179,10 @@ def test_undo_last_and_delete_entry(lyra):
|
|||||||
_, poker, modes, tools = lyra
|
_, poker, modes, tools = lyra
|
||||||
assert "undo_last" in modes.CASH.tools
|
assert "undo_last" in modes.CASH.tools
|
||||||
poker.start_session(venue="Meadows", stakes="2/5", buy_in=500)
|
poker.start_session(venue="Meadows", stakes="2/5", buy_in=500)
|
||||||
h1 = poker.log_hand(position="UTG", hole_cards="AA")
|
poker.log_hand(position="UTG", hole_cards="AA")
|
||||||
h2 = poker.log_hand(position="BTN", hole_cards="72o")
|
poker.log_hand(position="BTN", hole_cards="72o")
|
||||||
poker.log_stack(600); poker.log_stack(420)
|
poker.log_stack(600)
|
||||||
|
poker.log_stack(420)
|
||||||
poker.log_ritual("scar", content="punted")
|
poker.log_ritual("scar", content="punted")
|
||||||
poker.log_ritual("confidence", content="good fold")
|
poker.log_ritual("confidence", content="good fold")
|
||||||
|
|
||||||
@@ -215,6 +216,48 @@ def test_undo_last_tool(lyra):
|
|||||||
assert "one of" in tools.dispatch("undo_last", {"what": "banana"}, {}).lower()
|
assert "one of" in tools.dispatch("undo_last", {"what": "banana"}, {}).lower()
|
||||||
|
|
||||||
|
|
||||||
|
def test_update_session_edit(lyra):
|
||||||
|
_, poker, modes, tools = lyra
|
||||||
|
assert "update_session" in modes.CASH.tools
|
||||||
|
sid = poker.start_session(venue="Meadows", stakes="1/3", buy_in=300)
|
||||||
|
s = poker.update_session(sid, stakes="2/5", buy_in_total=600, cash_out=900, venue="Bellagio")
|
||||||
|
assert s["stakes"] == "2/5" and s["venue"] == "Bellagio"
|
||||||
|
assert s["buy_in_total"] == 600 and s["cash_out"] == 900
|
||||||
|
assert s["net"] == 300 # recomputed from cash_out - buy_in
|
||||||
|
# via the tool (edits the live/most-recent session)
|
||||||
|
out = tools.dispatch("update_session", {"mood": "locked in"}, {})
|
||||||
|
assert "updated" in out.lower() and poker.get_session(sid)["mood"] == "locked in"
|
||||||
|
assert "what to change" in tools.dispatch("update_session", {}, {}).lower()
|
||||||
|
|
||||||
|
|
||||||
|
def test_review_session_and_post_close_rituals(lyra):
|
||||||
|
_, poker, _, tools = lyra
|
||||||
|
sid = poker.start_session(venue="Meadows", stakes="2/5", buy_in=500)
|
||||||
|
poker.end_session(cash_out=720)
|
||||||
|
assert poker.live_session() is None
|
||||||
|
assert poker.review_session_id() == sid # most-recent closed session
|
||||||
|
|
||||||
|
# rituals attach to the closed session during review (no live session needed)
|
||||||
|
out = tools.dispatch("scar_note", {"content": "should've folded turn", "classification": "punt"}, {})
|
||||||
|
assert "logged" in out.lower()
|
||||||
|
tools.dispatch("confidence_bank", {"content": "good thin value river"}, {})
|
||||||
|
assert len(poker.list_rituals(session_id=sid, kinds=("scar",))) == 1
|
||||||
|
assert len(poker.list_rituals(session_id=sid, kinds=("confidence",))) == 1
|
||||||
|
|
||||||
|
|
||||||
|
def test_hud_for_past_session(lyra):
|
||||||
|
_, poker, _, _ = lyra
|
||||||
|
sid = poker.start_session(venue="Meadows", stakes="2/5", buy_in=500)
|
||||||
|
poker.log_hand(position="BTN", hole_cards="AKs")
|
||||||
|
poker.end_session(cash_out=650)
|
||||||
|
# a *new* live session so live HUD != the one we query
|
||||||
|
poker.start_session(venue="Wynn", stakes="1/3", buy_in=300)
|
||||||
|
past = poker.hud(sid)
|
||||||
|
assert past["session"]["id"] == sid and past["session"]["is_live"] is False
|
||||||
|
assert past["session"]["net"] == 150 and len(past["hands"]) == 1
|
||||||
|
assert poker.hud()["session"]["venue"] == "Wynn" # live one unaffected
|
||||||
|
|
||||||
|
|
||||||
def test_list_and_delete_session(lyra):
|
def test_list_and_delete_session(lyra):
|
||||||
_, poker, _, tools = lyra
|
_, poker, _, tools = lyra
|
||||||
keep = poker.start_session(venue="Meadows", stakes="1/3", buy_in=300)
|
keep = poker.start_session(venue="Meadows", stakes="1/3", buy_in=300)
|
||||||
@@ -244,9 +287,9 @@ def test_recent_sessions_tool(lyra):
|
|||||||
assert "Meadows" in out and "+220" in out
|
assert "Meadows" in out and "+220" in out
|
||||||
|
|
||||||
|
|
||||||
def test_rituals_require_live_session(lyra):
|
def test_rituals_require_a_session(lyra):
|
||||||
_, poker, _, tools = lyra
|
_, poker, _, tools = lyra
|
||||||
# tools degrade gracefully (no exception) when nothing is open
|
# with no session at all, the tool degrades gracefully (no exception)
|
||||||
assert "no live session" in tools.dispatch("scar_note", {"content": "x"}, {}).lower()
|
assert "no session" in tools.dispatch("scar_note", {"content": "x"}, {}).lower()
|
||||||
with pytest.raises(ValueError):
|
with pytest.raises(ValueError):
|
||||||
poker.log_ritual("scar", content="x")
|
poker.log_ritual("scar", content="x")
|
||||||
|
|||||||
Reference in New Issue
Block a user