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