/** * Analytics & Developer Console * * Hidden developer tool for real-time tournament diagnostics, validation checks, * and developer commands. The Console tab on the Tournament Bracket page, shown when it is * enabled in Global Settings (#devConsoleView, css/dev-console.css; showBracketView() in * js/bracket-rendering.js switches to it and away). * * Features: * - Real-time statistics (transactions, matches, players, lanes) * - 6 validation checks (lanes, referees, match state, transactions, player IDs, progression) * - Developer commands (re-render, recalculate, validate, refresh) * - Console output capture and display * - Auto-refresh every 2 seconds (pauses while scrolling) */ // Console output capture let consoleBuffer = []; let originalConsoleLog = null; let maxConsoleEntries = 100; let lastConsoleUpdateLength = 0; // Track when console actually changes // Auto-refresh system let analyticsRefreshInterval = null; let isUserScrolling = false; let scrollTimeout = null; // Current right pane view let currentView = 'overview'; /** * Start the Developer Console: capture console output, draw it, refresh every 2 seconds. * Called when its tab is shown; stopDeveloperConsole() when it is left. */ function startDeveloperConsole() { if (!document.getElementById('devConsoleView')) return; startConsoleCapture(); setupScrollDetection(); refreshAnalytics(); showQuickOverview(); startAutoRefresh(); console.log('Developer Console opened'); } /** * Stop the Developer Console's capture and refresh (its tab was left, or the page). * Safe to call when it is not running. */ function stopDeveloperConsole() { if (!originalConsoleLog && !analyticsRefreshInterval) return; stopConsoleCapture(); stopAutoRefresh(); console.log('Developer Console closed'); } /** * Start capturing console.log output */ function startConsoleCapture() { if (originalConsoleLog) return; // Already capturing originalConsoleLog = console.log; console.log = function(...args) { // Call original console.log originalConsoleLog.apply(console, args); // Capture to buffer const timestamp = new Date().toLocaleTimeString(); const message = args.map(arg => { if (typeof arg === 'object') { try { return JSON.stringify(arg, null, 2); } catch (e) { return String(arg); } } return String(arg); }).join(' '); consoleBuffer.push({ timestamp, message }); // Keep only last N entries if (consoleBuffer.length > maxConsoleEntries) { consoleBuffer.shift(); } // Update view if console is currently displayed if (currentView === 'console') { showConsoleOutput(); } }; } /** * Stop capturing console.log output */ function stopConsoleCapture() { if (originalConsoleLog) { console.log = originalConsoleLog; originalConsoleLog = null; } } /** * Start auto-refresh (every 2 seconds) */ function startAutoRefresh() { if (analyticsRefreshInterval) return; // Already running analyticsRefreshInterval = setInterval(() => { if (!isUserScrolling) { refreshAnalytics(); } }, 2000); } /** * Stop auto-refresh */ function stopAutoRefresh() { if (analyticsRefreshInterval) { clearInterval(analyticsRefreshInterval); analyticsRefreshInterval = null; } } /** * Detect user scrolling and pause refresh */ let scrollDetectionReady = false; function setupScrollDetection() { if (scrollDetectionReady) return; // the panes stay; listen once scrollDetectionReady = true; const contentArea = document.getElementById('analyticsContentArea'); const consoleFooter = document.getElementById('consoleOutputContent'); // the log scrolls, not its panel const handleScroll = () => { isUserScrolling = true; // Clear existing timeout if (scrollTimeout) { clearTimeout(scrollTimeout); } // Resume refresh 1 second after scroll stops scrollTimeout = setTimeout(() => { isUserScrolling = false; }, 1000); }; if (contentArea) { contentArea.addEventListener('scroll', handleScroll); } if (consoleFooter) { consoleFooter.addEventListener('scroll', handleScroll); } } /** * Refresh all analytics data and update left pane */ function refreshAnalytics() { updateStatisticsPane(); updateTimestamp(); // Refresh current view (some views update in real-time, others stay static) if (currentView === 'overview') { showQuickOverview(); } else if (currentView === 'lanes') { showLaneUsageDetails(); } // Always update console footer updateConsoleFooter(); } /** * Update statistics in left pane */ function updateStatisticsPane() { const stats = getTransactionStats(); const matchStats = getMatchStateStats(); const playerStats = getPlayerStats(); const laneStats = getLaneStats(); // Update Transaction Health const transactionEl = document.getElementById('stat-transactions'); if (transactionEl) { const percentage = Math.round((stats.total / 1000) * 100); transactionEl.innerHTML = `${_dcDot(percentage < 50 ? 'ok' : percentage < 80 ? 'warn' : 'bad')}${stats.total} of 1000 (${percentage}%)`; } // Update Match State const matchEl = document.getElementById('stat-matches'); if (matchEl) { matchEl.innerHTML = `${matchStats.completed} done · ${matchStats.live} live · ${matchStats.ready} ready`; } // Update Player Count const playerEl = document.getElementById('stat-players'); if (playerEl) { playerEl.innerHTML = `${playerStats.paid} paid · ${playerStats.unpaid} unpaid`; } // Update Lane Usage const laneEl = document.getElementById('stat-lanes'); if (laneEl) { const hasLiveConflicts = laneStats.conflicts > 0; const hasReadyConflicts = laneStats.readyConflicts > 0; laneEl.innerHTML = `${_dcDot(hasLiveConflicts ? 'bad' : hasReadyConflicts ? 'warn' : 'ok')}${laneStats.inUse} of ${laneStats.max} in use`; } // Update localStorage Usage const storageEl = document.getElementById('stat-storage'); if (storageEl) { const storageStats = getLocalStorageStats(); const percentage = Math.round((storageStats.used / storageStats.limit) * 100); storageEl.innerHTML = `${_dcDot(percentage < 50 ? 'ok' : percentage < 80 ? 'warn' : 'bad')}${storageStats.used.toFixed(2)} of ${storageStats.limit} MB`; } // Update Quick Overview (tournament duration) const overviewEl = document.getElementById('stat-overview'); if (overviewEl) { // Get tournament duration const timingStats = getTournamentTimingStats(); overviewEl.textContent = `Duration ${timingStats.tournamentDuration}`; } } /** A status dot for the left pane. @param {'ok'|'warn'|'bad'} tone */ function _dcDot(tone) { return ``; } /** * Update last updated timestamp */ function updateTimestamp() { const timestampEl = document.getElementById('analytics-timestamp'); if (timestampEl) { const now = new Date(); timestampEl.textContent = `Updated ${now.toLocaleTimeString()} · every 2 seconds`; } } /** * Get transaction statistics */ function getTransactionStats() { const history = getTournamentHistory ? getTournamentHistory() : []; const breakdown = { COMPLETE_MATCH: 0, START_MATCH: 0, STOP_MATCH: 0, ASSIGN_LANE: 0, ASSIGN_REFEREE: 0 }; history.forEach(tx => { if (breakdown.hasOwnProperty(tx.type)) { breakdown[tx.type]++; } }); return { total: history.length, breakdown: breakdown }; } /** * Get match state statistics */ function getMatchStateStats() { if (!matches || matches.length === 0) { return { total: 0, completed: 0, live: 0, ready: 0, pending: 0 }; } const stats = { total: matches.length, completed: 0, live: 0, ready: 0, pending: 0 }; matches.forEach(match => { const state = getMatchState ? getMatchState(match) : 'unknown'; if (stats.hasOwnProperty(state)) { stats[state]++; } }); return stats; } /** * Get player statistics */ function getPlayerStats() { if (!players || players.length === 0) { return { total: 0, paid: 0, unpaid: 0 }; } const paid = players.filter(p => p.paid).length; return { total: players.length, paid: paid, unpaid: players.length - paid }; } /** * Get lane usage statistics */ function getLaneStats() { const maxLanes = (config && config.lanes && config.lanes.maxLanes) || 10; const excludedLanes = (config && config.lanes && config.lanes.excludedLanes) || []; const usedLanes = getUsedLanes ? getUsedLanes() : []; const validation = validateLaneAssignments ? validateLaneAssignments() : { valid: true, conflicts: [], readyConflicts: [] }; // Calculate effective available lanes (total minus excluded) const availableLanes = maxLanes - excludedLanes.length; return { max: availableLanes, inUse: usedLanes.length, conflicts: validation.conflicts.length, readyConflicts: validation.readyConflicts ? validation.readyConflicts.length : 0 }; } /** * Get localStorage usage statistics */ function getLocalStorageStats() { let totalSize = 0; const items = {}; for (let key in localStorage) { if (localStorage.hasOwnProperty(key)) { const itemSize = (localStorage[key].length + key.length) * 2; // UTF-16 = 2 bytes per char items[key] = itemSize; totalSize += itemSize; } } // Browser limit is 10MB for modern browsers (Chrome 114+, Firefox, Safari, Edge) const limitBytes = 10 * 1024 * 1024; const usedMB = totalSize / 1024 / 1024; const limitMB = limitBytes / 1024 / 1024; return { used: usedMB, limit: limitMB, percentage: Math.round((usedMB / limitMB) * 100), items: items }; } // ============================================================================ // RIGHT PANE VIEWS // ============================================================================ /** * Calculate tournament timing statistics from transaction history */ function getTournamentTimingStats() { const history = getTournamentHistory ? getTournamentHistory() : []; const status = tournament?.status || 'setup'; // Find all START_MATCH and COMPLETE_MATCH transactions const startTxns = history.filter(t => t.type === 'START_MATCH'); const completeTxns = history.filter(t => t.type === 'COMPLETE_MATCH'); if (completeTxns.length === 0) { return { tournamentDuration: status === 'setup' ? 'Not started' : '0m', avgMatchTime: 'N/A', shortestMatch: 'N/A', longestMatch: 'N/A' }; } // Calculate match durations const matchDurations = []; completeTxns.forEach(completeTxn => { const matchId = completeTxn.matchId; // Find corresponding START_MATCH const startTxn = startTxns.find(t => t.matchId === matchId); if (startTxn) { const startTime = new Date(startTxn.timestamp); const endTime = new Date(completeTxn.timestamp); const durationMs = endTime - startTime; matchDurations.push({ matchId: matchId, duration: durationMs, durationMinutes: Math.round(durationMs / 1000 / 60) }); } }); // Calculate tournament duration let tournamentDuration = 'N/A'; if (startTxns.length > 0) { const firstStart = new Date(Math.min(...startTxns.map(t => new Date(t.timestamp)))); let endTime; if (status === 'completed') { // Use last COMPLETE_MATCH timestamp endTime = new Date(Math.max(...completeTxns.map(t => new Date(t.timestamp)))); } else { // Use current time (dynamic) endTime = new Date(); } const totalMs = endTime - firstStart; tournamentDuration = formatDuration(totalMs); } // Calculate average match time let avgMatchTime = 'N/A'; if (matchDurations.length > 0) { const avgMs = matchDurations.reduce((sum, m) => sum + m.duration, 0) / matchDurations.length; avgMatchTime = formatDuration(avgMs); } // Find shortest and longest matches let shortestMatch = 'N/A'; let longestMatch = 'N/A'; if (matchDurations.length > 0) { const shortest = matchDurations.reduce((min, m) => m.duration < min.duration ? m : min); const longest = matchDurations.reduce((max, m) => m.duration > max.duration ? m : max); shortestMatch = `${formatDuration(shortest.duration)} (${shortest.matchId})`; longestMatch = `${formatDuration(longest.duration)} (${longest.matchId})`; } return { tournamentDuration, avgMatchTime, shortestMatch, longestMatch }; } /** * Format duration from milliseconds to HH:MM:SS string */ function formatDuration(ms) { const totalSeconds = Math.floor(ms / 1000); const hours = Math.floor(totalSeconds / 3600); const minutes = Math.floor((totalSeconds % 3600) / 60); const seconds = totalSeconds % 60; // Always show hours if > 0, otherwise start with minutes if (hours > 0) { return `${hours}:${String(minutes).padStart(2, '0')}:${String(seconds).padStart(2, '0')}`; } else { return `${minutes}:${String(seconds).padStart(2, '0')}`; } } /** * Show Quick Overview (default view) */ function showQuickOverview() { currentView = 'overview'; const stats = getTransactionStats(); const matchStats = getMatchStateStats(); const laneStats = getLaneStats(); const validation = validateLaneAssignments ? validateLaneAssignments() : { valid: true }; const timingStats = getTournamentTimingStats(); const transactionPercentage = Math.round((stats.total / 1000) * 100); const matchPercentage = matchStats.total > 0 ? Math.round((matchStats.completed / matchStats.total) * 100) : 0; const laneHasLiveConflicts = laneStats.conflicts > 0; const laneHasReadyConflicts = laneStats.readyConflicts > 0; const refereeValidation = validateRefereeAssignments(); const refereeHasConflicts = !refereeValidation.valid; const allHealthy = !laneHasLiveConflicts && !laneHasReadyConflicts && !refereeHasConflicts; const hasCriticalIssues = laneHasLiveConflicts || refereeHasConflicts; const statusColor = hasCriticalIssues ? '#dc2626' : (laneHasReadyConflicts ? '#ca8a04' : '#166534'); const bgColor = hasCriticalIssues ? '#fef2f2' : (laneHasReadyConflicts ? '#fefce8' : '#f0fdf4'); const borderColor = hasCriticalIssues ? '#dc2626' : (laneHasReadyConflicts ? '#ca8a04' : '#166534'); const statusIcon = hasCriticalIssues ? '⚠️' : (laneHasReadyConflicts ? '⚠️' : '✓'); const html = `

