Merge pull request 'fix: invalidate stale predicted_rating after PDGA cycle rollover (#29)' (#31) from fix/invalidate-stale-predicted-rating-29 into main
Release / release (push) Failing after 6s
Release / release (push) Failing after 6s
This commit was merged in pull request #31.
This commit is contained in:
@@ -76,6 +76,16 @@ function initializeDatabase() {
|
|||||||
else logger.info('Successfully added cutoff_rating column');
|
else logger.info('Successfully added cutoff_rating column');
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const hasPredictedCalculatedAt = columns.some(col => col.name === 'predicted_calculated_at');
|
||||||
|
|
||||||
|
if (!hasPredictedCalculatedAt) {
|
||||||
|
logger.info('Adding predicted_calculated_at column to players table...');
|
||||||
|
db.run(`ALTER TABLE players ADD COLUMN predicted_calculated_at DATETIME DEFAULT NULL`, (err) => {
|
||||||
|
if (err) logger.error('Error adding predicted_calculated_at column:', err.message);
|
||||||
|
else logger.info('Successfully added predicted_calculated_at column');
|
||||||
|
});
|
||||||
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -187,11 +187,12 @@ function saveRoundHistoryToDB(pdgaNumber, roundData, isIncremental = false) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function savePredictedRatingToDB(pdgaNumber, predictedRating, stdDev = null, excludedRoundsCount = null, cutoffRating = null) {
|
function savePredictedRatingToDB(pdgaNumber, predictedRating, stdDev = null, excludedRoundsCount = null, cutoffRating = null, calculatedAt = null) {
|
||||||
|
const timestamp = calculatedAt || new Date().toISOString();
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
db.run(
|
db.run(
|
||||||
'UPDATE players SET predicted_rating = ?, std_dev = ?, excluded_rounds_count = ?, cutoff_rating = ? WHERE pdga_number = ?',
|
'UPDATE players SET predicted_rating = ?, std_dev = ?, excluded_rounds_count = ?, cutoff_rating = ?, predicted_calculated_at = ? WHERE pdga_number = ?',
|
||||||
[predictedRating, stdDev, excludedRoundsCount, cutoffRating, pdgaNumber],
|
[predictedRating, stdDev, excludedRoundsCount, cutoffRating, timestamp, pdgaNumber],
|
||||||
function(err) {
|
function(err) {
|
||||||
if (err) reject(err);
|
if (err) reject(err);
|
||||||
else resolve();
|
else resolve();
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
const { db } = require('../db');
|
const { db } = require('../db');
|
||||||
const { getPlayerFromDB, getRoundHistoryFromDB, savePredictedRatingToDB, savePlayerToDB, getMonthlyHistory, getAllMonthlyHistoriesFromDB, getAllRatingHistoriesFromDB } = require('../models/player');
|
const { getPlayerFromDB, getRoundHistoryFromDB, savePredictedRatingToDB, savePlayerToDB, getMonthlyHistory, getAllMonthlyHistoriesFromDB, getAllRatingHistoriesFromDB } = require('../models/player');
|
||||||
const { fetchPlayerDataHTTP, parsePlayerData } = require('../scrapers/player-http');
|
const { fetchPlayerDataHTTP, parsePlayerData } = require('../scrapers/player-http');
|
||||||
const { calculatePredictedRating } = require('./rating-calculator');
|
const { calculatePredictedRating, getPreviousPDGAUpdateDate } = require('./rating-calculator');
|
||||||
const logger = require('../logger');
|
const logger = require('../logger');
|
||||||
|
|
||||||
function formatDisplayDate(dateStr) {
|
function formatDisplayDate(dateStr) {
|
||||||
@@ -42,12 +42,33 @@ async function getPlayerDataFromDB(pdgaNumber, { includeMonthlyHistory = true }
|
|||||||
let stdDev = cachedPlayer.std_dev;
|
let stdDev = cachedPlayer.std_dev;
|
||||||
let excludedRoundsCount = cachedPlayer.excluded_rounds_count;
|
let excludedRoundsCount = cachedPlayer.excluded_rounds_count;
|
||||||
let cutoffRating = cachedPlayer.cutoff_rating;
|
let cutoffRating = cachedPlayer.cutoff_rating;
|
||||||
|
let predictedCalculatedAtRaw = cachedPlayer.predicted_calculated_at;
|
||||||
if (!predictedRating || predictedRating === 0) {
|
if (!predictedRating || predictedRating === 0) {
|
||||||
predictedRating = await getPredictedRatingFromDB(pdgaNumber);
|
predictedRating = await getPredictedRatingFromDB(pdgaNumber);
|
||||||
const updatedPlayer = await getPlayerFromDB(pdgaNumber);
|
const updatedPlayer = await getPlayerFromDB(pdgaNumber);
|
||||||
stdDev = updatedPlayer?.std_dev;
|
stdDev = updatedPlayer?.std_dev;
|
||||||
excludedRoundsCount = updatedPlayer?.excluded_rounds_count;
|
excludedRoundsCount = updatedPlayer?.excluded_rounds_count;
|
||||||
cutoffRating = updatedPlayer?.cutoff_rating;
|
cutoffRating = updatedPlayer?.cutoff_rating;
|
||||||
|
predictedCalculatedAtRaw = updatedPlayer?.predicted_calculated_at;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Staleness-check: invalidate cached predicted_rating if the PDGA cycle has
|
||||||
|
// rolled over since it was calculated. Don't recompute — round_history may be
|
||||||
|
// equally stale. UI will show "—" until the next manual refresh.
|
||||||
|
const predictedCalculatedAt = predictedCalculatedAtRaw
|
||||||
|
? new Date(predictedCalculatedAtRaw)
|
||||||
|
: null;
|
||||||
|
const previousUpdate = getPreviousPDGAUpdateDate();
|
||||||
|
const hasPredicted = predictedRating !== null && predictedRating !== 0;
|
||||||
|
const isStale = hasPredicted && (
|
||||||
|
predictedCalculatedAt === null || predictedCalculatedAt < previousUpdate
|
||||||
|
);
|
||||||
|
if (isStale) {
|
||||||
|
logger.debug(`PDGA ${pdgaNumber}: predicted rating stale (calculated ${predictedCalculatedAt?.toISOString() ?? 'unknown'}, cycle rolled ${previousUpdate.toISOString()})`);
|
||||||
|
predictedRating = null;
|
||||||
|
stdDev = null;
|
||||||
|
excludedRoundsCount = null;
|
||||||
|
cutoffRating = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const rating = cachedPlayer.current_rating;
|
const rating = cachedPlayer.current_rating;
|
||||||
|
|||||||
@@ -37,39 +37,43 @@ function parseDate(dateStr) {
|
|||||||
return new Date(dateStr);
|
return new Date(dateStr);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function secondTuesdayOf(year, month) {
|
||||||
|
const firstDay = new Date(year, month, 1);
|
||||||
|
const daysUntilTuesday = (2 - firstDay.getDay() + 7) % 7;
|
||||||
|
const firstTuesday = new Date(year, month, 1 + daysUntilTuesday);
|
||||||
|
const secondTuesday = new Date(firstTuesday);
|
||||||
|
secondTuesday.setDate(firstTuesday.getDate() + 7);
|
||||||
|
return secondTuesday;
|
||||||
|
}
|
||||||
|
|
||||||
function getNextPDGAUpdateDate() {
|
function getNextPDGAUpdateDate() {
|
||||||
const today = new Date();
|
const today = new Date();
|
||||||
const currentMonth = today.getMonth();
|
const currentMonth = today.getMonth();
|
||||||
const currentYear = today.getFullYear();
|
const currentYear = today.getFullYear();
|
||||||
|
|
||||||
const firstDayOfMonth = new Date(currentYear, currentMonth, 1);
|
const secondTuesday = secondTuesdayOf(currentYear, currentMonth);
|
||||||
const firstTuesday = new Date(firstDayOfMonth);
|
|
||||||
|
|
||||||
const daysUntilTuesday = (2 - firstDayOfMonth.getDay() + 7) % 7;
|
|
||||||
firstTuesday.setDate(1 + daysUntilTuesday);
|
|
||||||
|
|
||||||
const secondTuesday = new Date(firstTuesday);
|
|
||||||
secondTuesday.setDate(firstTuesday.getDate() + 7);
|
|
||||||
|
|
||||||
if (today <= secondTuesday) {
|
if (today <= secondTuesday) {
|
||||||
return secondTuesday;
|
return secondTuesday;
|
||||||
} else {
|
} else {
|
||||||
const nextMonth = currentMonth === 11 ? 0 : currentMonth + 1;
|
const nextMonth = currentMonth === 11 ? 0 : currentMonth + 1;
|
||||||
const nextYear = currentMonth === 11 ? currentYear + 1 : currentYear;
|
const nextYear = currentMonth === 11 ? currentYear + 1 : currentYear;
|
||||||
|
return secondTuesdayOf(nextYear, nextMonth);
|
||||||
const firstDayNextMonth = new Date(nextYear, nextMonth, 1);
|
|
||||||
const firstTuesdayNext = new Date(firstDayNextMonth);
|
|
||||||
|
|
||||||
const daysUntilTuesdayNext = (2 - firstDayNextMonth.getDay() + 7) % 7;
|
|
||||||
firstTuesdayNext.setDate(1 + daysUntilTuesdayNext);
|
|
||||||
|
|
||||||
const secondTuesdayNext = new Date(firstTuesdayNext);
|
|
||||||
secondTuesdayNext.setDate(firstTuesdayNext.getDate() + 7);
|
|
||||||
|
|
||||||
return secondTuesdayNext;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getPreviousPDGAUpdateDate() {
|
||||||
|
const today = new Date();
|
||||||
|
const year = today.getFullYear();
|
||||||
|
const month = today.getMonth();
|
||||||
|
const secondTuesday = secondTuesdayOf(year, month);
|
||||||
|
if (today > secondTuesday) return secondTuesday;
|
||||||
|
// Otherwise: last month's second Tuesday
|
||||||
|
const prevMonth = month === 0 ? 11 : month - 1;
|
||||||
|
const prevYear = month === 0 ? year - 1 : year;
|
||||||
|
return secondTuesdayOf(prevYear, prevMonth);
|
||||||
|
}
|
||||||
|
|
||||||
function calculateStandardDeviation(ratings) {
|
function calculateStandardDeviation(ratings) {
|
||||||
if (!ratings || ratings.length === 0) return 0;
|
if (!ratings || ratings.length === 0) return 0;
|
||||||
|
|
||||||
@@ -238,4 +242,4 @@ function calculatePredictedRating(roundRatings) {
|
|||||||
return { rating: finalRating, stdDev: Math.round(stdDev), debugLog, excludedRoundsCount, cutoffRating };
|
return { rating: finalRating, stdDev: Math.round(stdDev), debugLog, excludedRoundsCount, cutoffRating };
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = { parseDate, getNextPDGAUpdateDate, calculatePredictedRating, calculateStandardDeviation };
|
module.exports = { parseDate, getNextPDGAUpdateDate, getPreviousPDGAUpdateDate, calculatePredictedRating, calculateStandardDeviation };
|
||||||
|
|||||||
Reference in New Issue
Block a user