Compare commits

..

11 Commits

Author SHA1 Message Date
Release Bot f4c5e963d2 1.2.11
Release / release (push) Successful in 5s
Build and deploy / build-and-push (push) Successful in 21s
Build and deploy / deploy (push) Successful in 11s
2026-05-25 06:04:26 +00:00
shcizo 27d1bef8dd Merge pull request 'fix: move std-dev info to accordion, remove broken tooltip (#19)' (#20) from fix/std-dev-tooltip-positioning-and-discoverability-19 into main
Release / release (push) Successful in 7s
2026-05-25 08:04:14 +02:00
Samuel Enocsson 088e283dcf refactor: split round spread and rating range into separate accordion rows (#19) 2026-05-25 08:02:05 +02:00
Samuel Enocsson 5791d8e34f refactor: move std-dev info to accordion, remove tooltip (#19)
- Add "Round spread" row (±stdDev, range lo–hi) to desktop accordion
  (player-history.ejs) and mobile card expanded section (ratings-cards.ejs)
- Remove .std-dev-tooltip div and .std-dev-inline span from table partial
- Remove stdDevTooltipText, updateStdDevInline, initRatingsTooltips helpers
  and all call sites from players.js
- Remove .std-dev-tooltip and .std-dev-inline CSS rules; drop cursor:help
  from .rating-value
2026-05-25 07:54:46 +02:00
Samuel Enocsson 1ff768e2fa fix: address std-dev inline span refresh + style fixes (#19)
- A: create inline span when missing in refreshRoundHistory (was silently dropped)
- B: updateStdDevInline also called from refreshHistoryThenCalculate
- C: extract stdDevTooltipText + updateStdDevInline helpers; replace 3 call sites
- D: remove margin-left: 4px and bump font-size to 12px on .std-dev-inline
- E: guard against stdDev === 0 in EJS (truthy → != null)
2026-05-23 06:45:39 +02:00
Samuel Enocsson c3fb850de3 fix: reposition std-dev tooltip and surface ±-spread inline (#19) 2026-05-23 06:40:08 +02:00
Release Bot c69efa469e 1.2.10
Release / release (push) Successful in 5s
Build and deploy / build-and-push (push) Successful in 21s
Build and deploy / deploy (push) Successful in 8s
2026-05-22 20:11:32 +00:00
Samuel Enocsson e21e6f2ef0 fix: couple mobile add-bar offset to container padding via CSS var
Release / release (push) Successful in 7s
Previously .mobile-add-bar { margin-bottom: -80px } was hardcoded
to match .container { padding-bottom: 80px }. Now both reference
--m-container-pad-bottom so they can't drift out of sync.
2026-05-22 22:11:20 +02:00
Samuel Enocsson 141dc90db7 ci: add automated release workflow with conventional commits
Release / release (push) Successful in 5s
- New .gitea/workflows/release.yml triggers on push to main
- Scans commits since last v* tag and bumps strict semver
- feat: -> minor, fix: -> patch, !: or BREAKING CHANGE -> major
- Updates package.json + package-lock.json, commits as <version>,
  tags v<version>, pushes both
- Tag push triggers existing deploy.yml (PACKAGES_TOKEN is a PAT so
  cross-workflow triggers work)
- Skips silently when no releaseable commits are found
- Updates CLAUDE.md release section to reflect automation
2026-05-22 22:04:07 +02:00
Samuel Enocsson b4d9305550 1.2.9
Build and deploy / build-and-push (push) Successful in 22s
Build and deploy / deploy (push) Successful in 8s
2026-05-22 21:57:39 +02:00
shcizo e900b86e69 Merge pull request 'feat: mobile UI card layout for players and courses (#16)' (#18) from feat/mobile-ui-card-layout-16 into main 2026-05-22 21:49:48 +02:00
10 changed files with 118 additions and 77 deletions
+91
View File
@@ -0,0 +1,91 @@
name: Release
# Auto-tagging baserat på conventional commits sedan senaste taggen.
# feat: → minor bump (1.2.9 → 1.3.0)
# fix: → patch bump (1.2.9 → 1.2.10)
# "!:" eller "BREAKING CHANGE" → major bump (1.2.9 → 2.0.0)
# Andra prefix (chore:, docs:, ci:, refactor:, style:, test:) bumpar inte.
# Skippas tyst när inga releaseable commits hittas — t.ex. när workflow:n själv pushar version-commiten.
# Push av tag triggar deploy.yml (PACKAGES_TOKEN är en PAT så cross-workflow-triggers funkar).
on:
push:
branches: [main]
jobs:
release:
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
token: ${{ secrets.PACKAGES_TOKEN }}
- name: Configure git
run: |
git config user.name "Release Bot"
git config user.email "noreply@shcizo.se"
- name: Determine version bump
id: bump
run: |
set -euo pipefail
LAST_TAG=$(git describe --tags --abbrev=0 --match 'v*' 2>/dev/null || echo "")
if [ -z "$LAST_TAG" ]; then
RANGE=""
echo "No previous v* tag found — scanning all commits."
else
RANGE="${LAST_TAG}..HEAD"
echo "Scanning commits since ${LAST_TAG}."
fi
BUMP="none"
while IFS= read -r subject; do
[ -z "$subject" ] && continue
if echo "$subject" | grep -qE "^[a-z]+(\([^)]+\))?!:|BREAKING[ -]CHANGE"; then
BUMP="major"
break
elif echo "$subject" | grep -qE "^feat(\([^)]+\))?:"; then
[ "$BUMP" != "major" ] && BUMP="minor"
elif echo "$subject" | grep -qE "^fix(\([^)]+\))?:"; then
[ "$BUMP" = "none" ] && BUMP="patch"
fi
done < <(git log ${RANGE} --no-merges --pretty=format:%s)
echo "Determined bump: ${BUMP}"
echo "bump=${BUMP}" >> "$GITHUB_OUTPUT"
- name: Compute new version
if: steps.bump.outputs.bump != 'none'
id: version
run: |
set -euo pipefail
CURRENT=$(node -p "require('./package.json').version")
IFS=. read -r MAJ MIN PAT <<< "$CURRENT"
case "${{ steps.bump.outputs.bump }}" in
major) NEW="$((MAJ+1)).0.0" ;;
minor) NEW="${MAJ}.$((MIN+1)).0" ;;
patch) NEW="${MAJ}.${MIN}.$((PAT+1))" ;;
esac
echo "Bumping ${CURRENT} → ${NEW}"
echo "new=${NEW}" >> "$GITHUB_OUTPUT"
- name: Update files, commit, tag, push
if: steps.bump.outputs.bump != 'none'
run: |
set -euo pipefail
NEW="${{ steps.version.outputs.new }}"
npm version "$NEW" --no-git-tag-version
git add package.json package-lock.json
git commit -m "$NEW"
git tag "v${NEW}"
git push origin main "v${NEW}"
- name: No-op summary
if: steps.bump.outputs.bump == 'none'
run: echo "No feat/fix/breaking commits since last tag — no release."
+1 -1
View File
@@ -46,7 +46,7 @@ public/
- **Logging:** Use `require('./logger')` (or relative path). Never use `console.log/error` in backend code. Use appropriate Pino levels: `debug` for verbose/diagnostic data, `info` for operational status, `warn` for retries/degraded state, `error` for failures, `fatal` for startup crashes.
- **Frontend JS:** `console.error` is fine in `public/js/` — runs in browser, no Pino.
- **Commits:** Conventional commits (`feat:`, `fix:`, `refactor:`, `chore:`, `ci:`).
- **Releases:** Manual version bump — edit `version` in `package.json` + `package-lock.json`, commit as `<version>`, tag `v<version>`, push commit + tag (`git push origin main v<version>`). Triggers `.gitea/workflows/deploy.yml` which (1) builds and pushes the image to `gitea.shcizo.se/shcizo/pdga-rating:<tag>` + `:latest`, then (2) calls `package-updater-action` against `updater.shcizo.se/update` to roll out the new image. Required secrets: `PACKAGES_TOKEN` (PAT with `write:package`, for registry auth — the auto-injected `GITEA_TOKEN` does not have effective registry access) and `UPDATER_API_KEY` (for the updater endpoint). The action repo `shcizo/package-updater-action` is referenced via full Gitea URL (`https://gitea.shcizo.se/...`) since `uses:` defaults to GitHub.
- **Releases:** Automated via `.gitea/workflows/release.yml` on push to `main`. Workflow scans conventional commits sedan senaste `v*`-taggen och bumpar strikt semver: `feat:` → minor, `fix:` → patch, `!:`/`BREAKING CHANGE` → major. Andra prefix (`chore:`, `docs:`, `ci:`, `refactor:`, `style:`, `test:`) bumpar inte. Workflow:n uppdaterar `package.json` + `package-lock.json`, committar som `<version>`, taggar `v<version>` och pushar. Tag-pushen triggar `.gitea/workflows/deploy.yml` som (1) bygger + pushar image till `gitea.shcizo.se/shcizo/pdga-rating:<tag>` + `:latest`, sen (2) anropar `package-updater-action` mot `updater.shcizo.se/update`. Required secrets: `PACKAGES_TOKEN` (PAT med `write:package` + repo-push-scope — används av både release.yml för att pusha main/tag och deploy.yml för registry-auth; auto-injicerade `GITEA_TOKEN` triggar inte cross-workflows och saknar effective registry-access) och `UPDATER_API_KEY` (för updater-endpointen). Action-repot `shcizo/package-updater-action` refereras via full Gitea-URL (`https://gitea.shcizo.se/...`) eftersom `uses:` defaultar till GitHub. **Manuell tag** är fortfarande möjlig om release-workflow:n skippas eller behöver kringgås — pusha bara `v<version>`-tag direkt.
- **Scraping:** Two strategies per entity: direct HTTP (fast, preferred) with Puppeteer fallback (stealth plugin for anti-bot). Rate limiting must be respected.
- **Database:** Migrations run automatically on startup in `db.js`. Schema changes go there.
- **Templates:** EJS with shared layout in `views/partials/`. Pages use HTMX for dynamic content loading.
+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "pdga-ratings",
"version": "1.2.8",
"version": "1.2.11",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "pdga-ratings",
"version": "1.2.8",
"version": "1.2.11",
"dependencies": {
"ejs": "^4.0.1",
"express": "^4.18.2",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "pdga-ratings",
"version": "1.2.8",
"version": "1.2.11",
"description": "PDGA rating scraper and display",
"main": "server.js",
"scripts": {
+3 -2
View File
@@ -70,7 +70,8 @@
/* ── Container ──────────────────────────────────── */
.container {
padding: 10px 12px 80px;
--m-container-pad-bottom: 80px;
padding: 10px 12px var(--m-container-pad-bottom);
gap: 12px;
}
@@ -598,7 +599,7 @@
/* Negative margin to break out of container padding */
margin-left: -12px;
margin-right: -12px;
margin-bottom: -80px;
margin-bottom: calc(-1 * var(--m-container-pad-bottom));
}
.mobile-add-bar input {
-18
View File
@@ -94,24 +94,6 @@
box-shadow: var(--shadow-lg);
}
/* ── Tooltips ─────────────────────────────────── */
.std-dev-tooltip {
position: absolute;
background: var(--navy-900);
color: var(--text-inverse);
padding: 6px 10px;
border-radius: var(--radius-sm);
font-size: 12px;
font-family: var(--font-mono);
pointer-events: none;
z-index: 10000;
display: none;
white-space: nowrap;
box-shadow: var(--shadow-lg);
font-weight: 400;
}
/* ── Debug Icon ───────────────────────────────── */
.debug-icon:hover {
-51
View File
@@ -57,7 +57,6 @@ function setupAfterTableSwap() {
document.body.addEventListener('htmx:afterSwap', function(event) {
const target = event.detail.target;
if (target.id === 'ratings-table') {
initRatingsTooltips();
initChartsIn(target);
return;
}
@@ -67,31 +66,6 @@ function setupAfterTableSwap() {
});
}
function initRatingsTooltips() {
document.querySelectorAll('.predicted-value').forEach(span => {
const pdgaNumber = span.dataset.pdga;
const stdDev = span.dataset.stddev;
const tooltip = document.getElementById(`tooltip-stddev-${pdgaNumber}`);
if (stdDev && tooltip) {
setupTooltip(span, tooltip, () => `Standard Deviation: \u00b1${stdDev}`);
}
});
document.querySelectorAll('.rating-value').forEach(span => {
const pdgaNumber = span.dataset.pdga;
const rating = parseInt(span.dataset.rating);
const stdDev = parseInt(span.dataset.stddev);
const tooltip = document.getElementById(`tooltip-rating-${pdgaNumber}`);
if (rating && stdDev && tooltip) {
const minRating = rating - stdDev;
const maxRating = rating + stdDev;
setupTooltip(span, tooltip, () => `Rating Range: ${minRating} - ${maxRating} (\u00b1${stdDev})`);
}
});
}
function togglePlayerHistory(pdgaNumber) {
const historyRow = document.getElementById('history-' + pdgaNumber);
const contentDiv = document.getElementById('history-content-' + pdgaNumber);
@@ -191,16 +165,6 @@ async function refreshPlayer(pdgaNumber) {
if (ratingValue) {
ratingValue.textContent = data.player.rating || 'N/A';
ratingValue.dataset.rating = data.player.rating || '';
const stdDev = parseInt(ratingValue.dataset.stddev);
const rating = parseInt(data.player.rating);
const tooltip = document.getElementById(`tooltip-rating-${pdgaNumber}`);
if (rating && stdDev && tooltip) {
const minRating = rating - stdDev;
const maxRating = rating + stdDev;
replaceWithTooltip(ratingValue, tooltip, () => `Rating Range: ${minRating} - ${maxRating} (\u00b1${stdDev})`);
}
}
const deltaMonthPill = ratingCell ? ratingCell.querySelector('.delta-pill') : null;
@@ -232,11 +196,6 @@ async function refreshRoundHistory(pdgaNumber) {
if (predictedValue) {
predictedValue.textContent = data.predictedRating || 'N/A';
predictedValue.dataset.stddev = data.stdDev || '';
const tooltip = document.getElementById(`tooltip-stddev-${pdgaNumber}`);
if (data.stdDev && tooltip) {
replaceWithTooltip(predictedValue, tooltip, () => `Standard Deviation: \u00b1${data.stdDev}`);
}
}
}
@@ -245,16 +204,6 @@ async function refreshRoundHistory(pdgaNumber) {
const ratingValue = ratingCell ? ratingCell.querySelector('.rating-value') : null;
if (ratingValue && data.stdDev) {
ratingValue.dataset.stddev = data.stdDev;
const rating = parseInt(ratingValue.dataset.rating);
const stdDev = parseInt(data.stdDev);
const ratingTooltip = document.getElementById(`tooltip-rating-${pdgaNumber}`);
if (rating && stdDev && ratingTooltip) {
const minRating = rating - stdDev;
const maxRating = rating + stdDev;
replaceWithTooltip(ratingValue, ratingTooltip, () => `Rating Range: ${minRating} - ${maxRating} (\u00b1${stdDev})`);
}
}
}
} catch (error) {
+10
View File
@@ -26,6 +26,16 @@ const chartPdgaNumber = hasPlayer ? player.pdgaNumber : pdgaNumber;
<dt>Gap to predicted</dt>
<dd><%- include('delta-pill', { value: player.deltaPredicted, extraClass: 'delta-predicted-pill' }) %></dd>
</div>
<% if (player.stdDev != null && player.rating) { %>
<div>
<dt>Round spread</dt>
<dd>±<%= player.stdDev %></dd>
</div>
<div>
<dt>Rating range</dt>
<dd><%= player.rating - player.stdDev %><%= player.rating + player.stdDev %></dd>
</div>
<% } %>
</dl>
<button class="link-btn" onclick="showDebugInfo(<%= player.pdgaNumber %>)" style="margin-top: 4px;">View calculation details →</button>
</div>
+10
View File
@@ -114,6 +114,16 @@ function renderSparkline(values, opts) {
<dt>Gap to predicted</dt>
<dd><span class="delta-pill <%= predCls %> delta-predicted-pill"><span class="delta-glyph"><%= predGlyph %></span><span class="delta-num"><%= predNum %></span></span></dd>
</div>
<% if (player.stdDev != null && player.rating) { %>
<div>
<dt>Round spread</dt>
<dd>±<%= player.stdDev %></dd>
</div>
<div>
<dt>Rating range</dt>
<dd><%= player.rating - player.stdDev %><%= player.rating + player.stdDev %></dd>
</div>
<% } %>
</dl>
</div>
</div>
-2
View File
@@ -67,7 +67,6 @@ function renderSparkline(values, opts) {
<% } else { %>
<span class="rating-pending">Click to load</span>
<% } %>
<div class="std-dev-tooltip" id="tooltip-rating-<%= player.pdgaNumber %>"></div>
</td>
<td class="col-predicted" id="predicted-<%= player.pdgaNumber %>">
<% if (player.predictedRating) { %>
@@ -78,7 +77,6 @@ function renderSparkline(values, opts) {
<% } else { %>
<span class="rating-pending">—</span>
<% } %>
<div class="std-dev-tooltip" id="tooltip-stddev-<%= player.pdgaNumber %>"></div>
</td>
<td class="col-actions cell-actions" onclick="event.stopPropagation()">
<button class="icon-btn refresh-icon" onclick="refreshPlayerData(<%= player.pdgaNumber %>)" title="Refresh rating + prediction" aria-label="Refresh rating and prediction">