add weekly ELO rank sync from stat-check.com

- scripts/sync-elo.py: downloads Excel sheet, auto-detects columns,
  writes public/elo-data.json keyed by lowercase player name
- .gitlab-ci.yml: sync-elo stage (schedules only) commits the JSON
  then a normal push pipeline handles build + deploy; build/deploy
  skip on scheduled runs to avoid double deployment
- index.html + main.js: ELO Rank column with sort support; loads
  elo-data.json asynchronously and re-renders when ready; fails
  silently if data is missing

Requires one-time setup:
  1. GitLab project token (write_repository) stored as GITLAB_PUSH_TOKEN
  2. Pipeline schedule: cron 0 2 * * 3 (Wednesday 02:00 UTC) on main

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-06-26 20:47:30 -05:00
co-authored by Claude Sonnet 4.6
parent 3a57967214
commit 3e6a152206
5 changed files with 174 additions and 1 deletions
+35 -1
View File
@@ -60,6 +60,34 @@ const fmtPts = (n) => Number(n || 0).toLocaleString(undefined, { minimumFraction
const fmtWpct = (w, l, t) => { const g = (+w||0)+(+l||0)+(+t||0); return g ? ((+w||0)/g*100).toFixed(1)+'%' : '—'; };
const fullName = (u = {}) => [u.firstName, u.lastName].filter(Boolean).join(" ").trim();
/* ---- ELO data ---- */
let eloData = null;
async function loadElo() {
try {
const r = await fetch('/elo-data.json');
if (!r.ok) return;
const d = await r.json();
if (d && d.byName && d.count > 0) {
eloData = d;
if (lastState) render(lastState);
}
} catch (e) { /* ELO is optional — fail silently */ }
}
function lookupElo(r) {
if (!eloData) return null;
const name = fullName(r.user || {}).toLowerCase().trim();
if (eloData.byName[name]) return eloData.byName[name];
// Fall back to first + last only (strips middle initials)
const parts = name.split(/\s+/).filter(Boolean);
if (parts.length > 2) {
const short = parts[0] + ' ' + parts[parts.length - 1];
if (eloData.byName[short]) return eloData.byName[short];
}
return null;
}
/* ---- Sort state ---- */
let sortState = { col: 'pts', dir: 'desc' };
let lastState = null;
@@ -70,6 +98,7 @@ const sortCols = {
wlt: { key: r => (+r.wins||0) * 10000 - (+r.losses||0) * 100 - (+r.ties||0), defaultDir: 'desc' },
wpct: { key: r => { const g=(+r.wins||0)+(+r.losses||0)+(+r.ties||0); return g?(+r.wins||0)/g:-1; }, defaultDir: 'desc' },
rank: { key: r => +(r.placing || Infinity), defaultDir: 'asc' },
elo: { key: r => { const e = lookupElo(r); return e ? e.rank : Infinity; }, defaultDir: 'asc' },
};
function applySort(rows) {
@@ -223,7 +252,10 @@ function render(state) {
`<td class="text-right font-mono hidden sm:table-cell">` +
`<span class="text-success">${r.wins || 0}</span><span class="text-error">${r.losses || 0}</span>${r.ties || 0}</td>` +
`<td class="text-right font-mono hidden sm:table-cell">${fmtWpct(r.wins, r.losses, r.ties)}</td>` +
`<td class="text-right font-mono text-base-content/40 hidden sm:table-cell">#${r.placing ?? "—"}</td>`;
`<td class="text-right font-mono text-base-content/40 hidden sm:table-cell">#${r.placing ?? "—"}</td>` +
(eloData
? (() => { const e = lookupElo(r); return `<td class="text-right font-mono hidden sm:table-cell">${e ? `#${e.rank.toLocaleString()}` : '<span class="opacity-30">—</span>'}</td>`; })()
: `<td class="text-right font-mono text-base-content/20 hidden sm:table-cell">…</td>`);
tb.appendChild(tr);
});
}
@@ -290,6 +322,7 @@ function showSkeleton() {
`<td><div class="skeleton h-4 w-12 ml-auto"></div></td>` +
`<td class="hidden sm:table-cell"><div class="skeleton h-4 w-14 ml-auto"></div></td>` +
`<td class="hidden sm:table-cell"><div class="skeleton h-4 w-12 ml-auto"></div></td>` +
`<td class="hidden sm:table-cell"><div class="skeleton h-4 w-10 ml-auto"></div></td>` +
`<td class="hidden sm:table-cell"><div class="skeleton h-4 w-10 ml-auto"></div></td>`;
tb.appendChild(tr);
}
@@ -326,3 +359,4 @@ $("refresh").addEventListener("click", () => load(true));
initSortHeaders();
paintCache();
load(false);
loadElo();