diff --git a/index.html b/index.html index 19611b9..c22b385 100644 --- a/index.html +++ b/index.html @@ -253,6 +253,64 @@ display: inline-flex; align-items: center; } + .debug-icon:hover { + opacity: 1 !important; + color: #007bff !important; + transform: scale(1.1); + } + .debug-modal { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background-color: rgba(0, 0, 0, 0.5); + z-index: 10001; + display: none; + justify-content: center; + align-items: center; + } + .debug-content { + background: white; + border-radius: 8px; + padding: 20px; + max-width: 600px; + max-height: 80vh; + overflow-y: auto; + box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3); + } + .debug-header { + font-weight: bold; + font-size: 18px; + margin-bottom: 15px; + color: #333; + border-bottom: 2px solid #007bff; + padding-bottom: 10px; + } + .debug-log { + font-family: 'Courier New', monospace; + background-color: #f8f9fa; + border: 1px solid #e9ecef; + border-radius: 4px; + padding: 15px; + font-size: 12px; + line-height: 1.4; + white-space: pre-line; + color: #495057; + } + .debug-close { + position: absolute; + top: 10px; + right: 15px; + font-size: 24px; + color: #999; + cursor: pointer; + background: none; + border: none; + } + .debug-close:hover { + color: #333; + } @@ -272,6 +330,15 @@
+ +
+
+ +
Prediction Calculation Details
+
Loading...
+
+
+ diff --git a/server.js b/server.js index c283401..eb89d7b 100644 --- a/server.js +++ b/server.js @@ -548,10 +548,10 @@ async function getPredictedRating(browser, pdgaNumber, retries = 2) { try { console.log(`Predicted rating attempt ${attempt}/${retries} for PDGA ${pdgaNumber}`); const roundRatings = await getPlayerCompetitionRatings(browser, pdgaNumber); - const predictedRating = calculatePredictedRating(roundRatings); + const result = calculatePredictedRating(roundRatings); - if (predictedRating > 0) { - return predictedRating; + if (result.rating > 0) { + return result.rating; } if (attempt < retries) { @@ -579,10 +579,12 @@ async function getPredictedRatingFromDB(pdgaNumber) { // Convert to the format expected by calculatePredictedRating const roundRatings = roundHistory.map(round => ({ rating: round.rating, - date: new Date(round.date) + date: new Date(round.date), + competition: round.competition_name || 'Unknown' })); - return calculatePredictedRating(roundRatings); + const result = calculatePredictedRating(roundRatings); + return result.rating; } return 0; } catch (err) { @@ -1242,19 +1244,20 @@ function getNextPDGAUpdateDate() { } function calculatePredictedRating(roundRatings) { - console.log('\n=== PDGA RATING CALCULATION (Following Official Rules) ==='); + const debugLog = []; + debugLog.push('=== PDGA RATING CALCULATION (Following Official Rules) ==='); if (!roundRatings || roundRatings.length === 0) { - console.log('āŒ No rounds provided for prediction'); - return 0; + debugLog.push('āŒ No rounds provided for prediction'); + return { rating: 0, debugLog }; } - console.log(`šŸ“Š Starting with ${roundRatings.length} total rounds`); + debugLog.push(`šŸ“Š Starting with ${roundRatings.length} total rounds`); // PDGA Simulation: Only include rounds that would be rated by next update const nextUpdateDate = getNextPDGAUpdateDate(); - console.log(`šŸŽÆ PDGA Update Simulation: Next update date is ${nextUpdateDate.toDateString()}`); - console.log(` Only including rounds played before ${nextUpdateDate.toDateString()}`); + debugLog.push(`šŸŽÆ PDGA Update Simulation: Next update date is ${nextUpdateDate.toDateString()}`); + debugLog.push(` Only including rounds played before ${nextUpdateDate.toDateString()}`); // Sort all rounds by date (most recent first), but only include rounds before next update const allSortedRounds = roundRatings @@ -1262,25 +1265,25 @@ function calculatePredictedRating(roundRatings) { .sort((a, b) => b.date - a.date); if (allSortedRounds.length === 0) { - console.log('āŒ No valid rounds after filtering for update date'); - return 0; + debugLog.push('āŒ No valid rounds after filtering for update date'); + return { rating: 0, debugLog }; } - console.log(`šŸ“Š After update date filter: ${allSortedRounds.length} rounds`); + debugLog.push(`šŸ“Š After update date filter: ${allSortedRounds.length} rounds`); // PDGA Rule: Use rounds from 12 months prior to next update date const twelveMonthsBeforeUpdate = new Date(nextUpdateDate); twelveMonthsBeforeUpdate.setFullYear(twelveMonthsBeforeUpdate.getFullYear() - 1); const mostRecentDate = allSortedRounds[0].date; - console.log(`šŸ“… Most recent round: ${mostRecentDate.toDateString()}`); - console.log(`šŸ“… 12-month cutoff: ${twelveMonthsBeforeUpdate.toDateString()} (1 year before update)`); + debugLog.push(`šŸ“… Most recent round: ${mostRecentDate.toDateString()}`); + debugLog.push(`šŸ“… 12-month cutoff: ${twelveMonthsBeforeUpdate.toDateString()} (1 year before update)`); // Step 1: Get rounds from last 12 months before update let eligibleRounds = allSortedRounds.filter(r => r.date >= twelveMonthsBeforeUpdate); - console.log(`\nšŸ—“ļø 12-MONTH FILTERING:`); - console.log(`āœ… Rounds in last 12 months: ${eligibleRounds.length}`); + debugLog.push('šŸ—“ļø 12-MONTH FILTERING:'); + debugLog.push(`āœ… Rounds in last 12 months: ${eligibleRounds.length}`); // PDGA Rule: If fewer than 8 rounds in 12 months, extend to 24 months before update if (eligibleRounds.length < 8) { @@ -1288,17 +1291,17 @@ function calculatePredictedRating(roundRatings) { twentyFourMonthsBeforeUpdate.setFullYear(twentyFourMonthsBeforeUpdate.getFullYear() - 2); eligibleRounds = allSortedRounds.filter(r => r.date >= twentyFourMonthsBeforeUpdate); - console.log(`āš ļø Extended to 24 months before update (${twentyFourMonthsBeforeUpdate.toDateString()}) - now ${eligibleRounds.length} rounds`); + debugLog.push(`āš ļø Extended to 24 months before update (${twentyFourMonthsBeforeUpdate.toDateString()}) - now ${eligibleRounds.length} rounds`); } if (eligibleRounds.length === 0) { - console.log('āŒ No eligible rounds found'); - return 0; + debugLog.push('āŒ No eligible rounds found'); + return { rating: 0, debugLog }; } - console.log(`\nšŸ“ˆ ELIGIBLE ROUNDS: ${eligibleRounds.length}`); + debugLog.push(`šŸ“ˆ ELIGIBLE ROUNDS: ${eligibleRounds.length}`); eligibleRounds.forEach((round, index) => { - console.log(` ${index + 1}. ${round.date.toDateString()}: ${round.rating} (${round.competition})`); + debugLog.push(` ${index + 1}. ${round.date.toDateString()}: ${round.rating} (${round.competition})`); }); let workingRounds = [...eligibleRounds]; @@ -1306,13 +1309,13 @@ function calculatePredictedRating(roundRatings) { // PDGA Rule: Apply outlier exclusion if ≄7 rounds if (workingRatings.length >= 7) { - console.log(`\nšŸ” OUTLIER EXCLUSION (≄7 rounds available):`); + debugLog.push('šŸ” OUTLIER EXCLUSION (≄7 rounds available):'); const mean = workingRatings.reduce((sum, r) => sum + r, 0) / workingRatings.length; const stdDev = calculateStandardDeviation(workingRatings); - console.log(` Mean: ${mean.toFixed(1)}`); - console.log(` Std Dev: ${stdDev.toFixed(1)}`); + debugLog.push(` Mean: ${mean.toFixed(1)}`); + debugLog.push(` Std Dev: ${stdDev.toFixed(1)}`); // Two PDGA exclusion rules: // 1. More than 2.5 standard deviations below average @@ -1320,8 +1323,8 @@ function calculatePredictedRating(roundRatings) { // 2. More than 100 points below average const hundredPointCutoff = mean - 100; - console.log(` 2.5σ cutoff: ${stdDevCutoff.toFixed(1)}`); - console.log(` 100-point cutoff: ${hundredPointCutoff.toFixed(1)}`); + debugLog.push(` 2.5σ cutoff: ${stdDevCutoff.toFixed(1)}`); + debugLog.push(` 100-point cutoff: ${hundredPointCutoff.toFixed(1)}`); const filteredByStdDev = workingRatings.filter(rating => rating >= stdDevCutoff); const filteredBy100Points = workingRatings.filter(rating => rating >= hundredPointCutoff); @@ -1335,23 +1338,23 @@ function calculatePredictedRating(roundRatings) { const hundredPointOutliers = workingRatings.filter(rating => rating < hundredPointCutoff && rating >= stdDevCutoff); if (stdDevOutliers.length > 0) { - console.log(` āŒ 2.5σ outliers removed: ${stdDevOutliers.length} rounds`); + debugLog.push(` āŒ 2.5σ outliers removed: ${stdDevOutliers.length} rounds`); stdDevOutliers.forEach(rating => { const round = workingRounds.find(r => r.rating === rating); - console.log(` - ${rating} (${round.date.toDateString()}: ${round.competition})`); + debugLog.push(` - ${rating} (${round.date.toDateString()}: ${round.competition})`); }); } if (hundredPointOutliers.length > 0) { - console.log(` āŒ 100-point outliers removed: ${hundredPointOutliers.length} rounds`); + debugLog.push(` āŒ 100-point outliers removed: ${hundredPointOutliers.length} rounds`); hundredPointOutliers.forEach(rating => { const round = workingRounds.find(r => r.rating === rating); - console.log(` - ${rating} (${round.date.toDateString()}: ${round.competition})`); + debugLog.push(` - ${rating} (${round.date.toDateString()}: ${round.competition})`); }); } if (stdDevOutliers.length === 0 && hundredPointOutliers.length === 0) { - console.log(` āœ… No outliers detected`); + debugLog.push(` āœ… No outliers detected`); } // Keep filtered rounds only if we still have enough data @@ -1360,21 +1363,21 @@ function calculatePredictedRating(roundRatings) { round.rating >= stdDevCutoff && round.rating >= hundredPointCutoff ); workingRatings = filteredRatings; - console.log(` āœ… Using ${filteredRatings.length} rounds after outlier removal`); + debugLog.push(` āœ… Using ${filteredRatings.length} rounds after outlier removal`); } else { - console.log(` āš ļø Too few rounds after outlier removal (${filteredRatings.length}), keeping all rounds`); + debugLog.push(` āš ļø Too few rounds after outlier removal (${filteredRatings.length}), keeping all rounds`); } } else { - console.log(`\nā­ļø OUTLIER EXCLUSION SKIPPED (only ${workingRatings.length} rounds, need ≄7)`); + debugLog.push(`ā­ļø OUTLIER EXCLUSION SKIPPED (only ${workingRatings.length} rounds, need ≄7)`); } // PDGA Rule: Most recent 25% of rounds get double weight if ≄9 rounds - console.log(`\nāš–ļø WEIGHTING (Most recent 25% count double if ≄9 rounds):`); + debugLog.push('āš–ļø WEIGHTING (Most recent 25% count double if ≄9 rounds):'); const weightedRatings = []; if (workingRatings.length >= 9) { const recentCount = Math.round(workingRatings.length * 0.25); - console.log(` āœ… Double-weighting most recent ${recentCount} rounds`); + debugLog.push(` āœ… Double-weighting most recent ${recentCount} rounds`); // Add all ratings once weightedRatings.push(...workingRatings); @@ -1383,12 +1386,12 @@ function calculatePredictedRating(roundRatings) { for (let i = 0; i < recentCount; i++) { weightedRatings.push(workingRatings[i]); const round = workingRounds[i]; - console.log(` 2x weight: ${workingRatings[i]} (${round.date.toDateString()}: ${round.competition})`); + debugLog.push(` 2x weight: ${workingRatings[i]} (${round.date.toDateString()}: ${round.competition})`); } - console.log(` šŸ“Š Total values: ${workingRatings.length} + ${recentCount} double-weighted = ${weightedRatings.length}`); + debugLog.push(` šŸ“Š Total values: ${workingRatings.length} + ${recentCount} double-weighted = ${weightedRatings.length}`); } else { - console.log(` āž”ļø No double weighting (${workingRatings.length} rounds, need ≄9)`); + debugLog.push(` āž”ļø No double weighting (${workingRatings.length} rounds, need ≄9)`); weightedRatings.push(...workingRatings); } @@ -1397,14 +1400,14 @@ function calculatePredictedRating(roundRatings) { const average = sum / weightedRatings.length; const finalRating = Math.round(average); - console.log(`\nšŸŽÆ FINAL CALCULATION:`); - console.log(` Sum: ${sum}`); - console.log(` Count: ${weightedRatings.length}`); - console.log(` Average: ${average.toFixed(1)}`); - console.log(` Final Rating: ${finalRating}`); - console.log('=== END PDGA CALCULATION ===\n'); + debugLog.push('šŸŽÆ FINAL CALCULATION:'); + debugLog.push(` Sum: ${sum}`); + debugLog.push(` Count: ${weightedRatings.length}`); + debugLog.push(` Average: ${average.toFixed(1)}`); + debugLog.push(` Final Rating: ${finalRating}`); + debugLog.push('=== END PDGA CALCULATION ==='); - return finalRating; + return { rating: finalRating, debugLog }; } function calculateStandardDeviation(ratings) { @@ -2046,7 +2049,7 @@ app.post('/api/refresh-round-history/:pdgaNumber', async (req, res) => { competition: round.competition_name })); - const predictedRating = calculatePredictedRating(roundsForPrediction); + const result = calculatePredictedRating(roundsForPrediction); // Count official vs new rounds const officialCount = allRounds.filter(r => r.source === 'official').length; @@ -2054,7 +2057,8 @@ app.post('/api/refresh-round-history/:pdgaNumber', async (req, res) => { res.json({ success: true, - predictedRating, + predictedRating: result.rating, + debugLog: result.debugLog, totalRounds: roundsForPrediction.length, officialRounds: officialCount, newRounds: newCount, @@ -2187,11 +2191,12 @@ app.post('/api/predicted-rating/:pdgaNumber', async (req, res) => { competition: round.competition_name })); - const predictedRating = calculatePredictedRating(roundRatings); + const result = calculatePredictedRating(roundRatings); res.json({ pdgaNumber: parseInt(pdgaNumber), - predictedRating + predictedRating: result.rating, + debugLog: result.debugLog }); } catch (error) { console.error('Error calculating predicted rating:', error.message || error);