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:
Samuel Enocsson
2025-08-18 09:34:03 +02:00
parent 351a609f41
commit 1a5b3b9fb4
+32 -38
View File
@@ -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