Compare commits

...

2 Commits

Author SHA1 Message Date
Samuel Enocsson c7fb4a7068 fix: use re-fetched timestamp after recompute + rename helper var (#29)
Reviewer 1 flagged: staleness-check read predicted_calculated_at from
the original cachedPlayer snapshot even after recompute, so newly
calculated ratings (predicted_calculated_at = NULL in snapshot)
were immediately nulled by the staleness branch.

Fix: read predicted_calculated_at from updatedPlayer too.

Reviewer 2 nit: rename thisMonths → secondTuesday for consistency
with the original variable name in getNextPDGAUpdateDate.
2026-06-09 11:04:53 +02:00
Samuel Enocsson 27ffa096e4 fix: invalidate stale predicted_rating after PDGA cycle rollover (#29) 2026-06-09 10:58:42 +02:00
4 changed files with 60 additions and 24 deletions
+10
View File
@@ -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');
});
}
}); });
}); });
+4 -3
View File
@@ -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();
+22 -1
View File
@@ -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;
+24 -20
View File
@@ -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 };