feat: POST /hands endpoint + seat in HUD villains (recorder step 1)
- POST /hands: store a structured hand from the recorder; normalize_structured() (via store_hand_history) is the shape authority, so the client stays best-effort. Enriches villain dossiers from the recorded players, same as the parser path. Verified live: best-effort body -> normalized (version, completeness, hero sync). - _session_villains: surface each player's latest seat so the recorder can auto-place known players on the table. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
+4
-2
@@ -1185,12 +1185,14 @@ def _session_villains(sid: int) -> list[dict]:
|
|||||||
"SELECT p.name AS name, p.category AS category, p.tendencies AS tendencies, "
|
"SELECT p.name AS name, p.category AS category, p.tendencies AS tendencies, "
|
||||||
"p.adjustment AS adjustment, "
|
"p.adjustment AS adjustment, "
|
||||||
"(SELECT note FROM player_reads r2 WHERE r2.player_id = p.id "
|
"(SELECT note FROM player_reads r2 WHERE r2.player_id = p.id "
|
||||||
" AND r2.session_id = ? ORDER BY r2.id DESC LIMIT 1) AS last_note "
|
" AND r2.session_id = ? ORDER BY r2.id DESC LIMIT 1) AS last_note, "
|
||||||
|
"(SELECT seat FROM player_reads r3 WHERE r3.player_id = p.id "
|
||||||
|
" AND r3.session_id = ? ORDER BY r3.id DESC LIMIT 1) AS seat "
|
||||||
"FROM poker_players p "
|
"FROM poker_players p "
|
||||||
"WHERE p.id IN (SELECT DISTINCT player_id FROM player_reads "
|
"WHERE p.id IN (SELECT DISTINCT player_id FROM player_reads "
|
||||||
" WHERE session_id = ? AND player_id IS NOT NULL) "
|
" WHERE session_id = ? AND player_id IS NOT NULL) "
|
||||||
"ORDER BY p.updated_at DESC",
|
"ORDER BY p.updated_at DESC",
|
||||||
(sid, sid),
|
(sid, sid, sid),
|
||||||
).fetchall()
|
).fetchall()
|
||||||
return [dict(r) for r in rows]
|
return [dict(r) for r in rows]
|
||||||
|
|
||||||
|
|||||||
@@ -339,6 +339,24 @@ def create_app() -> FastAPI:
|
|||||||
async def hands_data(limit: int = 60) -> dict:
|
async def hands_data(limit: int = 60) -> dict:
|
||||||
return {"hands": poker.list_recent_hands(limit=limit)}
|
return {"hands": poker.list_recent_hands(limit=limit)}
|
||||||
|
|
||||||
|
@app.post("/hands")
|
||||||
|
async def hands_create(request: Request) -> dict:
|
||||||
|
"""Store a structured hand built by the recorder. Body:
|
||||||
|
{structured, session_id?, tag?, lesson?}. normalize_structured() (in
|
||||||
|
store_hand_history) is the authority on shape, so the client can be best-effort."""
|
||||||
|
body = await request.json()
|
||||||
|
structured = body.get("structured")
|
||||||
|
if not isinstance(structured, dict):
|
||||||
|
return {"ok": False, "error": "missing structured hand body"}
|
||||||
|
hid = await asyncio.to_thread(
|
||||||
|
poker.store_hand_history, structured,
|
||||||
|
session_id=body.get("session_id"), tag=body.get("tag"), lesson=body.get("lesson"),
|
||||||
|
)
|
||||||
|
# Enrich villain dossiers from the recorded players, same as the parser path.
|
||||||
|
await asyncio.to_thread(poker.link_hand_players, hid, structured, body.get("session_id"))
|
||||||
|
logbus.log("info", "hand recorded", id=hid, session=body.get("session_id"))
|
||||||
|
return {"ok": True, "id": hid}
|
||||||
|
|
||||||
@app.get("/recap/{session_id}")
|
@app.get("/recap/{session_id}")
|
||||||
async def recap_page() -> FileResponse:
|
async def recap_page() -> FileResponse:
|
||||||
return FileResponse(str(_STATIC / "recap.html"))
|
return FileResponse(str(_STATIC / "recap.html"))
|
||||||
|
|||||||
@@ -101,3 +101,11 @@ def test_list_recent_hands_flags_structured(poker):
|
|||||||
rows = {r["id"]: r for r in poker.list_recent_hands()}
|
rows = {r["id"]: r for r in poker.list_recent_hands()}
|
||||||
assert rows[structured_id]["has_structured"] is True
|
assert rows[structured_id]["has_structured"] is True
|
||||||
assert rows[flat_id]["has_structured"] is False
|
assert rows[flat_id]["has_structured"] is False
|
||||||
|
|
||||||
|
|
||||||
|
def test_hud_villains_carry_seat(poker):
|
||||||
|
"""The recorder auto-places known players, so the HUD bundle must expose their seat."""
|
||||||
|
poker.start_session(venue="Meadows", stakes="1/3", buy_in=400)
|
||||||
|
poker.add_read("3-bets light", seat="BTN", name="Sal", category="risky")
|
||||||
|
villains = {v["name"]: v for v in poker.hud()["villains"]}
|
||||||
|
assert villains["Sal"]["seat"] == "BTN"
|
||||||
|
|||||||
Reference in New Issue
Block a user