From 13ea56dc6646c7b568e57fbe3ce8367c19034382 Mon Sep 17 00:00:00 2001 From: Rim Date: Wed, 16 Apr 2025 09:07:54 -0400 Subject: [PATCH] refactor: optimize runtime --- app.js | 252 +++++++++++------------ src/js/backend.js | 216 +++++++++---------- src/js/frontend.js | 501 ++++++++++++++++++++++----------------------- 3 files changed, 474 insertions(+), 495 deletions(-) diff --git a/app.js b/app.js index 79fd437..c7c3df1 100644 --- a/app.js +++ b/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}`); diff --git a/src/js/backend.js b/src/js/backend.js index cef4b08..ecc1fa5 100644 --- a/src/js/backend.js +++ b/src/js/backend.js @@ -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; -} diff --git a/src/js/frontend.js b/src/js/frontend.js index 0c026a0..ca8509e 100644 --- a/src/js/frontend.js +++ b/src/js/frontend.js @@ -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, + }); +});