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:
+42
-1
@@ -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,
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user