add TSS API endpoints + web proxy (#1234)
Adds /api/tss/teams/* and /api/tss/leaderboard/teams to SREBOT/server.js, reading tss_battles.db and tss_teams.db with queries adapted to the team_id / team_name / teams_data schema. Mirrors the existing /api/squadrons/* endpoints. SREBOT/web/server.js gains matching proxy routes so the frontend can reach them via the website host. TSSBOT/BOT/storage.py picks up the columns and table the endpoints need to return meaningful data: teams_data.clanrating, team_members.points, and a new teams_points history table. All idempotent under the existing init_tss_dbs() call. Co-authored-by: Heidi <clippii@protonmail.com> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -22,6 +22,8 @@ const REPLAYS_PATH = path.join(STORAGE_ROOT, 'REPLAYS');
|
|||||||
const COMPS_PATH = path.join(STORAGE_ROOT, 'COMPS');
|
const COMPS_PATH = path.join(STORAGE_ROOT, 'COMPS');
|
||||||
const WL_DB_PATH = path.join(STORAGE_ROOT, 'wl.db');
|
const WL_DB_PATH = path.join(STORAGE_ROOT, 'wl.db');
|
||||||
const POINTS_DB_PATH = path.join(STORAGE_ROOT, 'points.db');
|
const POINTS_DB_PATH = path.join(STORAGE_ROOT, 'points.db');
|
||||||
|
const TSS_TEAMS_DB_PATH = path.join(STORAGE_ROOT, 'tss_teams.db');
|
||||||
|
const TSS_BATTLES_DB_PATH = path.join(STORAGE_ROOT, 'tss_battles.db');
|
||||||
fs.mkdirSync(REPLAYS_PATH, { recursive: true });
|
fs.mkdirSync(REPLAYS_PATH, { recursive: true });
|
||||||
|
|
||||||
function replayDataPath(sessionId) {
|
function replayDataPath(sessionId) {
|
||||||
@@ -123,8 +125,11 @@ const STATS_CACHE_TTL = 30 * 60 * 1000; // Global stats change slowly — cache
|
|||||||
const inflightRequests = new Map(); // dedup: key -> Promise
|
const inflightRequests = new Map(); // dedup: key -> Promise
|
||||||
let squadronLookupCache = null;
|
let squadronLookupCache = null;
|
||||||
let squadronLookupCacheTime = 0;
|
let squadronLookupCacheTime = 0;
|
||||||
|
let tssTeamLookupCache = null;
|
||||||
|
let tssTeamLookupCacheTime = 0;
|
||||||
const SQUADRON_CACHE_TTL = 30 * 60 * 1000;
|
const SQUADRON_CACHE_TTL = 30 * 60 * 1000;
|
||||||
let hasSquadronColumn = false;
|
let hasSquadronColumn = false;
|
||||||
|
let hasTssTeamColumn = false;
|
||||||
let nickLookupCache = null;
|
let nickLookupCache = null;
|
||||||
let nickLookupCacheTime = 0;
|
let nickLookupCacheTime = 0;
|
||||||
const performanceBenchmarkCache = new Map();
|
const performanceBenchmarkCache = new Map();
|
||||||
@@ -418,6 +423,87 @@ function loadSquadronLookupCached(callback) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function loadTssTeamLookupCached(callback) {
|
||||||
|
if (tssTeamLookupCache && Date.now() - tssTeamLookupCacheTime < SQUADRON_CACHE_TTL) {
|
||||||
|
return callback(tssTeamLookupCache);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!fs.existsSync(TSS_TEAMS_DB_PATH)) {
|
||||||
|
tssTeamLookupCache = {};
|
||||||
|
tssTeamLookupCacheTime = Date.now();
|
||||||
|
return callback({});
|
||||||
|
}
|
||||||
|
|
||||||
|
const teamsDb = new sqlite3.Database(TSS_TEAMS_DB_PATH, sqlite3.OPEN_READONLY, (err) => {
|
||||||
|
if (err) {
|
||||||
|
log.error('Failed to open TSS teams database', err);
|
||||||
|
tssTeamLookupCache = {};
|
||||||
|
tssTeamLookupCacheTime = Date.now();
|
||||||
|
return callback({});
|
||||||
|
}
|
||||||
|
|
||||||
|
teamsDb.all(`SELECT team_id AS clan_id, long_name, short_name, tag_name, members, clanrating FROM teams_data`, [], (err, rows) => {
|
||||||
|
teamsDb.close();
|
||||||
|
if (err) {
|
||||||
|
log.error('Error loading TSS team lookup data', err);
|
||||||
|
tssTeamLookupCache = {};
|
||||||
|
tssTeamLookupCacheTime = Date.now();
|
||||||
|
return callback(tssTeamLookupCache);
|
||||||
|
}
|
||||||
|
|
||||||
|
const lookup = {};
|
||||||
|
const addAlias = (key, row) => {
|
||||||
|
if (key === undefined || key === null || key === '') return;
|
||||||
|
lookup[key] = row;
|
||||||
|
const lower = String(key).toLowerCase();
|
||||||
|
if (!(lower in lookup)) lookup[lower] = row;
|
||||||
|
};
|
||||||
|
(rows || []).forEach(row => {
|
||||||
|
if (row.long_name) addAlias(row.long_name, row);
|
||||||
|
if (row.short_name) addAlias(row.short_name, row);
|
||||||
|
if (row.tag_name) addAlias(row.tag_name, row);
|
||||||
|
if (row.clan_id !== null && row.clan_id !== undefined) addAlias(String(row.clan_id), row);
|
||||||
|
});
|
||||||
|
|
||||||
|
tssTeamLookupCache = lookup;
|
||||||
|
tssTeamLookupCacheTime = Date.now();
|
||||||
|
log.info('TSS team lookup cache updated', { entries: (rows || []).length });
|
||||||
|
callback(tssTeamLookupCache);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveTssTeam(input, lookup) {
|
||||||
|
const raw = input == null ? '' : String(input).trim();
|
||||||
|
const hit = raw && lookup ? (lookup[raw] || lookup[raw.toLowerCase()]) : null;
|
||||||
|
const canonicalName = hit ? (hit.long_name || raw) : raw;
|
||||||
|
const variants = new Set();
|
||||||
|
if (raw) variants.add(raw);
|
||||||
|
if (canonicalName) variants.add(canonicalName);
|
||||||
|
if (hit) {
|
||||||
|
if (hit.long_name) variants.add(hit.long_name);
|
||||||
|
if (hit.short_name) variants.add(hit.short_name);
|
||||||
|
if (hit.tag_name) variants.add(hit.tag_name);
|
||||||
|
Object.keys(lookup || {}).forEach(key => {
|
||||||
|
if (lookup[key] && lookup[key].long_name === hit.long_name) variants.add(key);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
row: hit || null,
|
||||||
|
clanId: hit && hit.clan_id != null ? Number(hit.clan_id) : null,
|
||||||
|
canonicalName,
|
||||||
|
tagName: hit ? (hit.tag_name || hit.short_name || canonicalName) : raw,
|
||||||
|
variants: Array.from(variants).filter(Boolean),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function tssDbUnavailable(res) {
|
||||||
|
return res.status(404).json({
|
||||||
|
error: 'TSS database not found',
|
||||||
|
required_databases: ['tss_battles.db', 'tss_teams.db']
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function loadNickLookupCached(callback) {
|
function loadNickLookupCached(callback) {
|
||||||
if (nickLookupCache && Date.now() - nickLookupCacheTime < SQUADRON_CACHE_TTL) {
|
if (nickLookupCache && Date.now() - nickLookupCacheTime < SQUADRON_CACHE_TTL) {
|
||||||
return callback(nickLookupCache);
|
return callback(nickLookupCache);
|
||||||
@@ -1785,6 +1871,29 @@ const heavyDb = new sqlite3.Database(DB_PATH, sqlite3.OPEN_READONLY, (err) => {
|
|||||||
else log.info('Heavy-reader connection open');
|
else log.info('Heavy-reader connection open');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
let tssDb = null;
|
||||||
|
let tssHeavyDb = null;
|
||||||
|
if (fs.existsSync(TSS_BATTLES_DB_PATH)) {
|
||||||
|
tssHeavyDb = new sqlite3.Database(TSS_BATTLES_DB_PATH, sqlite3.OPEN_READONLY, (err) => {
|
||||||
|
if (err) log.warn('Failed to open TSS heavy reader connection:', err.message);
|
||||||
|
else log.info('TSS heavy-reader connection open');
|
||||||
|
});
|
||||||
|
tssDb = new sqlite3.Database(TSS_BATTLES_DB_PATH, sqlite3.OPEN_READONLY, (err) => {
|
||||||
|
if (err) {
|
||||||
|
log.error('Failed to open TSS battles database', err, { dbPath: TSS_BATTLES_DB_PATH });
|
||||||
|
tssDb = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
log.info('Connected to TSS battles database in read-only mode', { dbPath: TSS_BATTLES_DB_PATH });
|
||||||
|
tssDb.all("PRAGMA table_info(player_games_hist)", (schemaErr, cols) => {
|
||||||
|
if (!schemaErr) hasTssTeamColumn = cols.some(c => c.name === 'team_name');
|
||||||
|
log.info('TSS schema check complete', { hasTssTeamColumn });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
log.warn('TSS battles database not found; /api/tss endpoints will return 404', { dbPath: TSS_BATTLES_DB_PATH });
|
||||||
|
}
|
||||||
|
|
||||||
const db = new sqlite3.Database(DB_PATH, sqlite3.OPEN_READONLY, (err) => {
|
const db = new sqlite3.Database(DB_PATH, sqlite3.OPEN_READONLY, (err) => {
|
||||||
if (err) {
|
if (err) {
|
||||||
log.error('Failed to open database', err, { dbPath: DB_PATH });
|
log.error('Failed to open database', err, { dbPath: DB_PATH });
|
||||||
@@ -2372,6 +2481,7 @@ app.get('/health', (req, res) => {
|
|||||||
status: 'OK',
|
status: 'OK',
|
||||||
timestamp: new Date().toISOString(),
|
timestamp: new Date().toISOString(),
|
||||||
database: 'Connected',
|
database: 'Connected',
|
||||||
|
tss_database: tssDb ? 'Connected' : 'Unavailable',
|
||||||
ready: serverReady,
|
ready: serverReady,
|
||||||
uptime_ms: Date.now() - startupReadyAt,
|
uptime_ms: Date.now() - startupReadyAt,
|
||||||
});
|
});
|
||||||
@@ -3063,6 +3173,15 @@ const API_INFO = {
|
|||||||
"GET /api/squadrons/:squadronname": "Get squadron roster stats and summary",
|
"GET /api/squadrons/:squadronname": "Get squadron roster stats and summary",
|
||||||
"GET /api/squadrons/:squadronname/history": "Get squadron battle and rating history",
|
"GET /api/squadrons/:squadronname/history": "Get squadron battle and rating history",
|
||||||
"GET /api/squadrons/:squadronname/games": "Get squadron match list",
|
"GET /api/squadrons/:squadronname/games": "Get squadron match list",
|
||||||
|
"GET /api/tss/teams/resolve": "Resolve TSS team short/tag/long name to canonical metadata",
|
||||||
|
"GET /api/tss/leaderboard/teams": "Get TSS team leaderboards",
|
||||||
|
"GET /api/tss/leaderboard/squadrons": "Alias for TSS team leaderboards",
|
||||||
|
"GET /api/tss/teams/:teamname": "Get TSS team roster stats and summary",
|
||||||
|
"GET /api/tss/teams/:teamname/history": "Get TSS team battle and rating history",
|
||||||
|
"GET /api/tss/teams/:teamname/games": "Get TSS team match list",
|
||||||
|
"GET /api/tss/squadrons/:squadronname": "Alias for TSS team roster stats and summary",
|
||||||
|
"GET /api/debug/tss-teams-db-schema": "Inspect TSS teams database schema",
|
||||||
|
"GET /api/debug/tss-battles-db-schema": "Inspect TSS battles database schema",
|
||||||
"GET /health": "Health check endpoint",
|
"GET /health": "Health check endpoint",
|
||||||
"GET /api/info": "API information"
|
"GET /api/info": "API information"
|
||||||
},
|
},
|
||||||
@@ -3205,6 +3324,65 @@ const API_INFO = {
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
file: "tss_battles.db",
|
||||||
|
purpose: "TSS battle history store. Same schema as sq_battles.db and served by /api/tss endpoints.",
|
||||||
|
tables: [
|
||||||
|
{
|
||||||
|
name: "player_games_hist",
|
||||||
|
description: "One row per TSS player per battle / vehicle.",
|
||||||
|
used_by: [
|
||||||
|
"GET /api/tss/leaderboard/teams",
|
||||||
|
"GET /api/tss/leaderboard/squadrons",
|
||||||
|
"GET /api/tss/teams/:teamname",
|
||||||
|
"GET /api/tss/teams/:teamname/history",
|
||||||
|
"GET /api/tss/teams/:teamname/games",
|
||||||
|
"GET /api/tss/squadrons/:squadronname",
|
||||||
|
"GET /api/debug/tss-battles-db-schema"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "match_summary",
|
||||||
|
description: "One row per TSS match.",
|
||||||
|
used_by: [
|
||||||
|
"GET /api/tss/teams/:teamname/games",
|
||||||
|
"GET /api/debug/tss-battles-db-schema"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
file: "tss_teams.db",
|
||||||
|
purpose: "TSS team roster cache, team metadata, and historical point snapshots. Served by /api/tss endpoints.",
|
||||||
|
tables: [
|
||||||
|
{
|
||||||
|
name: "teams_data",
|
||||||
|
description: "Canonical TSS team directory and latest leaderboard snapshot.",
|
||||||
|
used_by: [
|
||||||
|
"GET /api/tss/teams/resolve",
|
||||||
|
"GET /api/tss/leaderboard/teams",
|
||||||
|
"GET /api/tss/teams/:teamname",
|
||||||
|
"GET /api/tss/teams/:teamname/history",
|
||||||
|
"GET /api/debug/tss-teams-db-schema"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "team_members",
|
||||||
|
description: "Current per-team member roster with cached nick and points values.",
|
||||||
|
used_by: [
|
||||||
|
"GET /api/tss/teams/:teamname"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "teams_points",
|
||||||
|
description: "Historical TSS team point snapshots keyed by unix time.",
|
||||||
|
used_by: [
|
||||||
|
"GET /api/tss/teams/:teamname/history",
|
||||||
|
"GET /api/debug/tss-teams-db-schema"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
};
|
};
|
||||||
@@ -4834,6 +5012,500 @@ app.get('/api/squadrons/:squadronname/games', (req, res) => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
function sendSqliteSchema(res, dbPath, label) {
|
||||||
|
if (!fs.existsSync(dbPath)) {
|
||||||
|
return res.status(404).json({
|
||||||
|
error: `${label} database not found`,
|
||||||
|
path: dbPath
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const schemaDb = new sqlite3.Database(dbPath, sqlite3.OPEN_READONLY, (err) => {
|
||||||
|
if (err) {
|
||||||
|
return res.status(500).json({
|
||||||
|
error: `Failed to open ${label} database`,
|
||||||
|
details: err.message
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
schemaDb.all(`SELECT name FROM sqlite_master WHERE type='table'`, (err, tables) => {
|
||||||
|
if (err) {
|
||||||
|
schemaDb.close();
|
||||||
|
return res.status(500).json({ error: 'Failed to get table list', details: err.message });
|
||||||
|
}
|
||||||
|
|
||||||
|
const tableSchemas = {};
|
||||||
|
let completedTables = 0;
|
||||||
|
if (!tables.length) {
|
||||||
|
schemaDb.close();
|
||||||
|
return res.json({ database_path: dbPath, tables: {}, message: `No tables found in ${label} database` });
|
||||||
|
}
|
||||||
|
|
||||||
|
tables.forEach(table => {
|
||||||
|
const tableName = table.name;
|
||||||
|
schemaDb.all(`PRAGMA table_info(${tableName})`, (schemaErr, schema) => {
|
||||||
|
if (schemaErr) {
|
||||||
|
tableSchemas[tableName] = { error: schemaErr.message };
|
||||||
|
completedTables++;
|
||||||
|
if (completedTables === tables.length) {
|
||||||
|
schemaDb.close();
|
||||||
|
res.json({ database_path: dbPath, tables: tableSchemas, timestamp: new Date().toISOString() });
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
schemaDb.all(`SELECT * FROM ${tableName} LIMIT 3`, (sampleErr, samples) => {
|
||||||
|
tableSchemas[tableName] = {
|
||||||
|
schema,
|
||||||
|
sample_records: sampleErr ? [] : (samples || []),
|
||||||
|
record_count: sampleErr ? 0 : (samples || []).length
|
||||||
|
};
|
||||||
|
completedTables++;
|
||||||
|
if (completedTables === tables.length) {
|
||||||
|
schemaDb.close();
|
||||||
|
res.json({ database_path: dbPath, tables: tableSchemas, timestamp: new Date().toISOString() });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
app.get('/api/debug/tss-teams-db-schema', requireAdminBearer, (req, res) => {
|
||||||
|
sendSqliteSchema(res, TSS_TEAMS_DB_PATH, 'TSS teams');
|
||||||
|
});
|
||||||
|
|
||||||
|
app.get('/api/debug/tss-battles-db-schema', requireAdminBearer, (req, res) => {
|
||||||
|
sendSqliteSchema(res, TSS_BATTLES_DB_PATH, 'TSS battles');
|
||||||
|
});
|
||||||
|
|
||||||
|
function tssAll(database, query, params = []) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
if (!database) return reject(new Error('TSS database unavailable'));
|
||||||
|
database.all(query, params, (err, rows) => err ? reject(err) : resolve(rows || []));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function tssGet(database, query, params = []) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
if (!database) return reject(new Error('TSS database unavailable'));
|
||||||
|
database.get(query, params, (err, row) => err ? reject(err) : resolve(row || null));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function tssDateParams(dateFilters) {
|
||||||
|
return [
|
||||||
|
dateFilters.startTimestamp,
|
||||||
|
dateFilters.startTimestamp,
|
||||||
|
dateFilters.endTimestamp,
|
||||||
|
dateFilters.endTimestamp,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
function tssAttrClause(alias, team) {
|
||||||
|
const prefix = alias ? `${alias}.` : '';
|
||||||
|
if (team.clanId != null) return { clause: `${prefix}team_id = ?`, params: [team.clanId] };
|
||||||
|
const variants = team.variants.length ? team.variants : [team.canonicalName];
|
||||||
|
return {
|
||||||
|
clause: `${prefix}team_name IN (${variants.map(() => '?').join(',')})`,
|
||||||
|
params: variants,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function registerTssTeamRoutes(teamBasePath) {
|
||||||
|
app.get(`${teamBasePath}/resolve`, (req, res) => {
|
||||||
|
const name = req.query.name || req.query.q || req.query.team || req.query.squadron || '';
|
||||||
|
if (!name) return res.status(400).json({ error: 'name query parameter is required' });
|
||||||
|
loadTssTeamLookupCached((lookup) => {
|
||||||
|
const team = resolveTssTeam(name, lookup);
|
||||||
|
res.json({
|
||||||
|
query: String(name),
|
||||||
|
resolved: !!team.row,
|
||||||
|
clan_id: team.clanId,
|
||||||
|
long_name: team.canonicalName,
|
||||||
|
short_name: team.row ? team.row.short_name : null,
|
||||||
|
tag_name: team.tagName,
|
||||||
|
members: team.row ? team.row.members : null,
|
||||||
|
clanrating: team.row ? team.row.clanrating : null,
|
||||||
|
source: 'tss_teams.db'
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
app.get(`${teamBasePath}/:teamname`, (req, res) => {
|
||||||
|
if (!tssDb) return tssDbUnavailable(res);
|
||||||
|
if (!hasTssTeamColumn) {
|
||||||
|
return res.json({
|
||||||
|
tag_name: req.params.teamname,
|
||||||
|
long_name: req.params.teamname,
|
||||||
|
team_summary: { player_count: 0, total_kills: 0, total_battles: 0, wins: 0 },
|
||||||
|
players: [],
|
||||||
|
migration_needed: true
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const { teamname } = req.params;
|
||||||
|
const dateFilters = parseDateFilters(req);
|
||||||
|
const { start_date, end_date, season, week } = req.query;
|
||||||
|
const cacheKey = `tss_team_detail_${teamBasePath}_${teamname}_${start_date || ''}_${end_date || ''}_${season || ''}_${week || ''}`;
|
||||||
|
const cached = getCachedResponse(cacheKey);
|
||||||
|
if (cached) return res.json(cached);
|
||||||
|
|
||||||
|
loadTssTeamLookupCached((lookup) => {
|
||||||
|
const team = resolveTssTeam(teamname, lookup);
|
||||||
|
const attr = tssAttrClause('p', team);
|
||||||
|
const summaryAttr = tssAttrClause('', team);
|
||||||
|
|
||||||
|
const membersPromise = new Promise((resolve) => {
|
||||||
|
if (!team.clanId || !fs.existsSync(TSS_TEAMS_DB_PATH)) return resolve([]);
|
||||||
|
const teamsDb = new sqlite3.Database(TSS_TEAMS_DB_PATH, sqlite3.OPEN_READONLY, (err) => {
|
||||||
|
if (err) return resolve([]);
|
||||||
|
teamsDb.all(`SELECT uid, nick, points FROM team_members WHERE team_id = ?`, [team.clanId], (err, rows) => {
|
||||||
|
teamsDb.close();
|
||||||
|
resolve(err ? [] : (rows || []));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const playerStatsQuery = `
|
||||||
|
SELECT
|
||||||
|
p.UID as uid,
|
||||||
|
MAX(p.nick) as nick,
|
||||||
|
SUM(p.ground_kills) as total_ground_kills,
|
||||||
|
SUM(p.air_kills) as total_air_kills,
|
||||||
|
SUM(p.ground_kills + p.air_kills) as total_kills,
|
||||||
|
SUM(p.assists) as total_assists,
|
||||||
|
SUM(p.captures) as total_captures,
|
||||||
|
SUM(p.deaths) as total_deaths,
|
||||||
|
COUNT(*) as total_battles,
|
||||||
|
COUNT(DISTINCT p.session_id) as sessions,
|
||||||
|
SUM(CASE WHEN UPPER(p.victor_bool) = 'WIN' THEN 1 ELSE 0 END) as wins
|
||||||
|
FROM player_games_hist p
|
||||||
|
WHERE ${attr.clause}
|
||||||
|
AND p.UID IS NOT NULL
|
||||||
|
AND (? IS NULL OR p.endtime_unix >= ?)
|
||||||
|
AND (? IS NULL OR p.endtime_unix <= ?)
|
||||||
|
GROUP BY p.UID
|
||||||
|
`;
|
||||||
|
const summaryQuery = `
|
||||||
|
SELECT
|
||||||
|
COUNT(DISTINCT session_id) as total_battles,
|
||||||
|
COUNT(DISTINCT CASE WHEN UPPER(victor_bool) = 'WIN' THEN session_id END) as wins,
|
||||||
|
SUM(ground_kills) as total_ground_kills,
|
||||||
|
SUM(air_kills) as total_air_kills,
|
||||||
|
SUM(ground_kills + air_kills) as total_kills,
|
||||||
|
SUM(assists) as total_assists,
|
||||||
|
SUM(captures) as total_captures,
|
||||||
|
SUM(deaths) as total_deaths
|
||||||
|
FROM player_games_hist
|
||||||
|
WHERE ${summaryAttr.clause}
|
||||||
|
AND UID IS NOT NULL
|
||||||
|
AND (? IS NULL OR endtime_unix >= ?)
|
||||||
|
AND (? IS NULL OR endtime_unix <= ?)
|
||||||
|
`;
|
||||||
|
|
||||||
|
Promise.all([
|
||||||
|
membersPromise,
|
||||||
|
tssAll(tssDb, playerStatsQuery, [...attr.params, ...tssDateParams(dateFilters)]),
|
||||||
|
tssGet(tssDb, summaryQuery, [...summaryAttr.params, ...tssDateParams(dateFilters)])
|
||||||
|
]).then(([memberRows, statsRows, summaryRow]) => {
|
||||||
|
const memberByUid = new Map((memberRows || []).map(r => [String(r.uid), r]));
|
||||||
|
const players = (statsRows || []).map(row => {
|
||||||
|
const uid = String(row.uid);
|
||||||
|
const member = memberByUid.get(uid) || {};
|
||||||
|
const kills = row.total_kills || 0;
|
||||||
|
const deaths = row.total_deaths || 0;
|
||||||
|
const battles = row.total_battles || 0;
|
||||||
|
const wins = row.wins || 0;
|
||||||
|
return {
|
||||||
|
uid,
|
||||||
|
nick: member.nick || row.nick || '',
|
||||||
|
points: member.points || 0,
|
||||||
|
total_kills: kills,
|
||||||
|
ground_kills: row.total_ground_kills || 0,
|
||||||
|
air_kills: row.total_air_kills || 0,
|
||||||
|
total_battles: battles,
|
||||||
|
wins,
|
||||||
|
win_rate: battles > 0 ? parseFloat(((wins / battles) * 100).toFixed(1)) : 0,
|
||||||
|
kdr: deaths > 0 ? parseFloat((kills / deaths).toFixed(1)) : kills,
|
||||||
|
deaths,
|
||||||
|
assists: row.total_assists || 0,
|
||||||
|
captures: row.total_captures || 0
|
||||||
|
};
|
||||||
|
}).sort((a, b) => b.total_kills - a.total_kills);
|
||||||
|
|
||||||
|
const summary = summaryRow || {};
|
||||||
|
const totalKills = summary.total_kills || 0;
|
||||||
|
const deaths = summary.total_deaths || 0;
|
||||||
|
const totalBattles = summary.total_battles || 0;
|
||||||
|
const wins = summary.wins || 0;
|
||||||
|
const response = {
|
||||||
|
data_set: 'tss',
|
||||||
|
date_filter: createDateFilterMetadata(dateFilters, start_date, end_date, season, week),
|
||||||
|
clan_id: team.clanId,
|
||||||
|
tag_name: team.tagName,
|
||||||
|
short_name: team.row ? (team.row.short_name || team.tagName) : team.tagName,
|
||||||
|
long_name: team.canonicalName,
|
||||||
|
team_summary: {
|
||||||
|
player_count: team.row ? (team.row.members || players.length) : players.length,
|
||||||
|
total_kills: totalKills,
|
||||||
|
ground_kills: summary.total_ground_kills || 0,
|
||||||
|
air_kills: summary.total_air_kills || 0,
|
||||||
|
total_battles: totalBattles,
|
||||||
|
wins,
|
||||||
|
win_rate: totalBattles > 0 ? parseFloat(((wins / totalBattles) * 100).toFixed(1)) : 0,
|
||||||
|
kdr: deaths > 0 ? parseFloat((totalKills / deaths).toFixed(1)) : totalKills,
|
||||||
|
deaths,
|
||||||
|
assists: summary.total_assists || 0,
|
||||||
|
captures: summary.total_captures || 0,
|
||||||
|
points: {
|
||||||
|
total_points: team.row ? (team.row.clanrating || 0) : 0,
|
||||||
|
has_points_data: !!(team.row && team.row.clanrating)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
squadron_summary: null,
|
||||||
|
players
|
||||||
|
};
|
||||||
|
response.squadron_summary = response.team_summary;
|
||||||
|
setCachedResponse(cacheKey, response);
|
||||||
|
res.json(response);
|
||||||
|
}).catch(err => {
|
||||||
|
log.error('Database error in TSS team details query', err, { teamName: teamname });
|
||||||
|
res.status(500).json({ error: 'Database error', errorCode: 'DB_TSS_TEAM_QUERY_FAILED' });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
app.get(`${teamBasePath}/:teamname/history`, (req, res) => {
|
||||||
|
if (!tssDb) return tssDbUnavailable(res);
|
||||||
|
const { teamname } = req.params;
|
||||||
|
const cacheKey = `tss_team_history_${teamBasePath}_${teamname}`;
|
||||||
|
const cached = getCachedResponse(cacheKey);
|
||||||
|
if (cached) return res.json(cached);
|
||||||
|
|
||||||
|
loadTssTeamLookupCached((lookup) => {
|
||||||
|
const team = resolveTssTeam(teamname, lookup);
|
||||||
|
const attr = tssAttrClause('', team);
|
||||||
|
const historyQuery = `
|
||||||
|
SELECT
|
||||||
|
date(endtime_unix, 'unixepoch') as period,
|
||||||
|
COUNT(DISTINCT session_id) as battles,
|
||||||
|
ROUND(SUM(CASE WHEN UPPER(victor_bool) = 'WIN' THEN 1.0 ELSE 0 END) * 100.0 / NULLIF(COUNT(*), 0), 1) as win_rate,
|
||||||
|
ROUND(CAST(SUM(ground_kills + air_kills) AS REAL) / MAX(SUM(deaths), 1), 2) as kdr
|
||||||
|
FROM player_games_hist
|
||||||
|
WHERE ${attr.clause}
|
||||||
|
AND UID IS NOT NULL
|
||||||
|
AND endtime_unix IS NOT NULL
|
||||||
|
GROUP BY period
|
||||||
|
ORDER BY period ASC
|
||||||
|
`;
|
||||||
|
|
||||||
|
const ratingPromise = new Promise((resolve) => {
|
||||||
|
if (!fs.existsSync(TSS_TEAMS_DB_PATH)) return resolve([]);
|
||||||
|
const teamsDb = new sqlite3.Database(TSS_TEAMS_DB_PATH, sqlite3.OPEN_READONLY, (err) => {
|
||||||
|
if (err) return resolve([]);
|
||||||
|
const ratingQuery = team.clanId
|
||||||
|
? `SELECT unix_time, total_score FROM teams_points WHERE team_id = ? ORDER BY unix_time ASC`
|
||||||
|
: `SELECT unix_time, total_score FROM teams_points WHERE long_name = ? ORDER BY unix_time ASC`;
|
||||||
|
teamsDb.all(ratingQuery, [team.clanId || team.canonicalName], (err, rows) => {
|
||||||
|
teamsDb.close();
|
||||||
|
resolve(err ? [] : (rows || []));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
Promise.all([
|
||||||
|
tssAll(tssDb, historyQuery, attr.params),
|
||||||
|
ratingPromise
|
||||||
|
]).then(([historyRows, ratingRows]) => {
|
||||||
|
const response = {
|
||||||
|
data_set: 'tss',
|
||||||
|
history: historyRows,
|
||||||
|
rating_hourly: ratingRows.map(r => ({ t: r.unix_time, rating: r.total_score }))
|
||||||
|
};
|
||||||
|
setCachedResponse(cacheKey, response);
|
||||||
|
res.json(response);
|
||||||
|
}).catch(err => {
|
||||||
|
log.error('Database error in TSS team history query', err, { teamName: teamname });
|
||||||
|
res.status(500).json({ error: 'Database error', errorCode: 'DB_TSS_TEAM_HISTORY_FAILED' });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
app.get(`${teamBasePath}/:teamname/games`, (req, res) => {
|
||||||
|
if (!tssDb) return tssDbUnavailable(res);
|
||||||
|
const { teamname } = req.params;
|
||||||
|
const dateFilters = parseDateFilters(req);
|
||||||
|
const cacheKey = `tss_team_games_${teamBasePath}_${teamname}_${dateFilters.startTimestamp || 'null'}_${dateFilters.endTimestamp || 'null'}`;
|
||||||
|
const cached = getCachedResponse(cacheKey);
|
||||||
|
if (cached) return res.json(cached);
|
||||||
|
|
||||||
|
loadTssTeamLookupCached((lookup) => {
|
||||||
|
const team = resolveTssTeam(teamname, lookup);
|
||||||
|
const attr = tssAttrClause('p', team);
|
||||||
|
const gamesQuery = `
|
||||||
|
SELECT
|
||||||
|
p.session_id,
|
||||||
|
MAX(p.endtime_unix) as endtime_unix,
|
||||||
|
GROUP_CONCAT(DISTINCT p.nick) as players,
|
||||||
|
COUNT(DISTINCT p.UID) as player_count,
|
||||||
|
SUM(p.ground_kills) as ground_kills,
|
||||||
|
SUM(p.air_kills) as air_kills,
|
||||||
|
SUM(p.assists) as assists,
|
||||||
|
SUM(p.captures) as captures,
|
||||||
|
SUM(p.deaths) as deaths,
|
||||||
|
CASE WHEN SUM(CASE WHEN UPPER(p.victor_bool) = 'WIN' THEN 1 ELSE 0 END) * 2 > COUNT(*) THEN 'Win' ELSE 'Loss' END as result,
|
||||||
|
ms.map_name
|
||||||
|
FROM player_games_hist p
|
||||||
|
LEFT JOIN match_summary ms ON ms.session_id = p.session_id
|
||||||
|
WHERE ${attr.clause}
|
||||||
|
AND p.UID IS NOT NULL
|
||||||
|
AND p.session_id IS NOT NULL
|
||||||
|
AND (? IS NULL OR p.endtime_unix >= ?)
|
||||||
|
AND (? IS NULL OR p.endtime_unix <= ?)
|
||||||
|
GROUP BY p.session_id
|
||||||
|
ORDER BY endtime_unix DESC
|
||||||
|
LIMIT 2000
|
||||||
|
`;
|
||||||
|
|
||||||
|
tssAll(tssDb, gamesQuery, [...attr.params, ...tssDateParams(dateFilters)]).then(rows => {
|
||||||
|
const response = {
|
||||||
|
data_set: 'tss',
|
||||||
|
tag_name: team.tagName,
|
||||||
|
long_name: team.canonicalName,
|
||||||
|
games: rows.map(row => ({
|
||||||
|
session_id: row.session_id,
|
||||||
|
timestamp: row.endtime_unix || 0,
|
||||||
|
map_name: row.map_name || null,
|
||||||
|
players: row.players || '',
|
||||||
|
player_count: row.player_count || 0,
|
||||||
|
stats: {
|
||||||
|
ground_kills: row.ground_kills || 0,
|
||||||
|
air_kills: row.air_kills || 0,
|
||||||
|
assists: row.assists || 0,
|
||||||
|
captures: row.captures || 0,
|
||||||
|
deaths: row.deaths || 0
|
||||||
|
},
|
||||||
|
result: row.result || 'Unknown'
|
||||||
|
})),
|
||||||
|
total_games_returned: rows.length
|
||||||
|
};
|
||||||
|
setCachedResponse(cacheKey, response);
|
||||||
|
res.json(response);
|
||||||
|
}).catch(err => {
|
||||||
|
log.error('Database error in TSS team games query', err, { teamName: teamname });
|
||||||
|
res.status(500).json({ error: 'Database error', errorCode: 'DB_TSS_TEAM_GAMES_FAILED' });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleTssTeamLeaderboard(req, res) {
|
||||||
|
if (!tssDb || !tssHeavyDb) return tssDbUnavailable(res);
|
||||||
|
const dateFilters = parseDateFilters(req);
|
||||||
|
const { start_date, end_date, season, week, limit } = req.query;
|
||||||
|
const maxRows = Math.max(1, Math.min(1000, parseInt(limit) || 1000));
|
||||||
|
const cacheKey = `tss_leaderboard_teams_${start_date || 'all'}_${end_date || 'all'}_${season || 'all'}_${week || 'all'}_${maxRows}`;
|
||||||
|
const cached = getCachedResponse(cacheKey);
|
||||||
|
if (cached) return res.json(cached);
|
||||||
|
|
||||||
|
const query = `
|
||||||
|
SELECT
|
||||||
|
team_id AS clan_id,
|
||||||
|
team_name AS squadron_name,
|
||||||
|
COUNT(DISTINCT UID) as player_count,
|
||||||
|
COUNT(*) as total_battles,
|
||||||
|
COUNT(DISTINCT session_id) as sessions,
|
||||||
|
COUNT(DISTINCT CASE WHEN UPPER(victor_bool) = 'WIN' THEN session_id END) as wins,
|
||||||
|
SUM(ground_kills + air_kills) as total_kills,
|
||||||
|
SUM(ground_kills) as ground_kills,
|
||||||
|
SUM(air_kills) as air_kills,
|
||||||
|
SUM(assists) as assists,
|
||||||
|
SUM(captures) as captures,
|
||||||
|
SUM(deaths) as deaths
|
||||||
|
FROM player_games_hist
|
||||||
|
WHERE team_name IS NOT NULL
|
||||||
|
AND team_name != ''
|
||||||
|
AND (? IS NULL OR endtime_unix >= ?)
|
||||||
|
AND (? IS NULL OR endtime_unix <= ?)
|
||||||
|
GROUP BY team_id, team_name
|
||||||
|
`;
|
||||||
|
|
||||||
|
Promise.all([
|
||||||
|
tssAll(tssHeavyDb, query, tssDateParams(dateFilters)),
|
||||||
|
new Promise(resolve => loadTssTeamLookupCached(resolve))
|
||||||
|
]).then(([rows, lookup]) => {
|
||||||
|
const consolidated = {};
|
||||||
|
for (const row of rows) {
|
||||||
|
const lookupRow = row.clan_id != null ? (lookup[String(row.clan_id)] || null) : null;
|
||||||
|
const key = row.clan_id != null ? `id:${row.clan_id}` : `name:${row.squadron_name}`;
|
||||||
|
if (!consolidated[key]) {
|
||||||
|
consolidated[key] = {
|
||||||
|
clan_id: row.clan_id || (lookupRow ? lookupRow.clan_id : null),
|
||||||
|
tag_name: lookupRow ? lookupRow.tag_name : row.squadron_name,
|
||||||
|
short_name: lookupRow ? lookupRow.short_name : row.squadron_name,
|
||||||
|
long_name: lookupRow ? lookupRow.long_name : row.squadron_name,
|
||||||
|
player_count: lookupRow ? (lookupRow.members || 0) : 0,
|
||||||
|
total_battles: 0,
|
||||||
|
wins: 0,
|
||||||
|
total_kills: 0,
|
||||||
|
ground_kills: 0,
|
||||||
|
air_kills: 0,
|
||||||
|
deaths: 0,
|
||||||
|
assists: 0,
|
||||||
|
captures: 0,
|
||||||
|
points: {
|
||||||
|
total_points: lookupRow ? (lookupRow.clanrating || 0) : 0,
|
||||||
|
has_points_data: !!(lookupRow && lookupRow.clanrating)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
const item = consolidated[key];
|
||||||
|
item.player_count = Math.max(item.player_count || 0, row.player_count || 0);
|
||||||
|
item.total_battles += row.sessions || row.total_battles || 0;
|
||||||
|
item.wins += row.wins || 0;
|
||||||
|
item.total_kills += row.total_kills || 0;
|
||||||
|
item.ground_kills += row.ground_kills || 0;
|
||||||
|
item.air_kills += row.air_kills || 0;
|
||||||
|
item.deaths += row.deaths || 0;
|
||||||
|
item.assists += row.assists || 0;
|
||||||
|
item.captures += row.captures || 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
const teams = Object.values(consolidated).map(team => ({
|
||||||
|
...team,
|
||||||
|
win_rate: team.total_battles > 0 ? parseFloat(((team.wins / team.total_battles) * 100).toFixed(1)) : 0,
|
||||||
|
kdr: team.deaths > 0 ? parseFloat((team.total_kills / team.deaths).toFixed(1)) : team.total_kills
|
||||||
|
})).sort((a, b) => {
|
||||||
|
if (a.points.has_points_data && b.points.has_points_data) return b.points.total_points - a.points.total_points;
|
||||||
|
if (a.points.has_points_data) return -1;
|
||||||
|
if (b.points.has_points_data) return 1;
|
||||||
|
return b.total_kills - a.total_kills;
|
||||||
|
});
|
||||||
|
|
||||||
|
const response = {
|
||||||
|
data_set: 'tss',
|
||||||
|
date_filter: createDateFilterMetadata(dateFilters, start_date, end_date, season, week),
|
||||||
|
total_teams: teams.length,
|
||||||
|
total_squadrons: teams.length,
|
||||||
|
teams: teams.slice(0, maxRows),
|
||||||
|
squadrons: teams.slice(0, maxRows)
|
||||||
|
};
|
||||||
|
setCachedResponse(cacheKey, response);
|
||||||
|
res.json(response);
|
||||||
|
}).catch(err => {
|
||||||
|
log.error('Database error in TSS team leaderboard', err);
|
||||||
|
res.status(500).json({ error: 'Database error', errorCode: 'DB_TSS_TEAM_LEADERBOARD_FAILED' });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
app.get('/api/tss/leaderboard/teams', handleTssTeamLeaderboard);
|
||||||
|
app.get('/api/tss/leaderboard/squadrons', handleTssTeamLeaderboard);
|
||||||
|
|
||||||
|
registerTssTeamRoutes('/api/tss/teams');
|
||||||
|
registerTssTeamRoutes('/api/tss/squadrons');
|
||||||
|
|
||||||
app.get('/api/leaderboard/stats', (req, res) => {
|
app.get('/api/leaderboard/stats', (req, res) => {
|
||||||
log.info('Leaderboard stats request received');
|
log.info('Leaderboard stats request received');
|
||||||
const dateFilters = parseDateFilters(req);
|
const dateFilters = parseDateFilters(req);
|
||||||
@@ -5927,6 +6599,10 @@ app.use((req, res) => {
|
|||||||
'GET /api/leaderboard/vehicles',
|
'GET /api/leaderboard/vehicles',
|
||||||
'GET /api/leaderboard/stats',
|
'GET /api/leaderboard/stats',
|
||||||
'GET /api/squadrons/:squadronname',
|
'GET /api/squadrons/:squadronname',
|
||||||
|
'GET /api/tss/leaderboard/teams',
|
||||||
|
'GET /api/tss/teams/:teamname',
|
||||||
|
'GET /api/tss/teams/:teamname/history',
|
||||||
|
'GET /api/tss/teams/:teamname/games',
|
||||||
'GET /api/analytics/maps/:squadron',
|
'GET /api/analytics/maps/:squadron',
|
||||||
'GET /api/analytics/player/squadmates/:uid',
|
'GET /api/analytics/player/squadmates/:uid',
|
||||||
'GET /api/analytics/time/:squadron',
|
'GET /api/analytics/time/:squadron',
|
||||||
@@ -5988,6 +6664,8 @@ process.on('unhandledRejection', (reason) => {
|
|||||||
|
|
||||||
process.on('SIGINT', () => {
|
process.on('SIGINT', () => {
|
||||||
console.log('\nShutting down server...');
|
console.log('\nShutting down server...');
|
||||||
|
if (tssHeavyDb) tssHeavyDb.close(() => {});
|
||||||
|
if (tssDb) tssDb.close(() => {});
|
||||||
db.close((err) => {
|
db.close((err) => {
|
||||||
if (err) {
|
if (err) {
|
||||||
console.error('Error closing database:', err.message);
|
console.error('Error closing database:', err.message);
|
||||||
|
|||||||
@@ -2181,6 +2181,98 @@ app.get('/api/squadrons/:name/games', cors(apiCorsOptions), async (req, res) =>
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ── TSS proxy routes ───────────────────────────────────────────────────────
|
||||||
|
// Mirrors the /api/squadrons/* proxy block above but targets the upstream
|
||||||
|
// /api/tss/* endpoints (defined in SREBOT/server.js) which read tss_battles.db
|
||||||
|
// and tss_teams.db. Both /api/tss/teams/* and /api/tss/squadrons/* are
|
||||||
|
// supported upstream — we proxy the canonical /teams/* path here.
|
||||||
|
const TSS_BACKEND = () => process.env.EXTERNAL_API_URL || 'http://127.0.0.1:6000';
|
||||||
|
|
||||||
|
function _forwardDateParams(req) {
|
||||||
|
const qp = new URLSearchParams();
|
||||||
|
if (req.query.start_date) qp.append('start_date', req.query.start_date);
|
||||||
|
if (req.query.end_date) qp.append('end_date', req.query.end_date);
|
||||||
|
if (req.query.season) qp.append('season', req.query.season);
|
||||||
|
if (req.query.week) qp.append('week', req.query.week);
|
||||||
|
return qp;
|
||||||
|
}
|
||||||
|
|
||||||
|
app.get('/api/tss/teams/resolve', cors(apiCorsOptions), async (req, res) => {
|
||||||
|
try {
|
||||||
|
const name = req.query.name || req.query.q || req.query.team || '';
|
||||||
|
const apiUrl = `${TSS_BACKEND()}/api/tss/teams/resolve?name=${encodeURIComponent(name)}`;
|
||||||
|
const response = await fetch(apiUrl);
|
||||||
|
if (!response.ok) return res.status(response.status).json({ error: 'Failed to resolve TSS team' });
|
||||||
|
res.json(await response.json());
|
||||||
|
} catch (err) {
|
||||||
|
log.error('Error resolving TSS team', err);
|
||||||
|
res.status(500).json({ error: 'Failed to resolve TSS team' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
app.get('/api/tss/leaderboard/teams', cors(apiCorsOptions), async (req, res) => {
|
||||||
|
try {
|
||||||
|
const qp = _forwardDateParams(req);
|
||||||
|
if (req.query.limit) qp.append('limit', req.query.limit);
|
||||||
|
const qs = qp.toString();
|
||||||
|
const apiUrl = `${TSS_BACKEND()}/api/tss/leaderboard/teams${qs ? '?' + qs : ''}`;
|
||||||
|
const response = await fetch(apiUrl);
|
||||||
|
if (!response.ok) return res.status(response.status).json({ error: 'Failed to fetch TSS leaderboard', teams: [], total_teams: 0 });
|
||||||
|
res.json(await response.json());
|
||||||
|
} catch (err) {
|
||||||
|
log.error('Error in TSS leaderboard proxy', err);
|
||||||
|
res.status(500).json({ error: 'Failed to fetch TSS leaderboard', teams: [], total_teams: 0 });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
app.get('/api/tss/teams/:name', cors(apiCorsOptions), async (req, res) => {
|
||||||
|
try {
|
||||||
|
const qs = _forwardDateParams(req).toString();
|
||||||
|
const apiUrl = `${TSS_BACKEND()}/api/tss/teams/${encodeURIComponent(req.params.name)}${qs ? '?' + qs : ''}`;
|
||||||
|
const response = await fetch(apiUrl);
|
||||||
|
if (!response.ok) {
|
||||||
|
if (response.status === 404) {
|
||||||
|
return res.status(404).json({ error: 'TSS team not found', players: [] });
|
||||||
|
}
|
||||||
|
return res.status(response.status).json({ error: 'Failed to fetch TSS team details' });
|
||||||
|
}
|
||||||
|
res.json(await response.json());
|
||||||
|
} catch (err) {
|
||||||
|
log.error('Error fetching TSS team details', err);
|
||||||
|
res.status(500).json({ error: 'Failed to fetch TSS team details' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
app.get('/api/tss/teams/:name/history', cors(apiCorsOptions), async (req, res) => {
|
||||||
|
try {
|
||||||
|
const apiUrl = `${TSS_BACKEND()}/api/tss/teams/${encodeURIComponent(req.params.name)}/history`;
|
||||||
|
const response = await fetch(apiUrl);
|
||||||
|
if (!response.ok) return res.status(response.status).json({ error: 'Failed to fetch TSS team history' });
|
||||||
|
res.json(await response.json());
|
||||||
|
} catch (err) {
|
||||||
|
log.error('Error fetching TSS team history', err);
|
||||||
|
res.status(500).json({ error: 'Failed to fetch TSS team history' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
app.get('/api/tss/teams/:name/games', cors(apiCorsOptions), async (req, res) => {
|
||||||
|
try {
|
||||||
|
const qs = _forwardDateParams(req).toString();
|
||||||
|
const apiUrl = `${TSS_BACKEND()}/api/tss/teams/${encodeURIComponent(req.params.name)}/games${qs ? '?' + qs : ''}`;
|
||||||
|
const response = await fetch(apiUrl);
|
||||||
|
if (!response.ok) {
|
||||||
|
if (response.status === 404) {
|
||||||
|
return res.status(404).json({ games: [], total_games_returned: 0, error: 'TSS team not found' });
|
||||||
|
}
|
||||||
|
return res.status(response.status).json({ error: 'Failed to fetch TSS team games' });
|
||||||
|
}
|
||||||
|
res.json(await response.json());
|
||||||
|
} catch (err) {
|
||||||
|
log.error('Error fetching TSS team games', err);
|
||||||
|
res.status(500).json({ error: 'Failed to fetch TSS team games', games: [], total_games_returned: 0 });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// API Routes with better error handling
|
// API Routes with better error handling
|
||||||
let statsCache = null;
|
let statsCache = null;
|
||||||
let statsCacheTime = 0;
|
let statsCacheTime = 0;
|
||||||
|
|||||||
Reference in New Issue
Block a user