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, "
|
||||
"p.adjustment AS adjustment, "
|
||||
"(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 "
|
||||
"WHERE p.id IN (SELECT DISTINCT player_id FROM player_reads "
|
||||
" WHERE session_id = ? AND player_id IS NOT NULL) "
|
||||
"ORDER BY p.updated_at DESC",
|
||||
(sid, sid),
|
||||
(sid, sid, sid),
|
||||
).fetchall()
|
||||
return [dict(r) for r in rows]
|
||||
|
||||
|
||||
@@ -339,6 +339,24 @@ def create_app() -> FastAPI:
|
||||
async def hands_data(limit: int = 60) -> dict:
|
||||
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}")
|
||||
async def recap_page() -> FileResponse:
|
||||
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()}
|
||||
assert rows[structured_id]["has_structured"] is True
|
||||
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