diff --git a/lyra/poker.py b/lyra/poker.py index 8d928c6..6987813 100644 --- a/lyra/poker.py +++ b/lyra/poker.py @@ -644,6 +644,28 @@ def update_hand(hand_id: int, **fields) -> dict | None: 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]: sid = _resolve(session_id) if sid is None: diff --git a/lyra/web/server.py b/lyra/web/server.py index 34933e3..10c24d0 100644 --- a/lyra/web/server.py +++ b/lyra/web/server.py @@ -192,6 +192,13 @@ def create_app() -> FastAPI: logbus.log("info", "hand edited", id=hand_id, fields=list(body)) 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}") async def hand_delete(hand_id: int) -> dict: """Delete a logged hand.""" diff --git a/lyra/web/static/hand.html b/lyra/web/static/hand.html index 0b475e7..d0168c7 100644 --- a/lyra/web/static/hand.html +++ b/lyra/web/static/hand.html @@ -282,8 +282,54 @@ const h = await r.json(); if(!h || !h.id){ document.getElementById('root').innerHTML='

Hand not found.

'; return; } render(h); + renderEditor(h); }catch(e){ document.getElementById('root').innerHTML='

Couldn\'t load the hand.

'; } } + + 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 = ` +
+ ✎ Edit this hand +
+ + + + + + +
+ + + +
+
+
`; + 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(); diff --git a/tests/test_poker.py b/tests/test_poker.py index 5b2c34f..3c19986 100644 --- a/tests/test_poker.py +++ b/tests/test_poker.py @@ -18,6 +18,22 @@ def lyra(tmp_path, monkeypatch): 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): poker = lyra from lyra import memory