feat: add monthlyHistory[] per player via getMonthlyHistory + bulk fetch (#6)
Add getMonthlyHistory() to models/player for single-player use and getAllMonthlyHistoriesFromDB() for bulk fetches (one query, grouped in memory). Wire monthlyHistory into all player objects returned by getPlayerDataFromDB and getAllRatingsFromDB. Bulk path pre-fetches in one query to avoid N extra per-player queries.
This commit is contained in:
@@ -551,3 +551,44 @@ a:hover {
|
|||||||
padding: 8px 6px;
|
padding: 8px 6px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ── Delta Pills ──────────────────────────────── */
|
||||||
|
|
||||||
|
.delta-pill {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 2px 8px;
|
||||||
|
border-radius: 999px;
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
font-feature-settings: "tnum", "zero";
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 500;
|
||||||
|
margin-top: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.delta-pill.up {
|
||||||
|
background: var(--up-soft);
|
||||||
|
color: var(--up);
|
||||||
|
}
|
||||||
|
|
||||||
|
.delta-pill.down {
|
||||||
|
background: var(--down-soft);
|
||||||
|
color: var(--down);
|
||||||
|
}
|
||||||
|
|
||||||
|
.delta-pill.flat {
|
||||||
|
background: oklch(0.95 0.004 260);
|
||||||
|
color: var(--ink-3);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Table Header Hints ───────────────────────── */
|
||||||
|
|
||||||
|
.th-hint {
|
||||||
|
display: block;
|
||||||
|
font-size: 9.5px;
|
||||||
|
font-weight: 400;
|
||||||
|
text-transform: none;
|
||||||
|
letter-spacing: 0;
|
||||||
|
color: var(--ink-3);
|
||||||
|
margin-top: 2px;
|
||||||
|
}
|
||||||
|
|||||||
+7
-13
@@ -96,16 +96,10 @@ async function refreshPlayer(pdgaNumber) {
|
|||||||
if (data.success) {
|
if (data.success) {
|
||||||
const row = document.getElementById(`row-${pdgaNumber}`);
|
const row = document.getElementById(`row-${pdgaNumber}`);
|
||||||
const ratingCell = row.querySelector('.rating');
|
const ratingCell = row.querySelector('.rating');
|
||||||
const ratingChangeCell = row.querySelector('.rating-change');
|
|
||||||
|
|
||||||
const nameLink = row.querySelector('.player-name a');
|
const nameLink = row.querySelector('.player-name a');
|
||||||
nameLink.textContent = data.player.name;
|
nameLink.textContent = data.player.name;
|
||||||
|
|
||||||
const ratingChangeText = data.player.ratingChange ?
|
|
||||||
(data.player.ratingChange > 0 ? `+${data.player.ratingChange}` : data.player.ratingChange.toString()) : 'N/A';
|
|
||||||
const ratingChangeClass = data.player.ratingChange > 0 ? 'positive' :
|
|
||||||
data.player.ratingChange < 0 ? 'negative' : 'neutral';
|
|
||||||
|
|
||||||
const ratingValue = ratingCell.querySelector('.rating-value');
|
const ratingValue = ratingCell.querySelector('.rating-value');
|
||||||
if (ratingValue) {
|
if (ratingValue) {
|
||||||
ratingValue.textContent = data.player.rating || 'N/A';
|
ratingValue.textContent = data.player.rating || 'N/A';
|
||||||
@@ -122,13 +116,13 @@ async function refreshPlayer(pdgaNumber) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (ratingChangeCell) ratingChangeCell.textContent = ratingChangeText;
|
const deltaMonthPill = ratingCell.querySelector('.delta-pill');
|
||||||
if (ratingChangeCell) ratingChangeCell.className = `rating-change ${ratingChangeClass} mobile-hide`;
|
if (deltaMonthPill && data.player.ratingChange != null) {
|
||||||
|
const pillChange = data.player.ratingChange;
|
||||||
const mobileChange = ratingCell.querySelector('.mobile-only.rating-change');
|
const pillText = pillChange > 0 ? `+${pillChange}` : pillChange.toString();
|
||||||
if (mobileChange) {
|
const pillClass = pillChange > 0 ? 'up' : pillChange < 0 ? 'down' : 'flat';
|
||||||
mobileChange.textContent = ratingChangeText;
|
deltaMonthPill.textContent = pillText;
|
||||||
mobileChange.className = `mobile-only rating-change ${ratingChangeClass}`;
|
deltaMonthPill.className = `delta-pill ${pillClass}`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
+84
-14
@@ -33,7 +33,7 @@ function getRatingHistoryFromDB(pdgaNumber) {
|
|||||||
db.get('SELECT id FROM players WHERE pdga_number = ?', [pdgaNumber], (err, player) => {
|
db.get('SELECT id FROM players WHERE pdga_number = ?', [pdgaNumber], (err, player) => {
|
||||||
if (err) return reject(err);
|
if (err) return reject(err);
|
||||||
if (!player) return resolve(null);
|
if (!player) return resolve(null);
|
||||||
|
|
||||||
db.all(
|
db.all(
|
||||||
'SELECT * FROM rating_history WHERE player_id = ? ORDER BY date ASC',
|
'SELECT * FROM rating_history WHERE player_id = ? ORDER BY date ASC',
|
||||||
[player.id],
|
[player.id],
|
||||||
@@ -51,26 +51,26 @@ function saveRatingHistoryToDB(pdgaNumber, ratingHistory) {
|
|||||||
db.get('SELECT id FROM players WHERE pdga_number = ?', [pdgaNumber], (err, player) => {
|
db.get('SELECT id FROM players WHERE pdga_number = ?', [pdgaNumber], (err, player) => {
|
||||||
if (err) return reject(err);
|
if (err) return reject(err);
|
||||||
if (!player) return reject(new Error('Player not found'));
|
if (!player) return reject(new Error('Player not found'));
|
||||||
|
|
||||||
db.run('DELETE FROM rating_history WHERE player_id = ?', [player.id], (err) => {
|
db.run('DELETE FROM rating_history WHERE player_id = ?', [player.id], (err) => {
|
||||||
if (err) return reject(err);
|
if (err) return reject(err);
|
||||||
|
|
||||||
if (ratingHistory.length === 0) {
|
if (ratingHistory.length === 0) {
|
||||||
return resolve();
|
return resolve();
|
||||||
}
|
}
|
||||||
|
|
||||||
let completed = 0;
|
let completed = 0;
|
||||||
const total = ratingHistory.length;
|
const total = ratingHistory.length;
|
||||||
|
|
||||||
ratingHistory.forEach(entry => {
|
ratingHistory.forEach(entry => {
|
||||||
const parsedDate = parseDate(entry.date);
|
const parsedDate = parseDate(entry.date);
|
||||||
|
|
||||||
db.run(
|
db.run(
|
||||||
'INSERT INTO rating_history (player_id, date, rating) VALUES (?, ?, ?)',
|
'INSERT INTO rating_history (player_id, date, rating) VALUES (?, ?, ?)',
|
||||||
[player.id, parsedDate.toISOString().split('T')[0], entry.rating],
|
[player.id, parsedDate.toISOString().split('T')[0], entry.rating],
|
||||||
(err) => {
|
(err) => {
|
||||||
if (err) return reject(err);
|
if (err) return reject(err);
|
||||||
|
|
||||||
completed++;
|
completed++;
|
||||||
if (completed === total) {
|
if (completed === total) {
|
||||||
resolve();
|
resolve();
|
||||||
@@ -88,7 +88,7 @@ function getRoundHistoryFromDB(pdgaNumber) {
|
|||||||
db.get('SELECT id FROM players WHERE pdga_number = ?', [pdgaNumber], (err, player) => {
|
db.get('SELECT id FROM players WHERE pdga_number = ?', [pdgaNumber], (err, player) => {
|
||||||
if (err) return reject(err);
|
if (err) return reject(err);
|
||||||
if (!player) return resolve([]);
|
if (!player) return resolve([]);
|
||||||
|
|
||||||
db.all(
|
db.all(
|
||||||
'SELECT * FROM round_history WHERE player_id = ? ORDER BY date DESC',
|
'SELECT * FROM round_history WHERE player_id = ? ORDER BY date DESC',
|
||||||
[player.id],
|
[player.id],
|
||||||
@@ -132,7 +132,7 @@ function saveRoundHistoryToDB(pdgaNumber, roundData, isIncremental = false) {
|
|||||||
db.get('SELECT id FROM players WHERE pdga_number = ?', [pdgaNumber], (err, player) => {
|
db.get('SELECT id FROM players WHERE pdga_number = ?', [pdgaNumber], (err, player) => {
|
||||||
if (err) return reject(err);
|
if (err) return reject(err);
|
||||||
if (!player) return reject(new Error('Player not found'));
|
if (!player) return reject(new Error('Player not found'));
|
||||||
|
|
||||||
const processRounds = () => {
|
const processRounds = () => {
|
||||||
if (roundData.length === 0) {
|
if (roundData.length === 0) {
|
||||||
db.run('UPDATE players SET last_round_update = datetime("now") WHERE pdga_number = ?', [pdgaNumber], (err) => {
|
db.run('UPDATE players SET last_round_update = datetime("now") WHERE pdga_number = ?', [pdgaNumber], (err) => {
|
||||||
@@ -141,13 +141,13 @@ function saveRoundHistoryToDB(pdgaNumber, roundData, isIncremental = false) {
|
|||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const stmt = db.prepare('INSERT OR REPLACE INTO round_history (player_id, date, competition_name, rating) VALUES (?, ?, ?, ?)');
|
const stmt = db.prepare('INSERT OR REPLACE INTO round_history (player_id, date, competition_name, rating) VALUES (?, ?, ?, ?)');
|
||||||
|
|
||||||
for (const round of roundData) {
|
for (const round of roundData) {
|
||||||
stmt.run([player.id, round.date.toISOString().split('T')[0], round.competition || 'Unknown', round.rating]);
|
stmt.run([player.id, round.date.toISOString().split('T')[0], round.competition || 'Unknown', round.rating]);
|
||||||
}
|
}
|
||||||
|
|
||||||
stmt.finalize((err) => {
|
stmt.finalize((err) => {
|
||||||
if (err) {
|
if (err) {
|
||||||
reject(err);
|
reject(err);
|
||||||
@@ -159,7 +159,7 @@ function saveRoundHistoryToDB(pdgaNumber, roundData, isIncremental = false) {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!isIncremental) {
|
if (!isIncremental) {
|
||||||
db.run('DELETE FROM round_history WHERE player_id = ?', [player.id], (err) => {
|
db.run('DELETE FROM round_history WHERE player_id = ?', [player.id], (err) => {
|
||||||
if (err) return reject(err);
|
if (err) return reject(err);
|
||||||
@@ -185,6 +185,74 @@ function savePredictedRatingToDB(pdgaNumber, predictedRating, stdDev = null) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns monthly rating snapshots for one player (latest entry per calendar month),
|
||||||
|
* ordered oldest → newest. At most `months` entries; [] if none.
|
||||||
|
*/
|
||||||
|
function getMonthlyHistory(pdgaNumber, months = 12) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
db.get('SELECT id FROM players WHERE pdga_number = ?', [pdgaNumber], (err, player) => {
|
||||||
|
if (err) return reject(err);
|
||||||
|
if (!player) return resolve([]);
|
||||||
|
|
||||||
|
db.all(
|
||||||
|
`SELECT rating
|
||||||
|
FROM rating_history
|
||||||
|
WHERE player_id = ?
|
||||||
|
AND date IN (
|
||||||
|
SELECT MAX(date)
|
||||||
|
FROM rating_history
|
||||||
|
WHERE player_id = ?
|
||||||
|
GROUP BY strftime('%Y-%m', date)
|
||||||
|
)
|
||||||
|
ORDER BY date ASC
|
||||||
|
LIMIT ?`,
|
||||||
|
[player.id, player.id, months],
|
||||||
|
(err, rows) => {
|
||||||
|
if (err) return reject(err);
|
||||||
|
resolve(rows.map(r => r.rating));
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetches the last `months` monthly rating snapshots for ALL players in one query.
|
||||||
|
* Returns a Map<pdgaNumber, number[]> (oldest → newest per player).
|
||||||
|
* Use this in bulk-fetch paths to avoid N+1 queries.
|
||||||
|
*/
|
||||||
|
function getAllMonthlyHistoriesFromDB(months = 12) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
db.all(
|
||||||
|
`SELECT p.pdga_number, rh.date, rh.rating
|
||||||
|
FROM rating_history rh
|
||||||
|
JOIN players p ON rh.player_id = p.id
|
||||||
|
INNER JOIN (
|
||||||
|
SELECT player_id, MAX(date) AS max_date
|
||||||
|
FROM rating_history
|
||||||
|
GROUP BY player_id, strftime('%Y-%m', date)
|
||||||
|
) latest ON rh.player_id = latest.player_id AND rh.date = latest.max_date
|
||||||
|
ORDER BY p.pdga_number, rh.date ASC`,
|
||||||
|
[],
|
||||||
|
(err, rows) => {
|
||||||
|
if (err) return reject(err);
|
||||||
|
|
||||||
|
const map = new Map();
|
||||||
|
for (const row of rows) {
|
||||||
|
if (!map.has(row.pdga_number)) map.set(row.pdga_number, []);
|
||||||
|
map.get(row.pdga_number).push(row.rating);
|
||||||
|
}
|
||||||
|
// Trim each player's history to the requested window
|
||||||
|
for (const [key, arr] of map) {
|
||||||
|
if (arr.length > months) map.set(key, arr.slice(-months));
|
||||||
|
}
|
||||||
|
resolve(map);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function getLastRefresh() {
|
function getLastRefresh() {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
db.get(
|
db.get(
|
||||||
@@ -208,5 +276,7 @@ module.exports = {
|
|||||||
updateLastRoundUpdateDate,
|
updateLastRoundUpdateDate,
|
||||||
saveRoundHistoryToDB,
|
saveRoundHistoryToDB,
|
||||||
savePredictedRatingToDB,
|
savePredictedRatingToDB,
|
||||||
getLastRefresh
|
getLastRefresh,
|
||||||
|
getMonthlyHistory,
|
||||||
|
getAllMonthlyHistoriesFromDB
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
const { db } = require('../db');
|
const { db } = require('../db');
|
||||||
const { getPlayerFromDB, getRoundHistoryFromDB, savePredictedRatingToDB, savePlayerToDB } = require('../models/player');
|
const { getPlayerFromDB, getRoundHistoryFromDB, savePredictedRatingToDB, savePlayerToDB, getMonthlyHistory, getAllMonthlyHistoriesFromDB } = require('../models/player');
|
||||||
const { fetchPlayerDataHTTP, parsePlayerData } = require('../scrapers/player-http');
|
const { fetchPlayerDataHTTP, parsePlayerData } = require('../scrapers/player-http');
|
||||||
const { calculatePredictedRating } = require('./rating-calculator');
|
const { calculatePredictedRating } = require('./rating-calculator');
|
||||||
const logger = require('../logger');
|
const logger = require('../logger');
|
||||||
|
|
||||||
async function getPlayerDataFromDB(pdgaNumber) {
|
async function getPlayerDataFromDB(pdgaNumber, { includeMonthlyHistory = true } = {}) {
|
||||||
try {
|
try {
|
||||||
const cachedPlayer = await getPlayerFromDB(pdgaNumber);
|
const cachedPlayer = await getPlayerFromDB(pdgaNumber);
|
||||||
if (cachedPlayer) {
|
if (cachedPlayer) {
|
||||||
@@ -23,6 +23,11 @@ async function getPlayerDataFromDB(pdgaNumber) {
|
|||||||
const resolvedPredicted = predictedRating > 0 ? predictedRating : null;
|
const resolvedPredicted = predictedRating > 0 ? predictedRating : null;
|
||||||
const resolvedStdDev = stdDev > 0 ? stdDev : null;
|
const resolvedStdDev = stdDev > 0 ? stdDev : null;
|
||||||
|
|
||||||
|
// Skip in bulk-fetch paths where caller supplies history via getAllMonthlyHistoriesFromDB
|
||||||
|
const monthlyHistory = includeMonthlyHistory
|
||||||
|
? await getMonthlyHistory(cachedPlayer.pdga_number)
|
||||||
|
: [];
|
||||||
|
|
||||||
return {
|
return {
|
||||||
pdgaNumber: cachedPlayer.pdga_number,
|
pdgaNumber: cachedPlayer.pdga_number,
|
||||||
name: cachedPlayer.name,
|
name: cachedPlayer.name,
|
||||||
@@ -34,7 +39,7 @@ async function getPlayerDataFromDB(pdgaNumber) {
|
|||||||
lastMonthRating: (rating != null && ratingChange != null) ? rating - ratingChange : null,
|
lastMonthRating: (rating != null && ratingChange != null) ? rating - ratingChange : null,
|
||||||
// gap between next predicted update and current rating (null when either is missing)
|
// gap between next predicted update and current rating (null when either is missing)
|
||||||
deltaPredicted: (resolvedPredicted != null && rating != null) ? resolvedPredicted - rating : null,
|
deltaPredicted: (resolvedPredicted != null && rating != null) ? resolvedPredicted - rating : null,
|
||||||
monthlyHistory: []
|
monthlyHistory
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
@@ -137,6 +142,9 @@ async function getAllRatingsFromDB(progressCallback = null) {
|
|||||||
|
|
||||||
logger.info(`Loading ${allPlayers.length} players from database...`);
|
logger.info(`Loading ${allPlayers.length} players from database...`);
|
||||||
|
|
||||||
|
// Fetch all monthly histories in one query so the per-player loop doesn't add N extra queries
|
||||||
|
const monthlyHistoryMap = await getAllMonthlyHistoriesFromDB(12);
|
||||||
|
|
||||||
const ratings = [];
|
const ratings = [];
|
||||||
const total = allPlayers.length;
|
const total = allPlayers.length;
|
||||||
|
|
||||||
@@ -154,9 +162,10 @@ async function getAllRatingsFromDB(progressCallback = null) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const playerData = await getPlayerDataFromDB(pdgaNumber);
|
const playerData = await getPlayerDataFromDB(pdgaNumber, { includeMonthlyHistory: false });
|
||||||
|
|
||||||
if (playerData) {
|
if (playerData) {
|
||||||
|
playerData.monthlyHistory = monthlyHistoryMap.get(pdgaNumber) ?? [];
|
||||||
ratings.push(playerData);
|
ratings.push(playerData);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -8,17 +8,18 @@
|
|||||||
<th>Player Name</th>
|
<th>Player Name</th>
|
||||||
<th class="mobile-hide">PDGA #</th>
|
<th class="mobile-hide">PDGA #</th>
|
||||||
<th>Rating</th>
|
<th>Rating</th>
|
||||||
<th class="mobile-hide">Change</th>
|
<th class="mobile-hide">Predicted<span class="th-hint">next official update</span></th>
|
||||||
<th class="mobile-hide">Predicted</th>
|
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<% ratings.forEach(function(player, index) {
|
<% ratings.forEach(function(player, index) {
|
||||||
var difference = player.predictedRating && player.rating ? player.predictedRating - player.rating : 0;
|
var ratingChange = player.ratingChange;
|
||||||
var diffText = difference > 0 ? '+' + difference : difference.toString();
|
var ratingChangePillText = ratingChange != null ? (ratingChange > 0 ? '+' + ratingChange : ratingChange.toString()) : null;
|
||||||
var diffClass = difference > 0 ? 'positive' : difference < 0 ? 'negative' : 'neutral';
|
var ratingChangePillClass = ratingChange > 0 ? 'up' : ratingChange < 0 ? 'down' : 'flat';
|
||||||
var ratingChangeText = player.ratingChange ? (player.ratingChange > 0 ? '+' + player.ratingChange : player.ratingChange.toString()) : 'N/A';
|
|
||||||
var ratingChangeClass = player.ratingChange > 0 ? 'positive' : player.ratingChange < 0 ? 'negative' : 'neutral';
|
var deltaPredicted = player.deltaPredicted ?? null;
|
||||||
|
var deltaPredictedPillText = deltaPredicted != null ? (deltaPredicted > 0 ? '+' + deltaPredicted : deltaPredicted.toString()) : null;
|
||||||
|
var deltaPredictedPillClass = deltaPredicted > 0 ? 'up' : deltaPredicted < 0 ? 'down' : 'flat';
|
||||||
%>
|
%>
|
||||||
<tr id="row-<%= player.pdgaNumber %>" class="expandable-row" onclick="togglePlayerHistory(<%= player.pdgaNumber %>)">
|
<tr id="row-<%= player.pdgaNumber %>" class="expandable-row" onclick="togglePlayerHistory(<%= player.pdgaNumber %>)">
|
||||||
<td class="mobile-hide"><%= index + 1 %></td>
|
<td class="mobile-hide"><%= index + 1 %></td>
|
||||||
@@ -32,21 +33,25 @@
|
|||||||
<span class="rating-value" data-rating="<%= player.rating || '' %>" data-stddev="<%= player.stdDev || '' %>" data-pdga="<%= player.pdgaNumber %>" style="cursor: help;"><%- player.rating || '<span style="color: var(--text-muted); font-style: italic;">Click refresh</span>' %></span>
|
<span class="rating-value" data-rating="<%= player.rating || '' %>" data-stddev="<%= player.stdDev || '' %>" data-pdga="<%= player.pdgaNumber %>" style="cursor: help;"><%- player.rating || '<span style="color: var(--text-muted); font-style: italic;">Click refresh</span>' %></span>
|
||||||
<i class="fas fa-sync-alt refresh-icon" onclick="refreshPlayer(<%= player.pdgaNumber %>)" title="Refresh player data"></i>
|
<i class="fas fa-sync-alt refresh-icon" onclick="refreshPlayer(<%= player.pdgaNumber %>)" title="Refresh player data"></i>
|
||||||
</div>
|
</div>
|
||||||
<div class="mobile-only rating-change <%= ratingChangeClass %>" style="font-size: 11px; margin-top: 2px;"><%= ratingChangeText %></div>
|
<% if (ratingChangePillText) { %>
|
||||||
|
<span class="delta-pill <%= ratingChangePillClass %>"><%= ratingChangePillText %></span>
|
||||||
|
<% } %>
|
||||||
<div class="std-dev-tooltip" id="tooltip-rating-<%= player.pdgaNumber %>"></div>
|
<div class="std-dev-tooltip" id="tooltip-rating-<%= player.pdgaNumber %>"></div>
|
||||||
</td>
|
</td>
|
||||||
<td class="rating-change <%= ratingChangeClass %> mobile-hide"><%= ratingChangeText %></td>
|
|
||||||
<td class="predicted-rating mobile-hide" id="predicted-<%= player.pdgaNumber %>">
|
<td class="predicted-rating mobile-hide" id="predicted-<%= player.pdgaNumber %>">
|
||||||
<div class="refresh-section">
|
<div class="refresh-section">
|
||||||
<span class="predicted-value" data-stddev="<%= player.stdDev || '' %>" data-pdga="<%= player.pdgaNumber %>" style="cursor: help;"><%= player.predictedRating || 'N/A' %></span>
|
<span class="predicted-value" data-stddev="<%= player.stdDev || '' %>" data-pdga="<%= player.pdgaNumber %>" style="cursor: help;"><%= player.predictedRating || 'N/A' %></span>
|
||||||
<i class="fas fa-question-circle debug-icon" onclick="showDebugInfo(<%= player.pdgaNumber %>)" title="Show calculation details" style="margin-left: 5px; color: var(--text-muted); cursor: pointer; opacity: 0.6;"></i>
|
<i class="fas fa-question-circle debug-icon" onclick="showDebugInfo(<%= player.pdgaNumber %>)" title="Show calculation details" style="margin-left: 5px; color: var(--text-muted); cursor: pointer; opacity: 0.6;"></i>
|
||||||
<i class="fas fa-sync-alt refresh-icon" onclick="refreshRoundHistory(<%= player.pdgaNumber %>)" title="Refresh prediction data"></i>
|
<i class="fas fa-sync-alt refresh-icon" onclick="refreshRoundHistory(<%= player.pdgaNumber %>)" title="Refresh prediction data"></i>
|
||||||
</div>
|
</div>
|
||||||
|
<% if (deltaPredictedPillText) { %>
|
||||||
|
<span class="delta-pill delta-predicted-pill <%= deltaPredictedPillClass %>"><%= deltaPredictedPillText %></span>
|
||||||
|
<% } %>
|
||||||
<div class="std-dev-tooltip" id="tooltip-stddev-<%= player.pdgaNumber %>"></div>
|
<div class="std-dev-tooltip" id="tooltip-stddev-<%= player.pdgaNumber %>"></div>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr id="history-<%= player.pdgaNumber %>" class="expanded-content">
|
<tr id="history-<%= player.pdgaNumber %>" class="expanded-content">
|
||||||
<td colspan="6" class="expanded-cell">
|
<td colspan="5" class="expanded-cell">
|
||||||
<div class="chart-title">
|
<div class="chart-title">
|
||||||
<div class="refresh-section">
|
<div class="refresh-section">
|
||||||
Rating History for <%= player.name %>
|
Rating History for <%= player.name %>
|
||||||
@@ -61,4 +66,4 @@
|
|||||||
<% }); %>
|
<% }); %>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
<% } %>
|
<% } %>
|
||||||
|
|||||||
Reference in New Issue
Block a user