Compare commits

...

14 Commits

Author SHA1 Message Date
Release Bot ada2dcb4ae 1.4.2
Release / release (push) Successful in 5s
Build and deploy / build-and-push (push) Successful in 33s
Build and deploy / deploy (push) Successful in 8s
2026-06-08 06:46:23 +00:00
shcizo 5ece854340 Merge pull request 'feat: add refresh button to mobile player card (#26)' (#27) from feat/mobile-card-refresh-button-26 into main
Release / release (push) Successful in 8s
2026-06-08 08:46:13 +02:00
Samuel Enocsson 2ef7de4e58 fix: spin only the icon glyph in mobile refresh button (#26) 2026-06-08 08:44:51 +02:00
Samuel Enocsson 16c045e7cc feat: add refresh button to mobile player card (#26) 2026-06-08 08:24:09 +02:00
Release Bot 8ee5cc3861 1.4.1
Release / release (push) Successful in 5s
Build and deploy / build-and-push (push) Successful in 23s
Build and deploy / deploy (push) Successful in 8s
2026-06-01 07:04:42 +00:00
shcizo 2561ee12ef Merge pull request 'fix: parse latest tournament from recent-events list on player page (#24)' (#25) from fix/parse-recent-events-tournament-24 into main
Release / release (push) Successful in 25s
2026-06-01 09:04:13 +02:00
Samuel Enocsson 0d2f0fa3a8 fix: skip recent-events tournament when extracted date predates afterDate (#24) 2026-06-01 08:57:51 +02:00
Samuel Enocsson ec3ae872da fix: parse latest tournament from recent-events list on player page (#24) 2026-06-01 08:53:12 +02:00
Release Bot a90f2d0e86 1.4.0
Release / release (push) Successful in 5s
Build and deploy / build-and-push (push) Successful in 21s
Build and deploy / deploy (push) Successful in 7s
2026-05-25 09:18:47 +00:00
shcizo f8233960d2 Merge pull request 'feat: show excluded rounds count and cutoff rating in player history (#21)' (#23) from feat/show-excluded-rounds-count-21 into main
Release / release (push) Successful in 7s
2026-05-25 11:18:35 +02:00
Samuel Enocsson 98a6c6be2e feat: show cutoff rating threshold in player history accordion (#21) 2026-05-25 11:12:01 +02:00
Samuel Enocsson 9138299ae0 Merge remote-tracking branch 'origin/main' into feat/show-excluded-rounds-count-21 2026-05-25 11:01:53 +02:00
Samuel Enocsson f2e30c62aa fix: zero excluded count in fallback, drop debug-icon orphan, align ejs guard (#21) 2026-05-25 09:44:46 +02:00
Samuel Enocsson 0beeb98002 feat: show excluded rounds count in player history accordion (#21) 2026-05-25 09:34:42 +02:00
14 changed files with 220 additions and 175 deletions
+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "pdga-ratings",
"version": "1.3.0",
"version": "1.4.2",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "pdga-ratings",
"version": "1.3.0",
"version": "1.4.2",
"dependencies": {
"ejs": "^4.0.1",
"express": "^4.18.2",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "pdga-ratings",
"version": "1.3.0",
"version": "1.4.2",
"description": "PDGA rating scraper and display",
"main": "server.js",
"scripts": {
+32
View File
@@ -342,6 +342,38 @@
transform: rotate(180deg);
}
/* Refresh button: hidden by default, revealed only when the card is open.
Larger than the desktop icon to give a comfortable touch target (≥44px). */
.m-card__head .m-refresh-icon {
display: none;
}
.m-card.is-open .m-card__head .m-refresh-icon {
display: grid;
width: 44px;
height: 44px;
margin-left: 0;
font-size: 15px;
opacity: 0.7;
flex-shrink: 0;
}
.m-card.is-open .m-card__head .m-refresh-icon:active {
opacity: 1;
color: var(--accent);
}
/* Spin only the icon glyph, not the 44px button box — otherwise the button's
lingering touch-hover frame (background + border) rotates too, which looks odd. */
.m-card.is-open .m-card__head .m-refresh-icon.spinning {
animation: none;
}
.m-card.is-open .m-card__head .m-refresh-icon.spinning i {
display: inline-block;
animation: spin 0.8s linear infinite;
}
.m-card__body {
display: grid;
grid-template-columns: minmax(0, 1fr) auto;
-76
View File
@@ -94,82 +94,6 @@
box-shadow: var(--shadow-lg);
}
/* ── Debug Icon ───────────────────────────────── */
.debug-icon:hover {
opacity: 1 !important;
color: var(--accent) !important;
}
/* ── Debug Modal ──────────────────────────────── */
.debug-modal {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(15, 23, 42, 0.5);
backdrop-filter: blur(4px);
z-index: 10001;
display: none;
justify-content: center;
align-items: center;
}
.debug-content {
background: var(--surface-1);
border-radius: var(--radius-lg);
padding: 24px;
max-width: 640px;
width: 90%;
max-height: 80vh;
overflow-y: auto;
box-shadow: var(--shadow-overlay);
position: relative;
}
.debug-header {
font-weight: 700;
font-size: 16px;
margin-bottom: 16px;
color: var(--text-primary);
padding-bottom: 12px;
border-bottom: 1px solid var(--border);
}
.debug-log {
font-family: var(--font-mono);
background: var(--surface-2);
border: 1px solid var(--border);
border-radius: var(--radius-md);
padding: 16px;
font-size: 12px;
line-height: 1.6;
white-space: pre-line;
color: var(--text-primary);
}
.debug-close {
position: absolute;
top: 12px;
right: 16px;
font-size: 22px;
color: var(--text-muted);
cursor: pointer;
background: none;
border: none;
padding: 4px;
border-radius: var(--radius-sm);
transition: color var(--transition), background var(--transition);
line-height: 1;
}
.debug-close:hover {
color: var(--text-primary);
background: var(--surface-3);
}
/* ── Add Player Modal ─────────────────────────── */
.modal {
+10 -47
View File
@@ -1,4 +1,3 @@
const cachedDebugInfo = {};
let pendingPlayerData = null;
let openPdgaNumber = null;
@@ -132,10 +131,16 @@ async function clearCache() {
// Refreshes both the current rating and the prediction in one click, then
// re-swaps the table so every derived value (deltas, pills, sparkline) reflects
// the new state. Cheaper than fine-grained DOM updates and guaranteed consistent
// because the server renders the truth.
// because the server renders the truth. The mobile cards partial is included
// inside ratings-table, so swapping #ratings-table re-renders both views at once.
async function refreshPlayerData(pdgaNumber) {
const icon = document.querySelector(`#row-${pdgaNumber} .cell-actions .refresh-icon`);
if (icon) icon.classList.add('spinning');
// The desktop row exists in the DOM even on mobile (hidden via CSS), so spin
// both possible icons; only the one visible in the active viewport is seen.
const icons = [
document.querySelector(`#row-${pdgaNumber} .cell-actions .refresh-icon`),
document.querySelector(`#m-card-${pdgaNumber} .m-refresh-icon`)
].filter(Boolean);
icons.forEach(icon => icon.classList.add('spinning'));
try {
await Promise.allSettled([
fetch(`/api/refresh-player/${pdgaNumber}`, { method: 'POST' }),
@@ -145,7 +150,7 @@ async function refreshPlayerData(pdgaNumber) {
} catch (error) {
console.error('Error refreshing player data:', error);
} finally {
if (icon) icon.classList.remove('spinning');
icons.forEach(icon => icon.classList.remove('spinning'));
}
}
@@ -186,10 +191,6 @@ async function refreshRoundHistory(pdgaNumber) {
}
if (data.success) {
if (data.debugLog) {
cachedDebugInfo[pdgaNumber] = data.debugLog;
}
const predictedCell = document.getElementById(`predicted-${pdgaNumber}`);
if (predictedCell) {
const predictedValue = predictedCell.querySelector('.predicted-value');
@@ -234,43 +235,6 @@ async function refreshRatingHistory(pdgaNumber) {
}
}
async function showDebugInfo(pdgaNumber) {
const modal = document.getElementById('debug-modal');
const header = document.getElementById('debug-header');
const log = document.getElementById('debug-log');
const playerNameElement = document.querySelector(`#row-${pdgaNumber} .player-name a`);
const playerName = playerNameElement ? playerNameElement.textContent : `PDGA #${pdgaNumber}`;
header.textContent = `Prediction Calculation Details - ${playerName}`;
log.textContent = 'Loading calculation details...';
modal.style.display = 'flex';
try {
if (cachedDebugInfo[pdgaNumber]) {
log.textContent = cachedDebugInfo[pdgaNumber].join('\n');
return;
}
const response = await fetch(`/api/refresh-round-history/${pdgaNumber}`, { method: 'POST' });
const data = await response.json();
if (data.success && data.debugLog) {
cachedDebugInfo[pdgaNumber] = data.debugLog;
log.textContent = data.debugLog.join('\n');
} else {
log.textContent = 'No debug information available. Try refreshing the prediction first.';
}
} catch (error) {
console.error('Error fetching debug info:', error);
log.textContent = 'Error loading debug information. Please try again.';
}
}
function closeDebugModal(event) {
document.getElementById('debug-modal').style.display = 'none';
}
async function searchAndAddPlayer(event) {
if (event) event.preventDefault();
const input = document.getElementById('pdga-number-input');
@@ -712,7 +676,6 @@ async function refreshHistoryThenCalculate(pdgaNumber) {
return;
}
if (data.debugLog) cachedDebugInfo[pdgaNumber] = data.debugLog;
const predictedCell = document.getElementById('predicted-' + pdgaNumber);
if (predictedCell) {
const predictedValue = predictedCell.querySelector('.predicted-value');
+18
View File
@@ -34,6 +34,8 @@ function initializeDatabase() {
const hasLastRoundUpdate = columns.some(col => col.name === 'last_round_update');
const hasPredictedRating = columns.some(col => col.name === 'predicted_rating');
const hasStdDev = columns.some(col => col.name === 'std_dev');
const hasExcludedRoundsCount = columns.some(col => col.name === 'excluded_rounds_count');
const hasCutoffRating = columns.some(col => col.name === 'cutoff_rating');
if (!hasLastRoundUpdate) {
logger.info('Adding last_round_update column to players table...');
@@ -58,6 +60,22 @@ function initializeDatabase() {
else logger.info('Successfully added std_dev column');
});
}
if (!hasExcludedRoundsCount) {
logger.info('Adding excluded_rounds_count column to players table...');
db.run(`ALTER TABLE players ADD COLUMN excluded_rounds_count INTEGER DEFAULT NULL`, (err) => {
if (err) logger.error('Error adding excluded_rounds_count column:', err.message);
else logger.info('Successfully added excluded_rounds_count column');
});
}
if (!hasCutoffRating) {
logger.info('Adding cutoff_rating column to players table...');
db.run(`ALTER TABLE players ADD COLUMN cutoff_rating INTEGER DEFAULT NULL`, (err) => {
if (err) logger.error('Error adding cutoff_rating column:', err.message);
else logger.info('Successfully added cutoff_rating column');
});
}
});
});
+3 -3
View File
@@ -172,11 +172,11 @@ function saveRoundHistoryToDB(pdgaNumber, roundData, isIncremental = false) {
});
}
function savePredictedRatingToDB(pdgaNumber, predictedRating, stdDev = null) {
function savePredictedRatingToDB(pdgaNumber, predictedRating, stdDev = null, excludedRoundsCount = null, cutoffRating = null) {
return new Promise((resolve, reject) => {
db.run(
'UPDATE players SET predicted_rating = ?, std_dev = ? WHERE pdga_number = ?',
[predictedRating, stdDev, pdgaNumber],
'UPDATE players SET predicted_rating = ?, std_dev = ?, excluded_rounds_count = ?, cutoff_rating = ? WHERE pdga_number = ?',
[predictedRating, stdDev, excludedRoundsCount, cutoffRating, pdgaNumber],
function(err) {
if (err) reject(err);
else resolve();
+3 -2
View File
@@ -400,7 +400,7 @@ router.post('/api/refresh-round-history/:pdgaNumber', async (req, res) => {
const result = calculatePredictedRating(roundsForPrediction);
await savePredictedRatingToDB(pdgaNumber, result.rating, result.stdDev);
await savePredictedRatingToDB(pdgaNumber, result.rating, result.stdDev, result.excludedRoundsCount, result.cutoffRating);
const officialCount = allRounds.filter(r => r.source === 'official').length;
const newCount = allRounds.filter(r => r.source === 'new').length;
@@ -409,7 +409,8 @@ router.post('/api/refresh-round-history/:pdgaNumber', async (req, res) => {
success: true,
predictedRating: result.rating,
stdDev: result.stdDev,
debugLog: result.debugLog,
excludedRoundsCount: result.excludedRoundsCount,
cutoffRating: result.cutoffRating,
totalRounds: roundsForPrediction.length,
officialRounds: officialCount,
newRounds: newCount,
+81 -8
View File
@@ -156,13 +156,18 @@ async function getNewTournamentRounds(browser, pdgaNumber, afterDate) {
logger.info(`Looking for tournaments after ${afterDate.toDateString()}...`);
const newTournamentUrls = await page.evaluate((afterTimestamp) => {
const { urls: newTournamentUrls, counts } = await page.evaluate((afterTimestamp) => {
const afterDate = new Date(afterTimestamp);
const tables = document.querySelectorAll('table[id*="player-results"]');
const urls = [];
const seenUrls = new Set();
let table = 0;
let recentEvents = 0;
let recentEventsAnchorsSeen = 0;
let recentEventsSkippedDuplicates = 0;
tables.forEach(table => {
const rows = table.querySelectorAll('tbody tr');
tables.forEach(tbl => {
const rows = tbl.querySelectorAll('tbody tr');
rows.forEach(row => {
const dateCell = row.querySelector('.dates');
const tournamentCell = row.querySelector('.tournament a');
@@ -178,11 +183,17 @@ async function getNewTournamentRounds(browser, pdgaNumber, afterDate) {
if (date > afterDate) {
const href = tournamentCell.getAttribute('href');
if (href) {
const absoluteUrl = new URL(href, location.origin).href;
if (!seenUrls.has(absoluteUrl)) {
seenUrls.add(absoluteUrl);
urls.push({
url: `https://www.pdga.com${href}`,
url: absoluteUrl,
date: dateStr,
name: tournamentCell.innerText.trim()
name: tournamentCell.innerText.trim(),
source: 'table'
});
table++;
}
}
}
}
@@ -190,18 +201,81 @@ async function getNewTournamentRounds(browser, pdgaNumber, afterDate) {
});
});
return urls;
const recentAnchors = document.querySelectorAll('.recent-events a[href*="/tour/event/"]');
recentAnchors.forEach(anchor => {
recentEventsAnchorsSeen++;
const href = anchor.getAttribute('href');
if (href) {
const absoluteUrl = new URL(href, location.origin).href;
if (seenUrls.has(absoluteUrl)) {
recentEventsSkippedDuplicates++;
} else {
seenUrls.add(absoluteUrl);
urls.push({
url: absoluteUrl,
date: null,
name: anchor.innerText.trim() || 'Recent event',
source: 'recent-events'
});
recentEvents++;
}
}
});
return { urls, counts: { table, recentEvents, recentEventsAnchorsSeen, recentEventsSkippedDuplicates } };
}, afterDate.getTime());
logger.info(`Found ${newTournamentUrls.length} new tournaments after ${afterDate.toDateString()}`);
logger.info({
pdgaNumber,
afterDate: afterDate.toISOString(),
tableMatches: counts.table,
recentEventsMatches: counts.recentEvents,
recentEventsAnchorsSeen: counts.recentEventsAnchorsSeen,
recentEventsSkippedDuplicates: counts.recentEventsSkippedDuplicates,
totalUrlsToScrape: newTournamentUrls.length
}, 'new tournament URL discovery completed');
for (const tournamentData of newTournamentUrls) {
try {
if (tournamentData.source === 'recent-events') {
logger.debug({ pdgaNumber, url: tournamentData.url }, 'recent-events: scraping tournament');
} else {
logger.info(`Scraping new tournament: ${tournamentData.name} (${tournamentData.date})`);
}
await page.goto(tournamentData.url, { waitUntil: 'domcontentloaded', timeout: 30000 });
await new Promise(r => setTimeout(r, 500));
let parsedDate;
if (tournamentData.date !== null) {
parsedDate = parseDate(tournamentData.date);
} else {
const eventDateStr = await page.evaluate(() => {
const body = document.body ? document.body.innerText : '';
const m = body.match(/\d{1,2}\s+to\s+\d{1,2}-[A-Za-z]{3}-\d{4}/)
|| body.match(/\d{1,2}-[A-Za-z]{3}-\d{4}/);
return m ? m[0] : null;
});
if (eventDateStr) {
parsedDate = parseDate(eventDateStr);
if (!(parsedDate > afterDate)) {
logger.warn({
pdgaNumber,
url: tournamentData.url,
eventDateStr,
parsedDate: parsedDate ? parsedDate.toISOString() : null,
afterDate: afterDate.toISOString()
}, 'recent-events: extracted event date is not newer than afterDate, likely captured a non-tournament date — skipping');
continue;
}
logger.debug({ pdgaNumber, url: tournamentData.url, eventDateStr }, 'recent-events: extracted date from event page');
} else {
logger.warn({ pdgaNumber, url: tournamentData.url }, 'recent-events: could not extract date from event page, skipping tournament');
continue;
}
}
const roundRatings = await page.evaluate((pdgaNum) => {
const rows = document.querySelectorAll('tr');
@@ -230,7 +304,6 @@ async function getNewTournamentRounds(browser, pdgaNumber, afterDate) {
}, pdgaNumber);
if (roundRatings.length > 0) {
const parsedDate = parseDate(tournamentData.date);
roundRatings.forEach(rating => {
newRounds.push({
rating,
+9 -1
View File
@@ -40,10 +40,14 @@ async function getPlayerDataFromDB(pdgaNumber, { includeMonthlyHistory = true }
let predictedRating = cachedPlayer.predicted_rating;
let stdDev = cachedPlayer.std_dev;
let excludedRoundsCount = cachedPlayer.excluded_rounds_count;
let cutoffRating = cachedPlayer.cutoff_rating;
if (!predictedRating || predictedRating === 0) {
predictedRating = await getPredictedRatingFromDB(pdgaNumber);
const updatedPlayer = await getPlayerFromDB(pdgaNumber);
stdDev = updatedPlayer?.std_dev;
excludedRoundsCount = updatedPlayer?.excluded_rounds_count;
cutoffRating = updatedPlayer?.cutoff_rating;
}
const rating = cachedPlayer.current_rating;
@@ -65,6 +69,8 @@ async function getPlayerDataFromDB(pdgaNumber, { includeMonthlyHistory = true }
ratingChange,
predictedRating: resolvedPredicted,
stdDev: resolvedStdDev,
excludedRoundsCount: (excludedRoundsCount != null && excludedRoundsCount >= 0) ? excludedRoundsCount : null,
cutoffRating: (cutoffRating != null && cutoffRating > 0) ? cutoffRating : null,
lastMonthRating,
// gap between next predicted update and current rating (null when either is missing)
deltaPredicted: (resolvedPredicted != null && rating != null) ? resolvedPredicted - rating : null,
@@ -145,7 +151,7 @@ async function getPredictedRatingFromDB(pdgaNumber) {
const result = calculatePredictedRating(roundRatings);
await savePredictedRatingToDB(pdgaNumber, result.rating, result.stdDev);
await savePredictedRatingToDB(pdgaNumber, result.rating, result.stdDev, result.excludedRoundsCount, result.cutoffRating);
return result.rating;
}
@@ -229,6 +235,8 @@ async function getAllRatingsFromDB(progressCallback = null) {
ratingChange: errorRatingChange,
predictedRating: null,
stdDev: null,
excludedRoundsCount: null,
cutoffRating: null,
lastMonthRating: (errorRating != null && errorRatingChange != null) ? errorRating - errorRatingChange : null,
deltaPredicted: null,
monthlyHistory: [],
+11 -4
View File
@@ -85,7 +85,7 @@ function calculatePredictedRating(roundRatings) {
if (!roundRatings || roundRatings.length === 0) {
debugLog.push('❌ No rounds provided for prediction');
return { rating: 0, debugLog };
return { rating: 0, debugLog, excludedRoundsCount: null, cutoffRating: null };
}
debugLog.push(`📊 Starting with ${roundRatings.length} total rounds`);
@@ -100,7 +100,7 @@ function calculatePredictedRating(roundRatings) {
if (allSortedRounds.length === 0) {
debugLog.push('❌ No valid rounds after filtering for update date');
return { rating: 0, debugLog };
return { rating: 0, debugLog, excludedRoundsCount: null, cutoffRating: null };
}
debugLog.push(`📊 After update date filter: ${allSortedRounds.length} rounds`);
@@ -127,7 +127,7 @@ function calculatePredictedRating(roundRatings) {
if (eligibleRounds.length === 0) {
debugLog.push('❌ No eligible rounds found');
return { rating: 0, debugLog };
return { rating: 0, debugLog, excludedRoundsCount: null, cutoffRating: null };
}
debugLog.push(`📈 ELIGIBLE ROUNDS: ${eligibleRounds.length}`);
@@ -137,6 +137,8 @@ function calculatePredictedRating(roundRatings) {
let workingRounds = [...eligibleRounds];
let workingRatings = workingRounds.map(r => r.rating);
let excludedRoundsCount = 0;
let cutoffRating = null;
if (workingRatings.length >= 7) {
debugLog.push('🔍 OUTLIER EXCLUSION (≥7 rounds available):');
@@ -160,6 +162,9 @@ function calculatePredictedRating(roundRatings) {
const stdDevOutliers = workingRatings.filter(rating => rating < stdDevCutoff);
const hundredPointOutliers = workingRatings.filter(rating => rating < hundredPointCutoff && rating >= stdDevCutoff);
excludedRoundsCount = stdDevOutliers.length + hundredPointOutliers.length;
cutoffRating = Math.round(Math.max(stdDevCutoff, hundredPointCutoff));
if (stdDevOutliers.length > 0) {
debugLog.push(` ❌ 2.5σ outliers removed: ${stdDevOutliers.length} rounds`);
stdDevOutliers.forEach(rating => {
@@ -188,6 +193,8 @@ function calculatePredictedRating(roundRatings) {
debugLog.push(` ✅ Using ${filteredRatings.length} rounds after outlier removal`);
} else {
debugLog.push(` ⚠️ Too few rounds after outlier removal (${filteredRatings.length}), keeping all rounds`);
excludedRoundsCount = 0;
cutoffRating = null;
}
} else {
debugLog.push(`⏭️ OUTLIER EXCLUSION SKIPPED (only ${workingRatings.length} rounds, need ≥7)`);
@@ -228,7 +235,7 @@ function calculatePredictedRating(roundRatings) {
debugLog.push(` Final Rating: ${finalRating}`);
debugLog.push('=== END PDGA CALCULATION ===');
return { rating: finalRating, stdDev: Math.round(stdDev), debugLog };
return { rating: finalRating, stdDev: Math.round(stdDev), debugLog, excludedRoundsCount, cutoffRating };
}
module.exports = { parseDate, getNextPDGAUpdateDate, calculatePredictedRating, calculateStandardDeviation };
-9
View File
@@ -95,15 +95,6 @@
`; %>
<% var modals = `
<!-- Debug Modal -->
<div id="debug-modal" class="debug-modal" onclick="closeDebugModal(event)">
<div class="debug-content" onclick="event.stopPropagation()">
<button class="debug-close" onclick="closeDebugModal()">&times;</button>
<div class="debug-header" id="debug-header">Prediction Calculation Details</div>
<div class="debug-log" id="debug-log">Loading...</div>
</div>
</div>
<!-- Add Player Confirmation Modal -->
<div id="add-player-modal" class="modal" onclick="closeAddPlayerModal(event)">
<div class="modal-content" onclick="event.stopPropagation()">
+12 -1
View File
@@ -36,8 +36,19 @@ const chartPdgaNumber = hasPlayer ? player.pdgaNumber : pdgaNumber;
<dd><%= player.rating - player.stdDev %><%= player.rating + player.stdDev %></dd>
</div>
<% } %>
<% if (player.excludedRoundsCount != null && player.rating) { %>
<div>
<dt>Excluded rounds</dt>
<dd><%= player.excludedRoundsCount %></dd>
</div>
<% } %>
<% if (player.cutoffRating != null && player.rating) { %>
<div>
<dt>Cutoff rating</dt>
<dd><%= player.cutoffRating %></dd>
</div>
<% } %>
</dl>
<button class="link-btn" onclick="showDebugInfo(<%= player.pdgaNumber %>)" style="margin-top: 4px;">View calculation details →</button>
</div>
<% } %>
+17
View File
@@ -65,6 +65,11 @@ function renderSparkline(values, opts) {
<span class="m-player-name"><%= player.name %></span>
<span class="m-pdga-num">#<%= player.pdgaNumber %></span>
</div>
<button class="icon-btn refresh-icon m-refresh-icon" type="button"
onclick="event.stopPropagation(); refreshPlayerData(<%= player.pdgaNumber %>)"
title="Refresh rating + prediction" aria-label="Refresh rating and prediction">
<i class="fas fa-sync-alt"></i>
</button>
<span class="m-chevron">&#9660;</span>
</div>
@@ -124,6 +129,18 @@ function renderSparkline(values, opts) {
<dd><%= player.rating - player.stdDev %><%= player.rating + player.stdDev %></dd>
</div>
<% } %>
<% if (player.excludedRoundsCount != null && player.rating) { %>
<div>
<dt>Excluded rounds</dt>
<dd><%= player.excludedRoundsCount %></dd>
</div>
<% } %>
<% if (player.cutoffRating != null && player.rating) { %>
<div>
<dt>Cutoff rating</dt>
<dd><%= player.cutoffRating %></dd>
</div>
<% } %>
</dl>
</div>
</div>