Quick Overview

${statusIcon} ${allHealthy ? 'Tournament Health: Good' : 'Issues Detected'}
Matches: ${matchStats.completed}/${matchStats.total} completed (${matchPercentage}%)
Transactions: ${stats.total}/1000 (${transactionPercentage}%)
Active: ${matchStats.live} live matches, ${matchStats.ready} ready
Lane conflicts (LIVE): ${laneHasLiveConflicts ? `🔴 ${laneStats.conflicts}` : '✅ None'}
Lane conflicts (READY): ${laneHasReadyConflicts ? `⚠️ ${laneStats.readyConflicts}` : '✅ None'}
Referee conflicts: ${refereeHasConflicts ? `⚠️ ${refereeValidation.details.length}` : '✅ None'}
${!tournament.readOnly ? `
Tournament duration: ${timingStats.tournamentDuration}
Average match time: ${timingStats.avgMatchTime}
Shortest match: ${timingStats.shortestMatch}
Longest match: ${timingStats.longestMatch}
` : ''}
`; updateRightPane(html); } /** * Show Transaction Breakdown */ function showTransactionBreakdown() { currentView = 'transactions'; const stats = getTransactionStats(); const percentage = Math.round((stats.total / 1000) * 100); const remaining = 1000 - stats.total; const isHealthy = percentage < 50; const statusColor = isHealthy ? '#166534' : (percentage < 80 ? '#ca8a04' : '#dc2626'); const bgColor = isHealthy ? '#f0fdf4' : (percentage < 80 ? '#fefce8' : '#fef2f2'); const borderColor = isHealthy ? '#166534' : (percentage < 80 ? '#ca8a04' : '#dc2626'); const statusIcon = isHealthy ? '✓' : (percentage < 80 ? '⚠️' : '🔴'); const statusText = isHealthy ? 'Healthy' : (percentage < 80 ? 'Moderate' : 'High'); let html = `

Transaction Breakdown (${stats.total} total)

${statusIcon} Status: ${statusText}
`; for (const [type, count] of Object.entries(stats.breakdown)) { const typePercentage = stats.total > 0 ? Math.round((count / stats.total) * 100) : 0; html += `
${type}: ${count} (${typePercentage}%)
`; } html += `
Storage: ${stats.total}/1000 entries (${percentage}%)
Capacity remaining: ${remaining} entries
`; updateRightPane(html); } /** * Show Match State Details */ function showMatchStateDetails() { currentView = 'matches'; const matchStats = getMatchStateStats(); // Group matches by state const matchesByState = { completed: [], live: [], ready: [], pending: [] }; if (matches && matches.length > 0) { matches.forEach(match => { const state = getMatchState ? getMatchState(match) : 'unknown'; if (matchesByState.hasOwnProperty(state)) { matchesByState[state].push(match.id); } }); } const hasActive = matchStats.live > 0 || matchStats.ready > 0; const statusColor = hasActive ? '#166534' : '#666'; const bgColor = hasActive ? '#f0fdf4' : '#f8f9fa'; const borderColor = hasActive ? '#166534' : '#c0c0c0'; const statusIcon = hasActive ? '✓' : 'ℹ️'; let html = `

Match State Breakdown (${matchStats.total} total)

${statusIcon} ${hasActive ? 'Active Tournament' : 'Tournament Status'}
`; // Display each state with color coding const stateColors = { completed: '#065f46', live: '#dc2626', ready: '#ca8a04', pending: '#666' }; for (const [state, matchIds] of Object.entries(matchesByState)) { const count = matchIds.length; const stateName = state.toUpperCase(); const color = stateColors[state] || '#666'; html += `
${stateName}: ${count} matches
`; if (count > 0) { html += `
${matchIds.join(', ')}
`; } html += `
`; } html += `
`; updateRightPane(html); } /** * Show Transaction History */ function showTransactionHistory(filterType = 'all', filterMatchId = '', filterSearch = '') { currentView = 'history'; const history = getTournamentHistory ? getTournamentHistory() : []; // Add original transaction numbers before filtering // History is stored newest first, so index 0 = newest transaction const historyWithNumbers = history.map((tx, idx) => ({ ...tx, originalNumber: history.length - idx // #N for newest, #1 for oldest })); // Apply filters let filteredHistory = [...historyWithNumbers]; // Filter by type if (filterType && filterType !== 'all') { filteredHistory = filteredHistory.filter(tx => tx.type === filterType); } // Filter by match ID if (filterMatchId && filterMatchId.trim() !== '') { const matchIdLower = filterMatchId.trim().toLowerCase(); filteredHistory = filteredHistory.filter(tx => (tx.matchId && tx.matchId.toLowerCase().includes(matchIdLower)) || (tx.description && tx.description.toLowerCase().includes(matchIdLower)) ); } // Filter by search text if (filterSearch && filterSearch.trim() !== '') { const searchLower = filterSearch.trim().toLowerCase(); filteredHistory = filteredHistory.filter(tx => tx.description && tx.description.toLowerCase().includes(searchLower) ); } const totalCount = history.length; const filteredCount = filteredHistory.length; const countText = filteredCount < totalCount ? `${filteredCount} of ${totalCount} total` : `${totalCount} total`; let html = `

Transaction History (${countText})

`; if (filteredHistory.length === 0) { if (totalCount === 0) { html += `

No transactions recorded

`; } else { html += `

No transactions match filters

`; } } else { // Display transactions with their original numbers preserved (even when filtered) // Transactions are already in newest-first order from storage filteredHistory.forEach((tx) => { const time = new Date(tx.timestamp).toLocaleTimeString(); html += `

#${tx.originalNumber} | ${time} | ${tx.type} | ${tx.description || 'No description'}

`; }); } html += `
Search Transaction History
`; updateRightPane(html); } /** * Apply transaction filters from form inputs */ function applyTransactionFilters() { const filterType = document.getElementById('filterType')?.value || 'all'; const filterMatchId = document.getElementById('filterMatchId')?.value || ''; const filterSearch = document.getElementById('filterSearch')?.value || ''; showTransactionHistory(filterType, filterMatchId, filterSearch); } /** * Show Console Output */ function showConsoleOutput() { currentView = 'console'; let html = `

