sync server.js from production (roster-vs-game-history freshness comparison for squadron identity, replacing the older per-request fallback approach)
This commit is contained in:
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user