diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index c9bdef9..439857e 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -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 diff --git a/index.html b/index.html index 3a1b269..8c3a934 100644 --- a/index.html +++ b/index.html @@ -76,7 +76,6 @@
- diff --git a/public/elo-data.json b/public/elo-data.json deleted file mode 100644 index f40bbc0..0000000 --- a/public/elo-data.json +++ /dev/null @@ -1 +0,0 @@ -{"updated":null,"count":0,"byName":{}} diff --git a/scripts/sync-elo.py b/scripts/sync-elo.py deleted file mode 100644 index 6ab4e4b..0000000 --- a/scripts/sync-elo.py +++ /dev/null @@ -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() diff --git a/src/main.js b/src/main.js index a18e63b..a8da12b 100644 --- a/src/main.js +++ b/src/main.js @@ -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) { `` + `` + - `` + - (eloData - ? (() => { const e = lookupElo(r); return ``; })() - : ``); + ``; tb.appendChild(tr); }); } @@ -322,7 +290,6 @@ function showSkeleton() { `