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 hasSquadronColumn = false;
|
||||||
let nickLookupCache = null;
|
let nickLookupCache = null;
|
||||||
let nickLookupCacheTime = 0;
|
let nickLookupCacheTime = 0;
|
||||||
|
let gameHistoryLookupCache = null;
|
||||||
|
let gameHistoryLookupCacheTime = 0;
|
||||||
const performanceBenchmarkCache = new Map();
|
const performanceBenchmarkCache = new Map();
|
||||||
const performanceBenchmarkInFlight = new Map();
|
const performanceBenchmarkInFlight = new Map();
|
||||||
const PERFORMANCE_BENCHMARK_CACHE_TTL = 60 * 60 * 1000;
|
const PERFORMANCE_BENCHMARK_CACHE_TTL = 60 * 60 * 1000;
|
||||||
@@ -535,7 +537,7 @@ function loadNickLookupCached(callback) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
squadronsDb.all(`
|
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
|
FROM squadron_members sm
|
||||||
LEFT JOIN squadrons_data sd ON sm.clan_id = sd.clan_id
|
LEFT JOIN squadrons_data sd ON sm.clan_id = sd.clan_id
|
||||||
`, [], (err, rows) => {
|
`, [], (err, rows) => {
|
||||||
@@ -545,13 +547,22 @@ function loadNickLookupCached(callback) {
|
|||||||
log.error('Error loading nick lookup data', err);
|
log.error('Error loading nick lookup data', err);
|
||||||
nickLookupCache = {};
|
nickLookupCache = {};
|
||||||
} else {
|
} 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 = {};
|
const lookup = {};
|
||||||
rows.forEach(row => {
|
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] = {
|
lookup[row.uid] = {
|
||||||
nick: row.nick,
|
nick: row.nick,
|
||||||
clan_id: row.sd_clan_id || row.sm_clan_id || null,
|
clan_id: row.sd_clan_id || row.sm_clan_id || null,
|
||||||
tag_name: row.tag_name,
|
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;
|
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 = []) {
|
function dbAll(query, params = []) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
db.all(query, params, (err, rows) => err ? reject(err) : resolve(rows || []));
|
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.
|
// Canonicalize a player's squadron identity using the squadrons_data lookup.
|
||||||
// Prefers clan_id from the squadron_members cache (joined to squadrons_data); otherwise
|
// Trusts whichever of the two signals is more recent: the roster-sync snapshot
|
||||||
// falls back to a string lookup against long_name / short_name / tag_name.
|
// (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,
|
// 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.
|
// so consumers can group/dedupe by clan_id (or short_name) regardless of which raw value was stored.
|
||||||
function resolveSquadronIdentity(cached, fallback, squadronLookup) {
|
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 clanId = null;
|
||||||
let tagName = null;
|
let tagName = null;
|
||||||
let shortName = null;
|
let shortName = null;
|
||||||
|
|
||||||
if (cached && cached.clan_id) {
|
if (useRoster) {
|
||||||
clanId = cached.clan_id;
|
clanId = cached.clan_id;
|
||||||
tagName = cached.tag_name || null;
|
tagName = cached.tag_name || null;
|
||||||
shortName = cached.short_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) {
|
if ((!clanId || !tagName || !shortName) && rawTag && squadronLookup) {
|
||||||
const sq = squadronLookup[rawTag];
|
const sq = squadronLookup[rawTag];
|
||||||
if (sq) {
|
if (sq) {
|
||||||
@@ -1269,9 +1326,20 @@ function cleanText(value) {
|
|||||||
return text || null;
|
return text || null;
|
||||||
}
|
}
|
||||||
|
|
||||||
function resolveCurrentSquadronIdentity(latestRow, squadronLookup, preferredClanId = null) {
|
function resolveCurrentSquadronIdentity(latestRow, squadronLookup, rosterHint = null) {
|
||||||
const effectiveClanId = preferredClanId != null
|
// Two independent, occasionally-disagreeing signals for "current squadron":
|
||||||
? Number(preferredClanId)
|
// 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);
|
: normalizeClanId(latestRow?.clan_id);
|
||||||
const lookupByClanId = effectiveClanId != null
|
const lookupByClanId = effectiveClanId != null
|
||||||
? squadronLookup[`__cid_${effectiveClanId}`] || squadronLookup[String(effectiveClanId)]
|
? squadronLookup[`__cid_${effectiveClanId}`] || squadronLookup[String(effectiveClanId)]
|
||||||
@@ -1300,7 +1368,7 @@ function resolveCurrentSquadronIdentity(latestRow, squadronLookup, preferredClan
|
|||||||
async function loadPlayerIdentitySnapshot(uid, squadronLookup, options = {}) {
|
async function loadPlayerIdentitySnapshot(uid, squadronLookup, options = {}) {
|
||||||
const latestRow = options.latestRow || await dbGetAsync(
|
const latestRow = options.latestRow || await dbGetAsync(
|
||||||
db,
|
db,
|
||||||
`SELECT nick, squadron_name, clan_id
|
`SELECT nick, squadron_name, clan_id, endtime_unix
|
||||||
FROM player_games_hist
|
FROM player_games_hist
|
||||||
WHERE UID = ? AND nick NOT LIKE 'coop/%'
|
WHERE UID = ? AND nick NOT LIKE 'coop/%'
|
||||||
ORDER BY session_id DESC
|
ORDER BY session_id DESC
|
||||||
@@ -1309,7 +1377,7 @@ async function loadPlayerIdentitySnapshot(uid, squadronLookup, options = {}) {
|
|||||||
);
|
);
|
||||||
if (!latestRow) return null;
|
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([
|
const [nickRows, squadronRows] = await Promise.all([
|
||||||
dbAllAsync(
|
dbAllAsync(
|
||||||
db,
|
db,
|
||||||
@@ -2016,7 +2084,7 @@ app.get('/api/player/:uid', (req, res) => {
|
|||||||
const { start_date, end_date, season, week } = req.query;
|
const { start_date, end_date, season, week } = req.query;
|
||||||
|
|
||||||
const latestNickQuery = `
|
const latestNickQuery = `
|
||||||
SELECT nick, squadron_name, clan_id
|
SELECT nick, squadron_name, clan_id, endtime_unix
|
||||||
FROM player_games_hist
|
FROM player_games_hist
|
||||||
WHERE UID = ? AND nick NOT LIKE 'coop/%'
|
WHERE UID = ? AND nick NOT LIKE 'coop/%'
|
||||||
ORDER BY session_id DESC
|
ORDER BY session_id DESC
|
||||||
@@ -2060,37 +2128,23 @@ app.get('/api/player/:uid', (req, res) => {
|
|||||||
const lookupPromise = new Promise((resolve) => {
|
const lookupPromise = new Promise((resolve) => {
|
||||||
loadSquadronLookupCached(resolve);
|
loadSquadronLookupCached(resolve);
|
||||||
});
|
});
|
||||||
// Authoritative current-roster membership. squadron_members is updated
|
// Roster-sync snapshot (squadron_members). resolveCurrentSquadronIdentity
|
||||||
// by the periodic squadron-sync, so this reflects "what squadron this
|
// weighs this against the last tracked game's timestamp rather than trusting
|
||||||
// player belongs to right now" — beats the most-recent player_games_hist
|
// it blindly — an orphaned row from a squadron whose sync has lapsed should
|
||||||
// row which can be stale post-rename if the player hasn't played a SQB
|
// lose to a more recent game record.
|
||||||
// since the rename.
|
|
||||||
const rosterPromise = new Promise((resolve) => {
|
const rosterPromise = new Promise((resolve) => {
|
||||||
const squadronsDbPath = path.join(STORAGE_ROOT, 'squadrons.db');
|
loadNickLookupCached((nickCache) => resolve(nickCache[uid] || null));
|
||||||
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));
|
|
||||||
}
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
Promise.all([nickPromise, statsPromise, lookupPromise, rosterPromise])
|
Promise.all([nickPromise, statsPromise, lookupPromise, rosterPromise])
|
||||||
.then(async ([nickRow, vehicleRows, squadronLookup, rosterClanId]) => {
|
.then(async ([nickRow, vehicleRows, squadronLookup, rosterHint]) => {
|
||||||
if (!nickRow) {
|
if (!nickRow) {
|
||||||
return jsonError(res, 404, 'Player not found', { uid: normalizeUid(uid) });
|
return jsonError(res, 404, 'Player not found', { uid: normalizeUid(uid) });
|
||||||
}
|
}
|
||||||
|
|
||||||
const identity = await loadPlayerIdentitySnapshot(uid, squadronLookup, {
|
const identity = await loadPlayerIdentitySnapshot(uid, squadronLookup, {
|
||||||
latestRow: nickRow,
|
latestRow: nickRow,
|
||||||
preferredClanId: rosterClanId,
|
rosterHint,
|
||||||
});
|
});
|
||||||
loadPerformanceBenchmarksCached(dateFilters, (benchmarks) => {
|
loadPerformanceBenchmarksCached(dateFilters, (benchmarks) => {
|
||||||
const vehicles = vehicleRows.map(row => {
|
const vehicles = vehicleRows.map(row => {
|
||||||
@@ -2330,7 +2384,7 @@ app.get('/api/search/:nickname', (req, res) => {
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
const latestNickQuery = `
|
const latestNickQuery = `
|
||||||
SELECT nick, squadron_name, clan_id
|
SELECT nick, squadron_name, clan_id, endtime_unix
|
||||||
FROM player_games_hist
|
FROM player_games_hist
|
||||||
WHERE UID = ? AND nick NOT LIKE 'coop/%'
|
WHERE UID = ? AND nick NOT LIKE 'coop/%'
|
||||||
ORDER BY session_id DESC
|
ORDER BY session_id DESC
|
||||||
@@ -2352,47 +2406,49 @@ app.get('/api/search/:nickname', (req, res) => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
loadSquadronLookupCached((squadronLookup) => {
|
loadNickLookupCached((nickCache) => {
|
||||||
const lookups = rows.map(row => (async () => {
|
loadSquadronLookupCached((squadronLookup) => {
|
||||||
const latestRow = await dbGetAsync(db, latestNickQuery, [row.UID]);
|
const lookups = rows.map(row => (async () => {
|
||||||
if (!latestRow) {
|
const latestRow = await dbGetAsync(db, latestNickQuery, [row.UID]);
|
||||||
return {
|
if (!latestRow) {
|
||||||
uid: normalizeUid(row.UID),
|
return {
|
||||||
nick: '',
|
uid: normalizeUid(row.UID),
|
||||||
previous_nicks: [],
|
nick: '',
|
||||||
squadron_name: '',
|
previous_nicks: [],
|
||||||
squadron_long_name: '',
|
squadron_name: '',
|
||||||
squadron_clan_id: null,
|
squadron_long_name: '',
|
||||||
previous_squadron_names: [],
|
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 => {
|
log.info('Search query executed', {
|
||||||
const finalResults = results.filter(Boolean);
|
searchTerm: searchTerm,
|
||||||
const response = {
|
nickname: nickname,
|
||||||
search_term: nickname.trim(),
|
resultsFound: finalResults.length,
|
||||||
results: finalResults,
|
firstFewResults: finalResults.slice(0, 3)
|
||||||
total_found: finalResults.length,
|
});
|
||||||
limited_to: 50
|
|
||||||
};
|
|
||||||
|
|
||||||
log.info('Search query executed', {
|
setCachedResponse(cacheKey, response);
|
||||||
searchTerm: searchTerm,
|
res.json(response);
|
||||||
nickname: nickname,
|
}).catch(identityErr => {
|
||||||
resultsFound: finalResults.length,
|
log.error('Failed to build player identity search response', identityErr, {
|
||||||
firstFewResults: finalResults.slice(0, 3)
|
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 });
|
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) => {
|
loadNickLookupCached((nickCache) => {
|
||||||
loadSquadronLookupCached((squadronLookup) => {
|
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 cached = nickCache[row.uid];
|
||||||
const response = {
|
const fb = gameHistoryCache[row.uid];
|
||||||
date_filter: createDateFilterMetadata(dateFilters, start_date, end_date, season, week),
|
const nick = cached ? cached.nick : (fb ? fb.nick : row.uid);
|
||||||
timeframe: dateFilters.hasFilter ? "filtered" : "all-time",
|
const sqId = resolveSquadronIdentity(cached, fb, squadronLookup);
|
||||||
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];
|
return {
|
||||||
const fb = fallbackMap[row.uid];
|
uid: row.uid,
|
||||||
const nick = cached ? cached.nick : (fb ? fb.nick : row.uid);
|
nick,
|
||||||
const sqId = resolveSquadronIdentity(cached, fb, squadronLookup);
|
squadron_name: sqId.tag_name,
|
||||||
|
squadron_short_name: sqId.short_name,
|
||||||
return {
|
squadron_clan_id: sqId.clan_id,
|
||||||
uid: row.uid,
|
total_kills: totalKills,
|
||||||
nick,
|
ground_kills: row.total_ground_kills || 0,
|
||||||
squadron_name: sqId.tag_name,
|
air_kills: row.total_air_kills || 0,
|
||||||
squadron_short_name: sqId.short_name,
|
total_battles: totalBattles,
|
||||||
squadron_clan_id: sqId.clan_id,
|
wins,
|
||||||
total_kills: totalKills,
|
win_rate: parseFloat(winRate.toFixed(1)),
|
||||||
ground_kills: row.total_ground_kills || 0,
|
kdr: parseFloat(kdr.toFixed(1)),
|
||||||
air_kills: row.total_air_kills || 0,
|
deaths,
|
||||||
total_battles: totalBattles,
|
assists: row.total_assists || 0,
|
||||||
wins,
|
captures: row.total_captures || 0,
|
||||||
win_rate: parseFloat(winRate.toFixed(1)),
|
total_score: Math.round(row.total_score || 0)
|
||||||
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);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Fallback for players not in any squadron — uses (UID, endtime_unix) index
|
log.info('Player leaderboard complete', {
|
||||||
if (uncoveredUids.length > 0) {
|
playersReturned: statsRows.length,
|
||||||
const fbPlaceholders = uncoveredUids.map(() => '?').join(',');
|
totalMs: Date.now() - queryStart
|
||||||
db.all(`
|
});
|
||||||
SELECT UID as uid, nick, squadron_name, MAX(endtime_unix)
|
|
||||||
FROM player_games_hist
|
resolve(response);
|
||||||
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({});
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -3760,68 +3795,50 @@ app.get('/api/leaderboard/vehicles', (req, res) => {
|
|||||||
return reject(err);
|
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) => {
|
loadNickLookupCached((nickCache) => {
|
||||||
loadSquadronLookupCached((squadronLookup) => {
|
loadSquadronLookupCached((squadronLookup) => {
|
||||||
const uniqueUids = [...new Set(vehicleRows.map(r => r.player_uid))];
|
loadGameHistoryLookupCached((gameHistoryCache) => {
|
||||||
const uncoveredUids = uniqueUids.filter(uid => !nickCache[uid]);
|
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 cached = nickCache[row.player_uid];
|
||||||
const response = {
|
const fb = gameHistoryCache[row.player_uid];
|
||||||
date_filter: createDateFilterMetadata(dateFilters, start_date, end_date, season, week),
|
const playerNick = cached ? cached.nick : (fb ? fb.nick : row.player_uid);
|
||||||
vehicles: vehicleRows.map(row => {
|
const sqId = resolveSquadronIdentity(cached, fb, squadronLookup);
|
||||||
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];
|
return {
|
||||||
const fb = fallbackMap[row.player_uid];
|
vehicle: row.vehicle,
|
||||||
const playerNick = cached ? cached.nick : (fb ? fb.nick : row.player_uid);
|
vehicle_internal: row.vehicle_internal || null,
|
||||||
const sqId = resolveSquadronIdentity(cached, fb, squadronLookup);
|
player_uid: normalizeUid(row.player_uid),
|
||||||
|
player_nick: playerNick,
|
||||||
return {
|
player_squadron_name: sqId.tag_name,
|
||||||
vehicle: row.vehicle,
|
player_squadron_short_name: sqId.short_name,
|
||||||
vehicle_internal: row.vehicle_internal || null,
|
player_squadron_clan_id: sqId.clan_id,
|
||||||
player_uid: normalizeUid(row.player_uid),
|
total_kills: totalKills,
|
||||||
player_nick: playerNick,
|
ground_kills: row.total_ground_kills || 0,
|
||||||
player_squadron_name: sqId.tag_name,
|
air_kills: row.total_air_kills || 0,
|
||||||
player_squadron_short_name: sqId.short_name,
|
battles, wins,
|
||||||
player_squadron_clan_id: sqId.clan_id,
|
win_rate: parseFloat(winRate.toFixed(1)),
|
||||||
total_kills: totalKills,
|
kdr: parseFloat(kdr.toFixed(1)),
|
||||||
ground_kills: row.total_ground_kills || 0,
|
deaths,
|
||||||
air_kills: row.total_air_kills || 0,
|
assists: row.total_assists || 0,
|
||||||
battles, wins,
|
captures: row.total_captures || 0
|
||||||
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);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
if (uncoveredUids.length > 0) {
|
log.info('Vehicle leaderboard complete', { vehiclesReturned: vehicleRows.length, totalMs: Date.now() - queryStart });
|
||||||
const fbPlaceholders = uncoveredUids.map(() => '?').join(',');
|
resolve(response);
|
||||||
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({});
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user