fix(server): mark games abandoned on room teardown + staging leaderboard

When handle_player_leave emptied a room or handle_end_game was invoked,
the room was removed from memory without touching games_v2. Periodic
cleanup only scans in-memory rooms, so those rows were stranded as
status='active' forever — staging had 42 orphans accumulated over 5h.

- event_store.update_game_abandoned: guarded UPDATE (status='active' only)
- GameLogger.log_game_abandoned{,_async}: fire-and-forget wrapper
- handle_end_game + handle_player_leave: flip status before remove_room
- LEADERBOARD_INCLUDE_TEST_DEFAULT: env override so staging can show
  soak-harness accounts by default; prod keeps them hidden

Verified on staging: 42 orphans swept on restart, soak accounts now
visible on /api/stats/leaderboard (rank 1-4).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
adlee-was-taken
2026-04-18 00:37:49 -04:00
parent 70498b1c33
commit d5f8eef6b3
7 changed files with 230 additions and 3 deletions

View File

@@ -12,6 +12,7 @@ from typing import Optional
from fastapi import APIRouter, Depends, HTTPException, Header, Query
from pydantic import BaseModel
from config import config
from models.user import User
from services.stats_service import StatsService
@@ -159,7 +160,7 @@ async def get_leaderboard(
metric: str = Query("wins", pattern="^(wins|win_rate|avg_score|knockouts|streak|rating)$"),
limit: int = Query(50, ge=1, le=100),
offset: int = Query(0, ge=0),
include_test: bool = Query(False, description="Include soak-harness test accounts"),
include_test: Optional[bool] = Query(None, description="Include soak-harness test accounts. Defaults to LEADERBOARD_INCLUDE_TEST_DEFAULT env (False in prod)."),
service: StatsService = Depends(get_stats_service_dep),
):
"""
@@ -173,9 +174,14 @@ async def get_leaderboard(
- streak: Best win streak
Players must have 5+ games to appear on leaderboards.
By default, soak-harness test accounts are hidden.
Soak-harness test accounts are hidden unless include_test is passed,
or LEADERBOARD_INCLUDE_TEST_DEFAULT is set True on the server (staging).
"""
entries = await service.get_leaderboard(metric, limit, offset, include_test)
effective_include_test = (
include_test if include_test is not None
else config.LEADERBOARD_INCLUDE_TEST_DEFAULT
)
entries = await service.get_leaderboard(metric, limit, offset, effective_include_test)
return {
"metric": metric,