Big poker mode changes and hotfixes. #5
@@ -644,6 +644,28 @@ def update_hand(hand_id: int, **fields) -> dict | None:
|
|||||||
return get_hand(hand_id)
|
return get_hand(hand_id)
|
||||||
|
|
||||||
|
|
||||||
|
def disown_hand(hand_id: int) -> dict | None:
|
||||||
|
"""Reclassify a hand as OBSERVED (not Brian's) — fixes one that was mislabeled
|
||||||
|
as his. Clears the flat hero fields and rewrites the structured JSON with
|
||||||
|
hero_involved=false so the replay stops showing him as hero."""
|
||||||
|
h = get_hand(hand_id)
|
||||||
|
if not h:
|
||||||
|
return None
|
||||||
|
structured = h.get("structured")
|
||||||
|
if isinstance(structured, str):
|
||||||
|
structured = _safe_json(structured)
|
||||||
|
if isinstance(structured, dict):
|
||||||
|
structured = normalize_structured({**structured, "hero_involved": False})
|
||||||
|
conn = _c()
|
||||||
|
with conn:
|
||||||
|
conn.execute(
|
||||||
|
"UPDATE poker_hands SET position = NULL, hole_cards = NULL, result = NULL, "
|
||||||
|
"structured = ? WHERE id = ?",
|
||||||
|
(json.dumps(structured) if structured else None, hand_id),
|
||||||
|
)
|
||||||
|
return get_hand(hand_id)
|
||||||
|
|
||||||
|
|
||||||
def list_hands(session_id: int | None = None) -> list[dict]:
|
def list_hands(session_id: int | None = None) -> list[dict]:
|
||||||
sid = _resolve(session_id)
|
sid = _resolve(session_id)
|
||||||
if sid is None:
|
if sid is None:
|
||||||
|
|||||||
@@ -192,6 +192,13 @@ def create_app() -> FastAPI:
|
|||||||
logbus.log("info", "hand edited", id=hand_id, fields=list(body))
|
logbus.log("info", "hand edited", id=hand_id, fields=list(body))
|
||||||
return {"ok": h is not None, "hand": h}
|
return {"ok": h is not None, "hand": h}
|
||||||
|
|
||||||
|
@app.post("/hand/{hand_id}/disown")
|
||||||
|
async def hand_disown(hand_id: int) -> dict:
|
||||||
|
"""Reclassify a hand as observed (not Brian's) — fix a misattributed one."""
|
||||||
|
h = await asyncio.to_thread(poker.disown_hand, hand_id)
|
||||||
|
logbus.log("info", "hand disowned", id=hand_id)
|
||||||
|
return {"ok": h is not None, "hand": h}
|
||||||
|
|
||||||
@app.delete("/hand/{hand_id}")
|
@app.delete("/hand/{hand_id}")
|
||||||
async def hand_delete(hand_id: int) -> dict:
|
async def hand_delete(hand_id: int) -> dict:
|
||||||
"""Delete a logged hand."""
|
"""Delete a logged hand."""
|
||||||
|
|||||||
@@ -282,8 +282,54 @@
|
|||||||
const h = await r.json();
|
const h = await r.json();
|
||||||
if(!h || !h.id){ document.getElementById('root').innerHTML='<p class="err">Hand not found.</p>'; return; }
|
if(!h || !h.id){ document.getElementById('root').innerHTML='<p class="err">Hand not found.</p>'; return; }
|
||||||
render(h);
|
render(h);
|
||||||
|
renderEditor(h);
|
||||||
}catch(e){ document.getElementById('root').innerHTML='<p class="err">Couldn\'t load the hand.</p>'; }
|
}catch(e){ document.getElementById('root').innerHTML='<p class="err">Couldn\'t load the hand.</p>'; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function renderEditor(h){
|
||||||
|
const wrap = document.createElement('div');
|
||||||
|
wrap.style.cssText = 'max-width:520px;margin:18px auto 0;border-top:1px solid #241a10;padding-top:12px;';
|
||||||
|
const tags = ['','well_played','leak','cooler','confidence','notable'];
|
||||||
|
wrap.innerHTML = `
|
||||||
|
<details style="font-size:.9rem;">
|
||||||
|
<summary style="cursor:pointer;color:var(--accent,#ff7a00);">✎ Edit this hand</summary>
|
||||||
|
<div style="display:flex;flex-direction:column;gap:8px;margin-top:10px;">
|
||||||
|
<label>Position <input id="e_pos" value="${esc(h.position||'')}" placeholder="e.g. CO (blank if not yours)"></label>
|
||||||
|
<label>Your cards <input id="e_hole" value="${esc(h.hole_cards||'')}" placeholder="e.g. As Ks (blank if not yours)"></label>
|
||||||
|
<label>Board <input id="e_board" value="${esc(h.board||'')}" placeholder="e.g. Tc 8s Js 6d"></label>
|
||||||
|
<label>Your net <input id="e_res" value="${h.result!=null?esc(h.result):''}" placeholder="+ / − chips (blank if not yours)"></label>
|
||||||
|
<label>Tag <select id="e_tag">${tags.map(t=>`<option value="${t}" ${h.tag===t?'selected':''}>${t||'—'}</option>`).join('')}</select></label>
|
||||||
|
<label>Lesson <input id="e_lesson" value="${esc(h.lesson||'')}"></label>
|
||||||
|
<div style="display:flex;flex-wrap:wrap;gap:8px;margin-top:4px;">
|
||||||
|
<button onclick="saveHand(${h.id})" style="border-color:var(--accent,#ff7a00);color:var(--accent,#ff7a00);">Save</button>
|
||||||
|
<button onclick="disown(${h.id})" title="It was someone else's hand — clear it from you">Not my hand</button>
|
||||||
|
<button onclick="delHand(${h.id})" style="margin-left:auto;color:#ff6b6b;">Delete</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</details>`;
|
||||||
|
wrap.querySelectorAll('input,select').forEach(el=>{el.style.cssText='font:inherit;font-size:.86rem;padding:5px 8px;border-radius:6px;border:1px solid #241a10;background:#0b0b0b;color:#e8e8e8;margin-left:8px;';});
|
||||||
|
wrap.querySelectorAll('label').forEach(el=>{el.style.cssText='display:flex;justify-content:space-between;align-items:center;color:#8a8a8a;';});
|
||||||
|
wrap.querySelectorAll('button').forEach(el=>{el.style.cssText+=';font:inherit;font-size:.84rem;padding:6px 12px;border-radius:7px;border:1px solid #241a10;background:#141414;color:#e8e8e8;cursor:pointer;';});
|
||||||
|
document.getElementById('root').appendChild(wrap);
|
||||||
|
}
|
||||||
|
const val = id => document.getElementById(id).value.trim();
|
||||||
|
async function saveHand(id){
|
||||||
|
const body = {position:val('e_pos'), hole_cards:val('e_hole'), board:val('e_board'),
|
||||||
|
tag:val('e_tag'), lesson:val('e_lesson')};
|
||||||
|
const res = val('e_res'); if(res!=='') body.result = Number(res);
|
||||||
|
await fetch(`/hand/${id}`,{method:'PATCH',headers:{'Content-Type':'application/json'},body:JSON.stringify(body)});
|
||||||
|
load();
|
||||||
|
}
|
||||||
|
async function disown(id){
|
||||||
|
if(!confirm("Mark this as someone else's hand? It'll be cleared from your stats.")) return;
|
||||||
|
await fetch(`/hand/${id}/disown`,{method:'POST'});
|
||||||
|
load();
|
||||||
|
}
|
||||||
|
async function delHand(id){
|
||||||
|
if(!confirm('Delete this hand for good?')) return;
|
||||||
|
await fetch(`/hand/${id}`,{method:'DELETE'});
|
||||||
|
location.href='/hands';
|
||||||
|
}
|
||||||
load();
|
load();
|
||||||
</script>
|
</script>
|
||||||
<script src="/nav.js"></script>
|
<script src="/nav.js"></script>
|
||||||
|
|||||||
@@ -18,6 +18,22 @@ def lyra(tmp_path, monkeypatch):
|
|||||||
return poker
|
return poker
|
||||||
|
|
||||||
|
|
||||||
|
def test_disown_hand_clears_hero_attribution(lyra):
|
||||||
|
poker = lyra
|
||||||
|
sid = poker.start_session(venue="Meadows", buy_in=300)
|
||||||
|
hid = poker.store_hand_history(
|
||||||
|
{"hero_involved": True, "hero_pos": "MP", "hero_cards": ["As", "3d"],
|
||||||
|
"players": [{"pos": "MP", "cards": ["As", "3d"]}],
|
||||||
|
"result": {"pot": 600, "hero_net": 304}}, session_id=sid, tag="notable")
|
||||||
|
h = poker.disown_hand(hid)
|
||||||
|
assert h["position"] is None and h["hole_cards"] is None and h["result"] is None
|
||||||
|
st = h["structured"]
|
||||||
|
if isinstance(st, str):
|
||||||
|
import json
|
||||||
|
st = json.loads(st)
|
||||||
|
assert st["hero_pos"] is None and not any(pl.get("hero") for pl in st["players"])
|
||||||
|
|
||||||
|
|
||||||
def test_hud_notes_scoped_to_session_by_tag(lyra):
|
def test_hud_notes_scoped_to_session_by_tag(lyra):
|
||||||
poker = lyra
|
poker = lyra
|
||||||
from lyra import memory
|
from lyra import memory
|
||||||
|
|||||||
Reference in New Issue
Block a user