Compare commits

...

8 Commits

Author SHA1 Message Date
shcizo 5198a1c0f4 Merge pull request 'fix: preserve predicted_rating via UPSERT in savePlayerToDB (#11)' (#28) from fix/upsert-preserve-predicted-rating-11 into main
Release / release (push) Successful in 5s
Build and deploy / build-and-push (push) Successful in 10s
Build and deploy / deploy (push) Successful in 4s
2026-06-08 09:53:01 +02:00
Samuel Enocsson 7297c0a16b fix: preserve predicted_rating via UPSERT in savePlayerToDB (#11)
INSERT OR REPLACE deletes the existing row and resets columns absent from
the VALUES list (predicted_rating, std_dev, last_round_update,
excluded_rounds_count, cutoff_rating) to NULL. Refresh-all called this for
every player, wiping predicted ratings table-wide.

Switch to a SQLite UPSERT keyed on pdga_number that updates only the four
scraped columns in place, leaving the predicted-rating columns and the
24h-cooldown timestamp untouched. Mirror course.js's lastID==0 SELECT
fallback so the function still returns a real player id on the update path.
2026-06-08 09:50:03 +02:00
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
6 changed files with 69 additions and 11 deletions
+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "pdga-ratings",
"version": "1.4.0",
"version": "1.4.2",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "pdga-ratings",
"version": "1.4.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.4.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;
+10 -4
View File
@@ -131,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' }),
@@ -144,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'));
}
}
+19 -4
View File
@@ -17,12 +17,27 @@ function getPlayerFromDB(pdgaNumber) {
function savePlayerToDB(playerData) {
return new Promise((resolve, reject) => {
db.run(
`INSERT OR REPLACE INTO players (pdga_number, name, current_rating, rating_change, last_updated)
VALUES (?, ?, ?, ?, datetime('now'))`,
// UPSERT (not INSERT OR REPLACE): updating in place preserves columns not
// listed here — predicted_rating, std_dev, last_round_update,
// excluded_rounds_count, cutoff_rating. INSERT OR REPLACE would delete the
// existing row and reset those to their DEFAULT (NULL).
`INSERT INTO players (pdga_number, name, current_rating, rating_change, last_updated)
VALUES (?, ?, ?, ?, datetime('now'))
ON CONFLICT(pdga_number) DO UPDATE SET
name = excluded.name,
current_rating = excluded.current_rating,
rating_change = excluded.rating_change,
last_updated = excluded.last_updated`,
[playerData.pdgaNumber, playerData.name, playerData.rating, playerData.ratingChange],
function(err) {
if (err) reject(err);
else resolve(this.lastID);
if (err) return reject(err);
// node-sqlite3 leaves lastID = 0 when ON CONFLICT triggers an UPDATE.
// Fall back to a SELECT to get the real id in that case.
if (this.lastID !== 0) return resolve(this.lastID);
db.get('SELECT id FROM players WHERE pdga_number = ?', [playerData.pdgaNumber], (err2, row) => {
if (err2) reject(err2);
else resolve(row ? row.id : 0);
});
}
);
});
+5
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>