diff --git a/src/routes/players.js b/src/routes/players.js index 12df148..0e313e9 100644 --- a/src/routes/players.js +++ b/src/routes/players.js @@ -7,7 +7,7 @@ const { getOfficialRatingHistory, getOptimizedPlayerRounds } = require('../scrap const { launchBrowser } = require('../scrapers/browser'); const { getPlayerDataFromDB, scrapePDGARating, getAllRatingsFromDB, refreshAllPlayersInDB, getPredictedRatingFromDB, formatDisplayDate } = require('../services/player-service'); const { getTopbarLocals } = require('../services/topbar-service'); -const { calculatePredictedRating } = require('../services/rating-calculator'); +const { calculatePredictedRating, getNextPDGAUpdateDate } = require('../services/rating-calculator'); const { calculateRequiredAverage } = require('../services/target-rating-calculator'); const logger = require('../logger'); @@ -449,9 +449,9 @@ router.get('/api/admin/player-state/:pdgaNumber', async (req, res) => { return res.status(404).json({ error: 'Player not found', pdgaNumber: parseInt(pdgaNumber) }); } const rounds = await getRoundHistoryFromDB(pdgaNumber); - const now = new Date(); - const twelveMonthsAgo = new Date(now); twelveMonthsAgo.setFullYear(now.getFullYear() - 1); - const twentyFourMonthsAgo = new Date(now); twentyFourMonthsAgo.setFullYear(now.getFullYear() - 2); + const cutoff = getNextPDGAUpdateDate(); + const twelveMonthsAgo = new Date(cutoff); twelveMonthsAgo.setFullYear(cutoff.getFullYear() - 1); + const twentyFourMonthsAgo = new Date(cutoff); twentyFourMonthsAgo.setFullYear(cutoff.getFullYear() - 2); const roundsInLast12mo = rounds.filter(r => new Date(r.date) >= twelveMonthsAgo).length; const roundsInLast24mo = rounds.filter(r => new Date(r.date) >= twentyFourMonthsAgo).length; @@ -468,6 +468,7 @@ router.get('/api/admin/player-state/:pdgaNumber', async (req, res) => { cutoffRating: player.cutoff_rating, lastUpdated: player.last_updated, lastRoundUpdate: player.last_round_update, + cutoffDate: cutoff.toISOString(), roundCount: rounds.length, roundsInLast12mo, roundsInLast24mo, @@ -475,7 +476,7 @@ router.get('/api/admin/player-state/:pdgaNumber', async (req, res) => { newestRound: dates[dates.length - 1] ?? null }); } catch (err) { - logger.error({ err: err.message, pdgaNumber }, 'admin player-state endpoint failed'); + logger.error({ err, pdgaNumber }, 'admin player-state endpoint failed'); res.status(500).json({ error: 'Failed to fetch player state', details: err.message }); } }); @@ -519,7 +520,7 @@ router.post('/api/calculate-target-rating/:pdgaNumber', async (req, res) => { competition: r.competition_name })); - const result = calculateRequiredAverage(roundRatings, target, numRounds); + const result = calculateRequiredAverage(roundRatings, target, numRounds, pdgaNum); logger.info(`Target rating calc for PDGA ${pdgaNum}: target=${target} rounds=${numRounds} -> avg=${result.requiredAverage}`); diff --git a/src/services/player-service.js b/src/services/player-service.js index a94b98e..3ce2e50 100644 --- a/src/services/player-service.js +++ b/src/services/player-service.js @@ -42,7 +42,9 @@ async function getPlayerDataFromDB(pdgaNumber, { includeMonthlyHistory = true } let stdDev = cachedPlayer.std_dev; let excludedRoundsCount = cachedPlayer.excluded_rounds_count; let cutoffRating = cachedPlayer.cutoff_rating; + let recomputeAttempted = false; if (!predictedRating || predictedRating === 0) { + recomputeAttempted = true; logger.debug({ pdgaNumber, dbValue: cachedPlayer.predicted_rating }, 'lazy recompute triggered for predicted_rating'); predictedRating = await getPredictedRatingFromDB(pdgaNumber); const updatedPlayer = await getPlayerFromDB(pdgaNumber); @@ -56,8 +58,9 @@ async function getPlayerDataFromDB(pdgaNumber, { includeMonthlyHistory = true } const rating = cachedPlayer.current_rating; const rawRatingChange = cachedPlayer.rating_change; - if (predictedRating != null && predictedRating <= 0) { - logger.warn({ pdgaNumber, dbValue: predictedRating }, 'predicted rating present but <= 0 — rendered as empty'); + // Only warn about ≤0 if it wasn't already explained by a failed recompute (which has its own log) + if (!recomputeAttempted && predictedRating != null && predictedRating <= 0) { + logger.warn({ pdgaNumber, dbValue: predictedRating }, 'predicted rating present but <= 0 in DB without recompute — rendered as empty'); } const resolvedPredicted = predictedRating > 0 ? predictedRating : null; const resolvedStdDev = stdDev > 0 ? stdDev : null; diff --git a/src/services/target-rating-calculator.js b/src/services/target-rating-calculator.js index 94d7f5b..953510c 100644 --- a/src/services/target-rating-calculator.js +++ b/src/services/target-rating-calculator.js @@ -1,14 +1,14 @@ const { calculatePredictedRating, getNextPDGAUpdateDate } = require('./rating-calculator'); const logger = require('../logger'); -function calculateRequiredAverage(roundRatings, targetRating, numRounds) { +function calculateRequiredAverage(roundRatings, targetRating, numRounds, pdgaNumber) { if (!Array.isArray(roundRatings) || roundRatings.length === 0) { const err = new Error('No round history'); err.code = 'NO_ROUNDS'; throw err; } - const currentPredicted = calculatePredictedRating(roundRatings, { callsite: 'target-rating-calculator' }).rating; + const currentPredicted = calculatePredictedRating(roundRatings, { pdgaNumber, callsite: 'target-rating-calculator' }).rating; const nextUpdate = getNextPDGAUpdateDate(); const syntheticDate = new Date(nextUpdate.getTime() - 24 * 60 * 60 * 1000);