revert: remove ELO rank feature (data source inaccessible)

stat-check.com's ELO data is behind Microsoft auth with no public
API or download endpoint available. Removing the sync script,
placeholder JSON, CI job, table column, and all related JS.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-06-26 21:23:28 -05:00
co-authored by Claude Sonnet 4.6
parent b77c3f950a
commit d39d7fc627
5 changed files with 1 additions and 178 deletions
-30
View File
@@ -1,33 +1,7 @@
stages:
- sync
- build
- deploy
# Runs only on scheduled pipelines (every Wednesday via GitLab schedule).
# Downloads the stat-check.com ELO Excel sheet, writes public/elo-data.json,
# then commits and pushes so a normal build+deploy pipeline picks it up.
# Requires CI variable: GITLAB_PUSH_TOKEN (project token, write_repository scope)
sync-elo:
stage: sync
only:
- schedules
script:
- pip3 install --break-system-packages requests openpyxl -q
- python3 scripts/sync-elo.py
- git config user.email ci@gateway-gamers.net
- 'git config user.name "GG CI Bot"'
- 'git remote set-url origin "https://oauth2:${GITLAB_PUSH_TOKEN}@gl.mando.club/mandalore/40k-rankings.gateway-gamers.net.git"'
- git add public/elo-data.json
- |
if git diff --staged --quiet; then
echo "No ELO changes, skipping commit."
else
git commit -m 'sync: update ELO data'
git push origin HEAD:main
fi
# Build and deploy skip on scheduled pipelines — the robot push above
# triggers its own normal pipeline that handles the actual build + deploy.
build:
stage: build
script:
@@ -39,8 +13,6 @@ build:
expire_in: 1 hour
only:
- main
except:
- schedules
deploy:
stage: deploy
@@ -48,8 +20,6 @@ deploy:
- rsync -avz --delete dist/ /var/www/domains/gateway-gamers.net/40k-rankings/
only:
- main
except:
- schedules
environment:
name: production
url: https://40k-rankings.gateway-gamers.net
-1
View File
@@ -76,7 +76,6 @@
<th class="text-right hidden sm:table-cell cursor-pointer select-none" data-sort="wlt">WLT</th>
<th class="text-right hidden sm:table-cell cursor-pointer select-none" data-sort="wpct">Win %</th>
<th class="text-right hidden sm:table-cell cursor-pointer select-none" data-sort="rank">ITC Rank</th>
<th class="text-right hidden sm:table-cell cursor-pointer select-none" data-sort="elo">ELO Rank</th>
</tr>
</thead>
<tbody id="rows"></tbody>
-1
View File
@@ -1 +0,0 @@
{"updated":null,"count":0,"byName":{}}
-111
View File
@@ -1,111 +0,0 @@
"""
sync-elo.py — download the stat-check.com ELO Excel sheet and emit public/elo-data.json.
Column auto-detection: scans the header row for keywords.
If headers change, check the CI log — it always prints what it found.
"""
import json
import sys
from datetime import datetime, timezone
from io import BytesIO
import requests
import openpyxl
DOWNLOAD_URL = (
"https://excel.officeapps.live.com/x/_layouts/XlFileHandler.aspx?WacUserType=WOPI&usid=4bcd36aa-91bc-4c95-a692-c2ef5de1d13a&NoAuth=1&waccluster=PCA1"
)
OUTPUT_PATH = "public/elo-data.json"
def find_col(headers, *keywords):
"""Return index of first header containing any keyword (case-insensitive)."""
for i, h in enumerate(headers):
hl = str(h).lower()
if any(kw in hl for kw in keywords):
return i
return None
def main():
print(f"Downloading ELO sheet…", flush=True)
resp = requests.get(DOWNLOAD_URL, timeout=120, headers={"User-Agent": "Mozilla/5.0"})
resp.raise_for_status()
content_type = resp.headers.get("Content-Type", "")
print(f"Downloaded {len(resp.content):,} bytes Content-Type: {content_type}", flush=True)
# Diagnose non-Excel responses before attempting to parse
if "html" in content_type.lower() or not resp.content.startswith(b"PK"):
print("ERROR: Response is not an Excel/ZIP file. First 500 chars:", flush=True)
print(resp.content[:500].decode("utf-8", errors="replace"), flush=True)
sys.exit(1)
wb = openpyxl.load_workbook(BytesIO(resp.content), read_only=True, data_only=True)
ws = wb.active
rows = ws.iter_rows(values_only=True)
raw_headers = next(rows)
headers = [str(c).strip() if c is not None else "" for c in raw_headers]
print(f"Headers ({len(headers)}): {headers}", flush=True)
rank_col = find_col(headers, "rank")
name_col = find_col(headers, "name", "player")
rating_col = find_col(headers, "rating", "elo", "score", "points")
if name_col is None:
print("ERROR: Could not find a name/player column. Check the headers above.", file=sys.stderr)
sys.exit(1)
print(
f"Mapped rank→col {rank_col} ('{headers[rank_col] if rank_col is not None else ''}') "
f"name→col {name_col} ('{headers[name_col]}') "
f"rating→col {rating_col} ('{headers[rating_col] if rating_col is not None else ''}')",
flush=True,
)
by_name = {}
skipped = 0
for seq, row in enumerate(rows, start=2):
raw_name = row[name_col] if len(row) > name_col else None
if not raw_name:
skipped += 1
continue
name = str(raw_name).strip()
key = name.lower()
raw_rank = row[rank_col] if rank_col is not None and len(row) > rank_col else None
raw_rating = row[rating_col] if rating_col is not None and len(row) > rating_col else None
try:
rank = int(raw_rank) if raw_rank is not None else seq
except (ValueError, TypeError):
rank = seq
try:
rating = round(float(raw_rating), 1) if raw_rating is not None else None
except (ValueError, TypeError):
rating = None
by_name[key] = {"rank": rank, "rating": rating, "name": name}
wb.close()
print(f"Parsed {len(by_name):,} players ({skipped} blank rows skipped)", flush=True)
output = {
"updated": datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ"),
"count": len(by_name),
"byName": by_name,
}
with open(OUTPUT_PATH, "w", encoding="utf-8") as f:
json.dump(output, f, separators=(",", ":"), ensure_ascii=False)
print(f"Wrote {OUTPUT_PATH}", flush=True)
if __name__ == "__main__":
main()
+1 -35
View File
@@ -60,34 +60,6 @@ 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;
@@ -98,7 +70,6 @@ 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) {
@@ -252,10 +223,7 @@ 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>` +
(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>`);
`<td class="text-right font-mono text-base-content/40 hidden sm:table-cell">#${r.placing ?? "—"}</td>`;
tb.appendChild(tr);
});
}
@@ -322,7 +290,6 @@ 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);
}
@@ -359,4 +326,3 @@ $("refresh").addEventListener("click", () => load(true));
initSortHeaders();
paintCache();
load(false);
loadElo();