Console Output (last ${consoleBuffer.length} entries)

`; if (consoleBuffer.length === 0) { html += `

No console output captured yet

`; } else { consoleBuffer.forEach(entry => { html += `

[${entry.timestamp}] ${escapeHtml(entry.message)}

`; }); } html += `
`; updateRightPane(html); } /** * Show Player Details */ function showPlayerDetails() { currentView = 'players'; if (!players || players.length === 0) { updateRightPane(`

Player Details

No players registered

`); return; } // Separate and sort players by paid status const paidPlayers = players.filter(p => p.paid).sort((a, b) => a.name.localeCompare(b.name)); const unpaidPlayers = players.filter(p => !p.paid).sort((a, b) => a.name.localeCompare(b.name)); const allPaid = unpaidPlayers.length === 0; const statusColor = allPaid ? '#166534' : '#ca8a04'; const bgColor = allPaid ? '#f0fdf4' : '#fefce8'; const borderColor = allPaid ? '#166534' : '#ca8a04'; const statusIcon = allPaid ? '✓' : '⚠️'; let html = `

Player Details (${players.length} total)

${statusIcon} ${allPaid ? 'All Players Paid' : `${unpaidPlayers.length} Unpaid Player${unpaidPlayers.length > 1 ? 's' : ''}`}
`; // Paid Players Section if (paidPlayers.length > 0) { html += `
✅ Paid Players (${paidPlayers.length})
`; paidPlayers.forEach(player => { html += `
${escapeHtml(player.name)} (ID: ${player.id})
`; }); html += `
`; } // Unpaid Players Section if (unpaidPlayers.length > 0) { html += `
⚠️ Unpaid Players (${unpaidPlayers.length})
`; unpaidPlayers.forEach(player => { html += `
${escapeHtml(player.name)} (ID: ${player.id})
`; }); html += `
`; } html += `
`; updateRightPane(html); } /** * Show Lane Usage Details */ function showLaneUsageDetails() { currentView = 'lanes'; const laneStats = getLaneStats(); const maxLanes = (config && config.lanes && config.lanes.maxLanes) || 10; const excludedLanes = (config && config.lanes && config.lanes.excludedLanes) || []; const validation = validateLaneAssignments ? validateLaneAssignments() : { valid: true, conflicts: [], readyConflicts: [] }; const hasLiveConflicts = validation.conflicts.length > 0; const hasReadyConflicts = validation.readyConflicts && validation.readyConflicts.length > 0; const hasAnyIssues = hasLiveConflicts || hasReadyConflicts; // Color coding: red for live conflicts, yellow for ready conflicts, green for all clear const statusColor = hasLiveConflicts ? '#dc2626' : (hasReadyConflicts ? '#ca8a04' : '#166534'); const bgColor = hasLiveConflicts ? '#fef2f2' : (hasReadyConflicts ? '#fefce8' : '#f0fdf4'); const borderColor = hasLiveConflicts ? '#dc2626' : (hasReadyConflicts ? '#ca8a04' : '#166534'); const statusIcon = hasLiveConflicts ? '🔴' : (hasReadyConflicts ? '⚠️' : '✓'); // Get live matches with lanes const liveMatchesWithLanes = []; const readyMatchesWithLanes = []; if (matches && matches.length > 0) { matches.forEach(match => { const state = getMatchState ? getMatchState(match) : 'unknown'; if (match.lane) { const player1Name = match.player1?.name || 'TBD'; const player2Name = match.player2?.name || 'TBD'; const matchInfo = { id: match.id, lane: match.lane, players: `${player1Name} vs ${player2Name}` }; if (state === 'live') { liveMatchesWithLanes.push(matchInfo); } else if (state === 'ready') { readyMatchesWithLanes.push(matchInfo); } } }); } // Sort by lane number liveMatchesWithLanes.sort((a, b) => a.lane - b.lane); readyMatchesWithLanes.sort((a, b) => a.lane - b.lane); // Build available lanes list (all non-excluded) const availableLanesList = []; for (let i = 1; i <= maxLanes; i++) { if (!excludedLanes.includes(i)) { availableLanesList.push(i); } } // Build active lanes list (lanes currently in use) const activeLanes = [...new Set([...liveMatchesWithLanes.map(m => m.lane), ...readyMatchesWithLanes.map(m => m.lane)])].sort((a, b) => a - b); const activeLanesCount = activeLanes.length; // Build status message let statusMessage = 'No Issues Detected'; if (hasLiveConflicts && hasReadyConflicts) { statusMessage = `${validation.conflicts.length} Live Conflict${validation.conflicts.length > 1 ? 's' : ''} + ${validation.readyConflicts.length} Ready Conflict${validation.readyConflicts.length > 1 ? 's' : ''}`; } else if (hasLiveConflicts) { statusMessage = `${validation.conflicts.length} Live Conflict${validation.conflicts.length > 1 ? 's' : ''} Detected`; } else if (hasReadyConflicts) { statusMessage = `${validation.readyConflicts.length} Ready Match${validation.readyConflicts.length > 1 ? 'es' : ''} with Duplicate Lanes`; } let html = `

Lane Usage Details

