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
+1
View File
@@ -702,6 +702,7 @@
"imgAxisWinRate": "Win Rate (%)",
"imgStatPeakRating": "Peak rating",
"imgStatRatingChange": "Rating change",
"imgStatPlaceFinished": "Place finished",
"imgStatTotalKills": "Total kills",
"imgStatTotalDeaths": "Total deaths",
"imgStatAssistsCaptures": "Assists / captures",
+8 -2
View File
@@ -1044,7 +1044,10 @@ app.get('/squadron/:clan_id/recap/:season.png', async (req, res) => {
try {
const stat = await fsp.stat(cachePath);
if (range.status === 'completed') {
serveFromCache = true;
// Only trust a completed-season cache if it was rendered after the
// season ended; mid-season files are stale snapshots (range.end is
// epoch seconds, mtimeMs is ms).
serveFromCache = stat.mtimeMs > range.end * 1000;
} else if (Date.now() - stat.mtimeMs < RECAP_TTL_MS) {
serveFromCache = true;
}
@@ -1112,7 +1115,10 @@ app.get('/players/:uid/recap/:season.png', async (req, res) => {
try {
const stat = await fsp.stat(cachePath);
if (range.status === 'completed') {
serveFromCache = true;
// Only trust a completed-season cache if it was rendered after the
// season ended; mid-season files are stale snapshots (range.end is
// epoch seconds, mtimeMs is ms).
serveFromCache = stat.mtimeMs > range.end * 1000;
} else if (Date.now() - stat.mtimeMs < RECAP_TTL_MS) {
serveFromCache = true;
}