From 9aa6db5b58ea6c5473cc87a1b238a2531af324c3 Mon Sep 17 00:00:00 2001 From: deploy-migration Date: Thu, 2 Jul 2026 14:59:33 +0000 Subject: [PATCH] sync server.js from production (roster-vs-game-history freshness comparison for squadron identity, replacing the older per-request fallback approach) --- server.js | 403 ++++++++++++++++++++++++++++-------------------------- 1 file changed, 210 insertions(+), 193 deletions(-) diff --git a/server.js b/server.js index 26a7bdd..967f8d7 100644 --- a/server.js +++ b/server.js @@ -138,6 +138,8 @@ const SQUADRON_CACHE_TTL = 30 * 60 * 1000; let hasSquadronColumn = false; let nickLookupCache = null; let nickLookupCacheTime = 0; +let gameHistoryLookupCache = null; +let gameHistoryLookupCacheTime = 0; const performanceBenchmarkCache = new Map(); const performanceBenchmarkInFlight = new Map(); const PERFORMANCE_BENCHMARK_CACHE_TTL = 60 * 60 * 1000; @@ -535,7 +537,7 @@ function loadNickLookupCached(callback) { } squadronsDb.all(` - SELECT sm.uid, sm.nick, sm.clan_id AS sm_clan_id, sd.clan_id AS sd_clan_id, sd.tag_name, sd.short_name + SELECT sm.uid, sm.nick, sm.clan_id AS sm_clan_id, sm.updated_at, sd.clan_id AS sd_clan_id, sd.tag_name, sd.short_name FROM squadron_members sm LEFT JOIN squadrons_data sd ON sm.clan_id = sd.clan_id `, [], (err, rows) => { @@ -545,13 +547,22 @@ function loadNickLookupCached(callback) { log.error('Error loading nick lookup data', err); nickLookupCache = {}; } else { + // A uid can have rows under multiple clan_ids (membership history + // isn't always cleaned up promptly), so keep only the freshest + // row per uid rather than letting scan order decide the winner. const lookup = {}; rows.forEach(row => { + const existing = lookup[row.uid]; + if (existing && existing.updated_at != null && row.updated_at != null + && existing.updated_at >= row.updated_at) { + return; + } lookup[row.uid] = { nick: row.nick, clan_id: row.sd_clan_id || row.sm_clan_id || null, tag_name: row.tag_name, - short_name: row.short_name + short_name: row.short_name, + updated_at: row.updated_at != null ? Number(row.updated_at) : null }; }); nickLookupCache = lookup; @@ -564,6 +575,44 @@ function loadNickLookupCached(callback) { }); } +// Bulk "latest tracked game per uid" lookup, shared across leaderboard endpoints +// so each request doesn't need its own per-uid fallback query. SQLite guarantees +// nick/squadron_name/clan_id come from the same row as MAX(endtime_unix) here +// since there's exactly one aggregate in the query (documented bare-column +// behavior, not a portable SQL assumption). +function loadGameHistoryLookupCached(callback) { + if (gameHistoryLookupCache && Date.now() - gameHistoryLookupCacheTime < SQUADRON_CACHE_TTL) { + return callback(gameHistoryLookupCache); + } + + heavyDb.all(` + SELECT UID as uid, nick, squadron_name, clan_id, MAX(endtime_unix) as endtime_unix + FROM player_games_hist + WHERE nick NOT LIKE 'coop/%' + GROUP BY UID + `, [], (err, rows) => { + if (err) { + log.error('Error loading game history lookup data', err); + gameHistoryLookupCache = gameHistoryLookupCache || {}; + } else { + const lookup = {}; + rows.forEach(row => { + lookup[row.uid] = { + nick: row.nick, + squadron_name: row.squadron_name, + clan_id: row.clan_id != null ? Number(row.clan_id) : null, + endtime_unix: row.endtime_unix != null ? Number(row.endtime_unix) : null + }; + }); + gameHistoryLookupCache = lookup; + log.info('Game history lookup cache updated', { entries: rows.length }); + } + + gameHistoryLookupCacheTime = Date.now(); + callback(gameHistoryLookupCache); + }); +} + function dbAll(query, params = []) { return new Promise((resolve, reject) => { db.all(query, params, (err, rows) => err ? reject(err) : resolve(rows || [])); @@ -1172,22 +1221,30 @@ function buildCompNotation(typeCounts) { } // Canonicalize a player's squadron identity using the squadrons_data lookup. -// Prefers clan_id from the squadron_members cache (joined to squadrons_data); otherwise -// falls back to a string lookup against long_name / short_name / tag_name. +// Trusts whichever of the two signals is more recent: the roster-sync snapshot +// (cached, from squadron_members) or the last tracked game (fallback, from +// player_games_hist) — see resolveCurrentSquadronIdentity for why neither one +// is reliable on its own (a roster row can outlive a squadron's sync going +// stale; a last game can predate a squadron change by weeks). // Always returns the canonical tag_name + short_name for the matched clan_id when possible, // so consumers can group/dedupe by clan_id (or short_name) regardless of which raw value was stored. function resolveSquadronIdentity(cached, fallback, squadronLookup) { + const rosterTime = cached?.updated_at != null ? Number(cached.updated_at) : null; + const gameTime = fallback?.endtime_unix != null ? Number(fallback.endtime_unix) : null; + const useRoster = cached?.clan_id != null + && (gameTime == null || (rosterTime != null && rosterTime >= gameTime)); + let clanId = null; let tagName = null; let shortName = null; - if (cached && cached.clan_id) { + if (useRoster) { clanId = cached.clan_id; tagName = cached.tag_name || null; shortName = cached.short_name || null; } - const rawTag = tagName || (cached ? cached.tag_name : null) || (fallback ? fallback.squadron_name : null); + const rawTag = tagName || (useRoster ? cached.tag_name : null) || (fallback ? fallback.squadron_name : null); if ((!clanId || !tagName || !shortName) && rawTag && squadronLookup) { const sq = squadronLookup[rawTag]; if (sq) { @@ -1269,9 +1326,20 @@ function cleanText(value) { return text || null; } -function resolveCurrentSquadronIdentity(latestRow, squadronLookup, preferredClanId = null) { - const effectiveClanId = preferredClanId != null - ? Number(preferredClanId) +function resolveCurrentSquadronIdentity(latestRow, squadronLookup, rosterHint = null) { + // Two independent, occasionally-disagreeing signals for "current squadron": + // the roster-sync snapshot (squadron_members, via rosterHint) and the tag on + // the player's last tracked game (player_games_hist, via latestRow). Neither + // is reliably up to date on its own — a roster row can be an orphan left over + // from a squadron whose sync has lapsed, and the last game can predate a + // squadron change by weeks. Trust whichever one is actually more recent. + const rosterTime = rosterHint?.updated_at != null ? Number(rosterHint.updated_at) : null; + const gameTime = latestRow?.endtime_unix != null ? Number(latestRow.endtime_unix) : null; + const useRoster = rosterHint?.clan_id != null + && (gameTime == null || (rosterTime != null && rosterTime >= gameTime)); + + const effectiveClanId = useRoster + ? Number(rosterHint.clan_id) : normalizeClanId(latestRow?.clan_id); const lookupByClanId = effectiveClanId != null ? squadronLookup[`__cid_${effectiveClanId}`] || squadronLookup[String(effectiveClanId)] @@ -1300,7 +1368,7 @@ function resolveCurrentSquadronIdentity(latestRow, squadronLookup, preferredClan async function loadPlayerIdentitySnapshot(uid, squadronLookup, options = {}) { const latestRow = options.latestRow || await dbGetAsync( db, - `SELECT nick, squadron_name, clan_id + `SELECT nick, squadron_name, clan_id, endtime_unix FROM player_games_hist WHERE UID = ? AND nick NOT LIKE 'coop/%' ORDER BY session_id DESC @@ -1309,7 +1377,7 @@ async function loadPlayerIdentitySnapshot(uid, squadronLookup, options = {}) { ); if (!latestRow) return null; - const currentIdentity = resolveCurrentSquadronIdentity(latestRow, squadronLookup, options.preferredClanId ?? null); + const currentIdentity = resolveCurrentSquadronIdentity(latestRow, squadronLookup, options.rosterHint ?? null); const [nickRows, squadronRows] = await Promise.all([ dbAllAsync( db, @@ -2016,7 +2084,7 @@ app.get('/api/player/:uid', (req, res) => { const { start_date, end_date, season, week } = req.query; const latestNickQuery = ` - SELECT nick, squadron_name, clan_id + SELECT nick, squadron_name, clan_id, endtime_unix FROM player_games_hist WHERE UID = ? AND nick NOT LIKE 'coop/%' ORDER BY session_id DESC @@ -2060,37 +2128,23 @@ app.get('/api/player/:uid', (req, res) => { const lookupPromise = new Promise((resolve) => { loadSquadronLookupCached(resolve); }); - // Authoritative current-roster membership. squadron_members is updated - // by the periodic squadron-sync, so this reflects "what squadron this - // player belongs to right now" — beats the most-recent player_games_hist - // row which can be stale post-rename if the player hasn't played a SQB - // since the rename. + // Roster-sync snapshot (squadron_members). resolveCurrentSquadronIdentity + // weighs this against the last tracked game's timestamp rather than trusting + // it blindly — an orphaned row from a squadron whose sync has lapsed should + // lose to a more recent game record. const rosterPromise = new Promise((resolve) => { - const squadronsDbPath = path.join(STORAGE_ROOT, 'squadrons.db'); - if (!fs.existsSync(squadronsDbPath)) return resolve(null); - const sdb = new sqlite3.Database(squadronsDbPath, sqlite3.OPEN_READONLY, (err) => { - if (err) return resolve(null); - sdb.get( - 'SELECT clan_id FROM squadron_members WHERE uid = ? ORDER BY updated_at DESC LIMIT 1', - [uid], - (qerr, row) => { - sdb.close(); - if (qerr || !row || row.clan_id == null) return resolve(null); - resolve(Number(row.clan_id)); - } - ); - }); + loadNickLookupCached((nickCache) => resolve(nickCache[uid] || null)); }); Promise.all([nickPromise, statsPromise, lookupPromise, rosterPromise]) - .then(async ([nickRow, vehicleRows, squadronLookup, rosterClanId]) => { + .then(async ([nickRow, vehicleRows, squadronLookup, rosterHint]) => { if (!nickRow) { return jsonError(res, 404, 'Player not found', { uid: normalizeUid(uid) }); } const identity = await loadPlayerIdentitySnapshot(uid, squadronLookup, { latestRow: nickRow, - preferredClanId: rosterClanId, + rosterHint, }); loadPerformanceBenchmarksCached(dateFilters, (benchmarks) => { const vehicles = vehicleRows.map(row => { @@ -2330,7 +2384,7 @@ app.get('/api/search/:nickname', (req, res) => { `; const latestNickQuery = ` - SELECT nick, squadron_name, clan_id + SELECT nick, squadron_name, clan_id, endtime_unix FROM player_games_hist WHERE UID = ? AND nick NOT LIKE 'coop/%' ORDER BY session_id DESC @@ -2352,47 +2406,49 @@ app.get('/api/search/:nickname', (req, res) => { }); } - loadSquadronLookupCached((squadronLookup) => { - const lookups = rows.map(row => (async () => { - const latestRow = await dbGetAsync(db, latestNickQuery, [row.UID]); - if (!latestRow) { - return { - uid: normalizeUid(row.UID), - nick: '', - previous_nicks: [], - squadron_name: '', - squadron_long_name: '', - squadron_clan_id: null, - previous_squadron_names: [], + loadNickLookupCached((nickCache) => { + loadSquadronLookupCached((squadronLookup) => { + const lookups = rows.map(row => (async () => { + const latestRow = await dbGetAsync(db, latestNickQuery, [row.UID]); + if (!latestRow) { + return { + uid: normalizeUid(row.UID), + nick: '', + previous_nicks: [], + squadron_name: '', + squadron_long_name: '', + squadron_clan_id: null, + previous_squadron_names: [], + }; + } + return loadPlayerIdentitySnapshot(row.UID, squadronLookup, { latestRow, rosterHint: nickCache[row.UID] || null }); + })()); + + Promise.all(lookups).then(results => { + const finalResults = results.filter(Boolean); + const response = { + search_term: nickname.trim(), + results: finalResults, + total_found: finalResults.length, + limited_to: 50 }; - } - return loadPlayerIdentitySnapshot(row.UID, squadronLookup, { latestRow }); - })()); - Promise.all(lookups).then(results => { - const finalResults = results.filter(Boolean); - const response = { - search_term: nickname.trim(), - results: finalResults, - total_found: finalResults.length, - limited_to: 50 - }; + log.info('Search query executed', { + searchTerm: searchTerm, + nickname: nickname, + resultsFound: finalResults.length, + firstFewResults: finalResults.slice(0, 3) + }); - log.info('Search query executed', { - searchTerm: searchTerm, - nickname: nickname, - resultsFound: finalResults.length, - firstFewResults: finalResults.slice(0, 3) + setCachedResponse(cacheKey, response); + res.json(response); + }).catch(identityErr => { + log.error('Failed to build player identity search response', identityErr, { + nickname, + endpoint: '/api/search/:nickname' + }); + jsonError(res, 500, 'Database error occurred', { errorCode: 'DB_SEARCH_QUERY_FAILED' }); }); - - setCachedResponse(cacheKey, response); - res.json(response); - }).catch(identityErr => { - log.error('Failed to build player identity search response', identityErr, { - nickname, - endpoint: '/api/search/:nickname' - }); - jsonError(res, 500, 'Database error occurred', { errorCode: 'DB_SEARCH_QUERY_FAILED' }); }); }); }); @@ -3609,78 +3665,57 @@ app.get('/api/leaderboard/players', (req, res) => { log.info('Player leaderboard aggregation done', { rows: statsRows.length, ms: Date.now() - queryStart }); - // Step 2: Nick/squadron lookup from squadron_members cache (instant, no heavy SQL) + // Step 2: nick/squadron lookup — both sources are shared, TTL-cached + // maps (no heavy SQL per request); resolveSquadronIdentity picks + // whichever of the two is more recent per player. loadNickLookupCached((nickCache) => { loadSquadronLookupCached((squadronLookup) => { - const uncoveredUids = statsRows.filter(r => !nickCache[r.uid]).map(r => r.uid); + loadGameHistoryLookupCached((gameHistoryCache) => { + const response = { + date_filter: createDateFilterMetadata(dateFilters, start_date, end_date, season, week), + timeframe: dateFilters.hasFilter ? "filtered" : "all-time", + total_players: statsRows.length, + players: statsRows.map(row => { + const totalKills = row.total_kills || 0; + const deaths = row.total_deaths || 0; + const wins = row.wins || 0; + const totalBattles = row.total_battles || 0; + const kdr = deaths > 0 ? (totalKills / deaths) : totalKills; + const winRate = totalBattles > 0 ? (wins / totalBattles) * 100 : 0; - const buildResponse = (fallbackMap) => { - const response = { - date_filter: createDateFilterMetadata(dateFilters, start_date, end_date, season, week), - timeframe: dateFilters.hasFilter ? "filtered" : "all-time", - total_players: statsRows.length, - players: statsRows.map(row => { - const totalKills = row.total_kills || 0; - const deaths = row.total_deaths || 0; - const wins = row.wins || 0; - const totalBattles = row.total_battles || 0; - const kdr = deaths > 0 ? (totalKills / deaths) : totalKills; - const winRate = totalBattles > 0 ? (wins / totalBattles) * 100 : 0; + const cached = nickCache[row.uid]; + const fb = gameHistoryCache[row.uid]; + const nick = cached ? cached.nick : (fb ? fb.nick : row.uid); + const sqId = resolveSquadronIdentity(cached, fb, squadronLookup); - const cached = nickCache[row.uid]; - const fb = fallbackMap[row.uid]; - const nick = cached ? cached.nick : (fb ? fb.nick : row.uid); - const sqId = resolveSquadronIdentity(cached, fb, squadronLookup); - - return { - uid: row.uid, - nick, - squadron_name: sqId.tag_name, - squadron_short_name: sqId.short_name, - squadron_clan_id: sqId.clan_id, - total_kills: totalKills, - ground_kills: row.total_ground_kills || 0, - air_kills: row.total_air_kills || 0, - total_battles: totalBattles, - wins, - win_rate: parseFloat(winRate.toFixed(1)), - kdr: parseFloat(kdr.toFixed(1)), - deaths, - assists: row.total_assists || 0, - captures: row.total_captures || 0, - total_score: Math.round(row.total_score || 0) - }; - }) - }; - - log.info('Player leaderboard complete', { - playersReturned: statsRows.length, - cachedNicks: statsRows.length - uncoveredUids.length, - fallbackNicks: Object.keys(fallbackMap).length, - uncoveredNicks: uncoveredUids.length - Object.keys(fallbackMap).length, - totalMs: Date.now() - queryStart - }); - - resolve(response); + return { + uid: row.uid, + nick, + squadron_name: sqId.tag_name, + squadron_short_name: sqId.short_name, + squadron_clan_id: sqId.clan_id, + total_kills: totalKills, + ground_kills: row.total_ground_kills || 0, + air_kills: row.total_air_kills || 0, + total_battles: totalBattles, + wins, + win_rate: parseFloat(winRate.toFixed(1)), + kdr: parseFloat(kdr.toFixed(1)), + deaths, + assists: row.total_assists || 0, + captures: row.total_captures || 0, + total_score: Math.round(row.total_score || 0) + }; + }) }; - // Fallback for players not in any squadron — uses (UID, endtime_unix) index - if (uncoveredUids.length > 0) { - const fbPlaceholders = uncoveredUids.map(() => '?').join(','); - db.all(` - SELECT UID as uid, nick, squadron_name, MAX(endtime_unix) - FROM player_games_hist - WHERE UID IN (${fbPlaceholders}) AND nick NOT LIKE 'coop/%' - GROUP BY UID - `, uncoveredUids, (err, fbRows) => { - const fbMap = {}; - if (!err && fbRows) fbRows.forEach(r => { fbMap[r.uid] = r; }); - if (err) log.warn('Fallback nick lookup failed', { error: err.message }); - buildResponse(fbMap); - }); - } else { - buildResponse({}); - } + log.info('Player leaderboard complete', { + playersReturned: statsRows.length, + totalMs: Date.now() - queryStart + }); + + resolve(response); + }); }); }); }); @@ -3760,68 +3795,50 @@ app.get('/api/leaderboard/vehicles', (req, res) => { return reject(err); } - // Nick/squadron lookup from squadron_members cache (instant) + // Nick/squadron lookup — shared, TTL-cached maps; resolveSquadronIdentity + // picks whichever of roster/game-history is more recent per player. loadNickLookupCached((nickCache) => { loadSquadronLookupCached((squadronLookup) => { - const uniqueUids = [...new Set(vehicleRows.map(r => r.player_uid))]; - const uncoveredUids = uniqueUids.filter(uid => !nickCache[uid]); + loadGameHistoryLookupCached((gameHistoryCache) => { + const response = { + date_filter: createDateFilterMetadata(dateFilters, start_date, end_date, season, week), + vehicles: vehicleRows.map(row => { + const totalKills = row.total_kills || 0; + const deaths = row.total_deaths || 0; + const wins = row.wins || 0; + const battles = row.battles || 0; + const kdr = deaths > 0 ? (totalKills / deaths) : totalKills; + const winRate = battles > 0 ? (wins / battles) * 100 : 0; - const buildResponse = (fallbackMap) => { - const response = { - date_filter: createDateFilterMetadata(dateFilters, start_date, end_date, season, week), - vehicles: vehicleRows.map(row => { - const totalKills = row.total_kills || 0; - const deaths = row.total_deaths || 0; - const wins = row.wins || 0; - const battles = row.battles || 0; - const kdr = deaths > 0 ? (totalKills / deaths) : totalKills; - const winRate = battles > 0 ? (wins / battles) * 100 : 0; + const cached = nickCache[row.player_uid]; + const fb = gameHistoryCache[row.player_uid]; + const playerNick = cached ? cached.nick : (fb ? fb.nick : row.player_uid); + const sqId = resolveSquadronIdentity(cached, fb, squadronLookup); - const cached = nickCache[row.player_uid]; - const fb = fallbackMap[row.player_uid]; - const playerNick = cached ? cached.nick : (fb ? fb.nick : row.player_uid); - const sqId = resolveSquadronIdentity(cached, fb, squadronLookup); - - return { - vehicle: row.vehicle, - vehicle_internal: row.vehicle_internal || null, - player_uid: normalizeUid(row.player_uid), - player_nick: playerNick, - player_squadron_name: sqId.tag_name, - player_squadron_short_name: sqId.short_name, - player_squadron_clan_id: sqId.clan_id, - total_kills: totalKills, - ground_kills: row.total_ground_kills || 0, - air_kills: row.total_air_kills || 0, - battles, wins, - win_rate: parseFloat(winRate.toFixed(1)), - kdr: parseFloat(kdr.toFixed(1)), - deaths, - assists: row.total_assists || 0, - captures: row.total_captures || 0 - }; - }) - }; - - log.info('Vehicle leaderboard complete', { vehiclesReturned: vehicleRows.length, totalMs: Date.now() - queryStart }); - resolve(response); + return { + vehicle: row.vehicle, + vehicle_internal: row.vehicle_internal || null, + player_uid: normalizeUid(row.player_uid), + player_nick: playerNick, + player_squadron_name: sqId.tag_name, + player_squadron_short_name: sqId.short_name, + player_squadron_clan_id: sqId.clan_id, + total_kills: totalKills, + ground_kills: row.total_ground_kills || 0, + air_kills: row.total_air_kills || 0, + battles, wins, + win_rate: parseFloat(winRate.toFixed(1)), + kdr: parseFloat(kdr.toFixed(1)), + deaths, + assists: row.total_assists || 0, + captures: row.total_captures || 0 + }; + }) }; - if (uncoveredUids.length > 0) { - const fbPlaceholders = uncoveredUids.map(() => '?').join(','); - db.all(` - SELECT UID as uid, nick, squadron_name, MAX(endtime_unix) - FROM player_games_hist - WHERE UID IN (${fbPlaceholders}) AND nick NOT LIKE 'coop/%' - GROUP BY UID - `, uncoveredUids, (err, fbRows) => { - const fbMap = {}; - if (!err && fbRows) fbRows.forEach(r => { fbMap[r.uid] = r; }); - buildResponse(fbMap); - }); - } else { - buildResponse({}); - } + log.info('Vehicle leaderboard complete', { vehiclesReturned: vehicleRows.length, totalMs: Date.now() - queryStart }); + resolve(response); + }); }); }); });