${statusIcon} ${statusMessage}
Available Lanes: ${availableLanesList.join(', ')} (${laneStats.max} total)
Excluded Lanes: ${excludedLanes.length > 0 ? excludedLanes.join(', ') : 'None'}
Active Lanes: ${activeLanes.length > 0 ? activeLanes.join(', ') : 'None'} (${activeLanesCount} in use)
`; // Live Matches section if (liveMatchesWithLanes.length > 0) { html += `
Live Matches Using Lanes:
`; liveMatchesWithLanes.forEach(match => { html += `
• Lane ${match.lane}: ${match.id} (${match.players})
`; }); html += `
`; } else { html += `
No lanes currently in use
`; } // Ready Matches section if (readyMatchesWithLanes.length > 0) { html += `
Ready Matches with Lane Assignments:
`; readyMatchesWithLanes.forEach(match => { html += `
• Lane ${match.lane}: ${match.id} (Ready - waiting to start)
`; }); html += `
`; } // Live Conflicts section (critical) if (hasLiveConflicts) { html += `
🔴 Lane Conflicts Detected (LIVE matches):
`; validation.conflicts.forEach(conflict => { html += `
• Lane ${conflict.lane}: ${conflict.matches.join(' and ')} (both LIVE)
`; }); html += `
`; } // Ready Conflicts section (warning) if (hasReadyConflicts) { html += `
⚠️ Duplicate Lane Assignments (READY matches):
`; validation.readyConflicts.forEach(conflict => { html += `
• Lane ${conflict.lane}: ${conflict.matches.join(', ')} (all READY)
`; }); html += `
ℹ️ These matches will conflict when started. Reassign lanes before starting matches.
`; } html += ` `; updateRightPane(html); } /** * Show localStorage Usage */ function showLocalStorageUsage() { currentView = 'storage'; const storageStats = getLocalStorageStats(); const isHealthy = storageStats.percentage < 50; const statusColor = isHealthy ? '#166534' : (storageStats.percentage < 80 ? '#ca8a04' : '#dc2626'); const bgColor = isHealthy ? '#f0fdf4' : (storageStats.percentage < 80 ? '#fefce8' : '#fef2f2'); const borderColor = isHealthy ? '#166534' : (storageStats.percentage < 80 ? '#ca8a04' : '#dc2626'); const statusIcon = isHealthy ? '✓' : (storageStats.percentage < 80 ? '⚠️' : '🔴'); const statusText = isHealthy ? 'Healthy' : (storageStats.percentage < 80 ? 'Moderate' : 'High'); // Categorize storage items const tournamentData = {}; const tournamentHistory = {}; const globalStorage = {}; const cleanupCandidates = {}; // Get valid tournament IDs from registry const allTournaments = JSON.parse(localStorage.getItem('dartsTournaments') || '[]'); const validTournamentIds = new Set(allTournaments.map(t => t.id)); for (const [key, sizeBytes] of Object.entries(storageStats.items)) { if (key.startsWith('tournament_') && key.endsWith('_history')) { // Extract tournament ID from history key const tournamentId = key.replace('tournament_', '').replace('_history', ''); if (validTournamentIds.has(parseInt(tournamentId))) { // Valid tournament history tournamentHistory[key] = sizeBytes; } else { // Orphaned history - not in registry cleanupCandidates[key] = sizeBytes; } } else if (key.startsWith('tournament_') && !key.endsWith('_history')) { // Extract tournament ID from data key const tournamentId = key.replace('tournament_', ''); if (validTournamentIds.has(parseInt(tournamentId))) { // Valid tournament data tournamentData[key] = sizeBytes; } else { // Orphaned tournament data - not in registry cleanupCandidates[key] = sizeBytes; } } else if (key === 'tournamentHistory') { // Old global history - cleanup candidate cleanupCandidates[key] = sizeBytes; } else { globalStorage[key] = sizeBytes; } } // Get current tournament info let currentTournamentHtml = ''; if (tournament && tournament.id) { const tournamentKey = `tournament_${tournament.id}`; const historyKey = `tournament_${tournament.id}_history`; const dataSize = tournamentData[tournamentKey] || 0; const historySize = tournamentHistory[historyKey] || 0; // Parse tournament data for breakdown // Use in-memory tournament object if localStorage doesn't have it let tournamentDataObj = null; let actualDataSize = dataSize; try { const tournamentDataStr = localStorage.getItem(tournamentKey); if (tournamentDataStr) { tournamentDataObj = JSON.parse(tournamentDataStr); actualDataSize = dataSize; } else { // Use in-memory tournament object tournamentDataObj = tournament; // Calculate actual size from in-memory data actualDataSize = (JSON.stringify(tournament).length) * 2; } } catch (e) { console.warn('Could not parse tournament data', e); // Fallback to in-memory tournament tournamentDataObj = tournament; actualDataSize = (JSON.stringify(tournament).length) * 2; } // Get history info const history = getTournamentHistory ? getTournamentHistory() : []; const oldestTxn = history.length > 0 ? new Date(history[history.length - 1].timestamp) : null; const newestTxn = history.length > 0 ? new Date(history[0].timestamp) : null; const avgTxnSize = history.length > 0 ? (historySize / history.length) : 0; // Calculate data breakdown let playersSize = 0, matchesSize = 0, bracketSize = 0, placementsSize = 0, metadataSize = 0; if (tournamentDataObj) { playersSize = (JSON.stringify(tournamentDataObj.players || players || []).length) * 2; matchesSize = (JSON.stringify(tournamentDataObj.matches || matches || []).length) * 2; bracketSize = (JSON.stringify(tournamentDataObj.bracket || []).length) * 2; placementsSize = (JSON.stringify(tournamentDataObj.placements || {}).length) * 2; metadataSize = actualDataSize - playersSize - matchesSize - bracketSize - placementsSize; } const completedMatches = matches ? matches.filter(m => m.completed).length : 0; const totalMatches = matches ? matches.length : 0; // Calculate total size using actual data size const totalSize = actualDataSize + historySize; currentTournamentHtml = `
📊 Current Tournament: ${escapeHtml(tournament.name)} (${escapeHtml(tournament.date)})
Tournament Data (${(actualDataSize / 1024 / 1024).toFixed(2)} MB):
${playersSize > 0 ? `- Players array: ${(playersSize / 1024).toFixed(2)} KB (${players ? players.length : 0} players)
` : ''} ${matchesSize > 0 ? `- Matches array: ${(matchesSize / 1024).toFixed(2)} KB (${totalMatches} matches, ${completedMatches} completed)
` : ''} ${bracketSize > 0 ? `- Bracket structure: ${(bracketSize / 1024).toFixed(2)} KB
` : ''} ${placementsSize > 0 ? `- Placements: ${(placementsSize / 1024).toFixed(2)} KB
` : ''} ${metadataSize > 0 ? `- Metadata: ${(metadataSize / 1024).toFixed(2)} KB
` : ''}
${history.length > 0 ? `
Transaction History (${(historySize / 1024 / 1024).toFixed(2)} MB):
- Total transactions: ${history.length}
- Average per transaction: ${(avgTxnSize / 1024).toFixed(2)} KB
${oldestTxn ? `- Oldest: ${oldestTxn.toLocaleString()}
` : ''} ${newestTxn ? `- Newest: ${newestTxn.toLocaleString()}
` : ''}
` : ''}
Total: ${(totalSize / 1024 / 1024).toFixed(2)} MB (${Math.round((totalSize / (storageStats.used * 1024 * 1024)) * 100)}%)
${history.length > 10 ? `
💡 Smart pruning could reduce history size by ~30-40%
` : ''}
`; } // Other tournaments summary let otherTournamentsHtml = ''; const otherTournaments = allTournaments.filter(t => !tournament || t.id !== tournament.id); if (otherTournaments.length > 0) { let otherTotalSize = 0; let otherHtml = ''; otherTournaments.forEach(t => { const tDataKey = `tournament_${t.id}`; const tHistoryKey = `tournament_${t.id}_history`; const tDataSize = tournamentData[tDataKey] || 0; const tHistorySize = tournamentHistory[tHistoryKey] || 0; const tTotalSize = tDataSize + tHistorySize; otherTotalSize += tTotalSize; otherHtml += `
- ${escapeHtml(t.name)} (${escapeHtml(t.date)}): ${(tTotalSize / 1024 / 1024).toFixed(2)} MB
`; }); otherTournamentsHtml = `
📦 Other Tournaments: ${otherTournaments.length} tournament${otherTournaments.length > 1 ? 's' : ''}
${otherHtml}
Total: ${(otherTotalSize / 1024 / 1024).toFixed(2)} MB (${Math.round((otherTotalSize / (storageStats.used * 1024 * 1024)) * 100)}%)
💡 To delete tournaments, use the Setup page
`; } // Global storage let globalHtml = ''; let globalTotal = 0; for (const [key, sizeBytes] of Object.entries(globalStorage)) { globalTotal += sizeBytes; const sizeDisplay = sizeBytes >= 1024 ? `${(sizeBytes / 1024 / 1024).toFixed(2)} MB` : `${(sizeBytes / 1024).toFixed(2)} KB`; let displayName = key; if (key === 'dartsTournaments') displayName = 'Tournament registry'; else if (key === 'dartsConfig') displayName = 'Config'; else if (key === 'savedPlayers') displayName = 'Saved Players (legacy - unused)'; else if (key === 'currentTournament') displayName = 'Current Tournament pointer'; else if (key === 'playerList') displayName = 'Player List'; globalHtml += `
- ${displayName}: ${sizeDisplay}
`; } let globalStorageHtml = globalTotal > 0 ? `
🌍 Global Storage
${globalHtml}
Total: ${(globalTotal / 1024 / 1024).toFixed(2)} MB (${Math.round((globalTotal / (storageStats.used * 1024 * 1024)) * 100)}%)
` : ''; // Cleanup candidates - Orphaned data not in registry let cleanupHtml = ''; if (Object.keys(cleanupCandidates).length > 0) { let cleanupTotal = 0; let cleanupItems = ''; for (const [key, sizeBytes] of Object.entries(cleanupCandidates)) { cleanupTotal += sizeBytes; // Determine label for this orphaned data let label = ''; if (key === 'tournamentHistory') { label = 'Old global history - pre-v4.0'; } else if (key.endsWith('_history')) { label = 'Orphaned history'; } else { label = 'Orphaned tournament data'; } const sizeDisplay = sizeBytes >= 1024 * 1024 ? `${(sizeBytes / 1024 / 1024).toFixed(2)} MB` : `${(sizeBytes / 1024).toFixed(2)} KB`; cleanupItems += `
- ${key} (${sizeDisplay}) [${label}]
To delete: localStorage.removeItem('${key}')
`; } cleanupHtml = `
⚠️ Orphaned Data (Not in Registry)
${cleanupItems}
💡 To delete a tournament properly, use the Setup page.
This section only shows data that isn't associated with any tournament.
`; } // High usage warning let highUsageWarning = ''; if (storageStats.percentage >= 80) { highUsageWarning = `
🔴 High Storage Usage Warning
Consider deleting older tournaments to free up space. You can export them first to preserve the data.
`; } let html = `

localStorage Usage

${statusIcon} Storage Status: ${statusText}
Total Used: ${storageStats.used.toFixed(2)} MB of ${storageStats.limit} MB (${storageStats.percentage}%)
Browser Limit: ${storageStats.limit} MB (Chrome 114+, Firefox, Safari, Edge)
${highUsageWarning} ${currentTournamentHtml} ${otherTournamentsHtml} ${globalStorageHtml} ${cleanupHtml} `; updateRightPane(html); } /** * Show Validation Results */ function showValidationResults() { currentView = 'validation'; const results = runAllValidations(); const timestamp = new Date().toLocaleTimeString(); // Count pass/fail const passCount = results.filter(r => r.valid).length; const failCount = results.filter(r => !r.valid).length; const allPassed = failCount === 0; const statusColor = allPassed ? '#166534' : '#dc2626'; const bgColor = allPassed ? '#f0fdf4' : '#fef2f2'; const borderColor = allPassed ? '#166534' : '#dc2626'; const statusIcon = allPassed ? '✓' : '⚠️'; let html = `

Validation Results

${statusIcon} ${allPassed ? 'All Checks Passed' : `${failCount} Issue${failCount > 1 ? 's' : ''} Detected`}
`; results.forEach(result => { const icon = result.valid ? '✅' : '⚠️'; const checkColor = result.valid ? '#065f46' : '#dc2626'; html += `
${icon} ${escapeHtml(result.name)}: ${escapeHtml(result.message)}
`; if (result.details && result.details.length > 0) { html += `
`; result.details.forEach(detail => { html += `
• ${detail}
`; }); html += `
`; } html += `
`; }); html += `
Validated at: ${timestamp}
`; updateRightPane(html); } /** * Update right pane content area (not console footer) */ function updateRightPane(html) { const contentArea = document.getElementById('analyticsContentArea'); if (contentArea) { contentArea.innerHTML = html; } // mark the left pane's item for this view (sub-views count as their parent) const parent = { 'pruning-preview': 'transaction-management' }; const current = parent[currentView] || currentView; document.querySelectorAll('#devConsoleView .dc-nav [data-dc]').forEach(b => { if (b.dataset.dc === current) b.setAttribute('aria-current', 'true'); else b.removeAttribute('aria-current'); }); } /** * Update console footer with latest console output * Only updates DOM if buffer length has changed (prevents unnecessary redraws) */ function updateConsoleFooter() { const consoleContent = document.getElementById('consoleOutputContent'); if (!consoleContent) return; // Only update if buffer length changed (new entries added) if (consoleBuffer.length === lastConsoleUpdateLength) { return; // No changes, skip update } lastConsoleUpdateLength = consoleBuffer.length; const count = document.getElementById('dcLogCount'); if (count) count.textContent = consoleBuffer.length ? `${consoleBuffer.length} lines` : ''; if (consoleBuffer.length === 0) { consoleContent.innerHTML = '
No console output yet.
'; return; } // Show last 50 entries (most recent at bottom) const entries = consoleBuffer.slice(-50); const html = entries.map(entry => { return `
${escapeHtml(entry.message)}
`; }).join(''); consoleContent.innerHTML = html; // Auto-scroll to bottom if user isn't actively scrolling the console if (!isUserScrolling) { consoleContent.scrollTop = consoleContent.scrollHeight; } } /** * Escape HTML to prevent injection */ // escapeHtml() is the shared canonical helper defined in main.js (quote-safe, // window-exported). This file's former private copy did not escape quotes. // ============================================================================ // VALIDATION CHECKS // ============================================================================ /** * Run all validation checks */ function runAllValidations() { return [ validateLaneConflicts(), validateRefereeAssignments(), validateMatchStateIntegrity(), validateTransactionCount(), validatePlayerIdIntegrity(), validateProgressionIntegrity() ]; } /** * Validate lane assignments */ function validateLaneConflicts() { const validation = validateLaneAssignments ? validateLaneAssignments() : { valid: true, conflicts: [] }; const details = validation.conflicts.map(conflict => `Lane ${conflict.lane}: Used by ${conflict.matches.join(', ')}` ); return { name: 'Lane Assignments', valid: validation.valid, message: validation.valid ? 'No conflicts detected' : `${validation.conflicts.length} conflict(s) detected`, details: details }; } /** * Validate referee assignments */ function validateRefereeAssignments() { if (!matches || matches.length === 0) { return { name: 'Referee Assignments', valid: true, message: 'No matches to validate', details: [] }; } const conflicts = []; const activeReferees = new Map(); // Track referee ID -> match ID for active matches matches.forEach(match => { // Check all active matches (PENDING, READY, LIVE) for duplicate referee assignments // A referee can only be assigned to one active match at a time (completed matches are fine) // Ignore "None" or empty referee assignments (not actual conflicts) if (match.referee && match.referee !== 'None' && match.referee !== '' && !match.completed) { if (activeReferees.has(match.referee)) { // ERROR: This referee is already assigned to another active match const firstMatch = activeReferees.get(match.referee); conflicts.push(`Referee assigned to both ${firstMatch} and ${match.id}`); } else { activeReferees.set(match.referee, match.id); } } }); return { name: 'Referee Assignments', valid: conflicts.length === 0, message: conflicts.length === 0 ? 'No conflicts detected' : `${conflicts.length} conflict(s) detected`, details: conflicts }; } /** * Validate match state integrity */ function validateMatchStateIntegrity() { if (!matches || matches.length === 0) { return { name: 'Match State Integrity', valid: true, message: 'No matches to validate', details: [] }; } const issues = []; matches.forEach(match => { if (match.completed) { // Completed matches must have winner and loser if (!match.winner || !match.loser) { issues.push(`${match.id}: Completed but missing winner/loser`); } } }); return { name: 'Match State Integrity', valid: issues.length === 0, message: issues.length === 0 ? 'All matches valid' : `${issues.length} issue(s) detected`, details: issues }; } /** * Validate transaction count */ function validateTransactionCount() { const history = getTournamentHistory ? getTournamentHistory() : []; const count = history.length; const percentage = Math.round((count / 1000) * 100); const valid = count < 800; // Warn if over 80% const status = count < 500 ? 'Healthy' : (count < 800 ? 'Moderate' : 'High'); return { name: 'Transaction Count', valid: valid, message: `${count}/1000 entries (${percentage}%) - ${status}`, details: [] }; } /** * Validate player ID integrity */ function validatePlayerIdIntegrity() { if (!matches || matches.length === 0 || !players || players.length === 0) { return { name: 'Player ID Integrity', valid: true, message: 'No data to validate', details: [] }; } const issues = []; const playerIds = new Set(players.map(p => p.id)); matches.forEach(match => { // Check player1 if (match.player1 && match.player1.id) { const player1Id = String(match.player1.id); // Skip special players (BYE, TBD, WALKOVER, placeholder IDs) // Placeholder IDs follow patterns like: bs-final-1, grand-final-2, fs-3-1, etc. const isPlaceholder = player1Id.startsWith('bs-') || player1Id.startsWith('fs-') || player1Id.startsWith('grand-final-'); if (!player1Id.startsWith('bye-') && !player1Id.startsWith('tbd-') && !player1Id.startsWith('walkover-') && !isPlaceholder && player1Id !== 'WALKOVER' && !playerIds.has(match.player1.id)) { issues.push(`${match.id}: player1 ID ${match.player1.id} not found in players array`); } } // Check player2 if (match.player2 && match.player2.id) { const player2Id = String(match.player2.id); // Skip special players (BYE, TBD, WALKOVER, placeholder IDs) // Placeholder IDs follow patterns like: bs-final-1, grand-final-2, fs-3-1, etc. const isPlaceholder = player2Id.startsWith('bs-') || player2Id.startsWith('fs-') || player2Id.startsWith('grand-final-'); if (!player2Id.startsWith('bye-') && !player2Id.startsWith('tbd-') && !player2Id.startsWith('walkover-') && !isPlaceholder && player2Id !== 'WALKOVER' && !playerIds.has(match.player2.id)) { issues.push(`${match.id}: player2 ID ${match.player2.id} not found in players array`); } } // Check referee if (match.referee && !playerIds.has(match.referee)) { issues.push(`${match.id}: referee ID ${match.referee} not found in players array`); } }); return { name: 'Player ID Integrity', valid: issues.length === 0, message: issues.length === 0 ? 'All player IDs valid' : `${issues.length} issue(s) detected`, details: issues }; } /** * Validate progression integrity */ function validateProgressionIntegrity() { if (!matches || matches.length === 0 || !tournament || !tournament.bracketSize) { return { name: 'Progression Integrity', valid: true, message: 'No data to validate', details: [] }; } const progression = DE_MATCH_PROGRESSION ? DE_MATCH_PROGRESSION[tournament.bracketSize] : null; if (!progression) { return { name: 'Progression Integrity', valid: true, message: 'No progression rules available', details: [] }; } const issues = []; matches.forEach(match => { if (match.completed && match.winner && match.loser) { const rule = progression[match.id]; if (!rule) return; // Check winner placement if (rule.winner) { const [destMatchId, destSlot] = rule.winner; const destMatch = matches.find(m => m.id === destMatchId); if (destMatch) { const expectedPlayer = destMatch[destSlot]; if (expectedPlayer && expectedPlayer.id !== match.winner.id) { issues.push(`${match.id}: Winner should be in ${destMatchId}.${destSlot}, found ${expectedPlayer.name || 'unknown'} instead`); } } } // Check loser placement if (rule.loser) { const [destMatchId, destSlot] = rule.loser; const destMatch = matches.find(m => m.id === destMatchId); if (destMatch) { const expectedPlayer = destMatch[destSlot]; if (expectedPlayer && expectedPlayer.id !== match.loser.id) { issues.push(`${match.id}: Loser should be in ${destMatchId}.${destSlot}, found ${expectedPlayer.name || 'unknown'} instead`); } } } } }); return { name: 'Progression Integrity', valid: issues.length === 0, message: issues.length === 0 ? 'All progressions valid' : `${issues.length} issue(s) detected`, details: issues }; } // ============================================================================ // COMMANDS // ============================================================================ /** * Show command execution feedback in right pane */ function showCommandFeedback(commandName, status, details) { currentView = 'command-feedback'; const timestamp = new Date().toLocaleTimeString(); const statusIcon = status === 'success' ? '✓' : '⚠️'; const statusColor = status === 'success' ? '#166534' : status === 'warning' ? '#92400e' : '#dc2626'; const bgColor = status === 'success' ? '#f0fdf4' : status === 'warning' ? '#fffbeb' : '#fef2f2'; const borderColor = status === 'success' ? '#166534' : status === 'warning' ? '#92400e' : '#dc2626'; // Format multi-line details const detailsHtml = details.split('\n') .map(line => line.trim()) .filter(line => line.length > 0) .map(line => { // Check if line starts with bullet character if (line.startsWith('•')) { return `
${escapeHtml(line)}
`; } return `
${escapeHtml(line)}
`; }) .join(''); const html = `

Command Executed: ${commandName}

${statusIcon} ${status === 'success' ? 'Success' : status === 'warning' ? 'Warning' : 'Error'}
${detailsHtml}
Executed at: ${timestamp}
`; updateRightPane(html); } /** * Command: Re-render Bracket */ function commandReRenderBracket() { console.log('Re-rendering bracket...'); if (typeof renderBracket === 'function') { const bracketSize = tournament?.bracketSize || 'unknown'; const totalMatches = matches?.length || 'unknown'; renderBracket(); console.log('✓ Bracket re-rendered'); showCommandFeedback('Re-render Bracket', 'success', `Bracket re-rendered successfully\n• Bracket size: ${bracketSize} players\n• Total matches: ${totalMatches}`); } else { console.error('renderBracket function not available'); showCommandFeedback('Re-render Bracket', 'error', 'renderBracket function not available'); } } /** * Command: Recalculate Rankings */ function commandRecalculateRankings() { console.log('Recalculating rankings...'); if (typeof calculateAllRankings === 'function') { const playerCount = players?.length || 0; const completedMatches = matches?.filter(m => m.completed).length || 0; const bracketSize = tournament?.bracketSize || 'unknown'; calculateAllRankings(); console.log('✓ Rankings recalculated'); showCommandFeedback('Recalculate Rankings', 'success', `Rankings recalculated successfully\n• Players ranked: ${playerCount}\n• Completed matches: ${completedMatches}\n• Bracket size: ${bracketSize} players`); } else { console.error('calculateAllRankings function not available'); showCommandFeedback('Recalculate Rankings', 'error', 'calculateAllRankings function not available'); } } /** * Command: Refresh All Dropdowns */ function commandRefreshDropdowns() { console.log('Refreshing all dropdowns...'); let refreshed = 0; const details = []; if (typeof refreshAllLaneDropdowns === 'function') { refreshAllLaneDropdowns(); refreshed++; details.push('Lane dropdowns'); } if (typeof refreshAllRefereeDropdowns === 'function') { refreshAllRefereeDropdowns(); refreshed++; details.push('Referee dropdowns'); } const matchCount = matches?.length || 0; console.log(`✓ Refreshed ${refreshed} dropdown type(s)`); showCommandFeedback('Refresh All Dropdowns', 'success', `Dropdowns refreshed successfully\n• Types refreshed: ${details.join(', ')}\n• Matches updated: ${matchCount}`); } /** * Command: Validate Everything */ function commandValidateEverything() { console.log('Running 6 validation checks...'); const results = runAllValidations(); // Log summary to console let passCount = 0; let failCount = 0; results.forEach(result => { if (result.valid) { passCount++; console.log(`✅ ${result.name}: ${result.message}`); } else { failCount++; console.log(`⚠️ ${result.name}: ${result.message}`); if (result.details && result.details.length > 0) { result.details.forEach(detail => { console.log(` - ${detail}`); }); } } }); console.log(`Validation complete: ${passCount} passed, ${failCount} failed`); console.log('(Full results displayed in right pane)'); // Show detailed results in right pane showValidationResults(); } /** * Command: Reset All Config to Defaults */ function commandResetAllConfig() { currentView = 'reset-config'; if (typeof configChangesOnReset !== 'function') { showCommandFeedback('Reset All Config', 'error', 'The default settings are not available'); return; } // Every setting that would change, walked from the defaults (configChangesOnReset()), by section const changes = configChangesOnReset(); const show = v => v === undefined ? '(not set)' : typeof v === 'boolean' ? (v ? 'On' : 'Off') : Array.isArray(v) ? (v.length ? v.join(', ') : 'None') : v === '' ? '(empty)' : String(v); const sections = []; changes.forEach(c => { let s = sections.find(x => x.name === c.section); if (!s) sections.push(s = { name: c.section, rows: [] }); s.rows.push(c); }); const comparisonHtml = changes.length ? '
' + sections.map(s => `
${escapeHtml(s.name)}
').join('') + '
' : '

