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
+42 -1
View File
@@ -369,6 +369,43 @@ def gather_squadron_rating(conn_sq: sqlite3.Connection, clan_id: int,
first=first, change=final - first)
def gather_squadron_place(conn_sq: sqlite3.Connection, clan_id: int,
season_start: int, season_end: int
) -> Optional[Tuple[int, int]]:
"""Finishing leaderboard place for the season.
We don't store a per-season position, but ``total_score`` in
squadrons_points IS the squadron rating that the in-game leaderboard ranks
by. So rank clans by their final score and read off this clan's rank.
Only clans with at least one snapshot *within the season window* are ranked
— a clan whose last data predates the season never appeared on that season's
ladder (dead/inactive clans drop off in-game), so counting them would inflate
the denominator. Each ranked clan uses its last in-season snapshot.
Returns (place, total_ranked) or None if this clan wasn't active in-season.
Works for in-progress seasons too (season_end in the future → last snapshot
so far = current standing).
"""
cur = conn_sq.execute(
"SELECT p.clan_id, p.total_score "
"FROM squadrons_points p "
"JOIN (SELECT clan_id, MAX(unix_time) AS mt FROM squadrons_points "
" WHERE unix_time BETWEEN ? AND ? GROUP BY clan_id) m "
" ON m.clan_id = p.clan_id AND m.mt = p.unix_time",
(season_start, season_end),
)
board = [(cid, sc) for cid, sc in cur.fetchall() if sc is not None]
if not board:
return None
board.sort(key=lambda r: r[1], reverse=True)
total = len(board)
for i, (cid, _sc) in enumerate(board):
if cid == clan_id:
return i + 1, total
return None
@dataclass
class MatchRow:
session_id: str
@@ -1070,6 +1107,7 @@ def render_squadron_card(out_path: Path, ident: SquadronIdent, season: str,
rolling_battles: List[Tuple[int, int]],
season_start: int, season_end: int,
week_boundaries: List[int],
place: Optional[Tuple[int, int]] = None,
theme: str = "light",
lang: str = DEFAULT_LANG) -> None:
pal = theme_palette(theme)
@@ -1219,7 +1257,7 @@ def render_squadron_card(out_path: Path, ident: SquadronIdent, season: str,
# Short rows render in 2 columns; wide rows span full width.
short_rows: List[Tuple[str, str]] = [
(t("imgStatPeakRating"), f"{_fmt_int(rating.peak)} ({_fmt_date(rating.peak_ts)})"),
(t("imgStatRatingChange"), f"{'+' if (rating.change or 0) >= 0 else ''}{_fmt_int(rating.change)}"),
(t("imgStatPlaceFinished"), f"#{_fmt_int(place[0])} / {_fmt_int(place[1])}" if place else ""),
(t("imgStatTotalKills"), f"{_fmt_int(players.total_kills)} ({_fmt_int(players.ground_kills)} {t('imgGroundShort')} / {_fmt_int(players.air_kills)} {t('imgAirShort')})"),
(t("imgStatTotalDeaths"), _fmt_int(players.deaths)),
(t("imgStatAssistsCaptures"), f"{_fmt_int(players.assists)} / {_fmt_int(players.captures)}"),
@@ -1677,6 +1715,8 @@ def run_squadron(args: Args) -> int:
logging.info(f"resolved {ident.short_name} / {ident.long_name}")
rating = gather_squadron_rating(conn_sq, args.clan_id, args.season_start, args.season_end)
logging.info(f"rating: series={len(rating.series)} final={rating.final} peak={rating.peak} change={rating.change}")
place = gather_squadron_place(conn_sq, args.clan_id, args.season_start, args.season_end)
logging.info(f"place: {place}")
with _open_ro(SQ_BATTLES_DB) as conn_b:
stream = gather_squadron_match_stream(conn_b, ident.short_name, args.season_start, args.season_end)
@@ -1707,6 +1747,7 @@ def run_squadron(args: Args) -> int:
rolling_wr, rolling_kd, rolling_battles,
args.season_start, args.season_end,
args.week_boundaries,
place=place,
theme=args.theme,
lang=args.lang,
)