Remove proxy - load data directly from DSJ

This commit is contained in:
2026-05-24 17:37:22 +02:00
parent 3944c7d5bb
commit 20a59166a8
2 changed files with 127 additions and 324 deletions

81
app.js
View File

@@ -19,7 +19,7 @@ let roundPgns = {};
let pollId = 0; let pollId = 0;
async function fetchRoundPGN(round) { 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; if (!res.ok) return null;
return await res.text(); return await res.text();
} }
@@ -45,8 +45,6 @@ async function loadPGN(showOverlay = true) {
} }
} }
const statusResponse = await fetch('http://localhost:8111/status').catch(() => null);
// Aktuelle Runde immer frisch holen // Aktuelle Runde immer frisch holen
const pgnText = await fetchRoundPGN(currentRound); const pgnText = await fetchRoundPGN(currentRound);
if (currentPollId !== pollId) return; if (currentPollId !== pollId) return;
@@ -59,11 +57,6 @@ async function loadPGN(showOverlay = true) {
if (text) roundPgns[nextRound] = text; 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 // Alle PGNs kombinieren
const combinedPgn = Object.values(roundPgns).join('\n\n'); const combinedPgn = Object.values(roundPgns).join('\n\n');
const allGames = parsePGN(combinedPgn); const allGames = parsePGN(combinedPgn);
@@ -137,6 +130,7 @@ function updateBoard() {
if (board) { if (board) {
board.position(chess.fen(), true); board.position(chess.fen(), true);
board.orientation(orientation); board.orientation(orientation);
highlightLastMove();
} else { } else {
board = Chessboard('board', { board = Chessboard('board', {
position: chess.fen(), position: chess.fen(),
@@ -179,6 +173,7 @@ function goToMove(index) {
board.position(chess.fen(), true); board.position(chess.fen(), true);
highlightActivePlayer(); highlightActivePlayer();
highlightLastMove();
updateClocks(index); updateClocks(index);
document.querySelectorAll('#moves-list .move').forEach(el => el.classList.remove('current')); 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 * Aktualisiert die Zugliste
*/ */
@@ -394,14 +411,40 @@ 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() { async function updateStandings() {
try { 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'); if (!res.ok) throw new Error('Fehler beim Laden');
const data = await res.json(); const html = await res.text();
if (data && data.round) currentRound = data.round;
const roundMatch = html.match(/Tabellenstand\s+nach\s+der\s+(\d+)\.\s*Runde/);
if (roundMatch) {
currentRound = parseInt(roundMatch[1]);
}
const rows = html.matchAll(/<tr[^>]*>(.*?)<\/tr>/gs);
for (const row of rows) {
if (!row[1].includes('Lara Kiesewetter')) continue;
const cells = row[1].matchAll(/<td[^>]*>(.*?)<\/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'); const container = document.getElementById('standings-content');
if (!data || data.error) { if (!data || data.error) {
container.innerHTML = '<div class="standings-loading">Daten nicht verfügbar</div>'; container.innerHTML = '<div class="standings-loading">Daten nicht verfügbar</div>';
@@ -428,9 +471,13 @@ async function updateStandings() {
<span class="standings-value">${data.losses}</span> <span class="standings-value">${data.losses}</span>
</div> </div>
`; `;
serverLastFetch = Date.now();
return;
}
}
document.getElementById('standings-content').innerHTML = '<div class="standings-loading">Daten nicht verfügbar</div>';
} catch (err) { } catch (err) {
document.getElementById('standings-content').innerHTML = document.getElementById('standings-content').innerHTML = '<div class="standings-loading">Daten nicht verfügbar</div>';
'<div class="standings-loading">Daten nicht verfügbar</div>';
} }
} }
@@ -447,7 +494,7 @@ function formatClock(clockStr) {
* Update timestamp * Update timestamp
*/ */
function updateTimestamp() { function updateTimestamp() {
const time = serverLastFetch ? new Date(serverLastFetch) : new Date(); const time = new Date();
document.getElementById('last-update').textContent = document.getElementById('last-update').textContent =
`Letztes Update: ${time.toLocaleTimeString('de-DE')}`; `Letztes Update: ${time.toLocaleTimeString('de-DE')}`;
} }
@@ -473,7 +520,9 @@ function startAutoRefresh() {
timer = setInterval(() => { timer = setInterval(() => {
if (pollId !== myId) { clearInterval(pollInterval); clearInterval(timer); return; } 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`; document.getElementById('refresh-timer').textContent = `${s}s`;
}, 1000); }, 1000);
} }

258
server.py
View File

@@ -1,228 +1,22 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
""" """
Lokal Proxy-Server für Lara's Schachturnier Lokaler Server für Lara Schachturnier
Lädt die PGN-Datei und stellt sie mit CORS-Headern bereit. Serviert statische Dateien direkt.
""" """
import http.server import http.server
import socketserver import socketserver
import urllib.request
import urllib.parse
import urllib.error
import sys
import os 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)) 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) class StaticHandler(http.server.BaseHTTPRequestHandler):
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"<tr[^>]*>(.*?)</tr>", html, re.DOTALL | re.IGNORECASE)
for row in rows:
if "Lara Kiesewetter" not in row:
continue
cells = re.findall(r"<td[^>]*>(.*?)</td>", 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):
def do_GET(self): def do_GET(self):
parsed_path = urllib.parse.urlparse(self.path)
parts = parsed_path.path.strip("/").split("/")
if len(parts) == 2 and parts[0] == "pgn" and parts[1].isdigit():
round_num = int(parts[1])
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()
}
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())
else:
# Statische Dateien aus dem Verzeichnis
if self.path == "/": if self.path == "/":
self.path = "/index.html" self.path = "/index.html"
filepath = os.path.join(os.path.dirname(os.path.abspath(__file__)), self.path.lstrip("/")) filepath = os.path.join(os.path.dirname(os.path.abspath(__file__)), self.path.lstrip("/"))
if os.path.isfile(filepath): if os.path.isfile(filepath):
content_types = { content_types = {
".html": "text/html", ".html": "text/html",
@@ -249,26 +43,9 @@ class PGNHandler(http.server.BaseHTTPRequestHandler):
self.end_headers() self.end_headers()
def log_message(self, format, *args): def log_message(self, format, *args):
print(f"[{datetime.now().strftime('%H:%M:%S')}] {args[0]}") print(f"[{self.log_date_time_string()}] {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}")
def main(): def main():
global current_round
print("=" * 50) print("=" * 50)
print(" [TROPHY] Lara Kiesewetter Live Schachturnier") print(" [TROPHY] Lara Kiesewetter Live Schachturnier")
print("=" * 50) print("=" * 50)
@@ -276,35 +53,12 @@ def main():
print(f" Drücke Ctrl+C zum Beenden") print(f" Drücke Ctrl+C zum Beenden")
print("=" * 50) print("=" * 50)
# Runde aus Tabelle ermitteln with socketserver.TCPServer(("", PORT), StaticHandler) as httpd:
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:
print(f"\n[SERVER] Server gestartet: http://localhost:{PORT}\n") print(f"\n[SERVER] Server gestartet: http://localhost:{PORT}\n")
try: try:
httpd.serve_forever() httpd.serve_forever()
except KeyboardInterrupt: except KeyboardInterrupt:
print("\n[BYE] Server gestoppt.") print("\n[BYE] Server gestoppt.")
if __name__ == "__main__": if __name__ == "__main__":
main() main()