Paginate the leaderboard from materialized player ratings
CI / Build and push docker image (push) Successful in 1m19s

Store each player's leaderboard totals (wins, accumulated points and the
latest display name) on player_rating alongside the Elo rating, updated
in the same transaction as the match, so the board is a single indexed
table query instead of aggregating every participation on each request.

GET /api/leaderboard is now page-number paginated (page/limit) and
returns the page, page count and total, plus the caller's own rank and
page when authenticated; focus=me returns the caller's page directly.
The migration adds the columns, backfills them from the match history
(inserting rows for players from pre-Elo matches at the initial rating)
and replaces the index. backfill_elo rebuilds the aggregates too.

The web page opens on the page containing the signed-in player, marks
their row and adds a numbered footer pager.
This commit is contained in:
2026-10-09 16:42:47 +08:00
parent ae7ab5ce60
commit 1ab748a34f
16 changed files with 818 additions and 167 deletions
+6 -1
View File
@@ -190,7 +190,12 @@ class ScopaWebSocketTest(unittest.TestCase):
self.assertEqual(1, game["last_move"]["seat"])
self.assertEqual(card, game["last_move"]["card"])
if options:
self.assertEqual(2, game["players"][1]["captured_count"])
# The pile grows by the captured table cards
# plus the card just played.
self.assertEqual(
len(options[0]) + 1,
game["players"][1]["captured_count"],
)
else:
self.assertEqual(5, len(game["table"]))
+16 -2
View File
@@ -133,11 +133,25 @@ class ScoponeStatsTest(unittest.TestCase):
self.assertIn("your_elo_delta", results[0])
self.assertEqual(4, len(results[0]["players"]))
board = await client.get("/api/leaderboard")
with oidc_user("alice"):
board = await client.get("/api/leaderboard")
self.assertEqual(200, board.status_code)
by_sub = {r["user_sub"]: r for r in board.json()["results"]}
body = board.json()
by_sub = {r["user_sub"]: r for r in body["results"]}
self.assertEqual(4, len(by_sub))
self.assertTrue(all(r["matches"] == 1 for r in by_sub.values()))
self.assertEqual(1, body["page"])
self.assertEqual(1, body["pages"])
self.assertEqual(4, body["total"])
# The caller's own rank rides along on the response.
self.assertIsNotNone(body["your_rank"])
self.assertEqual(1, body["your_page"])
paged = await client.get("/api/leaderboard?limit=1&page=2")
self.assertEqual(200, paged.status_code)
self.assertEqual(4, paged.json()["pages"])
self.assertEqual(2, paged.json()["page"])
self.assertEqual(1, len(paged.json()["results"]))
async def _finished_briscola_session() -> GameSession: