Implement database-first architecture with user-controlled refresh
- Make database the single source of truth (no automatic cache expiration) - Page loads now instant: read ratings + predictions directly from DB - Remove automatic PDGA scraping on page load for performance - Only scrape PDGA when user explicitly clicks refresh icons - Add getPlayerDataFromDB() for fast DB-only player loading - Separate scrapePDGARating() for explicit refresh operations only - Remove delays from page load path (DB reads don't need rate limiting) - Skip players not in DB rather than auto-scraping on page load - User controls data freshness via refresh buttons Performance: Page loads ~5+ minutes → instant DB reads User experience: Predictable, fast, user-controlled data freshness 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -98,7 +98,7 @@ function initializeDatabase() {
|
||||
function getPlayerFromDB(pdgaNumber) {
|
||||
return new Promise((resolve, reject) => {
|
||||
db.get(
|
||||
'SELECT * FROM players WHERE pdga_number = ? AND datetime(last_updated) > datetime("now", "-24 hours")',
|
||||
'SELECT * FROM players WHERE pdga_number = ?',
|
||||
[pdgaNumber],
|
||||
(err, row) => {
|
||||
if (err) reject(err);
|
||||
@@ -406,23 +406,32 @@ function parsePlayerData(html, pdgaNumber) {
|
||||
}
|
||||
}
|
||||
|
||||
async function scrapePDGARating(pdgaNumber, retries = 3) {
|
||||
// Check database first
|
||||
// Function to get player data from DB only (for page loads)
|
||||
async function getPlayerDataFromDB(pdgaNumber) {
|
||||
try {
|
||||
const cachedPlayer = await getPlayerFromDB(pdgaNumber);
|
||||
if (cachedPlayer) {
|
||||
console.log(`Using cached data from DB for PDGA ${pdgaNumber}`);
|
||||
console.log(`Loading PDGA ${pdgaNumber} from DB (source of truth)`);
|
||||
const predictedRating = await getPredictedRatingFromDB(pdgaNumber);
|
||||
|
||||
return {
|
||||
pdgaNumber: cachedPlayer.pdga_number,
|
||||
name: cachedPlayer.name,
|
||||
rating: cachedPlayer.current_rating,
|
||||
ratingChange: cachedPlayer.rating_change,
|
||||
predictedRating: null
|
||||
predictedRating: predictedRating > 0 ? predictedRating : null
|
||||
};
|
||||
}
|
||||
return null; // No data in DB
|
||||
} catch (err) {
|
||||
console.error(`Database error for PDGA ${pdgaNumber}:`, err.message);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// Function for explicit refresh (scrape PDGA + update DB)
|
||||
async function scrapePDGARating(pdgaNumber, retries = 3) {
|
||||
console.log(`=== Refreshing PDGA ${pdgaNumber} from PDGA website ===`);
|
||||
|
||||
for (let attempt = 1; attempt <= retries; attempt++) {
|
||||
try {
|
||||
@@ -1375,35 +1384,16 @@ async function getAllRatingsFromDB(progressCallback = null) {
|
||||
}
|
||||
|
||||
try {
|
||||
// Load from database only
|
||||
const cachedPlayer = await getPlayerFromDB(pdgaNumber);
|
||||
// Load from database only (source of truth)
|
||||
const playerData = await getPlayerDataFromDB(pdgaNumber);
|
||||
|
||||
let playerData;
|
||||
if (cachedPlayer) {
|
||||
// Calculate prediction from database
|
||||
console.log(`Calculating prediction for PDGA ${pdgaNumber}...`);
|
||||
const predictedRating = await getPredictedRatingFromDB(pdgaNumber);
|
||||
|
||||
playerData = {
|
||||
pdgaNumber: cachedPlayer.pdga_number,
|
||||
name: cachedPlayer.name,
|
||||
rating: cachedPlayer.current_rating,
|
||||
ratingChange: cachedPlayer.rating_change,
|
||||
predictedRating: predictedRating > 0 ? predictedRating : null
|
||||
};
|
||||
if (playerData) {
|
||||
ratings.push(playerData);
|
||||
} else {
|
||||
// If not in database, create placeholder entry with PDGA number
|
||||
playerData = {
|
||||
pdgaNumber: parseInt(pdgaNumber),
|
||||
name: `PDGA #${pdgaNumber}`,
|
||||
rating: null,
|
||||
ratingChange: null,
|
||||
predictedRating: null
|
||||
};
|
||||
console.log(`PDGA ${pdgaNumber} not found in DB - skipping (page load)`);
|
||||
// Skip players not in DB for page loads
|
||||
}
|
||||
|
||||
ratings.push(playerData);
|
||||
|
||||
if (progressCallback) {
|
||||
progressCallback({
|
||||
current: i + 1,
|
||||
@@ -1534,21 +1524,25 @@ async function getAllRatingsWithScraping(progressCallback = null) {
|
||||
}
|
||||
|
||||
try {
|
||||
const playerData = await scrapePDGARating(pdgaNumber);
|
||||
ratings.push(playerData);
|
||||
const playerData = await getPlayerDataFromDB(pdgaNumber);
|
||||
if (playerData) {
|
||||
ratings.push(playerData);
|
||||
} else {
|
||||
console.log(`PDGA ${pdgaNumber} not found in DB - skipping`);
|
||||
// Skip players not in DB instead of scraping
|
||||
}
|
||||
|
||||
if (progressCallback) {
|
||||
progressCallback({
|
||||
current: i + 1,
|
||||
total,
|
||||
pdgaNumber,
|
||||
status: 'completed',
|
||||
name: playerData.name
|
||||
status: playerData ? 'completed' : 'skipped',
|
||||
name: playerData ? playerData.name : 'Not in DB'
|
||||
});
|
||||
}
|
||||
|
||||
// Always delay for bulk scraping to be respectful
|
||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||
// No delay needed - just reading from DB
|
||||
} catch (error) {
|
||||
console.error(`Failed to scrape PDGA ${pdgaNumber}:`, error.message);
|
||||
const errorData = {
|
||||
@@ -2041,10 +2035,10 @@ app.post('/api/predicted-rating/:pdgaNumber', async (req, res) => {
|
||||
try {
|
||||
const { pdgaNumber } = req.params;
|
||||
|
||||
// Check database first for cached round history
|
||||
// Always check database first (source of truth)
|
||||
const cachedPrediction = await getPredictedRatingFromDB(pdgaNumber);
|
||||
if (cachedPrediction > 0) {
|
||||
console.log(`Using cached round history for PDGA ${pdgaNumber} prediction`);
|
||||
console.log(`Using DB round history for PDGA ${pdgaNumber} prediction (source of truth)`);
|
||||
res.json({
|
||||
pdgaNumber: parseInt(pdgaNumber),
|
||||
predictedRating: cachedPrediction
|
||||
|
||||
Reference in New Issue
Block a user