feat: /players browser + identity review queue UI

Phase 4. A Players page that browses the whole villain file (named + nameless,
expandable to episodic recall — reads, notable hands, stats, descriptors) and, at
the top, the identity-resolution queue: possible-merges (same/different/dismiss)
and needs-clarification tasks. Rename a nameless villain, set category, or run a
dupe scan inline.

- poker.players_overview() for the list.
- routes: /players (page), /players/data, /player/{id}/data, POST /player/{id}
  (rename/retag), POST /identity/{id}/resolve, POST /players/scan.
- nav: 👤 Players.

Full suite green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-03 22:54:54 +00:00
parent f2944ed402
commit 3b3878ada1
4 changed files with 219 additions and 0 deletions
+13
View File
@@ -1523,6 +1523,19 @@ def get_villain_file(name: str | None = None, venue: str | None = None) -> list[
return [dict(r) for r in _c().execute(sql, params).fetchall()]
def players_overview() -> list[dict]:
"""All villains for the browser: identity + observation count + last seen,
named players first, then most-recently-updated."""
rows = _c().execute(
"SELECT p.id, p.name, p.named, p.venue, p.category, p.descriptors, "
"p.tendencies, p.adjustment, p.updated_at, "
"(SELECT COUNT(*) FROM player_observations o WHERE o.player_id = p.id) AS obs, "
"(SELECT COUNT(*) FROM player_reads r WHERE r.player_id = p.id) AS reads "
"FROM poker_players p ORDER BY p.named DESC, p.updated_at DESC"
).fetchall()
return [dict(r) for r in rows]
def villain_recall(player_id: int) -> dict | None:
"""Episodic recall for one villain: who, how often/where seen, last seen, the
notable hands against him (with ids to link), reads, and stats. This is the