Files
project-lyra/lyra/web/static/history.html
T
serversdown e1e89c07e4 feat: poker session history — browse, delete, and Lyra lookup
Answers three gaps: no way to delete a single poker session (only clear_all),
no way to browse past sessions, and Lyra could only see aggregate stats.

- poker.list_sessions() (per-session summary + hand count + recap flag) and
  poker.delete_session() (removes a session + its hands/reads/observations/
  stacks/rituals; keeps the persistent villain file).
- /history page (date, stakes, venue, net, hours, recap link, per-row delete with
  confirm) + /history/data + DELETE /history/{id}. Nav links from chat + HUD.
- recent_sessions read tool, added to the shared lookups so Lyra can answer
  "how'd my last few sessions go?" in either mode.
- Delete is UI/CLI only — deliberately not a Lyra tool.
- test_modes.py +2 (list/delete, recent_sessions); 44 green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-20 00:21:48 +00:00

105 lines
5.6 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />
<meta name="theme-color" content="#070707" />
<title>Lyra — Sessions</title>
<style>
:root{--bg:#070707;--bg-elev:#0e0e0e;--bg-line:#141414;--border:#2a1d12;--text:#e8e8e8;
--fade:#8a8a8a;--accent:#ff7a00;--good:#8fd694;--low:#ff6b6b;--mid:#ffb347;}
*{box-sizing:border-box;}
html,body{margin:0;min-height:100%;background:var(--bg);color:var(--text);
font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif;-webkit-text-size-adjust:100%;}
header{position:sticky;top:0;z-index:10;background:var(--bg-elev);border-bottom:1px solid var(--border);
padding:env(safe-area-inset-top) 14px 0;}
.topbar{display:flex;align-items:center;gap:10px;padding:13px 0;}
.topbar h1{font-size:1.05rem;margin:0;font-weight:600;}
.topbar a.back{color:var(--accent);text-decoration:none;font-size:.92rem;}
.count{margin-left:auto;color:var(--fade);font-size:.8rem;}
main{max-width:640px;margin:0 auto;padding:12px 12px 40px;}
.summary{display:flex;gap:8px;flex-wrap:wrap;margin-bottom:12px;}
.pill{font-size:.8rem;color:var(--fade);background:var(--bg-elev);border:1px solid var(--border);
border-radius:999px;padding:4px 11px;} .pill b{color:var(--text);}
.row{display:flex;align-items:center;gap:12px;background:var(--bg-elev);border:1px solid var(--border);
border-radius:10px;padding:10px 12px;margin-bottom:8px;}
.row .body{flex:1;min-width:0;text-decoration:none;color:var(--text);}
.row .body:active{opacity:.7;}
.ln1{font-size:.95rem;} .ln1 .live{color:var(--accent);font-size:.7rem;border:1px solid var(--accent);
border-radius:999px;padding:0 6px;margin-left:6px;text-transform:uppercase;letter-spacing:.4px;}
.ln2{font-size:.76rem;color:var(--fade);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;}
.net{flex:none;font-variant-numeric:tabular-nums;font-weight:700;}
.net.up{color:var(--good);} .net.down{color:var(--low);} .net.flat{color:var(--fade);}
.del{flex:none;background:none;border:1px solid var(--border);color:var(--fade);border-radius:8px;
padding:6px 9px;cursor:pointer;-webkit-tap-highlight-color:transparent;font-size:.9rem;}
.del:active{background:#3a1414;color:var(--low);border-color:var(--low);}
.empty{color:var(--fade);text-align:center;padding:46px 16px;}
</style>
</head>
<body>
<header>
<div class="topbar">
<h1>📚 Sessions</h1>
<a class="back" href="/">← Chat</a>
<a class="back" href="/session">🎬 Live</a>
<span class="count" id="count"></span>
</div>
</header>
<main id="root"><p class="empty">Loading…</p></main>
<script>
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?'+$':n<0?'-$':'$')+Math.abs(n).toLocaleString();}
function netClass(v){return v==null?'flat':v>0?'up':v<0?'down':'flat';}
async function del(id, label){
if(!confirm(`Delete session ${label}? This removes its hands, reads, stacks and rituals. Can't be undone.`)) return;
try{
const r=await fetch(`/history/${id}`,{method:'DELETE'});
if(!r.ok) throw new Error('HTTP '+r.status);
load();
}catch(e){alert('Delete failed: '+e.message);}
}
async function load(){
const root=document.getElementById('root');
try{
const r=await fetch('/history/data',{cache:'no-store'});
const sessions=(await r.json()).sessions||[];
document.getElementById('count').textContent=`${sessions.length} session${sessions.length===1?'':'s'}`;
if(!sessions.length){root.innerHTML='<p class="empty">No sessions yet. Start one from chat in ♠ Cash mode.</p>';return;}
const closed=sessions.filter(s=>s.net!=null);
const totNet=closed.reduce((a,s)=>a+(s.net||0),0);
const totHrs=closed.reduce((a,s)=>a+(s.hours||0),0);
const summary=`<div class="summary">
<span class="pill"><b>${sessions.length}</b> sessions</span>
<span class="pill">net <b>${money(totNet)}</b></span>
${totHrs?`<span class="pill"><b>${totHrs.toFixed(1)}h</b></span>`:''}
${totHrs?`<span class="pill">${money(Math.round(totNet/totHrs))}/hr</span>`:''}
</div>`;
root.innerHTML=summary+sessions.map(s=>{
const title=[s.stakes,s.game].filter(Boolean).join(' ')||'Session';
const live=s.status==='live'?'<span class="live">live</span>':'';
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 net=s.net!=null?money(s.net):(s.status==='live'?'live':'—');
return `<div class="row">
<a class="body" href="${href}">
<div class="ln1">${esc(title)} <span style="color:var(--fade)">@ ${esc(s.venue||'?')}</span>${live}</div>
<div class="ln2">${esc(meta)}${s.has_recap?' · recap ✓':''}</div>
</a>
<span class="net ${netClass(s.net)}">${net}</span>
<button class="del" title="Delete session" onclick="del(${s.id}, '#${s.id} ${esc(title)}')">🗑</button>
</div>`;
}).join('');
}catch(e){root.innerHTML='<p class="empty">Couldn\'t load sessions.</p>';}
}
load();
</script>
</body>
</html>