Every setting is already at its default.

'; const html = `

Reset All Config to Defaults

⚠️ Warning: this cannot be undone
This puts the Global Settings back to their defaults. The server connection is kept (the server address, API key and server ID). Your tournament data (matches, players, brackets, history) is not affected.
What will change (${changes.length} setting${changes.length === 1 ? '' : 's'}):
${comparisonHtml}
To confirm this action:
Type RESET in the box below and click "Reset Config"
`; updateRightPane(html); } /** * Execute Reset All Config (called from confirmation button) */ function executeResetAllConfig() { const confirmInput = document.getElementById('resetConfirmInput'); if (!confirmInput || confirmInput.value.trim().toUpperCase() !== 'RESET') { alert('Please type RESET to confirm'); return; } console.log('Resetting all config to defaults...'); if (typeof defaultConfigKeepingConnection !== 'function') { console.error('The default settings are not available'); showCommandFeedback('Reset All Config', 'error', 'The default settings are not available'); return; } try { const changed = configChangesOnReset().length; // Reset to the defaults, keeping the server connection (defaultConfigKeepingConnection()) config = defaultConfigKeepingConnection(); // Save to localStorage if (typeof saveGlobalConfig === 'function') { saveGlobalConfig(); } else { localStorage.setItem('dartsConfig', JSON.stringify(config)); } console.log('✓ Config reset to defaults'); console.log('✓ Saved to localStorage'); // Build success feedback with comparison let feedback = 'Settings reset to their defaults. The page will reload in 2 seconds to apply them.\n\n'; feedback += `• ${changed} setting${changed === 1 ? '' : 's'} changed\n`; feedback += '• The server connection is kept\n'; showCommandFeedback('Reset All Config', 'success', feedback); // Auto-refresh after 2 seconds setTimeout(() => { console.log('Reloading page to apply config changes...'); location.reload(); }, 2000); } catch (error) { console.error('❌ Error resetting config:', error); showCommandFeedback('Reset All Config', 'error', `Error resetting config: ${error.message}`); } } /** * Command: Toggle Read-Only Status */ function commandToggleReadOnly() { if (!tournament) { showCommandFeedback('Toggle Read-Only', 'error', 'No active tournament'); return; } const wasReadOnly = tournament.readOnly || false; const newReadOnly = !wasReadOnly; console.log(`Toggling read-only status: ${wasReadOnly} → ${newReadOnly}`); // Toggle the read-only flag tournament.readOnly = newReadOnly; // Save the tournament with new status if (typeof saveTournament === 'function') { saveTournament(); } // Re-render bracket to update match icons (checkmark ✓ vs undo ↻) if (typeof renderBracket === 'function') { renderBracket(); } const statusText = newReadOnly ? 'READ-ONLY' : 'READ-WRITE'; const statusColor = newReadOnly ? '#dc2626' : '#059669'; const icon = newReadOnly ? '🔒' : '🔓'; console.log(`✓ Tournament is now ${statusText}`); showCommandFeedback('Toggle Read-Only', 'success', `${icon} Tournament set to ${statusText}\n\n${newReadOnly ? '• Undo operations disabled\n• Match modifications disabled\n• Tournament data protected' : '• Undo operations enabled\n• Match modifications enabled\n• Tournament fully editable'}`); } // ============================================================================ // Command: Late Registration // ============================================================================ /** Module-level state persisted between Late Registration steps. */ let lateRegState = null; /** * Helper: scans FS Round 1 BYE slots and returns eligibility results. * Used by both the eligibility display step and the execution step. * * @returns {{eligible: string[], ineligible: Array<{matchId: string, blocking: string[]}>, total: number} | null} */ function runLateRegistrationEligibility() { if (!tournament || !matches || matches.length === 0) return null; const progressionTable = getProgressionTable(); const history = getTournamentHistory(); const byeMatches = matches.filter(m => m.id && /^FS-1-\d+$/.test(m.id) && (m.player1?.isBye || m.player2?.isBye) ); if (byeMatches.length === 0) return { eligible: [], ineligible: [], total: 0 }; const eligible = []; const ineligible = []; byeMatches.forEach(byeMatch => { const progression = progressionTable && progressionTable[byeMatch.id]; const blockingMatches = []; if (progression) { const formatBlockingMatch = (targetMatchId, isLive) => { const m = matches.find(x => x.id === targetMatchId); const players = m ? `${m.player1?.name || '?'} vs ${m.player2?.name || '?'}` : ''; return isLive ? `${targetMatchId} (${players}, live)` : `${targetMatchId} (${players})`; }; if (progression.winner) { const [targetMatchId] = progression.winner; const targetMatch = matches.find(m => m.id === targetMatchId); if (targetMatch && targetMatch.active) { blockingMatches.push(formatBlockingMatch(targetMatchId, true)); } else if (targetMatch && targetMatch.completed) { const targetTransaction = history.find(t => t.matchId === targetMatchId && t.type === 'COMPLETE_MATCH' && t.completionType === 'MANUAL'); if (targetTransaction) blockingMatches.push(formatBlockingMatch(targetMatchId, false)); } } if (progression.loser) { const [targetMatchId] = progression.loser; const targetMatch = matches.find(m => m.id === targetMatchId); if (targetMatch && targetMatch.active) { blockingMatches.push(formatBlockingMatch(targetMatchId, true)); } else if (targetMatch && targetMatch.completed) { const targetTransaction = history.find(t => t.matchId === targetMatchId && t.type === 'COMPLETE_MATCH' && t.completionType === 'MANUAL'); if (targetTransaction) blockingMatches.push(formatBlockingMatch(targetMatchId, false)); } } } if (blockingMatches.length === 0) { eligible.push(byeMatch.id); } else { ineligible.push({ matchId: byeMatch.id, blocking: blockingMatches }); } }); return { eligible, ineligible, total: byeMatches.length }; } /** * Command: Late Registration — entry point. * Runs eligibility check and, if slots are available, shows player name input form. */ function commandLateRegistration() { if (!tournament || !matches || matches.length === 0) { showCommandFeedback('Late Registration', 'error', 'No active tournament.'); return; } const result = runLateRegistrationEligibility(); if (!result || result.total === 0) { showCommandFeedback('Late Registration', 'error', 'No BYE slots found in Frontside Round 1.\n\nLate Registration is not possible for this tournament.'); return; } const { eligible, ineligible, total } = result; const eligibleCount = eligible.length; let message = `${eligibleCount} of ${total} BYE slot${total !== 1 ? 's' : ''} are eligible for late registration in FS Round 1.\n\n`; if (eligibleCount === 0) { message += '⛔ Late Registration is not possible — all BYE slots are blocked.\n\n'; if (ineligible.length > 0) { message += 'Ineligible slots:\n'; ineligible.forEach(({ matchId, blocking }) => { message += `• ${matchId} — blocked by: ${blocking.join(', ')}\n`; }); } showCommandFeedback('Late Registration', 'error', message); return; } if (ineligible.length > 0) { message += '⚠️ Draw fairness warning: not all BYE slots are available. The new player will be placed randomly among the eligible slots only.\n\n'; message += 'Ineligible slots:\n'; ineligible.forEach(({ matchId, blocking }) => { message += `• ${matchId} — blocked by: ${blocking.join(', ')}\n`; }); message += '\nBlocked matches must be stopped (if live) or undone (if completed) to become eligible. All affected matches will need to be replayed from the beginning.\n'; } else { message += '✓ All BYE slots are eligible. The new player will be placed randomly.\n\n'; } const status = ineligible.length > 0 ? 'warning' : 'success'; const statusColor = status === 'success' ? '#166534' : '#92400e'; const bgColor = status === 'success' ? '#f0fdf4' : '#fffbeb'; const statusIcon = status === 'success' ? '✓' : '⚠️'; const statusLabel = status === 'success' ? 'Success' : 'Warning'; const detailsHtml = message.split('\n') .map(line => line.trim()) .filter(line => line.length > 0) .map(line => line.startsWith('•') ? `
${line}
` : `
${line}
`) .join(''); const html = `

Command Executed: Late Registration

