Fix caching issue - load fresh PGN for current round

This commit is contained in:
2026-05-24 16:57:16 +02:00
parent 812cd3b24f
commit 3944c7d5bb
3 changed files with 111 additions and 81 deletions

View File

@@ -1,4 +1,5 @@
# Agenten-Regeln # Agenten-Regeln
- Nach jeder Änderung am Proxy (server.py) oder an den Requests (app.js) muss der Proxy neu gestartet werden. - Nach jeder Änderung am Proxy (server.py) oder an den Requests (app.js) muss der Proxy neu gestartet werden: erst alten Prozess killen (`Get-Process -Name python | Stop-Process -Force`), dann unsichtbar starten.
- Commits und Push erfolgen nur durch expliziten Befehl des Nutzers. - Commits und Push erfolgen nur durch expliziten Befehl des Nutzers.
- Der Server (server.py) wird immer unsichtbar gestartet: `Start-Process -NoNewWindow -FilePath "python" -ArgumentList "server.py"`

83
app.js
View File

@@ -14,44 +14,59 @@ let laraColor = null;
let currentMoveIndex = -1; let currentMoveIndex = -1;
let userSelectedGame = false; let userSelectedGame = false;
let userScrolledMoves = false; let userScrolledMoves = false;
let lastMtime = 0;
let currentRound = 0; let currentRound = 0;
let roundPgns = {};
let pollId = 0; let pollId = 0;
/** async function fetchRoundPGN(round) {
* Lädt die PGN-Datei und aktualisiert die Anzeige const res = await fetch(`http://localhost:8111/pgn/${round}`);
*/ if (!res.ok) return null;
return await res.text();
}
async function loadPGN(showOverlay = true) { async function loadPGN(showOverlay = true) {
const currentPollId = ++pollId; const currentPollId = pollId;
if (showOverlay) showLoading(true); if (showOverlay) showLoading(true);
hideError(); hideError();
try { try {
// Zuerst Tabelle abrufen, um die aktuelle Runde zu ermitteln
await updateStandings(); await updateStandings();
if (currentRound === 0) { if (currentRound === 0) {
if (showOverlay) showLoading(false); if (showOverlay) showLoading(false);
return; return;
} }
const [pgnResponse, statusResponse] = await Promise.all([
fetch(`http://localhost:8111/pgn/${currentRound}?since=${lastMtime}`), // Fehlende vergangene Runden einmalig nachladen
fetch('http://localhost:8111/status').catch(() => null) for (let r = 1; r < currentRound; r++) {
]); if (roundPgns[r] === undefined) {
const text = await fetchRoundPGN(r);
if (text !== null) roundPgns[r] = text;
}
}
const statusResponse = await fetch('http://localhost:8111/status').catch(() => null);
// Aktuelle Runde immer frisch holen
const pgnText = await fetchRoundPGN(currentRound);
if (currentPollId !== pollId) return; if (currentPollId !== pollId) return;
if (!pgnResponse.ok) throw new Error(`HTTP ${pgnResponse.status}`); if (pgnText !== null) roundPgns[currentRound] = pgnText;
const mtimeHeader = pgnResponse.headers.get('X-Cache-Mtime'); // Nächste Runde prüfen (sobald verfügbar, einmalig holen)
if (mtimeHeader) lastMtime = parseFloat(mtimeHeader); const nextRound = currentRound + 1;
if (roundPgns[nextRound] === undefined) {
const text = await fetchRoundPGN(nextRound).catch(() => null);
if (text) roundPgns[nextRound] = text;
}
if (statusResponse && statusResponse.ok) { if (statusResponse && statusResponse.ok) {
const status = await statusResponse.json(); const status = await statusResponse.json();
serverLastFetch = status.last_fetch ? status.last_fetch * 1000 : null; serverLastFetch = status.last_fetch ? status.last_fetch * 1000 : null;
} }
const pgnText = await pgnResponse.text(); // Alle PGNs kombinieren
const allGames = parsePGN(pgnText); const combinedPgn = Object.values(roundPgns).join('\n\n');
const allGames = parsePGN(combinedPgn);
allLaraGames = filterLaraGames(allGames); allLaraGames = filterLaraGames(allGames);
if (allLaraGames.length === 0) { if (allLaraGames.length === 0) {
@@ -59,7 +74,6 @@ async function loadPGN(showOverlay = true) {
return; return;
} }
// Nur automatisch wechseln, wenn der Benutzer keine andere Partie ausgewählt hat
if (!userSelectedGame) { if (!userSelectedGame) {
const liveGame = getLiveGame(allLaraGames); const liveGame = getLiveGame(allLaraGames);
const newGame = liveGame || getLatestGame(allLaraGames); const newGame = liveGame || getLatestGame(allLaraGames);
@@ -439,18 +453,29 @@ function updateTimestamp() {
} }
/** /**
* Start long-polling: nach jeder Antwort sofort die nächste Anfrage stellen * Startet Auto-Refresh alle 30 Sekunden
*/ */
function startAutoRefresh() { function startAutoRefresh() {
document.getElementById('refresh-timer').textContent = '● Live';
document.getElementById('refresh-timer').style.color = '#4ade80';
const myId = ++pollId; const myId = ++pollId;
(async function poll() { let lastUpdate = Date.now();
while (pollId === myId) { let pollInterval, timer;
await loadPGN(false);
} document.getElementById('refresh-timer').textContent = '0s';
})(); document.getElementById('refresh-timer').style.color = '#4ade80';
loadPGN(true);
pollInterval = setInterval(() => {
if (pollId !== myId) { clearInterval(pollInterval); clearInterval(timer); return; }
loadPGN(false);
lastUpdate = Date.now();
}, 30000);
timer = setInterval(() => {
if (pollId !== myId) { clearInterval(pollInterval); clearInterval(timer); return; }
const s = Math.floor((Date.now() - lastUpdate) / 1000);
document.getElementById('refresh-timer').textContent = `${s}s`;
}, 1000);
} }
/** /**
@@ -474,7 +499,7 @@ function hideError() {
*/ */
document.getElementById('refresh-btn').addEventListener('click', () => { document.getElementById('refresh-btn').addEventListener('click', () => {
pollId++; pollId++;
loadPGN(true).then(() => startAutoRefresh()); startAutoRefresh();
}); });
/** /**

106
server.py
View File

@@ -25,73 +25,70 @@ CACHE_TTL = int(os.environ.get("CACHE_TTL", 31))
os.makedirs(CACHE_DIR, exist_ok=True) os.makedirs(CACHE_DIR, exist_ok=True)
last_fetch_time = None
last_standings_fetch_time = None last_standings_fetch_time = None
current_round = 0 # wird aus der Tabelle ermittelt current_round = 0 # wird aus der Tabelle ermittelt
def get_pgn_url(): def get_pgn_url(round_num=None):
return f"https://www.deutsche-schachjugend.de/2026/odjm-d/partien/{current_round}.pgn" 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(): def get_cache_file(round_num=None):
return os.path.join(CACHE_DIR, f"runde-{current_round}.pgn") if round_num is None:
round_num = current_round
return os.path.join(CACHE_DIR, f"runde-{round_num}.pgn")
def fetch_pgn(): def fetch_pgn(round_num=None):
"""Lädt die PGN-Datei von der URL als Bytes. """Lädt die PGN-Datei von der URL als Bytes.
Nutzt If-Modified-Since gibt (data, changed) zurück. Gibt (data, True) bei Erfolg oder (None, False) bei Fehler zurück."""
changed=False bedeutet 304 Not Modified (keine neuen Daten).""" if round_num is None:
global last_fetch_time round_num = current_round
if current_round == 0: if round_num == 0:
return None, False return None, False
url = get_pgn_url() url = get_pgn_url(round_num)
try: try:
headers = {"User-Agent": "Mozilla/5.0"} headers = {"User-Agent": "Mozilla/5.0"}
if last_fetch_time:
headers["If-Modified-Since"] = datetime.utcfromtimestamp(last_fetch_time).strftime("%a, %d %b %Y %H:%M:%S GMT")
req = urllib.request.Request(url, headers=headers) req = urllib.request.Request(url, headers=headers)
with urllib.request.urlopen(req, timeout=31) as response: with urllib.request.urlopen(req, timeout=31) as response:
data = response.read() data = response.read()
last_fetch_time = time.time()
return data, True return data, True
except urllib.error.HTTPError as e: except urllib.error.HTTPError as e:
if e.code == 304: print(f"[{datetime.now().strftime('%H:%M:%S')}] HTTP-Fehler (Runde {round_num}): {e}")
return None, False
print(f"[{datetime.now().strftime('%H:%M:%S')}] HTTP-Fehler: {e}")
return None, False return None, False
except Exception as e: except Exception as e:
print(f"[{datetime.now().strftime('%H:%M:%S')}] Fehler beim Laden: {e}") print(f"[{datetime.now().strftime('%H:%M:%S')}] Fehler beim Laden (Runde {round_num}): {e}")
return None, False return None, False
def get_pgn_content_longpoll(since=0): def get_round_pgn(round_num):
"""Long-Poll: Blockiert, bis sich die PGN-Datei tatsächlich ändert. """Gibt PGN-Daten für eine Runde zurück.
Antwortet nur bei neuen Daten, nie mit unverändertem Stand.""" Bei der aktuellen Runde wird immer live von der DSJ geholt."""
while True: cache_file = get_cache_file(round_num)
cache_file = get_cache_file()
if os.path.exists(cache_file):
mtime = os.path.getmtime(cache_file)
if mtime > since:
with open(cache_file, "rb") as f:
return f.read(), mtime
age = time.time() - mtime
if age >= CACHE_TTL:
content, changed = fetch_pgn()
if changed and content is not None:
with open(cache_file, "wb") as f:
f.write(content)
new_mtime = os.path.getmtime(cache_file)
print(f"[{datetime.now().strftime('%H:%M:%S')}] PGN via Long-Poll aktualisiert ({len(content)} Bytes)")
return content, new_mtime
else:
content, changed = fetch_pgn()
if changed and content is not None:
with open(cache_file, "wb") as f:
f.write(content)
return content, os.path.getmtime(cache_file)
time.sleep(5) 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(): def fetch_standings():
@@ -165,19 +162,27 @@ class PGNHandler(http.server.BaseHTTPRequestHandler):
parts = parsed_path.path.strip("/").split("/") parts = parsed_path.path.strip("/").split("/")
if len(parts) == 2 and parts[0] == "pgn" and parts[1].isdigit(): if len(parts) == 2 and parts[0] == "pgn" and parts[1].isdigit():
params = urllib.parse.parse_qs(parsed_path.query) round_num = int(parts[1])
since = float(params.get("since", [0])[0])
content, mtime = get_pgn_content_longpoll(since) 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: if content:
self.send_response(200) self.send_response(200)
self.send_header("Content-Type", "text/plain; charset=utf-8") self.send_header("Content-Type", "text/plain; charset=utf-8")
self.send_header("Access-Control-Allow-Origin", "*") self.send_header("Access-Control-Allow-Origin", "*")
self.send_header("Cache-Control", "no-cache") self.send_header("Cache-Control", "no-cache")
self.send_header("X-Cache-Mtime", str(mtime))
self.end_headers() self.end_headers()
self.wfile.write(content) self.wfile.write(content)
else: else:
self.send_response(502) self.send_response(404)
self.send_header("Content-Type", "application/json") self.send_header("Content-Type", "application/json")
self.send_header("Access-Control-Allow-Origin", "*") self.send_header("Access-Control-Allow-Origin", "*")
self.end_headers() self.end_headers()
@@ -191,8 +196,6 @@ class PGNHandler(http.server.BaseHTTPRequestHandler):
self.end_headers() self.end_headers()
status = { status = {
"status": "ok", "status": "ok",
"last_fetch": last_fetch_time,
"cache_ttl": CACHE_TTL,
"server_time": time.time() "server_time": time.time()
} }
self.wfile.write(json.dumps(status).encode()) self.wfile.write(json.dumps(status).encode())
@@ -259,6 +262,7 @@ def background_refresh():
cache_file = get_cache_file() cache_file = get_cache_file()
with open(cache_file, "wb") as f: with open(cache_file, "wb") as f:
f.write(content) f.write(content)
print(f"[{datetime.now().strftime('%H:%M:%S')}] Hintergrund-Refresh: PGN aktualisiert ({len(content)} Bytes)")
except Exception as e: except Exception as e:
print(f"[{datetime.now().strftime('%H:%M:%S')}] Hintergrund-Refresh Fehler: {e}") print(f"[{datetime.now().strftime('%H:%M:%S')}] Hintergrund-Refresh Fehler: {e}")