diff --git a/public/css/players.css b/public/css/players.css index ce221a3..844d152 100644 --- a/public/css/players.css +++ b/public/css/players.css @@ -101,75 +101,6 @@ color: var(--accent) !important; } -/* ── Debug Modal ──────────────────────────────── */ - -.debug-modal { - position: fixed; - top: 0; - left: 0; - width: 100%; - height: 100%; - background: rgba(15, 23, 42, 0.5); - backdrop-filter: blur(4px); - z-index: 10001; - display: none; - justify-content: center; - align-items: center; -} - -.debug-content { - background: var(--surface-1); - border-radius: var(--radius-lg); - padding: 24px; - max-width: 640px; - width: 90%; - max-height: 80vh; - overflow-y: auto; - box-shadow: var(--shadow-overlay); - position: relative; -} - -.debug-header { - font-weight: 700; - font-size: 16px; - margin-bottom: 16px; - color: var(--text-primary); - padding-bottom: 12px; - border-bottom: 1px solid var(--border); -} - -.debug-log { - font-family: var(--font-mono); - background: var(--surface-2); - border: 1px solid var(--border); - border-radius: var(--radius-md); - padding: 16px; - font-size: 12px; - line-height: 1.6; - white-space: pre-line; - color: var(--text-primary); -} - -.debug-close { - position: absolute; - top: 12px; - right: 16px; - font-size: 22px; - color: var(--text-muted); - cursor: pointer; - background: none; - border: none; - padding: 4px; - border-radius: var(--radius-sm); - transition: color var(--transition), background var(--transition); - line-height: 1; -} - -.debug-close:hover { - color: var(--text-primary); - background: var(--surface-3); -} - /* ── Add Player Modal ─────────────────────────── */ .modal { diff --git a/public/js/players.js b/public/js/players.js index 45d3841..7334ee4 100644 --- a/public/js/players.js +++ b/public/js/players.js @@ -1,4 +1,3 @@ -const cachedDebugInfo = {}; let pendingPlayerData = null; let openPdgaNumber = null; @@ -186,10 +185,6 @@ async function refreshRoundHistory(pdgaNumber) { } if (data.success) { - if (data.debugLog) { - cachedDebugInfo[pdgaNumber] = data.debugLog; - } - const predictedCell = document.getElementById(`predicted-${pdgaNumber}`); if (predictedCell) { const predictedValue = predictedCell.querySelector('.predicted-value'); @@ -234,43 +229,6 @@ async function refreshRatingHistory(pdgaNumber) { } } -async function showDebugInfo(pdgaNumber) { - const modal = document.getElementById('debug-modal'); - const header = document.getElementById('debug-header'); - const log = document.getElementById('debug-log'); - - const playerNameElement = document.querySelector(`#row-${pdgaNumber} .player-name a`); - const playerName = playerNameElement ? playerNameElement.textContent : `PDGA #${pdgaNumber}`; - - header.textContent = `Prediction Calculation Details - ${playerName}`; - log.textContent = 'Loading calculation details...'; - modal.style.display = 'flex'; - - try { - if (cachedDebugInfo[pdgaNumber]) { - log.textContent = cachedDebugInfo[pdgaNumber].join('\n'); - return; - } - - const response = await fetch(`/api/refresh-round-history/${pdgaNumber}`, { method: 'POST' }); - const data = await response.json(); - - if (data.success && data.debugLog) { - cachedDebugInfo[pdgaNumber] = data.debugLog; - log.textContent = data.debugLog.join('\n'); - } else { - log.textContent = 'No debug information available. Try refreshing the prediction first.'; - } - } catch (error) { - console.error('Error fetching debug info:', error); - log.textContent = 'Error loading debug information. Please try again.'; - } -} - -function closeDebugModal(event) { - document.getElementById('debug-modal').style.display = 'none'; -} - async function searchAndAddPlayer(event) { if (event) event.preventDefault(); const input = document.getElementById('pdga-number-input'); @@ -712,7 +670,6 @@ async function refreshHistoryThenCalculate(pdgaNumber) { return; } - if (data.debugLog) cachedDebugInfo[pdgaNumber] = data.debugLog; const predictedCell = document.getElementById('predicted-' + pdgaNumber); if (predictedCell) { const predictedValue = predictedCell.querySelector('.predicted-value'); diff --git a/src/db.js b/src/db.js index ed781fa..8163638 100644 --- a/src/db.js +++ b/src/db.js @@ -34,6 +34,7 @@ function initializeDatabase() { const hasLastRoundUpdate = columns.some(col => col.name === 'last_round_update'); const hasPredictedRating = columns.some(col => col.name === 'predicted_rating'); const hasStdDev = columns.some(col => col.name === 'std_dev'); + const hasExcludedRoundsCount = columns.some(col => col.name === 'excluded_rounds_count'); if (!hasLastRoundUpdate) { logger.info('Adding last_round_update column to players table...'); @@ -58,6 +59,14 @@ function initializeDatabase() { else logger.info('Successfully added std_dev column'); }); } + + if (!hasExcludedRoundsCount) { + logger.info('Adding excluded_rounds_count column to players table...'); + db.run(`ALTER TABLE players ADD COLUMN excluded_rounds_count INTEGER DEFAULT NULL`, (err) => { + if (err) logger.error('Error adding excluded_rounds_count column:', err.message); + else logger.info('Successfully added excluded_rounds_count column'); + }); + } }); }); diff --git a/src/models/player.js b/src/models/player.js index 979d388..1d63b28 100644 --- a/src/models/player.js +++ b/src/models/player.js @@ -172,11 +172,11 @@ function saveRoundHistoryToDB(pdgaNumber, roundData, isIncremental = false) { }); } -function savePredictedRatingToDB(pdgaNumber, predictedRating, stdDev = null) { +function savePredictedRatingToDB(pdgaNumber, predictedRating, stdDev = null, excludedRoundsCount = null) { return new Promise((resolve, reject) => { db.run( - 'UPDATE players SET predicted_rating = ?, std_dev = ? WHERE pdga_number = ?', - [predictedRating, stdDev, pdgaNumber], + 'UPDATE players SET predicted_rating = ?, std_dev = ?, excluded_rounds_count = ? WHERE pdga_number = ?', + [predictedRating, stdDev, excludedRoundsCount, pdgaNumber], function(err) { if (err) reject(err); else resolve(); diff --git a/src/routes/players.js b/src/routes/players.js index 783c5f3..bd0434b 100644 --- a/src/routes/players.js +++ b/src/routes/players.js @@ -400,7 +400,7 @@ router.post('/api/refresh-round-history/:pdgaNumber', async (req, res) => { const result = calculatePredictedRating(roundsForPrediction); - await savePredictedRatingToDB(pdgaNumber, result.rating, result.stdDev); + await savePredictedRatingToDB(pdgaNumber, result.rating, result.stdDev, result.excludedRoundsCount); const officialCount = allRounds.filter(r => r.source === 'official').length; const newCount = allRounds.filter(r => r.source === 'new').length; @@ -409,7 +409,7 @@ router.post('/api/refresh-round-history/:pdgaNumber', async (req, res) => { success: true, predictedRating: result.rating, stdDev: result.stdDev, - debugLog: result.debugLog, + excludedRoundsCount: result.excludedRoundsCount, totalRounds: roundsForPrediction.length, officialRounds: officialCount, newRounds: newCount, diff --git a/src/services/player-service.js b/src/services/player-service.js index 98a4517..4de0b5b 100644 --- a/src/services/player-service.js +++ b/src/services/player-service.js @@ -40,10 +40,12 @@ async function getPlayerDataFromDB(pdgaNumber, { includeMonthlyHistory = true } let predictedRating = cachedPlayer.predicted_rating; let stdDev = cachedPlayer.std_dev; + let excludedRoundsCount = cachedPlayer.excluded_rounds_count; if (!predictedRating || predictedRating === 0) { predictedRating = await getPredictedRatingFromDB(pdgaNumber); const updatedPlayer = await getPlayerFromDB(pdgaNumber); stdDev = updatedPlayer?.std_dev; + excludedRoundsCount = updatedPlayer?.excluded_rounds_count; } const rating = cachedPlayer.current_rating; @@ -65,6 +67,7 @@ async function getPlayerDataFromDB(pdgaNumber, { includeMonthlyHistory = true } ratingChange, predictedRating: resolvedPredicted, stdDev: resolvedStdDev, + excludedRoundsCount: (excludedRoundsCount != null && excludedRoundsCount >= 0) ? excludedRoundsCount : null, lastMonthRating, // gap between next predicted update and current rating (null when either is missing) deltaPredicted: (resolvedPredicted != null && rating != null) ? resolvedPredicted - rating : null, @@ -145,7 +148,7 @@ async function getPredictedRatingFromDB(pdgaNumber) { const result = calculatePredictedRating(roundRatings); - await savePredictedRatingToDB(pdgaNumber, result.rating, result.stdDev); + await savePredictedRatingToDB(pdgaNumber, result.rating, result.stdDev, result.excludedRoundsCount); return result.rating; } @@ -229,6 +232,7 @@ async function getAllRatingsFromDB(progressCallback = null) { ratingChange: errorRatingChange, predictedRating: null, stdDev: null, + excludedRoundsCount: null, lastMonthRating: (errorRating != null && errorRatingChange != null) ? errorRating - errorRatingChange : null, deltaPredicted: null, monthlyHistory: [], diff --git a/src/services/rating-calculator.js b/src/services/rating-calculator.js index f915102..ad7adcc 100644 --- a/src/services/rating-calculator.js +++ b/src/services/rating-calculator.js @@ -85,7 +85,7 @@ function calculatePredictedRating(roundRatings) { if (!roundRatings || roundRatings.length === 0) { debugLog.push('❌ No rounds provided for prediction'); - return { rating: 0, debugLog }; + return { rating: 0, debugLog, excludedRoundsCount: null }; } debugLog.push(`📊 Starting with ${roundRatings.length} total rounds`); @@ -100,7 +100,7 @@ function calculatePredictedRating(roundRatings) { if (allSortedRounds.length === 0) { debugLog.push('❌ No valid rounds after filtering for update date'); - return { rating: 0, debugLog }; + return { rating: 0, debugLog, excludedRoundsCount: null }; } debugLog.push(`📊 After update date filter: ${allSortedRounds.length} rounds`); @@ -127,7 +127,7 @@ function calculatePredictedRating(roundRatings) { if (eligibleRounds.length === 0) { debugLog.push('❌ No eligible rounds found'); - return { rating: 0, debugLog }; + return { rating: 0, debugLog, excludedRoundsCount: null }; } debugLog.push(`📈 ELIGIBLE ROUNDS: ${eligibleRounds.length}`); @@ -137,6 +137,7 @@ function calculatePredictedRating(roundRatings) { let workingRounds = [...eligibleRounds]; let workingRatings = workingRounds.map(r => r.rating); + let excludedRoundsCount = 0; if (workingRatings.length >= 7) { debugLog.push('🔍 OUTLIER EXCLUSION (≥7 rounds available):'); @@ -160,6 +161,8 @@ function calculatePredictedRating(roundRatings) { const stdDevOutliers = workingRatings.filter(rating => rating < stdDevCutoff); const hundredPointOutliers = workingRatings.filter(rating => rating < hundredPointCutoff && rating >= stdDevCutoff); + excludedRoundsCount = stdDevOutliers.length + hundredPointOutliers.length; + if (stdDevOutliers.length > 0) { debugLog.push(` ❌ 2.5σ outliers removed: ${stdDevOutliers.length} rounds`); stdDevOutliers.forEach(rating => { @@ -228,7 +231,7 @@ function calculatePredictedRating(roundRatings) { debugLog.push(` Final Rating: ${finalRating}`); debugLog.push('=== END PDGA CALCULATION ==='); - return { rating: finalRating, stdDev: Math.round(stdDev), debugLog }; + return { rating: finalRating, stdDev: Math.round(stdDev), debugLog, excludedRoundsCount }; } module.exports = { parseDate, getNextPDGAUpdateDate, calculatePredictedRating, calculateStandardDeviation }; diff --git a/views/pages/index.ejs b/views/pages/index.ejs index a3d994f..fb67a5e 100644 --- a/views/pages/index.ejs +++ b/views/pages/index.ejs @@ -95,15 +95,6 @@ `; %> <% var modals = ` - -
-
- -
Prediction Calculation Details
-
Loading...
-
-
- <% } %> diff --git a/views/partials/ratings-cards.ejs b/views/partials/ratings-cards.ejs index bd6d81f..41d3f21 100644 --- a/views/partials/ratings-cards.ejs +++ b/views/partials/ratings-cards.ejs @@ -124,6 +124,12 @@ function renderSparkline(values, opts) {
<%= player.rating - player.stdDev %>–<%= player.rating + player.stdDev %>
<% } %> + <% if (player.excludedRoundsCount != null && player.rating != null) { %> +
+
Excluded rounds
+
<%= player.excludedRoundsCount %>
+
+ <% } %>