${statusIcon} ${statusLabel}
${detailsHtml}
Late Registration adds a new player to the tournament. The player must be entering the full tournament from the start — they will be placed randomly in an available walkover spot in Frontside Round 1, replacing a BYE.
Register New Player
`; currentView = 'command-feedback'; updateRightPane(html); setTimeout(() => document.getElementById('lateRegPlayerName')?.focus(), 100); } /** * Late Registration Step 2: validate player name, pick random eligible slot, show confirmation gate. */ function commandLateRegistrationStep2() { const nameInput = document.getElementById('lateRegPlayerName'); if (!nameInput) { showCommandFeedback('Late Registration', 'error', 'Session expired. Please run Late Registration again.'); return; } const playerName = nameInput.value.trim(); if (!playerName) { nameInput.style.borderColor = '#dc2626'; nameInput.focus(); return; } // Duplicate check if (players.find(p => p.name.toLowerCase() === playerName.toLowerCase())) { currentView = 'command-feedback'; updateRightPane(`

Command Executed: Late Registration

⚠️ Error
A player named ${playerName} is already registered in this tournament.
← Try again `); return; } // Re-run eligibility (state may have changed since Step 1) const result = runLateRegistrationEligibility(); if (!result || result.eligible.length === 0) { showCommandFeedback('Late Registration', 'error', 'No eligible BYE slots remain. The tournament state may have changed. Please run Late Registration again.'); return; } // Randomly select a slot — operator never chooses const { eligible } = result; const slotMatchId = eligible[Math.floor(Math.random() * eligible.length)]; const slotMatch = matches.find(m => m.id === slotMatchId); // Opponent is the non-BYE player in the selected slot const opponent = slotMatch.player1?.isBye ? slotMatch.player2 : slotMatch.player1; const opponentName = opponent?.name || 'TBD'; // Store state for Step 3 lateRegState = { playerName, slotMatchId }; const html = `

Command Executed: Late Registration

⚠️ Confirm Placement
${playerName} will be placed in ${slotMatchId} vs ${opponentName}.
This operation cannot be undone.
Type ${playerName} to confirm:
← Cancel `; currentView = 'command-feedback'; updateRightPane(html); setTimeout(() => document.getElementById('lateRegConfirmName')?.focus(), 100); } /** * Late Registration Step 3: execute — register player, undo BYE auto-advance, place in bracket. */ function commandLateRegistrationStep3() { if (!lateRegState) { showCommandFeedback('Late Registration', 'error', 'Session expired. Please run Late Registration again.'); return; } const { playerName, slotMatchId } = lateRegState; const confirmInput = document.getElementById('lateRegConfirmName'); // Exact-match confirmation gate if (!confirmInput || confirmInput.value !== playerName) { if (confirmInput) { confirmInput.style.borderColor = '#dc2626'; confirmInput.focus(); } return; } lateRegState = null; // 1. Create and register the player (reusing addPlayer logic, bypassing the in-progress guard) // the late player comes from the player database (found by name, or created) const entry = typeof PlayerRegistry !== 'undefined' ? PlayerRegistry.findOrCreate(playerName) : null; const newPlayer = { id: Date.now(), name: entry ? entry.short : playerName, registryId: entry ? entry.id : undefined, paid: true, stats: { shortLegs: 0, highOuts: [], tons: 0, oneEighties: 0 }, placement: null, eliminated: false }; players.push(newPlayer); if (typeof updatePlayersDisplay === 'function') updatePlayersDisplay(); if (typeof updatePlayerCount === 'function') updatePlayerCount(); // 2. Find the AUTO transaction for the BYE auto-advance const history = getTournamentHistory(); const autoTransaction = history.find(t => t.matchId === slotMatchId && t.type === 'COMPLETE_MATCH' && t.completionType === 'AUTO' ); if (!autoTransaction) { showCommandFeedback('Late Registration', 'error', `Could not find the auto-advance transaction for ${slotMatchId}.\n\nPlayer was registered but not placed in the bracket. Please contact support.`); return; } // 3. Undo the BYE auto-advance using the surgical undo machinery undoManualTransaction(autoTransaction.id); // 4. Replace the BYE slot with the new player const slotMatch = matches.find(m => m.id === slotMatchId); if (slotMatch) { if (slotMatch.player1?.isBye) { slotMatch.player1 = { name: playerName, id: newPlayer.id }; } else { slotMatch.player2 = { name: playerName, id: newPlayer.id }; } } // 5. Persist the updated match data before recording the transaction if (typeof saveTournament === 'function') { saveTournament(); } // 6. Record LATE_REGISTRATION transaction in the audit log saveTransaction({ id: `tx_${Date.now()}`, type: 'LATE_REGISTRATION', completionType: 'MANUAL', description: `Late Registration: ${playerName} placed in ${slotMatchId}`, timestamp: new Date().toISOString(), matchId: slotMatchId, playerName: playerName, playerId: newPlayer.id }); // 7. Refresh UI with the updated bracket if (typeof refreshTournamentUI === 'function') { refreshTournamentUI(); } // 7. Show success showCommandFeedback('Late Registration', 'success', `${playerName} has been registered and placed in ${slotMatchId}.\n\nThe bracket has been updated. The match is now ready to play.`); } // ============================================================================ // TRANSACTION LOG MANAGEMENT // ============================================================================ /** * Show Transaction Log Management view */ function showTransactionLogManagement() { currentView = 'transaction-management'; const history = getTournamentHistory ? getTournamentHistory() : []; const stats = getTransactionStats(); const matchStats = getMatchStateStats(); const completedMatches = matches?.filter(m => m.completed) || []; const activeMatches = matchStats.live + matchStats.ready; // Calculate storage (rough estimate: ~2 bytes per char in UTF-16) const storageEstimate = ((JSON.stringify(history).length * 2) / 1024 / 1024).toFixed(2); const percentage = Math.round((stats.total / 1000) * 100); // Estimate smart pruning savings const estimate = estimateSmartPruning(); const estimatedSavings = estimate.removable; const estimatedPercentage = stats.total > 0 ? Math.round((estimatedSavings / stats.total) * 100) : 0; const estimatedStorageSaved = ((JSON.stringify(history).length * estimatedPercentage / 100 * 2) / 1024 / 1024).toFixed(2); const html = `

Transaction Log Management

Current Status:

• Total transactions: ${stats.total}/1000 (${percentage}%)

• Storage used: ~${storageEstimate} MB

• Completed matches: ${completedMatches.length}

• Active matches: ${activeMatches} (${matchStats.live} live, ${matchStats.ready} ready)

Smart Pruning

Removes redundant transactions for completed matches:

Estimated savings: ~${estimatedSavings} transactions (~${estimatedStorageSaved} MB, ${estimatedPercentage}% reduction)

Matches affected: ${completedMatches.length} completed matches

Safety: ✅ Safe - Only removes redundant history

Transaction Breakdown:
${Object.entries(stats.breakdown).map(([type, count]) => { const typePercentage = stats.total > 0 ? Math.round((count / stats.total) * 100) : 0; let note = ''; if (type === 'COMPLETE_MATCH') note = ' - Never pruned'; else if (type === 'ASSIGN_LANE' || type === 'ASSIGN_REFEREE') note = ' - Keep only last selection'; else if (type === 'START_MATCH' || type === 'STOP_MATCH') note = ' - Redundant after completion'; return `

• ${type}: ${count} (${typePercentage}%)${note}

`; }).join('')}

ℹ️ Note: Pruning removes redundant transaction history for completed matches. This is safe - the undo system uses separate state snapshots and will continue to work normally.

`; updateRightPane(html); } /** * Estimate smart pruning savings */ function estimateSmartPruning() { const history = getTournamentHistory ? getTournamentHistory() : []; const completedMatches = matches?.filter(m => m.completed) || []; let removableCount = 0; completedMatches.forEach(match => { const matchId = match.id; // Match strictly on matchId — a description substring check would let // 'FS-1-1' claim transactions belonging to FS-1-10..FS-1-16 const matchTxns = history.filter(t => t.matchId === matchId); // Count removable transactions const lanes = matchTxns.filter(t => t.type === 'ASSIGN_LANE'); const refs = matchTxns.filter(t => t.type === 'ASSIGN_REFEREE'); const startStop = matchTxns.filter(t => t.type === 'START_MATCH' || t.type === 'STOP_MATCH'); // Remove all but last lane assignment if (lanes.length > 1) { removableCount += lanes.length - 1; } // Remove all but last referee assignment if (refs.length > 1) { removableCount += refs.length - 1; } // Remove all start/stop removableCount += startStop.length; }); return { removable: removableCount, completedMatches: completedMatches.length }; } /** * Preview smart pruning - show what will be removed */ function previewSmartPruning() { currentView = 'pruning-preview'; const history = getTournamentHistory ? getTournamentHistory() : []; const completedMatches = matches?.filter(m => m.completed) || []; const toRemove = []; const analysis = { lanes: { total: 0, keep: 0 }, refs: { total: 0, keep: 0 }, starts: 0, stops: 0 }; completedMatches.forEach(match => { const matchId = match.id; // Match strictly on matchId — a description substring check would let // 'FS-1-1' claim transactions belonging to FS-1-10..FS-1-16 const matchTxns = history.filter(t => t.matchId === matchId); const lanes = matchTxns.filter(t => t.type === 'ASSIGN_LANE'); const refs = matchTxns.filter(t => t.type === 'ASSIGN_REFEREE'); const starts = matchTxns.filter(t => t.type === 'START_MATCH'); const stops = matchTxns.filter(t => t.type === 'STOP_MATCH'); analysis.lanes.total += lanes.length; analysis.refs.total += refs.length; analysis.starts += starts.length; analysis.stops += stops.length; // Keep only last lane assignment if (lanes.length > 0) { analysis.lanes.keep++; toRemove.push(...lanes.slice(0, -1)); } // Keep only last referee assignment if (refs.length > 0) { analysis.refs.keep++; toRemove.push(...refs.slice(0, -1)); } // Remove all start/stop toRemove.push(...starts, ...stops); }); const totalRemovable = toRemove.length; const totalAfter = history.length - totalRemovable; const reductionPercentage = history.length > 0 ? Math.round((totalRemovable / history.length) * 100) : 0; const storageBefore = ((JSON.stringify(history).length * 2) / 1024 / 1024).toFixed(2); const storageAfter = ((JSON.stringify(history).length * (100 - reductionPercentage) / 100 * 2) / 1024 / 1024).toFixed(2); const storageSaved = (storageBefore - storageAfter).toFixed(2); const html = `

