Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 088e283dcf | |||
| 5791d8e34f | |||
| 1ff768e2fa | |||
| c3fb850de3 | |||
| c69efa469e | |||
| e21e6f2ef0 | |||
| 141dc90db7 |
@@ -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."
|
||||
@@ -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.
|
||||
|
||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "pdga-ratings",
|
||||
"version": "1.2.9",
|
||||
"version": "1.2.10",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "pdga-ratings",
|
||||
"version": "1.2.9",
|
||||
"version": "1.2.10",
|
||||
"dependencies": {
|
||||
"ejs": "^4.0.1",
|
||||
"express": "^4.18.2",
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "pdga-ratings",
|
||||
"version": "1.2.9",
|
||||
"version": "1.2.10",
|
||||
"description": "PDGA rating scraper and display",
|
||||
"main": "server.js",
|
||||
"scripts": {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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">
|
||||
|
||||
Reference in New Issue
Block a user