fix(recap): speed up /card lookup, fix stale completed-season cache, add Place Finished

- /card player lookup was a leading-wildcard substring match with a Python
  ulower() UDF over 6.16M player_games_hist rows — a full scan measured at
  27s live before the ~2s render. Add a prefix fast-path
  (nick LIKE 'name%' COLLATE NOCASE) that uses the existing NOCASE index
  (~1ms), falling back to the ulower substring scan only when the prefix
  finds nothing. Same fix applied to _resolve_player_uids_batch (compare/stats).
  Lookup: 27,205ms -> 1ms.

- Completed-season recap cache served ANY cached PNG forever, including files
  rendered mid-season (frozen at whatever point they were last viewed). Only
  serve from cache when the file's mtime is after season end; otherwise
  re-render. Fixed in both BOT/utils.py and web/server.js (shared cache).

- Replace squadron card "Rating change" with "Place finished" (#rank / total),
  derived by ranking clans by final total_score among those active in-season.

- Add (CARD)/(RECAP) timing logs for prod observability.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
deploy
2026-07-01 19:20:26 +00:00
parent 61236a8267
commit c0214eaaae
5 changed files with 114 additions and 22 deletions
+15 -1
View File
@@ -2161,14 +2161,19 @@ async def _get_recap(
try:
stat = cache_path.stat()
if season_range["status"] == "completed":
serve_from_cache = True
# A completed season's card is only final if it was rendered AFTER
# the season ended. Files rendered mid-season are stale snapshots
# frozen at whatever point they were last viewed — re-render those.
serve_from_cache = stat.st_mtime > season_range["end"]
elif (time.time() - stat.st_mtime) < RECAP_TTL_SECONDS:
serve_from_cache = True
except FileNotFoundError:
pass
ident = clan_id if mode == "squadron" else uid
if not serve_from_cache:
cache_dir.mkdir(parents=True, exist_ok=True)
t0 = time.monotonic()
await _spawn_recap_render(
mode,
clan_id=clan_id,
@@ -2181,6 +2186,15 @@ async def _get_recap(
theme=theme,
lang=lang,
)
logging.info(
"(RECAP) %s id=%s season=%s theme=%s lang=%s result=fresh ms=%d",
mode, ident, season, theme, lang, (time.monotonic() - t0) * 1000,
)
else:
logging.info(
"(RECAP) %s id=%s season=%s theme=%s lang=%s result=cache",
mode, ident, season, theme, lang,
)
if not cache_path.exists():
raise RecapError("recap file missing after render")