Smart Pruning Preview

ℹ️ Note: Pruning removes redundant transaction history for completed matches. This is safe - the undo system uses separate state snapshots and will continue to work normally.
Analysis complete for ${completedMatches.length} completed matches
Transactions to Remove:

• ${analysis.lanes.total - analysis.lanes.keep} old lane assignments (${analysis.lanes.total} total → ${analysis.lanes.keep} remain)

• ${analysis.refs.total - analysis.refs.keep} old referee assignments (${analysis.refs.total} total → ${analysis.refs.keep} remain)

• ${analysis.starts} match start events (all redundant)

• ${analysis.stops} match stop events (all redundant)

Total to remove: ${totalRemovable} transactions (${reductionPercentage}% reduction)

Transactions to Keep:

• ${completedMatches.length} match completions (all preserved)

• ${analysis.lanes.keep} final lane assignments (one per completed match)

• ${analysis.refs.keep} final referee assignments (one per completed match)

• All active match transactions (untouched)

Storage Impact:

• Before: ${history.length} transactions (~${storageBefore} MB)

• After: ${totalAfter} transactions (~${storageAfter} MB)

• Savings: ~${storageSaved} MB (${reductionPercentage}% reduction)

✅ Undo System Safety:
`; updateRightPane(html); } /** * Execute smart pruning */ function executeSmartPruning() { console.log('Executing smart transaction pruning...'); const history = getTournamentHistory ? getTournamentHistory() : []; const completedMatches = matches?.filter(m => m.completed) || []; if (!history || history.length === 0) { showCommandFeedback('Smart Pruning', 'error', 'No transaction history to prune'); return; } if (completedMatches.length === 0) { showCommandFeedback('Smart Pruning', 'error', 'No completed matches found - nothing to prune'); return; } const toRemove = []; const analysis = { lanes: 0, refs: 0, starts: 0, stops: 0 }; console.log(`Processing ${completedMatches.length} completed matches...`); completedMatches.forEach(match => { const matchId = match.id; // Match strictly on matchId — a description substring check would let // 'FS-1-1' claim transactions belonging to FS-1-10..FS-1-16 const matchTxns = history.filter(t => t.matchId === matchId); console.log(`Match ${matchId}: Found ${matchTxns.length} transactions`); const lanes = matchTxns.filter(t => t.type === 'ASSIGN_LANE'); const refs = matchTxns.filter(t => t.type === 'ASSIGN_REFEREE'); const starts = matchTxns.filter(t => t.type === 'START_MATCH'); const stops = matchTxns.filter(t => t.type === 'STOP_MATCH'); console.log(` - Lanes: ${lanes.length}, Refs: ${refs.length}, Starts: ${starts.length}, Stops: ${stops.length}`); // Keep only last lane assignment, remove rest if (lanes.length > 1) { const oldLanes = lanes.slice(0, -1); toRemove.push(...oldLanes); analysis.lanes += oldLanes.length; console.log(` - Removing ${oldLanes.length} old lane assignments`); } // Keep only last referee assignment, remove rest if (refs.length > 1) { const oldRefs = refs.slice(0, -1); toRemove.push(...oldRefs); analysis.refs += oldRefs.length; console.log(` - Removing ${oldRefs.length} old referee assignments`); } // Remove ALL start/stop (completion is sufficient) if (starts.length > 0 || stops.length > 0) { toRemove.push(...starts, ...stops); analysis.starts += starts.length; analysis.stops += stops.length; console.log(` - Removing ${starts.length} starts and ${stops.length} stops`); } }); console.log(`Total transactions to remove: ${toRemove.length}`); // Filter out transactions to remove // Skip transactions with no identifier — an undefined key would match // (and drop) every other id-less transaction const keyOf = t => t.id || t.timestamp || null; const idsToRemove = new Set(toRemove.map(keyOf).filter(k => k !== null)); console.log(`Created removal set with ${idsToRemove.size} unique IDs`); const prunedHistory = history.filter(t => { const key = keyOf(t); return key === null || !idsToRemove.has(key); }); const totalRemoved = toRemove.length; const before = history.length; const after = prunedHistory.length; const reductionPercentage = Math.round((totalRemoved / before) * 100); const storageBefore = ((JSON.stringify(history).length * 2) / 1024 / 1024).toFixed(2); const storageAfter = ((JSON.stringify(prunedHistory).length * 2) / 1024 / 1024).toFixed(2); const storageSaved = (storageBefore - storageAfter).toFixed(2); // Save pruned history to localStorage if (!tournament || !tournament.id) { console.error('No active tournament - cannot save pruned history'); showCommandFeedback('Smart Pruning', 'error', 'No active tournament'); return; } try { const historyKey = `tournament_${tournament.id}_history`; localStorage.setItem(historyKey, JSON.stringify(prunedHistory)); console.log(`✓ Smart pruning complete: ${totalRemoved} transactions removed`); console.log(`✓ Pruned history saved to localStorage`); } catch (error) { console.error('Failed to save pruned history:', error); showCommandFeedback('Smart Pruning', 'error', `Failed to save pruned history\n• Error: ${error.message}`); return; } showCommandFeedback('Smart Pruning', 'success', `Pruned redundant transactions for ${completedMatches.length} completed matches\n\n` + `Removed by Type:\n` + `• ${analysis.lanes} old lane assignments (kept final assignments)\n` + `• ${analysis.refs} old referee assignments (kept final assignments)\n` + `• ${analysis.starts} match start events (kept completion records)\n` + `• ${analysis.stops} match stop events (kept completion records)\n\n` + `Total Removed: ${totalRemoved} transactions\n\n` + `Results:\n` + `• Before: ${before} transactions (~${storageBefore} MB)\n` + `• After: ${after} transactions (~${storageAfter} MB)\n` + `• Reduction: ↓ ${totalRemoved} (${reductionPercentage}% fewer transactions)\n` + `• Storage freed: ~${storageSaved} MB (${Math.round(storageSaved/storageBefore*100)}% space saved)\n\n` + `Verification:\n` + `• All ${completedMatches.length} match results preserved\n` + `• Final lane assignments intact\n` + `• Final referee assignments intact\n` + `• Active matches untouched`); } /** * Clear console output */ function clearConsoleOutput() { consoleBuffer = []; console.log('Console output cleared'); // No need to change view - console is always visible in footer } /** * Copy console output to clipboard */ function copyConsoleOutput() { if (consoleBuffer.length === 0) { alert('No console output to copy'); return; } const text = consoleBuffer.map(entry => `[${entry.timestamp}] ${entry.message}`).join('\n'); navigator.clipboard.writeText(text).then(() => { console.log('Console output copied to clipboard'); alert('Console output copied to clipboard'); }).catch(err => { console.error('Failed to copy to clipboard:', err); alert('Failed to copy to clipboard'); }); } /** * Toggle Console Output visibility */ function toggleConsoleOutput() { const content = document.getElementById('consoleOutputContent'); const icon = document.getElementById('consoleToggleIcon'); if (content && icon) { const open = content.style.display === 'none'; content.style.display = open ? 'block' : 'none'; icon.textContent = open ? '▼' : '▶'; const footer = document.getElementById('analyticsConsoleFooter'); if (footer) footer.classList.toggle('dc-open', open); if (open) content.scrollTop = content.scrollHeight; } } /** * Debug Analytics (callable from browser console) */ function debugAnalytics() { console.log('=== DEVELOPER CONSOLE DEBUG ==='); const results = runAllValidations(); results.forEach(result => { console.log(`${result.valid ? '✅' : '⚠️'} ${result.name}: ${result.message}`); if (result.details.length > 0) { result.details.forEach(detail => console.log(` - ${detail}`)); } }); } // ============================================================================ // INITIALIZATION // ============================================================================ // Make functions globally accessible if (typeof window !== 'undefined') { window.startDeveloperConsole = startDeveloperConsole; window.stopDeveloperConsole = stopDeveloperConsole; window.debugAnalytics = debugAnalytics; window.showQuickOverview = showQuickOverview; window.showTransactionBreakdown = showTransactionBreakdown; window.showMatchStateDetails = showMatchStateDetails; window.showTransactionHistory = showTransactionHistory; window.showConsoleOutput = showConsoleOutput; window.showPlayerDetails = showPlayerDetails; window.showLaneUsageDetails = showLaneUsageDetails; window.showValidationResults = showValidationResults; window.commandReRenderBracket = commandReRenderBracket; window.commandRecalculateRankings = commandRecalculateRankings; window.commandRefreshDropdowns = commandRefreshDropdowns; window.commandValidateEverything = commandValidateEverything; window.commandResetAllConfig = commandResetAllConfig; window.commandToggleReadOnly = commandToggleReadOnly; window.commandLateRegistration = commandLateRegistration; window.commandLateRegistrationStep2 = commandLateRegistrationStep2; window.commandLateRegistrationStep3 = commandLateRegistrationStep3; window.executeResetAllConfig = executeResetAllConfig; window.clearConsoleOutput = clearConsoleOutput; window.copyConsoleOutput = copyConsoleOutput; window.toggleConsoleOutput = toggleConsoleOutput; window.showTransactionLogManagement = showTransactionLogManagement; window.previewSmartPruning = previewSmartPruning; window.executeSmartPruning = executeSmartPruning; window.showLocalStorageUsage = showLocalStorageUsage; window.applyTransactionFilters = applyTransactionFilters; } console.log('✓ Analytics module loaded');