Fix caching issue - load fresh PGN for current round
This commit is contained in:
@@ -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"`
|
||||||
|
|||||||
75
app.js
75
app.js
@@ -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}`),
|
|
||||||
fetch('http://localhost:8111/status').catch(() => null)
|
|
||||||
]);
|
|
||||||
|
|
||||||
|
// Fehlende vergangene Runden einmalig nachladen
|
||||||
|
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';
|
const myId = ++pollId;
|
||||||
|
let lastUpdate = Date.now();
|
||||||
|
let pollInterval, timer;
|
||||||
|
|
||||||
|
document.getElementById('refresh-timer').textContent = '0s';
|
||||||
document.getElementById('refresh-timer').style.color = '#4ade80';
|
document.getElementById('refresh-timer').style.color = '#4ade80';
|
||||||
|
|
||||||
const myId = ++pollId;
|
loadPGN(true);
|
||||||
(async function poll() {
|
|
||||||
while (pollId === myId) {
|
pollInterval = setInterval(() => {
|
||||||
await loadPGN(false);
|
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
106
server.py
@@ -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}")
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user