Files
lara-schach-live/app.js
Martin 2ad3dab7f8 feat: deployment support and production readiness
- Add deployment section to README (systemd, Docker, nginx reverse proxy)
- Add environment variable support to server.py (PORT, PGN_URL, CACHE_TTL)
- Update .gitignore for Python artifacts and lara-chess directory
- Improve board/navigation UX in app.js (userScrolledMoves tracking,
  updateClocks function, better game switching)
2026-05-24 15:12:17 +02:00

456 lines
14 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* Lara Kiesewetter Live Schachturnier
* Haupt-Application
*/
const PGN_URL = 'https://www.deutsche-schachjugend.de/2026/odjm-d/partien/gesamt-utf8.pgn';
const REFRESH_INTERVAL = 10000; // 10 Sekunden
const PLAYER_NAME = 'Kiesewetter, Lara';
let board = null;
let chess = null;
let currentGame = null;
let allLaraGames = [];
let refreshTimer = null;
let countdown = 0;
let serverLastFetch = null;
let laraColor = null;
let currentMoveIndex = -1;
let userSelectedGame = false;
let userScrolledMoves = false;
/**
* Lädt die PGN-Datei und aktualisiert die Anzeige
*/
async function loadPGN(showOverlay = true) {
if (showOverlay) showLoading(true);
hideError();
try {
const [pgnResponse, statusResponse] = await Promise.all([
fetch('http://localhost:8111/pgn'),
fetch('http://localhost:8111/status').catch(() => null)
]);
if (!pgnResponse.ok) throw new Error(`HTTP ${pgnResponse.status}`);
if (statusResponse && statusResponse.ok) {
const status = await statusResponse.json();
serverLastFetch = status.last_fetch ? status.last_fetch * 1000 : null;
}
const pgnText = await pgnResponse.text();
const allGames = parsePGN(pgnText);
allLaraGames = filterLaraGames(allGames);
if (allLaraGames.length === 0) {
showError('Keine Partien von Lara gefunden.');
return;
}
// Nur automatisch wechseln, wenn der Benutzer keine andere Partie ausgewählt hat
if (!userSelectedGame) {
const liveGame = getLiveGame(allLaraGames);
const newGame = liveGame || getLatestGame(allLaraGames);
if (newGame !== currentGame) {
userScrolledMoves = false;
}
currentGame = newGame;
}
updateBoard();
updatePlayerInfo();
updateMovesList();
updateAllGamesList();
updateTimestamp();
showLoading(false);
} catch (error) {
console.error('Fehler beim Laden:', error);
showError(`Fehler: ${error.message}`);
showLoading(false);
}
}
/**
* Aktualisiert das Schachbrett
*/
function updateBoard() {
if (!currentGame) return;
chess = new Chess();
// Spiegele das Brett, wenn Lara Schwarz hat
const laraIsBlack = currentGame.black.toLowerCase().includes('kiesewetter');
const orientation = laraIsBlack ? 'black' : 'white';
laraColor = laraIsBlack ? 'b' : 'w';
// Führe alle Züge aus
const nonResultMoves = currentGame.moves.filter(m => !m.isResult);
for (const move of nonResultMoves) {
try {
chess.move(move.san);
} catch (e) {
// Ignoriere ungültige Züge
}
}
if (!userScrolledMoves) {
currentMoveIndex = nonResultMoves.length - 1;
} else if (currentMoveIndex >= 0) {
// Benutzer hat navigiert zeige Brett an seinem ausgewählten Zug
chess = new Chess();
for (let i = 0; i <= currentMoveIndex && i < nonResultMoves.length; i++) {
try {
chess.move(nonResultMoves[i].san);
} catch (e) {
break;
}
}
}
if (board) {
board.position(chess.fen(), true);
board.orientation(orientation);
} else {
board = Chessboard('board', {
position: chess.fen(),
orientation: orientation,
pieceTheme: 'https://chessboardjs.com/img/chesspieces/wikipedia/{piece}.png',
draggable: false,
spawnMoveError: false
});
// Click auf Züge
document.getElementById('moves-list').addEventListener('click', handleMoveClick);
}
// Highlight active player
highlightActivePlayer();
}
/**
* Gehe zu einem bestimmten Zug (Index)
*/
function goToMove(index) {
if (!currentGame) return;
const nonResultMoves = currentGame.moves.filter(m => !m.isResult);
if (index < -1) index = -1;
if (index >= nonResultMoves.length) index = nonResultMoves.length - 1;
userScrolledMoves = index < nonResultMoves.length - 1;
currentMoveIndex = index;
chess = new Chess();
for (let i = 0; i <= index; i++) {
try {
chess.move(nonResultMoves[i].san);
} catch (err) {
break;
}
}
board.position(chess.fen(), true);
highlightActivePlayer();
updateClocks(index);
document.querySelectorAll('#moves-list .move').forEach(el => el.classList.remove('current'));
if (index >= 0) {
const moveEl = document.querySelector(`#moves-list [data-index="${index}"]`);
if (moveEl) moveEl.classList.add('current');
}
}
/**
* Aktualisiert die Spielerinformationen
*/
function updatePlayerInfo() {
if (!currentGame) return;
const laraIsWhite = currentGame.white.toLowerCase().includes('kiesewetter');
const laraPlayer = laraIsWhite ? currentGame.white : currentGame.black;
const laraElo = laraIsWhite ? currentGame.whiteElo : currentGame.blackElo;
const laraEmoji = laraIsWhite ? '⬜' : '⬛';
const oppPlayer = laraIsWhite ? currentGame.black : currentGame.white;
const oppElo = laraIsWhite ? currentGame.blackElo : currentGame.whiteElo;
const oppEmoji = laraIsWhite ? '⬛' : '⬜';
// Top-Panel (player-black) = Gegner
document.getElementById('black-name').textContent = oppPlayer;
document.getElementById('black-elo').textContent = oppElo ? `(ELO: ${oppElo})` : '';
document.querySelector('#player-black .player-avatar').textContent = oppEmoji;
// Bottom-Panel (player-white) = Lara
document.getElementById('white-name').textContent = laraPlayer;
document.getElementById('white-elo').textContent = laraElo ? `(ELO: ${laraElo})` : '';
document.querySelector('#player-white .player-avatar').textContent = laraEmoji;
updateClocks(currentMoveIndex);
// Round info
const roundInfo = `Runde ${currentGame.round} ${currentGame.event || 'Turnier'}`;
document.getElementById('round-info').textContent = roundInfo;
// Result info
const resultEl = document.getElementById('result-info');
if (currentGame.isLive) {
resultEl.innerHTML = '<span style="color: #4ade80;">● Laufen</span>';
} else {
resultEl.textContent = `Ergebnis: ${currentGame.result}`;
}
}
/**
* Aktualisiert die Uhrenanzeige basierend auf dem aktuellen Zugindex
*/
function updateClocks(moveIndex) {
if (!currentGame) return;
const laraIsWhite = currentGame.white.toLowerCase().includes('kiesewetter');
let whiteClock, blackClock;
if (moveIndex < 0) {
whiteClock = currentGame.whiteClock;
blackClock = currentGame.blackClock;
} else {
const nonResultMoves = currentGame.moves.filter(m => !m.isResult);
if (moveIndex < nonResultMoves.length) {
whiteClock = nonResultMoves[moveIndex].whiteClock;
blackClock = nonResultMoves[moveIndex].blackClock;
}
}
const laraClock = laraIsWhite ? whiteClock : blackClock;
const oppClock = laraIsWhite ? blackClock : whiteClock;
document.getElementById('black-clock').textContent = formatClock(oppClock);
document.getElementById('white-clock').textContent = formatClock(laraClock);
}
/**
* Highlight den Spieler, der gerade am Zug ist
*/
function highlightActivePlayer() {
if (!chess || !currentGame) return;
const laraPanel = document.getElementById('player-white');
const oppPanel = document.getElementById('player-black');
laraPanel.classList.remove('active');
oppPanel.classList.remove('active');
const laraIsWhite = currentGame.white.toLowerCase().includes('kiesewetter');
const isLaraTurn = (chess.turn() === 'w' && laraIsWhite) || (chess.turn() === 'b' && !laraIsWhite);
if (isLaraTurn) {
laraPanel.classList.add('active');
} else {
oppPanel.classList.add('active');
}
}
/**
* Aktualisiert die Zugliste
*/
function updateMovesList() {
if (!currentGame) return;
const movesList = document.getElementById('moves-list');
movesList.innerHTML = '';
const nonResultMoves = currentGame.moves.filter(m => !m.isResult);
const laraIsWhite = currentGame.white.toLowerCase().includes('kiesewetter');
for (let i = 0; i < nonResultMoves.length; i += 2) {
const moveNumber = Math.floor(i / 2) + 1;
const numSpan = document.createElement('span');
numSpan.className = 'move-number';
numSpan.textContent = `${moveNumber}.`;
movesList.appendChild(numSpan);
if (laraIsWhite) {
// Lara (Weiß) zuerst
const laraMove = document.createElement('span');
laraMove.className = 'move lara-move';
laraMove.textContent = nonResultMoves[i].san;
laraMove.dataset.index = i;
movesList.appendChild(laraMove);
if (i + 1 < nonResultMoves.length) {
const oppMove = document.createElement('span');
oppMove.className = 'move opp-move';
oppMove.textContent = nonResultMoves[i + 1].san;
oppMove.dataset.index = i + 1;
movesList.appendChild(oppMove);
}
} else {
// Lara (Schwarz) Gegner zuerst, dann Lara
const oppMove = document.createElement('span');
oppMove.className = 'move opp-move';
oppMove.textContent = nonResultMoves[i].san;
oppMove.dataset.index = i;
movesList.appendChild(oppMove);
if (i + 1 < nonResultMoves.length) {
const laraMove = document.createElement('span');
laraMove.className = 'move lara-move';
laraMove.textContent = nonResultMoves[i + 1].san;
laraMove.dataset.index = i + 1;
movesList.appendChild(laraMove);
}
}
}
// Mark current move
if (currentMoveIndex >= 0) {
const curMove = movesList.querySelector(`[data-index="${currentMoveIndex}"]`);
if (curMove) curMove.classList.add('current');
}
// Scroll to bottom nur wenn Benutzer nicht manuell navigiert hat
if (!userScrolledMoves) {
movesList.scrollTop = movesList.scrollHeight;
}
}
/**
* Klick auf einen Zug in der Liste
*/
function handleMoveClick(e) {
if (!e.target.classList.contains('move') || !currentGame) return;
goToMove(parseInt(e.target.dataset.index));
}
/**
* Aktualisiert die Liste aller Partien
*/
function updateAllGamesList() {
const list = document.getElementById('all-games-list');
list.innerHTML = '';
// Sort by round
const sorted = [...allLaraGames].sort((a, b) => {
return (parseInt(a.round) || 0) - (parseInt(b.round) || 0);
});
for (const game of sorted) {
const entry = document.createElement('div');
entry.className = 'game-entry';
if (currentGame && game === currentGame) entry.classList.add('active');
const laraIsWhite = game.white.toLowerCase().includes('kiesewetter');
const opponent = laraIsWhite ? game.black : game.white;
const color = laraIsWhite ? '⬜ Weiß' : '⬛ Schwarz';
entry.innerHTML = `
<div class="game-round">Runde ${game.round}</div>
<div class="game-players">Lara ${color} vs ${opponent}</div>
<div class="game-result">${game.isLive ? '● Laufen' : game.result}</div>
`;
entry.addEventListener('click', () => {
userSelectedGame = true;
userScrolledMoves = false;
currentGame = game;
updateBoard();
updatePlayerInfo();
updateMovesList();
updateAllGamesList();
});
list.appendChild(entry);
}
}
/**
* Format clock string
*/
function formatClock(clockStr) {
if (!clockStr) return '--:--:--';
// Format is HH:MM:SS
return clockStr;
}
/**
* Update timestamp
*/
function updateTimestamp() {
const time = serverLastFetch ? new Date(serverLastFetch) : new Date();
document.getElementById('last-update').textContent =
`Letztes Update: ${time.toLocaleTimeString('de-DE')}`;
}
/**
* Start auto-refresh
*/
function startAutoRefresh() {
countdown = REFRESH_INTERVAL / 1000;
if (refreshTimer) clearInterval(refreshTimer);
refreshTimer = setInterval(() => {
countdown--;
const mins = Math.floor(countdown / 60);
const secs = countdown % 60;
document.getElementById('refresh-timer').textContent =
`Nächstes Update in: ${String(mins).padStart(2, '0')}:${String(secs).padStart(2, '0')}`;
if (countdown <= 0) {
countdown = REFRESH_INTERVAL / 1000;
loadPGN(false);
}
}, 1000);
}
/**
* UI Helpers
*/
function showLoading(show) {
document.getElementById('loading-overlay').style.display = show ? 'flex' : 'none';
}
function showError(msg) {
document.getElementById('error-message').textContent = msg;
document.getElementById('error-overlay').style.display = 'flex';
}
function hideError() {
document.getElementById('error-overlay').style.display = 'none';
}
/**
* Manual refresh button
*/
document.getElementById('refresh-btn').addEventListener('click', () => {
countdown = REFRESH_INTERVAL / 1000;
loadPGN();
});
/**
* Pfeiltasten-Navigation
*/
document.addEventListener('keydown', (e) => {
if (!currentGame) return;
const nonResultMoves = currentGame.moves.filter(m => !m.isResult);
if (e.key === 'ArrowLeft') {
e.preventDefault();
goToMove(currentMoveIndex - 1);
} else if (e.key === 'ArrowRight') {
e.preventDefault();
goToMove(currentMoveIndex + 1);
}
});
/**
* Init
*/
loadPGN();
startAutoRefresh();