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) {
|
function getPlayerFromDB(pdgaNumber) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
db.get(
|
db.get(
|
||||||
'SELECT * FROM players WHERE pdga_number = ? AND datetime(last_updated) > datetime("now", "-24 hours")',
|
'SELECT * FROM players WHERE pdga_number = ?',
|
||||||
[pdgaNumber],
|
[pdgaNumber],
|
||||||
(err, row) => {
|
(err, row) => {
|
||||||
if (err) reject(err);
|
if (err) reject(err);
|
||||||
@@ -406,23 +406,32 @@ function parsePlayerData(html, pdgaNumber) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function scrapePDGARating(pdgaNumber, retries = 3) {
|
// Function to get player data from DB only (for page loads)
|
||||||
// Check database first
|
async function getPlayerDataFromDB(pdgaNumber) {
|
||||||
try {
|
try {
|
||||||
const cachedPlayer = await getPlayerFromDB(pdgaNumber);
|
const cachedPlayer = await getPlayerFromDB(pdgaNumber);
|
||||||
if (cachedPlayer) {
|
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 {
|
return {
|
||||||
pdgaNumber: cachedPlayer.pdga_number,
|
pdgaNumber: cachedPlayer.pdga_number,
|
||||||
name: cachedPlayer.name,
|
name: cachedPlayer.name,
|
||||||
rating: cachedPlayer.current_rating,
|
rating: cachedPlayer.current_rating,
|
||||||
ratingChange: cachedPlayer.rating_change,
|
ratingChange: cachedPlayer.rating_change,
|
||||||
predictedRating: null
|
predictedRating: predictedRating > 0 ? predictedRating : null
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
return null; // No data in DB
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(`Database error for PDGA ${pdgaNumber}:`, err.message);
|
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++) {
|
for (let attempt = 1; attempt <= retries; attempt++) {
|
||||||
try {
|
try {
|
||||||
@@ -1375,35 +1384,16 @@ async function getAllRatingsFromDB(progressCallback = null) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Load from database only
|
// Load from database only (source of truth)
|
||||||
const cachedPlayer = await getPlayerFromDB(pdgaNumber);
|
const playerData = await getPlayerDataFromDB(pdgaNumber);
|
||||||
|
|
||||||
let playerData;
|
if (playerData) {
|
||||||
if (cachedPlayer) {
|
ratings.push(playerData);
|
||||||
// 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
|
|
||||||
};
|
|
||||||
} else {
|
} else {
|
||||||
// If not in database, create placeholder entry with PDGA number
|
console.log(`PDGA ${pdgaNumber} not found in DB - skipping (page load)`);
|
||||||
playerData = {
|
// Skip players not in DB for page loads
|
||||||
pdgaNumber: parseInt(pdgaNumber),
|
|
||||||
name: `PDGA #${pdgaNumber}`,
|
|
||||||
rating: null,
|
|
||||||
ratingChange: null,
|
|
||||||
predictedRating: null
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ratings.push(playerData);
|
|
||||||
|
|
||||||
if (progressCallback) {
|
if (progressCallback) {
|
||||||
progressCallback({
|
progressCallback({
|
||||||
current: i + 1,
|
current: i + 1,
|
||||||
@@ -1534,21 +1524,25 @@ async function getAllRatingsWithScraping(progressCallback = null) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const playerData = await scrapePDGARating(pdgaNumber);
|
const playerData = await getPlayerDataFromDB(pdgaNumber);
|
||||||
ratings.push(playerData);
|
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) {
|
if (progressCallback) {
|
||||||
progressCallback({
|
progressCallback({
|
||||||
current: i + 1,
|
current: i + 1,
|
||||||
total,
|
total,
|
||||||
pdgaNumber,
|
pdgaNumber,
|
||||||
status: 'completed',
|
status: playerData ? 'completed' : 'skipped',
|
||||||
name: playerData.name
|
name: playerData ? playerData.name : 'Not in DB'
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Always delay for bulk scraping to be respectful
|
// No delay needed - just reading from DB
|
||||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`Failed to scrape PDGA ${pdgaNumber}:`, error.message);
|
console.error(`Failed to scrape PDGA ${pdgaNumber}:`, error.message);
|
||||||
const errorData = {
|
const errorData = {
|
||||||
@@ -2041,10 +2035,10 @@ app.post('/api/predicted-rating/:pdgaNumber', async (req, res) => {
|
|||||||
try {
|
try {
|
||||||
const { pdgaNumber } = req.params;
|
const { pdgaNumber } = req.params;
|
||||||
|
|
||||||
// Check database first for cached round history
|
// Always check database first (source of truth)
|
||||||
const cachedPrediction = await getPredictedRatingFromDB(pdgaNumber);
|
const cachedPrediction = await getPredictedRatingFromDB(pdgaNumber);
|
||||||
if (cachedPrediction > 0) {
|
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({
|
res.json({
|
||||||
pdgaNumber: parseInt(pdgaNumber),
|
pdgaNumber: parseInt(pdgaNumber),
|
||||||
predictedRating: cachedPrediction
|
predictedRating: cachedPrediction
|
||||||
|
|||||||
Reference in New Issue
Block a user