refactor: optimize runtime
This commit is contained in:
parent
18284f7483
commit
13ea56dc66
252
app.js
252
app.js
@ -6,6 +6,7 @@ const API = require('./src/js/index.js');
|
||||
const demoTracker = require('./src/js/demoTracker.js');
|
||||
const { logger } = require('./src/js/logger.js');
|
||||
const favicon = require('serve-favicon');
|
||||
const fs = require('fs');
|
||||
const app = express();
|
||||
const port = process.env.PORT || 3512;
|
||||
require('./src/js/utils.js');
|
||||
@ -13,7 +14,6 @@ require('./src/js/utils.js');
|
||||
app.set('trust proxy', true);
|
||||
|
||||
// Middleware
|
||||
// app.use(bodyParser.json({ limit: '10mb' }));
|
||||
app.use(bodyParser.urlencoded({ extended: true, limit: '10mb' }));
|
||||
app.use(express.static(__dirname));
|
||||
app.use(express.static(path.join(__dirname, 'public')));
|
||||
@ -27,17 +27,8 @@ app.use(
|
||||
},
|
||||
})
|
||||
);
|
||||
// app.use(express.raw({ type: 'application/json', limit: '10mb' }));
|
||||
app.use(demoTracker.demoModeMiddleware);
|
||||
|
||||
// Set up demo mode cleanup interval
|
||||
setInterval(
|
||||
() => demoTracker.demoModeRequestTracker.cleanupExpiredEntries(),
|
||||
3600000
|
||||
); // Clean up every hour
|
||||
|
||||
const fs = require('fs');
|
||||
|
||||
// Initialize key replacements
|
||||
let keyReplacements = {};
|
||||
|
||||
@ -213,6 +204,80 @@ const handleApiError = (error, res) => {
|
||||
});
|
||||
};
|
||||
|
||||
// Helper function to remove sensitive data from headers
|
||||
function sanitizeHeaders(headers) {
|
||||
const safeHeaders = { ...headers };
|
||||
|
||||
// Remove potential sensitive information
|
||||
const sensitiveHeaders = ['authorization', 'cookie', 'set-cookie'];
|
||||
sensitiveHeaders.forEach((header) => {
|
||||
if (safeHeaders[header]) {
|
||||
safeHeaders[header] = '[REDACTED]';
|
||||
}
|
||||
});
|
||||
|
||||
return safeHeaders;
|
||||
}
|
||||
|
||||
// Set up demo mode cleanup interval
|
||||
setInterval(
|
||||
() => demoTracker.demoModeRequestTracker.cleanupExpiredEntries(),
|
||||
3600000
|
||||
); // Clean up every hour
|
||||
|
||||
app.get('/health', (req, res) => {
|
||||
const clientIP =
|
||||
req.headers['cf-connecting-ip'] ||
|
||||
req.headers['x-forwarded-for']?.split(',')[0] ||
|
||||
req.ip ||
|
||||
req.connection.remoteAddress;
|
||||
|
||||
const isDemoMode = demoTracker.isDemoModeActive();
|
||||
res.json({
|
||||
status: 'ok',
|
||||
timestamp: global.Utils.toIsoString(new Date()),
|
||||
demoMode: isDemoMode,
|
||||
requestsRemaining:
|
||||
isDemoMode ?
|
||||
demoTracker.demoModeRequestTracker.maxRequestsPerHour -
|
||||
(demoTracker.demoModeRequestTracker.requests.get(clientIP)?.count || 0)
|
||||
: null,
|
||||
});
|
||||
});
|
||||
|
||||
// Serve the main HTML file
|
||||
app.get('/', async (req, res) => {
|
||||
// Check demo mode query parameter
|
||||
if (req.query.demo === 'true') {
|
||||
await demoTracker.initializeDemoMode(true);
|
||||
} else if (req.query.demo === 'false') {
|
||||
await demoTracker.initializeDemoMode(false);
|
||||
}
|
||||
|
||||
res.sendFile(path.join(__dirname, 'src', 'index.html'));
|
||||
});
|
||||
|
||||
// Demo mode route - enables demo mode when visited
|
||||
app.get('/demo', async (req, res) => {
|
||||
const success = await demoTracker.initializeDemoMode(true);
|
||||
|
||||
if (success) {
|
||||
logger.info('Demo mode activated via /demo route');
|
||||
res.redirect('/?demo=true');
|
||||
} else {
|
||||
res
|
||||
.status(500)
|
||||
.send('Failed to enable demo mode. Check server logs for details.');
|
||||
}
|
||||
});
|
||||
|
||||
// Demo mode deactivation route
|
||||
app.get('/demo/disable', async (req, res) => {
|
||||
await demoTracker.initializeDemoMode(false);
|
||||
logger.info('Demo mode deactivated via /demo/disable route');
|
||||
res.redirect('/?demo=false');
|
||||
});
|
||||
|
||||
// API endpoint to fetch stats
|
||||
app.post('/api/stats', async (req, res) => {
|
||||
logger.debug('Received request for /api/stats');
|
||||
@ -507,16 +572,6 @@ app.post('/api/matches', async (req, res) => {
|
||||
logger.debug(`Processing Options - Sanitize: ${sanitize}, Replace Keys: ${replaceKeys}`);
|
||||
logger.debug("========================"); */
|
||||
|
||||
if (!username) {
|
||||
return res.status(400).json({ error: 'Username is required' });
|
||||
}
|
||||
|
||||
if (!username || !ssoToken) {
|
||||
return res
|
||||
.status(400)
|
||||
.json({ error: 'Username and SSO Token are required' });
|
||||
}
|
||||
|
||||
const defaultToken = demoTracker.getDefaultSsoToken();
|
||||
if (!ssoToken && defaultToken) {
|
||||
ssoToken = defaultToken;
|
||||
@ -529,6 +584,16 @@ app.post('/api/matches', async (req, res) => {
|
||||
});
|
||||
}
|
||||
|
||||
if (!username) {
|
||||
return res.status(400).json({ error: 'Username is required' });
|
||||
}
|
||||
|
||||
if (!username || !ssoToken) {
|
||||
return res
|
||||
.status(400)
|
||||
.json({ error: 'Username and SSO Token are required' });
|
||||
}
|
||||
|
||||
try {
|
||||
await ensureLogin(ssoToken);
|
||||
} catch (loginError) {
|
||||
@ -670,16 +735,6 @@ app.post('/api/matchInfo', async (req, res) => {
|
||||
logger.debug(`Processing Options - Sanitize: ${sanitize}, Replace Keys: ${replaceKeys}`);
|
||||
logger.debug("=========================="); */
|
||||
|
||||
if (!matchId) {
|
||||
return res.status(400).json({ error: 'Match ID is required' });
|
||||
}
|
||||
|
||||
if (!matchId || !ssoToken) {
|
||||
return res
|
||||
.status(400)
|
||||
.json({ error: 'Match ID and SSO Token are required' });
|
||||
}
|
||||
|
||||
const defaultToken = demoTracker.getDefaultSsoToken();
|
||||
if (!ssoToken && defaultToken) {
|
||||
ssoToken = defaultToken;
|
||||
@ -692,6 +747,16 @@ app.post('/api/matchInfo', async (req, res) => {
|
||||
});
|
||||
}
|
||||
|
||||
if (!matchId) {
|
||||
return res.status(400).json({ error: 'Match ID is required' });
|
||||
}
|
||||
|
||||
if (!matchId || !ssoToken) {
|
||||
return res
|
||||
.status(400)
|
||||
.json({ error: 'Match ID and SSO Token are required' });
|
||||
}
|
||||
|
||||
try {
|
||||
await ensureLogin(ssoToken);
|
||||
} catch (loginError) {
|
||||
@ -820,10 +885,17 @@ app.post('/api/user', async (req, res) => {
|
||||
logger.debug(`Processing Options - Sanitize: ${sanitize}, Replace Keys: ${replaceKeys}`);
|
||||
logger.debug("========================="); */
|
||||
|
||||
/*
|
||||
if (!ssoToken) {
|
||||
return res.status(400).json({ error: 'SSO Token is required' });
|
||||
} */
|
||||
const defaultToken = demoTracker.getDefaultSsoToken();
|
||||
if (!ssoToken && defaultToken) {
|
||||
ssoToken = defaultToken;
|
||||
logger.info('Using default SSO token for demo mode');
|
||||
} else if (!ssoToken) {
|
||||
return res.status(200).json({
|
||||
status: 'error',
|
||||
message: 'SSO Token is required',
|
||||
timestamp: global.Utils.toIsoString(new Date()),
|
||||
});
|
||||
}
|
||||
|
||||
// For eventFeed and identities, username is not required
|
||||
if (
|
||||
@ -837,18 +909,6 @@ app.post('/api/user', async (req, res) => {
|
||||
.json({ error: 'Username is required for this API call' });
|
||||
}
|
||||
|
||||
const defaultToken = demoTracker.getDefaultSsoToken();
|
||||
if (!ssoToken && defaultToken) {
|
||||
ssoToken = defaultToken;
|
||||
logger.info('Using default SSO token for demo mode');
|
||||
} else if (!ssoToken) {
|
||||
return res.status(200).json({
|
||||
status: 'error',
|
||||
message: 'SSO Token is required',
|
||||
timestamp: global.Utils.toIsoString(new Date()),
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
await ensureLogin(ssoToken);
|
||||
} catch (loginError) {
|
||||
@ -966,16 +1026,6 @@ app.post('/api/search', async (req, res) => {
|
||||
logger.debug(`Processing Options - Sanitize: ${sanitize}, Replace Keys: ${replaceKeys}`);
|
||||
logger.debug("=========================="); */
|
||||
|
||||
if (!username) {
|
||||
return res.status(400).json({ error: 'Username is required' });
|
||||
}
|
||||
|
||||
if (!username || !ssoToken) {
|
||||
return res
|
||||
.status(400)
|
||||
.json({ error: 'Username and SSO Token are required' });
|
||||
}
|
||||
|
||||
const defaultToken = demoTracker.getDefaultSsoToken();
|
||||
if (!ssoToken && defaultToken) {
|
||||
ssoToken = defaultToken;
|
||||
@ -988,6 +1038,16 @@ app.post('/api/search', async (req, res) => {
|
||||
});
|
||||
}
|
||||
|
||||
if (!username) {
|
||||
return res.status(400).json({ error: 'Username is required' });
|
||||
}
|
||||
|
||||
if (!username || !ssoToken) {
|
||||
return res
|
||||
.status(400)
|
||||
.json({ error: 'Username and SSO Token are required' });
|
||||
}
|
||||
|
||||
try {
|
||||
await ensureLogin(ssoToken);
|
||||
} catch (loginError) {
|
||||
@ -1092,21 +1152,6 @@ app.post('/api/log', (req, res) => {
|
||||
res.status(200).send();
|
||||
});
|
||||
|
||||
// Helper function to remove sensitive data from headers
|
||||
function sanitizeHeaders(headers) {
|
||||
const safeHeaders = { ...headers };
|
||||
|
||||
// Remove potential sensitive information
|
||||
const sensitiveHeaders = ['authorization', 'cookie', 'set-cookie'];
|
||||
sensitiveHeaders.forEach((header) => {
|
||||
if (safeHeaders[header]) {
|
||||
safeHeaders[header] = '[REDACTED]';
|
||||
}
|
||||
});
|
||||
|
||||
return safeHeaders;
|
||||
}
|
||||
|
||||
// Database storage function
|
||||
/*
|
||||
function storeLogInDatabase(logData) {
|
||||
@ -1116,71 +1161,6 @@ function storeLogInDatabase(logData) {
|
||||
}
|
||||
*/
|
||||
|
||||
app.get('/health', (req, res) => {
|
||||
const clientIP =
|
||||
req.headers['cf-connecting-ip'] ||
|
||||
req.headers['x-forwarded-for']?.split(',')[0] ||
|
||||
req.ip ||
|
||||
req.connection.remoteAddress;
|
||||
|
||||
const isDemoMode = demoTracker.isDemoModeActive();
|
||||
res.json({
|
||||
status: 'ok',
|
||||
timestamp: global.Utils.toIsoString(new Date()),
|
||||
demoMode: isDemoMode,
|
||||
requestsRemaining:
|
||||
isDemoMode ?
|
||||
demoTracker.demoModeRequestTracker.maxRequestsPerHour -
|
||||
(demoTracker.demoModeRequestTracker.requests.get(clientIP)?.count || 0)
|
||||
: null,
|
||||
});
|
||||
});
|
||||
|
||||
/*
|
||||
app.get('/demo', (req, res) => {
|
||||
// Set a cookie or session variable to enable demo mode
|
||||
if (!req.session) {
|
||||
req.session = {};
|
||||
}
|
||||
req.session.forceDemoMode = true;
|
||||
|
||||
// Redirect to the main app
|
||||
res.redirect('/');
|
||||
}); */
|
||||
|
||||
// Demo mode route - enables demo mode when visited
|
||||
app.get('/demo', async (req, res) => {
|
||||
const success = await demoTracker.initializeDemoMode(true);
|
||||
|
||||
if (success) {
|
||||
logger.info('Demo mode activated via /demo route');
|
||||
res.redirect('/?demo=true');
|
||||
} else {
|
||||
res
|
||||
.status(500)
|
||||
.send('Failed to enable demo mode. Check server logs for details.');
|
||||
}
|
||||
});
|
||||
|
||||
// Demo mode deactivation route
|
||||
app.get('/demo/disable', async (req, res) => {
|
||||
await demoTracker.initializeDemoMode(false);
|
||||
logger.info('Demo mode deactivated via /demo/disable route');
|
||||
res.redirect('/?demo=false');
|
||||
});
|
||||
|
||||
// Serve the main HTML file
|
||||
app.get('/', async (req, res) => {
|
||||
// Check demo mode query parameter
|
||||
if (req.query.demo === 'true') {
|
||||
await demoTracker.initializeDemoMode(true);
|
||||
} else if (req.query.demo === 'false') {
|
||||
await demoTracker.initializeDemoMode(false);
|
||||
}
|
||||
|
||||
res.sendFile(path.join(__dirname, 'src', 'index.html'));
|
||||
});
|
||||
|
||||
// Start the server
|
||||
app.listen(port, () => {
|
||||
logger.info(`Server running on http://localhost:${port}`);
|
||||
|
@ -1,3 +1,9 @@
|
||||
window.appState = {
|
||||
currentData: null,
|
||||
outputFormat: 'json',
|
||||
tutorialDismissed: false,
|
||||
};
|
||||
|
||||
// Export the functions that frontend.js needs to call
|
||||
window.backendAPI = {
|
||||
fetchData,
|
||||
@ -7,16 +13,57 @@ window.backendAPI = {
|
||||
processTimestamps,
|
||||
};
|
||||
|
||||
window.appState = {
|
||||
currentData: null,
|
||||
outputFormat: 'json',
|
||||
tutorialDismissed: false,
|
||||
};
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
// Backend-specific initialization
|
||||
});
|
||||
|
||||
// Function to convert seconds to human readable duration
|
||||
function formatDuration(seconds) {
|
||||
if (!seconds || isNaN(seconds)) return seconds;
|
||||
|
||||
// Convert to number in case it's a string
|
||||
const totalSeconds = parseFloat(seconds);
|
||||
|
||||
// Calculate days, hours, minutes, seconds
|
||||
const days = Math.floor(totalSeconds / 86400);
|
||||
const hours = Math.floor((totalSeconds % 86400) / 3600);
|
||||
const minutes = Math.floor((totalSeconds % 3600) / 60);
|
||||
const remainingSeconds = Math.floor(totalSeconds % 60);
|
||||
|
||||
return `${days} Days ${hours} Hours ${minutes} Minutes ${remainingSeconds} Seconds`;
|
||||
}
|
||||
|
||||
// Function to convert epoch time to human-readable format
|
||||
function formatEpochTime(epoch, timezone) {
|
||||
if (!epoch) return epoch;
|
||||
|
||||
// Check if epoch is in milliseconds (13 digits) or seconds (10 digits)
|
||||
const epochNumber = parseInt(epoch);
|
||||
if (isNaN(epochNumber)) return epoch;
|
||||
|
||||
// Convert to milliseconds if needed
|
||||
const epochMs =
|
||||
epochNumber.toString().length <= 10 ? epochNumber * 1000 : epochNumber;
|
||||
|
||||
// Parse the timezone offset
|
||||
let offset = 0;
|
||||
if (timezone !== 'UTC') {
|
||||
const match = timezone.match(/GMT([+-])(\d+)(?::(\d+))?/);
|
||||
if (match) {
|
||||
const sign = match[1] === '+' ? 1 : -1;
|
||||
const hours = parseInt(match[2]);
|
||||
const minutes = match[3] ? parseInt(match[3]) : 0;
|
||||
offset = sign * (hours * 60 + minutes) * 60 * 1000;
|
||||
}
|
||||
}
|
||||
|
||||
// Create a date object and adjust for timezone
|
||||
const date = new Date(epochMs + offset);
|
||||
|
||||
// Format the date
|
||||
return date.toUTCString().replace('GMT', timezone);
|
||||
}
|
||||
|
||||
// YAML conversion function
|
||||
function jsonToYAML(json) {
|
||||
const INDENT_SIZE = 2;
|
||||
@ -76,6 +123,61 @@ function jsonToYAML(json) {
|
||||
return formatValue(json, 0).substring(1); // Remove first newline
|
||||
}
|
||||
|
||||
// Function to recursively process timestamps and durations in the data
|
||||
function processTimestamps(
|
||||
data,
|
||||
timezone,
|
||||
keysToConvert = [
|
||||
'date',
|
||||
'dateAdded',
|
||||
'utcStartSeconds',
|
||||
'utcEndSeconds',
|
||||
'timestamp',
|
||||
'startTime',
|
||||
'endTime',
|
||||
],
|
||||
durationKeys = [
|
||||
'time',
|
||||
'timePlayedTotal',
|
||||
'timePlayed',
|
||||
'avgLifeTime',
|
||||
'duration',
|
||||
'objTime',
|
||||
]
|
||||
) {
|
||||
if (!data || typeof data !== 'object') return data;
|
||||
|
||||
if (Array.isArray(data)) {
|
||||
return data.map((item) =>
|
||||
processTimestamps(item, timezone, keysToConvert, durationKeys)
|
||||
);
|
||||
}
|
||||
|
||||
const result = {};
|
||||
for (const [key, value] of Object.entries(data)) {
|
||||
if (keysToConvert.includes(key) && typeof value === 'number') {
|
||||
result[key] = formatEpochTime(value, timezone);
|
||||
} else if (
|
||||
durationKeys.includes(key) &&
|
||||
typeof value === 'number' &&
|
||||
document.getElementById('replaceKeysOption').checked
|
||||
) {
|
||||
result[key] = formatDuration(value);
|
||||
} else if (typeof value === 'object' && value !== null) {
|
||||
result[key] = processTimestamps(
|
||||
value,
|
||||
timezone,
|
||||
keysToConvert,
|
||||
durationKeys
|
||||
);
|
||||
} else {
|
||||
result[key] = value;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
// Common fetch function
|
||||
async function fetchData(endpoint, requestData) {
|
||||
console.log(`[CLIENT] Request to ${endpoint} at ${new Date().toISOString()}`);
|
||||
@ -169,105 +271,3 @@ async function fetchData(endpoint, requestData) {
|
||||
loadingElement.style.display = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
// Function to convert seconds to human readable duration
|
||||
function formatDuration(seconds) {
|
||||
if (!seconds || isNaN(seconds)) return seconds;
|
||||
|
||||
// Convert to number in case it's a string
|
||||
const totalSeconds = parseFloat(seconds);
|
||||
|
||||
// Calculate days, hours, minutes, seconds
|
||||
const days = Math.floor(totalSeconds / 86400);
|
||||
const hours = Math.floor((totalSeconds % 86400) / 3600);
|
||||
const minutes = Math.floor((totalSeconds % 3600) / 60);
|
||||
const remainingSeconds = Math.floor(totalSeconds % 60);
|
||||
|
||||
return `${days} Days ${hours} Hours ${minutes} Minutes ${remainingSeconds} Seconds`;
|
||||
}
|
||||
|
||||
// Function to convert epoch time to human-readable format
|
||||
function formatEpochTime(epoch, timezone) {
|
||||
if (!epoch) return epoch;
|
||||
|
||||
// Check if epoch is in milliseconds (13 digits) or seconds (10 digits)
|
||||
const epochNumber = parseInt(epoch);
|
||||
if (isNaN(epochNumber)) return epoch;
|
||||
|
||||
// Convert to milliseconds if needed
|
||||
const epochMs =
|
||||
epochNumber.toString().length <= 10 ? epochNumber * 1000 : epochNumber;
|
||||
|
||||
// Parse the timezone offset
|
||||
let offset = 0;
|
||||
if (timezone !== 'UTC') {
|
||||
const match = timezone.match(/GMT([+-])(\d+)(?::(\d+))?/);
|
||||
if (match) {
|
||||
const sign = match[1] === '+' ? 1 : -1;
|
||||
const hours = parseInt(match[2]);
|
||||
const minutes = match[3] ? parseInt(match[3]) : 0;
|
||||
offset = sign * (hours * 60 + minutes) * 60 * 1000;
|
||||
}
|
||||
}
|
||||
|
||||
// Create a date object and adjust for timezone
|
||||
const date = new Date(epochMs + offset);
|
||||
|
||||
// Format the date
|
||||
return date.toUTCString().replace('GMT', timezone);
|
||||
}
|
||||
|
||||
// Function to recursively process timestamps and durations in the data
|
||||
function processTimestamps(
|
||||
data,
|
||||
timezone,
|
||||
keysToConvert = [
|
||||
'date',
|
||||
'dateAdded',
|
||||
'utcStartSeconds',
|
||||
'utcEndSeconds',
|
||||
'timestamp',
|
||||
'startTime',
|
||||
'endTime',
|
||||
],
|
||||
durationKeys = [
|
||||
'time',
|
||||
'timePlayedTotal',
|
||||
'timePlayed',
|
||||
'avgLifeTime',
|
||||
'duration',
|
||||
'objTime',
|
||||
]
|
||||
) {
|
||||
if (!data || typeof data !== 'object') return data;
|
||||
|
||||
if (Array.isArray(data)) {
|
||||
return data.map((item) =>
|
||||
processTimestamps(item, timezone, keysToConvert, durationKeys)
|
||||
);
|
||||
}
|
||||
|
||||
const result = {};
|
||||
for (const [key, value] of Object.entries(data)) {
|
||||
if (keysToConvert.includes(key) && typeof value === 'number') {
|
||||
result[key] = formatEpochTime(value, timezone);
|
||||
} else if (
|
||||
durationKeys.includes(key) &&
|
||||
typeof value === 'number' &&
|
||||
document.getElementById('replaceKeysOption').checked
|
||||
) {
|
||||
result[key] = formatDuration(value);
|
||||
} else if (typeof value === 'object' && value !== null) {
|
||||
result[key] = processTimestamps(
|
||||
value,
|
||||
timezone,
|
||||
keysToConvert,
|
||||
durationKeys
|
||||
);
|
||||
} else {
|
||||
result[key] = value;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
@ -3,6 +3,61 @@ window.uiAPI = {
|
||||
displayError,
|
||||
};
|
||||
|
||||
// Function to handle time and duration conversion
|
||||
function displayResults(data) {
|
||||
const resultsElement = document.getElementById('results');
|
||||
const downloadContainer = document.getElementById('download-container');
|
||||
|
||||
// Apply time conversion if enabled
|
||||
const convertTime = document.getElementById('convertTimeOption').checked;
|
||||
const replaceKeys = document.getElementById('replaceKeysOption').checked;
|
||||
let displayData = data;
|
||||
|
||||
if (convertTime || replaceKeys) {
|
||||
const timezone = document.getElementById('timezoneSelect').value;
|
||||
displayData = window.backendAPI.processTimestamps(
|
||||
structuredClone(data),
|
||||
timezone
|
||||
); // Use structured clone API instead of JSON.parse/stringify
|
||||
// displayData = window.backendAPI.processTimestamps(JSON.parse(JSON.stringify(data)), timezone);
|
||||
}
|
||||
|
||||
// Format the data
|
||||
let formattedData = '';
|
||||
if (window.appState.outputFormat === 'yaml') {
|
||||
formattedData = window.backendAPI.jsonToYAML(displayData);
|
||||
document.getElementById('downloadJson').textContent = 'Download YAML Data';
|
||||
} else {
|
||||
formattedData = JSON.stringify(displayData, null, 2);
|
||||
document.getElementById('downloadJson').textContent = 'Download JSON Data';
|
||||
}
|
||||
|
||||
resultsElement.textContent = formattedData;
|
||||
resultsElement.style.display = 'block';
|
||||
downloadContainer.style.display = 'block';
|
||||
}
|
||||
|
||||
// Helper function to display errors
|
||||
function displayError(message) {
|
||||
const errorElement = document.getElementById('error');
|
||||
const loadingElement = document.getElementById('loading');
|
||||
const resultsElement = document.getElementById('results');
|
||||
|
||||
errorElement.textContent = message;
|
||||
loadingElement.style.display = 'none';
|
||||
|
||||
// Clear previous results to ensure they can be redrawn
|
||||
resultsElement.style.display = 'none';
|
||||
resultsElement.textContent = '';
|
||||
|
||||
// Keep tutorial hidden if previously dismissed
|
||||
if (window.appState.tutorialDismissed) {
|
||||
document.querySelectorAll('.tutorial').forEach((element) => {
|
||||
element.style.display = 'none';
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Configure client-side logging settings
|
||||
const clientLogger = {
|
||||
// Control how much we log to reduce spam
|
||||
@ -57,25 +112,6 @@ const clientLogger = {
|
||||
},
|
||||
};
|
||||
|
||||
window.addEventListener('popstate', checkDemoMode);
|
||||
|
||||
// Initialize once DOM is loaded
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
initTabSwitching();
|
||||
addEnterKeyListeners();
|
||||
setupDownloadButton();
|
||||
setupFormatSelector();
|
||||
setupProcessingOptions();
|
||||
setupTimeOptions();
|
||||
addSyncListeners();
|
||||
initializeSessionTracking();
|
||||
checkDemoMode();
|
||||
// Call initially and then set up a refresh interval
|
||||
updateRequestLimitInfo();
|
||||
});
|
||||
|
||||
setInterval(updateRequestLimitInfo, 60000); // Update every minute
|
||||
|
||||
// Function to check if we're in demo mode
|
||||
function checkDemoMode() {
|
||||
console.log('Checking for demo mode...');
|
||||
@ -182,6 +218,23 @@ function updateRequestLimitInfo() {
|
||||
});
|
||||
}
|
||||
|
||||
window.addEventListener('popstate', checkDemoMode);
|
||||
|
||||
// Initialize once DOM is loaded
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
checkDemoMode(); // Check demo mode first
|
||||
initTabSwitching();
|
||||
addEnterKeyListeners();
|
||||
addSyncListeners();
|
||||
setupProcessingOptions();
|
||||
setupFormatSelector();
|
||||
setupTimeOptions();
|
||||
setupDownloadButton();
|
||||
initializeSessionTracking();
|
||||
// Call initially and then set up a refresh interval
|
||||
updateRequestLimitInfo();
|
||||
});
|
||||
|
||||
// Tab switching logic
|
||||
function initTabSwitching() {
|
||||
document.querySelectorAll('.tab').forEach((tab) => {
|
||||
@ -200,6 +253,60 @@ function initTabSwitching() {
|
||||
});
|
||||
}
|
||||
|
||||
function triggerActiveTabButton() {
|
||||
const activeTab = document
|
||||
.querySelector('.tab.active')
|
||||
.getAttribute('data-tab');
|
||||
switch (activeTab) {
|
||||
case 'stats':
|
||||
document.getElementById('fetchStats').click();
|
||||
break;
|
||||
case 'matches':
|
||||
document.getElementById('fetchMatches').click();
|
||||
break;
|
||||
case 'user':
|
||||
document.getElementById('fetchUserInfo').click();
|
||||
break;
|
||||
case 'other':
|
||||
document.getElementById('fuzzySearch').click();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
function addEnterKeyListeners() {
|
||||
// Use event delegation for handling Enter key press
|
||||
document.addEventListener('keypress', function (event) {
|
||||
if (event.key === 'Enter') {
|
||||
// Get the active element
|
||||
const activeElement = document.activeElement;
|
||||
|
||||
if (!activeElement || !activeElement.id) return;
|
||||
|
||||
// Mapping of input fields to their submit buttons
|
||||
const inputToButtonMapping = {
|
||||
ssoToken: null, // Will trigger active tab button
|
||||
username: null, // Will trigger active tab button
|
||||
matchUsername: 'fetchMatches',
|
||||
matchId: 'fetchMatchInfo',
|
||||
userUsername: 'fetchUserInfo',
|
||||
searchUsername: 'fuzzySearch',
|
||||
};
|
||||
|
||||
if (activeElement.id in inputToButtonMapping) {
|
||||
if (inputToButtonMapping[activeElement.id]) {
|
||||
// Click the specific button
|
||||
document
|
||||
.getElementById(inputToButtonMapping[activeElement.id])
|
||||
.click();
|
||||
} else {
|
||||
// Trigger the active tab button
|
||||
triggerActiveTabButton();
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Setup processing options (sanitize/replace)
|
||||
function setupProcessingOptions() {
|
||||
document
|
||||
@ -240,238 +347,6 @@ function setupFormatSelector() {
|
||||
});
|
||||
}
|
||||
|
||||
// Fetch stats
|
||||
document.getElementById('fetchStats').addEventListener('click', async () => {
|
||||
const username = document.getElementById('username').value.trim();
|
||||
const ssoToken = document.getElementById('ssoToken').value.trim();
|
||||
const platform = document.getElementById('platform').value;
|
||||
const game = document.getElementById('game').value;
|
||||
const apiCall = document.getElementById('apiCall').value;
|
||||
|
||||
const sanitize = document.getElementById('sanitizeOption').checked;
|
||||
const replaceKeys = document.getElementById('replaceKeysOption').checked;
|
||||
|
||||
await window.backendAPI.fetchData('/api/stats', {
|
||||
username,
|
||||
ssoToken,
|
||||
platform,
|
||||
game,
|
||||
apiCall,
|
||||
sanitize,
|
||||
replaceKeys,
|
||||
});
|
||||
});
|
||||
|
||||
// Fetch match history
|
||||
document.getElementById('fetchMatches').addEventListener('click', async () => {
|
||||
const username = document.getElementById('matchUsername').value.trim();
|
||||
const ssoToken = document.getElementById('ssoToken').value.trim();
|
||||
const platform = document.getElementById('matchPlatform').value;
|
||||
const game = document.getElementById('matchGame').value;
|
||||
|
||||
const sanitize = document.getElementById('sanitizeOption').checked;
|
||||
const replaceKeys = document.getElementById('replaceKeysOption').checked;
|
||||
|
||||
await window.backendAPI.fetchData('/api/matches', {
|
||||
username,
|
||||
ssoToken,
|
||||
platform,
|
||||
game,
|
||||
sanitize,
|
||||
replaceKeys,
|
||||
});
|
||||
});
|
||||
|
||||
// Fetch match details
|
||||
document
|
||||
.getElementById('fetchMatchInfo')
|
||||
.addEventListener('click', async () => {
|
||||
const matchId = document.getElementById('matchId').value.trim();
|
||||
const ssoToken = document.getElementById('ssoToken').value.trim();
|
||||
const platform = document.getElementById('matchPlatform').value;
|
||||
const game = document.getElementById('matchGame').value;
|
||||
|
||||
const sanitize = document.getElementById('sanitizeOption').checked;
|
||||
const replaceKeys = document.getElementById('replaceKeysOption').checked;
|
||||
|
||||
if (!matchId) {
|
||||
displayError('Match ID is required');
|
||||
return;
|
||||
}
|
||||
|
||||
await window.backendAPI.fetchData('/api/matchInfo', {
|
||||
matchId,
|
||||
ssoToken,
|
||||
platform,
|
||||
game,
|
||||
sanitize,
|
||||
replaceKeys,
|
||||
});
|
||||
});
|
||||
|
||||
// Fetch user info
|
||||
document.getElementById('fetchUserInfo').addEventListener('click', async () => {
|
||||
const username = document.getElementById('userUsername').value.trim();
|
||||
const ssoToken = document.getElementById('ssoToken').value.trim();
|
||||
const platform = document.getElementById('userPlatform').value;
|
||||
const userCall = document.getElementById('userCall').value;
|
||||
|
||||
const sanitize = document.getElementById('sanitizeOption').checked;
|
||||
const replaceKeys = document.getElementById('replaceKeysOption').checked;
|
||||
|
||||
// For event feed and identities, username is not required
|
||||
if (
|
||||
!username &&
|
||||
userCall !== 'eventFeed' &&
|
||||
userCall !== 'friendFeed' &&
|
||||
userCall !== 'identities'
|
||||
) {
|
||||
displayError('Username is required for this API call');
|
||||
return;
|
||||
}
|
||||
|
||||
await window.backendAPI.fetchData('/api/user', {
|
||||
username,
|
||||
ssoToken,
|
||||
platform,
|
||||
userCall,
|
||||
sanitize,
|
||||
replaceKeys,
|
||||
});
|
||||
});
|
||||
|
||||
// Fuzzy search
|
||||
document.getElementById('fuzzySearch').addEventListener('click', async () => {
|
||||
const username = document.getElementById('searchUsername').value.trim();
|
||||
const ssoToken = document.getElementById('ssoToken').value.trim();
|
||||
const platform = document.getElementById('searchPlatform').value;
|
||||
|
||||
const sanitize = document.getElementById('sanitizeOption').checked;
|
||||
const replaceKeys = document.getElementById('replaceKeysOption').checked;
|
||||
|
||||
if (!username) {
|
||||
displayError('Username is required for search');
|
||||
return;
|
||||
}
|
||||
|
||||
await window.backendAPI.fetchData('/api/search', {
|
||||
username,
|
||||
ssoToken,
|
||||
platform,
|
||||
sanitize,
|
||||
replaceKeys,
|
||||
});
|
||||
});
|
||||
|
||||
// Function to handle time and duration conversion
|
||||
function displayResults(data) {
|
||||
const resultsElement = document.getElementById('results');
|
||||
const downloadContainer = document.getElementById('download-container');
|
||||
|
||||
// Apply time conversion if enabled
|
||||
const convertTime = document.getElementById('convertTimeOption').checked;
|
||||
const replaceKeys = document.getElementById('replaceKeysOption').checked;
|
||||
let displayData = data;
|
||||
|
||||
if (convertTime || replaceKeys) {
|
||||
const timezone = document.getElementById('timezoneSelect').value;
|
||||
displayData = window.backendAPI.processTimestamps(
|
||||
structuredClone(data),
|
||||
timezone
|
||||
); // Use structured clone API instead of JSON.parse/stringify
|
||||
// displayData = window.backendAPI.processTimestamps(JSON.parse(JSON.stringify(data)), timezone);
|
||||
}
|
||||
|
||||
// Format the data
|
||||
let formattedData = '';
|
||||
if (window.appState.outputFormat === 'yaml') {
|
||||
formattedData = window.backendAPI.jsonToYAML(displayData);
|
||||
document.getElementById('downloadJson').textContent = 'Download YAML Data';
|
||||
} else {
|
||||
formattedData = JSON.stringify(displayData, null, 2);
|
||||
document.getElementById('downloadJson').textContent = 'Download JSON Data';
|
||||
}
|
||||
|
||||
resultsElement.textContent = formattedData;
|
||||
resultsElement.style.display = 'block';
|
||||
downloadContainer.style.display = 'block';
|
||||
}
|
||||
|
||||
// Helper function to display errors
|
||||
function displayError(message) {
|
||||
const errorElement = document.getElementById('error');
|
||||
const loadingElement = document.getElementById('loading');
|
||||
const resultsElement = document.getElementById('results');
|
||||
|
||||
errorElement.textContent = message;
|
||||
loadingElement.style.display = 'none';
|
||||
|
||||
// Clear previous results to ensure they can be redrawn
|
||||
resultsElement.style.display = 'none';
|
||||
resultsElement.textContent = '';
|
||||
|
||||
// Keep tutorial hidden if previously dismissed
|
||||
if (window.appState.tutorialDismissed) {
|
||||
document.querySelectorAll('.tutorial').forEach((element) => {
|
||||
element.style.display = 'none';
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function addEnterKeyListeners() {
|
||||
// Use event delegation for handling Enter key press
|
||||
document.addEventListener('keypress', function (event) {
|
||||
if (event.key === 'Enter') {
|
||||
// Get the active element
|
||||
const activeElement = document.activeElement;
|
||||
|
||||
if (!activeElement || !activeElement.id) return;
|
||||
|
||||
// Mapping of input fields to their submit buttons
|
||||
const inputToButtonMapping = {
|
||||
ssoToken: null, // Will trigger active tab button
|
||||
username: null, // Will trigger active tab button
|
||||
matchUsername: 'fetchMatches',
|
||||
matchId: 'fetchMatchInfo',
|
||||
userUsername: 'fetchUserInfo',
|
||||
searchUsername: 'fuzzySearch',
|
||||
};
|
||||
|
||||
if (activeElement.id in inputToButtonMapping) {
|
||||
if (inputToButtonMapping[activeElement.id]) {
|
||||
// Click the specific button
|
||||
document
|
||||
.getElementById(inputToButtonMapping[activeElement.id])
|
||||
.click();
|
||||
} else {
|
||||
// Trigger the active tab button
|
||||
triggerActiveTabButton();
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function triggerActiveTabButton() {
|
||||
const activeTab = document
|
||||
.querySelector('.tab.active')
|
||||
.getAttribute('data-tab');
|
||||
switch (activeTab) {
|
||||
case 'stats':
|
||||
document.getElementById('fetchStats').click();
|
||||
break;
|
||||
case 'matches':
|
||||
document.getElementById('fetchMatches').click();
|
||||
break;
|
||||
case 'user':
|
||||
document.getElementById('fetchUserInfo').click();
|
||||
break;
|
||||
case 'other':
|
||||
document.getElementById('fuzzySearch').click();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Time options
|
||||
function setupTimeOptions() {
|
||||
const convertTimeCheckbox = document.getElementById('convertTimeOption');
|
||||
@ -535,6 +410,7 @@ function setupDownloadButton() {
|
||||
document.body.removeChild(a);
|
||||
});
|
||||
}
|
||||
|
||||
// Function to synchronize username across tabs
|
||||
function syncUsernames() {
|
||||
const mainUsername = document.getElementById('username').value.trim();
|
||||
@ -823,3 +699,126 @@ function initializeSessionTracking() {
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Fetch stats
|
||||
document.getElementById('fetchStats').addEventListener('click', async () => {
|
||||
const username = document.getElementById('username').value.trim();
|
||||
const ssoToken = document.getElementById('ssoToken').value.trim();
|
||||
const platform = document.getElementById('platform').value;
|
||||
const game = document.getElementById('game').value;
|
||||
const apiCall = document.getElementById('apiCall').value;
|
||||
|
||||
const sanitize = document.getElementById('sanitizeOption').checked;
|
||||
const replaceKeys = document.getElementById('replaceKeysOption').checked;
|
||||
|
||||
await window.backendAPI.fetchData('/api/stats', {
|
||||
username,
|
||||
ssoToken,
|
||||
platform,
|
||||
game,
|
||||
apiCall,
|
||||
sanitize,
|
||||
replaceKeys,
|
||||
});
|
||||
});
|
||||
|
||||
// Fetch match history
|
||||
document.getElementById('fetchMatches').addEventListener('click', async () => {
|
||||
const username = document.getElementById('matchUsername').value.trim();
|
||||
const ssoToken = document.getElementById('ssoToken').value.trim();
|
||||
const platform = document.getElementById('matchPlatform').value;
|
||||
const game = document.getElementById('matchGame').value;
|
||||
|
||||
const sanitize = document.getElementById('sanitizeOption').checked;
|
||||
const replaceKeys = document.getElementById('replaceKeysOption').checked;
|
||||
|
||||
await window.backendAPI.fetchData('/api/matches', {
|
||||
username,
|
||||
ssoToken,
|
||||
platform,
|
||||
game,
|
||||
sanitize,
|
||||
replaceKeys,
|
||||
});
|
||||
});
|
||||
|
||||
// Fetch match details
|
||||
document
|
||||
.getElementById('fetchMatchInfo')
|
||||
.addEventListener('click', async () => {
|
||||
const matchId = document.getElementById('matchId').value.trim();
|
||||
const ssoToken = document.getElementById('ssoToken').value.trim();
|
||||
const platform = document.getElementById('matchPlatform').value;
|
||||
const game = document.getElementById('matchGame').value;
|
||||
|
||||
const sanitize = document.getElementById('sanitizeOption').checked;
|
||||
const replaceKeys = document.getElementById('replaceKeysOption').checked;
|
||||
|
||||
if (!matchId) {
|
||||
displayError('Match ID is required');
|
||||
return;
|
||||
}
|
||||
|
||||
await window.backendAPI.fetchData('/api/matchInfo', {
|
||||
matchId,
|
||||
ssoToken,
|
||||
platform,
|
||||
game,
|
||||
sanitize,
|
||||
replaceKeys,
|
||||
});
|
||||
});
|
||||
|
||||
// Fetch user info
|
||||
document.getElementById('fetchUserInfo').addEventListener('click', async () => {
|
||||
const username = document.getElementById('userUsername').value.trim();
|
||||
const ssoToken = document.getElementById('ssoToken').value.trim();
|
||||
const platform = document.getElementById('userPlatform').value;
|
||||
const userCall = document.getElementById('userCall').value;
|
||||
|
||||
const sanitize = document.getElementById('sanitizeOption').checked;
|
||||
const replaceKeys = document.getElementById('replaceKeysOption').checked;
|
||||
|
||||
// For event feed and identities, username is not required
|
||||
if (
|
||||
!username &&
|
||||
userCall !== 'eventFeed' &&
|
||||
userCall !== 'friendFeed' &&
|
||||
userCall !== 'identities'
|
||||
) {
|
||||
displayError('Username is required for this API call');
|
||||
return;
|
||||
}
|
||||
|
||||
await window.backendAPI.fetchData('/api/user', {
|
||||
username,
|
||||
ssoToken,
|
||||
platform,
|
||||
userCall,
|
||||
sanitize,
|
||||
replaceKeys,
|
||||
});
|
||||
});
|
||||
|
||||
// Fuzzy search
|
||||
document.getElementById('fuzzySearch').addEventListener('click', async () => {
|
||||
const username = document.getElementById('searchUsername').value.trim();
|
||||
const ssoToken = document.getElementById('ssoToken').value.trim();
|
||||
const platform = document.getElementById('searchPlatform').value;
|
||||
|
||||
const sanitize = document.getElementById('sanitizeOption').checked;
|
||||
const replaceKeys = document.getElementById('replaceKeysOption').checked;
|
||||
|
||||
if (!username) {
|
||||
displayError('Username is required for search');
|
||||
return;
|
||||
}
|
||||
|
||||
await window.backendAPI.fetchData('/api/search', {
|
||||
username,
|
||||
ssoToken,
|
||||
platform,
|
||||
sanitize,
|
||||
replaceKeys,
|
||||
});
|
||||
});
|
||||
|
Loading…
x
Reference in New Issue
Block a user