From 20a59166a84246379dd4aeef1b4cc06eceb2b704 Mon Sep 17 00:00:00 2001 From: Martin Date: Sun, 24 May 2026 17:37:22 +0200 Subject: [PATCH] Remove proxy - load data directly from DSJ --- app.js | 141 +++++++++++++++++-------- server.py | 310 ++++++------------------------------------------------ 2 files changed, 127 insertions(+), 324 deletions(-) diff --git a/app.js b/app.js index 1348b88..57b6667 100644 --- a/app.js +++ b/app.js @@ -19,7 +19,7 @@ let roundPgns = {}; let pollId = 0; async function fetchRoundPGN(round) { - const res = await fetch(`http://localhost:8111/pgn/${round}`); + const res = await fetch(`https://www.deutsche-schachjugend.de/2026/odjm-d/partien/${round}.pgn`); if (!res.ok) return null; return await res.text(); } @@ -28,7 +28,7 @@ async function loadPGN(showOverlay = true) { const currentPollId = pollId; if (showOverlay) showLoading(true); hideError(); - + try { await updateStandings(); @@ -45,8 +45,6 @@ async function loadPGN(showOverlay = true) { } } - const statusResponse = await fetch('http://localhost:8111/status').catch(() => null); - // Aktuelle Runde immer frisch holen const pgnText = await fetchRoundPGN(currentRound); if (currentPollId !== pollId) return; @@ -59,21 +57,16 @@ async function loadPGN(showOverlay = true) { if (text) roundPgns[nextRound] = text; } - if (statusResponse && statusResponse.ok) { - const status = await statusResponse.json(); - serverLastFetch = status.last_fetch ? status.last_fetch * 1000 : null; - } - // Alle PGNs kombinieren const combinedPgn = Object.values(roundPgns).join('\n\n'); const allGames = parsePGN(combinedPgn); allLaraGames = filterLaraGames(allGames); - + if (allLaraGames.length === 0) { showError('Keine Partien von Lara gefunden.'); return; } - + if (!userSelectedGame) { const liveGame = getLiveGame(allLaraGames); const newGame = liveGame || getLatestGame(allLaraGames); @@ -89,7 +82,7 @@ async function loadPGN(showOverlay = true) { updateTimestamp(); showLoading(false); - + } catch (error) { if (currentPollId !== pollId) return; console.error('Fehler beim Laden:', error); @@ -137,6 +130,7 @@ function updateBoard() { if (board) { board.position(chess.fen(), true); board.orientation(orientation); + highlightLastMove(); } else { board = Chessboard('board', { position: chess.fen(), @@ -179,6 +173,7 @@ function goToMove(index) { board.position(chess.fen(), true); highlightActivePlayer(); + highlightLastMove(); updateClocks(index); document.querySelectorAll('#moves-list .move').forEach(el => el.classList.remove('current')); @@ -279,6 +274,28 @@ function highlightActivePlayer() { } } +/** + * Highlight den letzten Zug auf dem Brett + */ +function highlightLastMove() { + if (!board || !chess) return; + + const nonResultMoves = currentGame.moves.filter(m => !m.isResult); + + if (currentMoveIndex >= 0 && currentMoveIndex < nonResultMoves.length) { + const lastMove = nonResultMoves[currentMoveIndex]; + + // Parse die SAN-Züge, um Start- und Zielfelder zu finden + const moves = chess.history({ verbose: true }); + if (moves.length > 0) { + const lastMoveData = moves[moves.length - 1]; + board.highlightSquare(lastMoveData.from, lastMoveData.to); + } + } else { + board.clearHighlights(); + } +} + /** * Aktualisiert die Zugliste */ @@ -394,43 +411,73 @@ function updateAllGamesList() { } /** - * Lädt die Turniertabelle vom Proxy und zeigt Laras Platzierung an + * Lädt die Turniertabelle vom DSJ und zeigt Laras Platzierung an */ async function updateStandings() { try { - const res = await fetch('http://localhost:8111/standings'); + const res = await fetch('https://www.deutsche-schachjugend.de/2026/odjm-d/tabelle/'); if (!res.ok) throw new Error('Fehler beim Laden'); - const data = await res.json(); - if (data && data.round) currentRound = data.round; - const container = document.getElementById('standings-content'); - if (!data || data.error) { - container.innerHTML = '
Daten nicht verfügbar
'; - return; + const html = await res.text(); + + const roundMatch = html.match(/Tabellenstand\s+nach\s+der\s+(\d+)\.\s*Runde/); + if (roundMatch) { + currentRound = parseInt(roundMatch[1]); } - container.innerHTML = ` -
${data.rank}.
-
Tabellenplatz
-
${data.round_info || 'nach Runde 1'}
-
- Punkte - ${data.points} -
-
- Siege - ${data.wins} -
-
- Unentschieden - ${data.draws} -
-
- Niederlagen - ${data.losses} -
- `; + + const rows = html.matchAll(/]*>(.*?)<\/tr>/gs); + for (const row of rows) { + if (!row[1].includes('Lara Kiesewetter')) continue; + + const cells = row[1].matchAll(/]*>(.*?)<\/td>/gs); + const clean = []; + for (const cell of cells) { + clean.push(cell[1].replace(/<[^>]+>/g, '').trim()); + } + + if (clean.length >= 9) { + const data = { + rank: clean[0], + player: 'Lara Kiesewetter', + wins: clean[5], + draws: clean[6], + losses: clean[7], + points: clean[8], + round_info: roundMatch ? `nach der ${roundMatch[1]}. Runde` : '', + round: currentRound, + }; + const container = document.getElementById('standings-content'); + if (!data || data.error) { + container.innerHTML = '
Daten nicht verfügbar
'; + return; + } + container.innerHTML = ` +
${data.rank}.
+
Tabellenplatz
+
${data.round_info || 'nach Runde 1'}
+
+ Punkte + ${data.points} +
+
+ Siege + ${data.wins} +
+
+ Unentschieden + ${data.draws} +
+
+ Niederlagen + ${data.losses} +
+ `; + serverLastFetch = Date.now(); + return; + } + } + document.getElementById('standings-content').innerHTML = '
Daten nicht verfügbar
'; } catch (err) { - document.getElementById('standings-content').innerHTML = - '
Daten nicht verfügbar
'; + document.getElementById('standings-content').innerHTML = '
Daten nicht verfügbar
'; } } @@ -447,8 +494,8 @@ function formatClock(clockStr) { * Update timestamp */ function updateTimestamp() { - const time = serverLastFetch ? new Date(serverLastFetch) : new Date(); - document.getElementById('last-update').textContent = + const time = new Date(); + document.getElementById('last-update').textContent = `Letztes Update: ${time.toLocaleTimeString('de-DE')}`; } @@ -473,7 +520,9 @@ function startAutoRefresh() { timer = setInterval(() => { if (pollId !== myId) { clearInterval(pollInterval); clearInterval(timer); return; } - const s = Math.floor((Date.now() - lastUpdate) / 1000); + const elapsed = Date.now() - lastUpdate; + const remaining = Math.max(0, 30000 - elapsed); + const s = Math.floor(remaining / 1000); document.getElementById('refresh-timer').textContent = `${s}s`; }, 1000); } diff --git a/server.py b/server.py index 526c5a2..2613e89 100644 --- a/server.py +++ b/server.py @@ -1,310 +1,64 @@ #!/usr/bin/env python3 """ -Lokal Proxy-Server für Lara's Schachturnier -Lädt die PGN-Datei und stellt sie mit CORS-Headern bereit. +Lokaler Server für Lara Schachturnier +Serviert statische Dateien direkt. """ import http.server import socketserver -import urllib.request -import urllib.parse -import urllib.error -import sys import os -import threading -import time -import json -import re -from datetime import datetime -STANDINGS_URL = "https://www.deutsche-schachjugend.de/2026/odjm-d/tabelle/" PORT = int(os.environ.get("PORT", 8111)) -CACHE_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "cache") -STANDINGS_CACHE_FILE = os.path.join(CACHE_DIR, "standings.json") -CACHE_TTL = int(os.environ.get("CACHE_TTL", 31)) -os.makedirs(CACHE_DIR, exist_ok=True) - -last_standings_fetch_time = None -current_round = 0 # wird aus der Tabelle ermittelt - - -def get_pgn_url(round_num=None): - if round_num is None: - round_num = current_round - return f"https://www.deutsche-schachjugend.de/2026/odjm-d/partien/{round_num}.pgn" - - -def get_cache_file(round_num=None): - if round_num is None: - round_num = current_round - return os.path.join(CACHE_DIR, f"runde-{round_num}.pgn") - - -def fetch_pgn(round_num=None): - """Lädt die PGN-Datei von der URL als Bytes. - Gibt (data, True) bei Erfolg oder (None, False) bei Fehler zurück.""" - if round_num is None: - round_num = current_round - if round_num == 0: - return None, False - url = get_pgn_url(round_num) - try: - headers = {"User-Agent": "Mozilla/5.0"} - req = urllib.request.Request(url, headers=headers) - with urllib.request.urlopen(req, timeout=31) as response: - data = response.read() - return data, True - except urllib.error.HTTPError as e: - print(f"[{datetime.now().strftime('%H:%M:%S')}] HTTP-Fehler (Runde {round_num}): {e}") - return None, False - except Exception as e: - print(f"[{datetime.now().strftime('%H:%M:%S')}] Fehler beim Laden (Runde {round_num}): {e}") - return None, False - - -def get_round_pgn(round_num): - """Gibt PGN-Daten für eine Runde zurück. - Bei der aktuellen Runde wird immer live von der DSJ geholt.""" - cache_file = get_cache_file(round_num) - - if round_num == current_round: - content, changed = fetch_pgn(round_num) - if changed and content is not None: - with open(cache_file, "wb") as f: - f.write(content) - return content, os.path.getmtime(cache_file) - if os.path.exists(cache_file): - with open(cache_file, "rb") as f: - return f.read(), os.path.getmtime(cache_file) - return None, 0 - - if os.path.exists(cache_file): - with open(cache_file, "rb") as f: - return f.read(), os.path.getmtime(cache_file) - - content, changed = fetch_pgn(round_num) - if changed and content is not None: - with open(cache_file, "wb") as f: - f.write(content) - return content, os.path.getmtime(cache_file) - return None, 0 - - -def fetch_standings(): - """Lädt die Tabellenseite und parst Laras Platzierung. - Ermittelt dabei auch die aktuelle Runde.""" - global last_standings_fetch_time, current_round - try: - headers = {"User-Agent": "Mozilla/5.0"} - if last_standings_fetch_time: - headers["modified_since"] = str(last_standings_fetch_time) - req = urllib.request.Request(STANDINGS_URL, headers=headers) - with urllib.request.urlopen(req, timeout=31) as response: - html = response.read().decode("utf-8", errors="replace") - last_standings_fetch_time = time.time() - - round_info = "" - m = re.search(r"Tabellenstand\s+nach\s+der\s+(\d+)\.\s*Runde", html) - if m: - detected = int(m.group(1)) - if detected != current_round: - old = current_round - current_round = detected - print(f"[{datetime.now().strftime('%H:%M:%S')}] Runde erkannt: {current_round} (war {old})") - round_info = f"nach der {m.group(1)}. Runde" - - rows = re.findall(r"]*>(.*?)", html, re.DOTALL | re.IGNORECASE) - for row in rows: - if "Lara Kiesewetter" not in row: - continue - cells = re.findall(r"]*>(.*?)", row, re.DOTALL) - clean = [re.sub(r"<[^>]+>", "", c).strip() for c in cells] - if len(clean) >= 9: - return { - "rank": clean[0], - "player": "Lara Kiesewetter", - "wins": clean[5], - "draws": clean[6], - "losses": clean[7], - "points": clean[8], - "round_info": round_info, - "round": current_round, - } - return None - except Exception as e: - print(f"Fehler beim Laden der Tabelle: {e}") - return None - - -def get_standings_content(): - """Gibt Tabellenstand als Dict zurück, nutzt Cache.""" - now = time.time() - - if os.path.exists(STANDINGS_CACHE_FILE): - age = now - os.path.getmtime(STANDINGS_CACHE_FILE) - if age < 300: # 5 Minuten Cache - with open(STANDINGS_CACHE_FILE, "r") as f: - return json.load(f) - - data = fetch_standings() - if data: - with open(STANDINGS_CACHE_FILE, "w") as f: - json.dump(data, f) - print(f"[{datetime.now().strftime('%H:%M:%S')}] Tabellenstand aktualisiert") - return data - return None - - -class PGNHandler(http.server.BaseHTTPRequestHandler): +class StaticHandler(http.server.BaseHTTPRequestHandler): def do_GET(self): - parsed_path = urllib.parse.urlparse(self.path) + if self.path == "/": + self.path = "/index.html" - parts = parsed_path.path.strip("/").split("/") - if len(parts) == 2 and parts[0] == "pgn" and parts[1].isdigit(): - round_num = int(parts[1]) + filepath = os.path.join(os.path.dirname(os.path.abspath(__file__)), self.path.lstrip("/")) - if round_num <= 0 or round_num > current_round + 1: - self.send_response(404) - self.send_header("Content-Type", "application/json") - self.send_header("Access-Control-Allow-Origin", "*") - self.end_headers() - self.wfile.write(json.dumps({"error": "Runde nicht verf\u00fcgbar"}).encode()) - return - - content, mtime = get_round_pgn(round_num) - - if content: - self.send_response(200) - self.send_header("Content-Type", "text/plain; charset=utf-8") - self.send_header("Access-Control-Allow-Origin", "*") - self.send_header("Cache-Control", "no-cache") - self.end_headers() - self.wfile.write(content) - else: - self.send_response(404) - self.send_header("Content-Type", "application/json") - self.send_header("Access-Control-Allow-Origin", "*") - self.end_headers() - self.wfile.write(json.dumps({"error": "Konnte PGN nicht laden"}).encode()) - - elif parsed_path.path == "/status": - self.send_response(200) - self.send_header("Content-Type", "application/json") - self.send_header("Access-Control-Allow-Origin", "*") - self.send_header("Cache-Control", "no-cache") - self.end_headers() - status = { - "status": "ok", - "server_time": time.time() + if os.path.isfile(filepath): + content_types = { + ".html": "text/html", + ".css": "text/css", + ".js": "application/javascript", + ".json": "application/json", + ".png": "image/png", + ".jpg": "image/jpeg", + ".svg": "image/svg+xml", } - self.wfile.write(json.dumps(status).encode()) - - elif parsed_path.path == "/standings": - data = get_standings_content() - if data: - self.send_response(200) - self.send_header("Content-Type", "application/json") - self.send_header("Access-Control-Allow-Origin", "*") - self.send_header("Cache-Control", "max-age=300") - self.end_headers() - self.wfile.write(json.dumps(data).encode()) - else: - self.send_response(502) - self.send_header("Content-Type", "application/json") - self.send_header("Access-Control-Allow-Origin", "*") - self.end_headers() - err = {"error": "Tabellendaten nicht verf\u00fcgbar"} - self.wfile.write(json.dumps(err).encode()) - + ext = os.path.splitext(filepath)[1] + content_type = content_types.get(ext, "application/octet-stream") + + with open(filepath, "rb") as f: + content = f.read() + + self.send_response(200) + self.send_header("Content-Type", f"{content_type}; charset=utf-8") + self.send_header("Access-Control-Allow-Origin", "*") + self.end_headers() + self.wfile.write(content) else: - # Statische Dateien aus dem Verzeichnis - if self.path == "/": - self.path = "/index.html" - - filepath = os.path.join(os.path.dirname(os.path.abspath(__file__)), self.path.lstrip("/")) - if os.path.isfile(filepath): - content_types = { - ".html": "text/html", - ".css": "text/css", - ".js": "application/javascript", - ".json": "application/json", - ".png": "image/png", - ".jpg": "image/jpeg", - ".svg": "image/svg+xml", - } - ext = os.path.splitext(filepath)[1] - content_type = content_types.get(ext, "application/octet-stream") - - with open(filepath, "rb") as f: - content = f.read() - - self.send_response(200) - self.send_header("Content-Type", f"{content_type}; charset=utf-8") - self.send_header("Access-Control-Allow-Origin", "*") - self.end_headers() - self.wfile.write(content) - else: - self.send_response(404) - self.end_headers() - + self.send_response(404) + self.end_headers() + def log_message(self, format, *args): - print(f"[{datetime.now().strftime('%H:%M:%S')}] {args[0]}") - - -def background_refresh(): - """Aktualisiert den Cache im Hintergrund.""" - while True: - time.sleep(CACHE_TTL) - try: - content, changed = fetch_pgn() - if changed and content is not None: - cache_file = get_cache_file() - with open(cache_file, "wb") as f: - f.write(content) - print(f"[{datetime.now().strftime('%H:%M:%S')}] Hintergrund-Refresh: PGN aktualisiert ({len(content)} Bytes)") - except Exception as e: - print(f"[{datetime.now().strftime('%H:%M:%S')}] Hintergrund-Refresh Fehler: {e}") - + print(f"[{self.log_date_time_string()}] {args[0]}") def main(): - global current_round print("=" * 50) print(" [TROPHY] Lara Kiesewetter – Live Schachturnier") print("=" * 50) print(f" Server läuft auf: http://localhost:{PORT}") print(f" Drücke Ctrl+C zum Beenden") print("=" * 50) - - # Runde aus Tabelle ermitteln - standings = fetch_standings() - if standings and current_round > 0: - print(f"[OK] Aktuelle Runde: {current_round}") - else: - current_round = 1 - print("[INFO] Starte mit Runde 1 (Tabelle noch nicht verfügbar)") - content, changed = fetch_pgn() - if changed and content is not None: - cache_file = get_cache_file() - with open(cache_file, "wb") as f: - f.write(content) - print(f"[OK] PGN geladen ({len(content)} Bytes)") - else: - print("[WARN] Initialer Ladeversuch fehlgeschlagen, wird wiederholt...") - - # Hintergrund-Refresh Thread starten - refresh_thread = threading.Thread(target=background_refresh, daemon=True) - refresh_thread.start() - - # Server starten - with socketserver.TCPServer(("", PORT), PGNHandler) as httpd: + with socketserver.TCPServer(("", PORT), StaticHandler) as httpd: print(f"\n[SERVER] Server gestartet: http://localhost:{PORT}\n") try: httpd.serve_forever() except KeyboardInterrupt: print("\n[BYE] Server gestoppt.") - if __name__ == "__main__": - main() + main() \ No newline at end of file