feat: view past sessions, edit session details, log rituals while reviewing
- View any past session as a read-only HUD: /session?id=N (hud(session_id) +
/session/data?id=); /history rows now link there. Closed sessions show played
duration + final net; recap link when one exists.
- Edit session details during or after play: poker.update_session (recomputes net
when buy-in/cash-out change), PATCH /session/{id}, an update_session tool ("venue
was actually Bellagio", "I bought in for 600"), and an inline ✎ Edit form on the HUD.
- Rituals attach to the most-recent session post-close (poker.review_session_id),
so scar/confidence/reset work while reviewing after you rack up.
- Edit form is poll-safe (won't clobber mid-edit); past-session view doesn't poll.
- test_modes.py +3 (edit, review rituals, past-session HUD); 49 green.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
+11
-3
@@ -108,11 +108,19 @@ def create_app() -> FastAPI:
|
||||
return FileResponse(str(_STATIC / "session.html"))
|
||||
|
||||
@app.get("/session/data")
|
||||
async def session_hud_data() -> dict:
|
||||
"""The current live session's HUD bundle (or {session: None} if none open)."""
|
||||
bundle = await asyncio.to_thread(poker.hud)
|
||||
async def session_hud_data(id: int | None = None) -> dict:
|
||||
"""HUD bundle for the live session, or a specific past session via ?id=."""
|
||||
bundle = await asyncio.to_thread(poker.hud, id)
|
||||
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}")
|
||||
async def delete_entry(kind: str, entry_id: int) -> dict:
|
||||
"""Delete one HUD entry (hand | stack | read | ritual) by id."""
|
||||
|
||||
@@ -85,7 +85,7 @@
|
||||
const date=(s.started_at||'').slice(0,10);
|
||||
const meta=[date,s.venue,`${s.hands} hand${s.hands===1?'':'s'}`,
|
||||
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':'—');
|
||||
return `<div class="row">
|
||||
<a class="body" href="${href}">
|
||||
|
||||
@@ -84,6 +84,21 @@
|
||||
.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; }
|
||||
.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; }
|
||||
.err { color: var(--low); text-align: center; padding: 30px; }
|
||||
.big-empty { text-align: center; padding: 50px 20px; color: var(--fade); }
|
||||
@@ -108,6 +123,8 @@
|
||||
const root = document.getElementById('root');
|
||||
const dot = document.getElementById('dot');
|
||||
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 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);
|
||||
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.
|
||||
function sparkline(series){
|
||||
@@ -148,6 +175,28 @@
|
||||
|
||||
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.
|
||||
async function del(kind, id){
|
||||
if(!confirm('Delete this entry?')) return;
|
||||
@@ -168,6 +217,7 @@
|
||||
updatedEl.textContent = '';
|
||||
return;
|
||||
}
|
||||
curSession = s;
|
||||
const stack = data.stack || {};
|
||||
const hands = data.hands || [];
|
||||
const villains = data.villains || [];
|
||||
@@ -190,14 +240,29 @@
|
||||
<div class="card">
|
||||
<div class="sess-top">
|
||||
<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 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>
|
||||
${!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"><b>${hands.length}</b> hands</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>
|
||||
|
||||
@@ -272,8 +337,11 @@
|
||||
}
|
||||
|
||||
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 {
|
||||
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();
|
||||
data._fetched = new Date().toISOString();
|
||||
dot.classList.add('pulse'); setTimeout(() => dot.classList.remove('pulse'), 400);
|
||||
@@ -284,7 +352,7 @@
|
||||
}
|
||||
|
||||
refresh();
|
||||
setInterval(refresh, 5000);
|
||||
if (!SID) setInterval(refresh, 5000); // live HUD polls; a past session is static
|
||||
document.addEventListener('visibilitychange', () => { if (!document.hidden) refresh(); });
|
||||
</script>
|
||||
</body>
|
||||
|
||||
Reference in New Issue
Block a user