From f20570fc0365ca046f06c581bd2d849b927cc3ab Mon Sep 17 00:00:00 2001 From: serversdown Date: Fri, 3 Jul 2026 22:56:45 +0000 Subject: [PATCH] fix: dedupe player-edit route + strip embedding blob from JSON MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Follow-up to the /players build: - the new POST /player/{id} collided with the existing PATCH route (F811); fold the name→named-flip into the existing PATCH and point the UI at it. - villain_recall / update_player returned the raw row including descriptor_embedding (bytes) → PydanticSerializationError on the API. Strip it. Co-Authored-By: Claude Opus 4.8 (1M context) --- lyra/poker.py | 7 ++++++- lyra/web/server.py | 23 ++++++++++------------- lyra/web/static/players.html | 4 ++-- 3 files changed, 18 insertions(+), 16 deletions(-) diff --git a/lyra/poker.py b/lyra/poker.py index 917e575..29d578a 100644 --- a/lyra/poker.py +++ b/lyra/poker.py @@ -1073,7 +1073,11 @@ def update_player(player_id: int, **fields) -> dict | None: conn.execute(f"UPDATE poker_players SET {', '.join(sets)} WHERE id = ?", (*vals, player_id)) row = _c().execute("SELECT * FROM poker_players WHERE id = ?", (player_id,)).fetchone() - return dict(row) if row else None + if not row: + return None + d = dict(row) + d.pop("descriptor_embedding", None) # raw bytes — not JSON-serializable + return d # --- villain identity resolution (nameless villains; see docs/SCOUTING_DESK.md) --- @@ -1544,6 +1548,7 @@ def villain_recall(player_id: int) -> dict | None: if not p: return None p = dict(p) + p.pop("descriptor_embedding", None) # raw bytes — not JSON-serializable, not needed obs = [dict(r) for r in _c().execute( "SELECT o.*, s.venue AS s_venue, s.started_at AS s_at FROM player_observations o " "LEFT JOIN poker_sessions s ON s.id = o.session_id WHERE o.player_id = ? " diff --git a/lyra/web/server.py b/lyra/web/server.py index d3d892c..0c874ce 100644 --- a/lyra/web/server.py +++ b/lyra/web/server.py @@ -212,9 +212,17 @@ def create_app() -> FastAPI: @app.patch("/player/{player_id}") async def player_update(player_id: int, request: Request) -> dict: - """Edit a player's dossier (rename, fix tendencies).""" + """Edit a player's dossier (rename, fix tendencies). Setting `name` on a + nameless (descriptor) villain promotes it to a real handle (named=1).""" body = await request.json() - p = await asyncio.to_thread(lambda: poker.update_player(player_id, **body)) + + def _apply(): + if body.get("name"): + poker.name_villain(player_id, body["name"]) + rest = {k: v for k, v in body.items() if k != "name"} + return poker.update_player(player_id, **rest) if rest else poker.villain_recall(player_id) + + p = await asyncio.to_thread(_apply) logbus.log("info", "player edited", id=player_id, fields=list(body)) return {"ok": p is not None, "player": p} @@ -450,17 +458,6 @@ def create_app() -> FastAPI: async def player_data(player_id: int) -> dict: return poker.villain_recall(player_id) or {} - @app.post("/player/{player_id}") - async def player_update(player_id: int, request: Request) -> dict: - body = await request.json() - fields = {k: body[k] for k in ("name", "venue", "category", "tendencies", - "adjustment", "description") if body.get(k) is not None} - if body.get("name"): # naming via the browser flips it to a real handle - poker.name_villain(player_id, body["name"]) - fields.pop("name", None) - row = poker.update_player(player_id, **fields) if fields else None - return {"ok": True, "player": row} - @app.post("/identity/{task_id}/resolve") async def identity_resolve(task_id: int, request: Request) -> dict: body = await request.json() diff --git a/lyra/web/static/players.html b/lyra/web/static/players.html index 213b070..7e2d42c 100644 --- a/lyra/web/static/players.html +++ b/lyra/web/static/players.html @@ -144,12 +144,12 @@ async function rename(id){ const v=document.getElementById('nm'+id).value.trim(); if(!v)return; - await fetch(`/player/${id}`,{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({name:v})}); + await fetch(`/player/${id}`,{method:'PATCH',headers:{'Content-Type':'application/json'},body:JSON.stringify({name:v})}); load(); } async function setCat(id){ const v=document.getElementById('cat'+id).value; - await fetch(`/player/${id}`,{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({category:v})}); + await fetch(`/player/${id}`,{method:'PATCH',headers:{'Content-Type':'application/json'},body:JSON.stringify({category:v})}); } async function resolveTask(id,action,kw){ await fetch(`/identity/${id}/resolve`,{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({action,...kw})});