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)
This commit is contained in:
2026-05-24 15:12:17 +02:00
parent 0d94cac60c
commit 2ad3dab7f8
4 changed files with 139 additions and 15 deletions

72
app.js
View File

@@ -4,7 +4,7 @@
*/
const PGN_URL = 'https://www.deutsche-schachjugend.de/2026/odjm-d/partien/gesamt-utf8.pgn';
const REFRESH_INTERVAL = 60000; // 60 Sekunden
const REFRESH_INTERVAL = 10000; // 10 Sekunden
const PLAYER_NAME = 'Kiesewetter, Lara';
let board = null;
@@ -17,12 +17,13 @@ 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() {
showLoading(true);
async function loadPGN(showOverlay = true) {
if (showOverlay) showLoading(true);
hideError();
try {
@@ -50,7 +51,11 @@ async function loadPGN() {
// Nur automatisch wechseln, wenn der Benutzer keine andere Partie ausgewählt hat
if (!userSelectedGame) {
const liveGame = getLiveGame(allLaraGames);
currentGame = liveGame || getLatestGame(allLaraGames);
const newGame = liveGame || getLatestGame(allLaraGames);
if (newGame !== currentGame) {
userScrolledMoves = false;
}
currentGame = newGame;
}
updateBoard();
updatePlayerInfo();
@@ -89,7 +94,19 @@ function updateBoard() {
// Ignoriere ungültige Züge
}
}
currentMoveIndex = nonResultMoves.length - 1;
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);
@@ -122,6 +139,7 @@ function goToMove(index) {
if (index < -1) index = -1;
if (index >= nonResultMoves.length) index = nonResultMoves.length - 1;
userScrolledMoves = index < nonResultMoves.length - 1;
currentMoveIndex = index;
chess = new Chess();
@@ -135,6 +153,7 @@ function goToMove(index) {
board.position(chess.fen(), true);
highlightActivePlayer();
updateClocks(index);
document.querySelectorAll('#moves-list .move').forEach(el => el.classList.remove('current'));
if (index >= 0) {
@@ -153,26 +172,24 @@ function updatePlayerInfo() {
const laraPlayer = laraIsWhite ? currentGame.white : currentGame.black;
const laraElo = laraIsWhite ? currentGame.whiteElo : currentGame.blackElo;
const laraClock = laraIsWhite ? currentGame.whiteClock : currentGame.blackClock;
const laraEmoji = laraIsWhite ? '⬜' : '⬛';
const oppPlayer = laraIsWhite ? currentGame.black : currentGame.white;
const oppElo = laraIsWhite ? currentGame.blackElo : currentGame.whiteElo;
const oppClock = laraIsWhite ? currentGame.blackClock : currentGame.whiteClock;
const oppEmoji = laraIsWhite ? '⬛' : '⬜';
// Top-Panel (player-black) = Gegner
document.getElementById('black-name').textContent = oppPlayer;
document.getElementById('black-elo').textContent = oppElo ? `(ELO: ${oppElo})` : '';
document.getElementById('black-clock').textContent = formatClock(oppClock);
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.getElementById('white-clock').textContent = formatClock(laraClock);
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;
@@ -186,6 +203,34 @@ function updatePlayerInfo() {
}
}
/**
* 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
*/
@@ -267,8 +312,10 @@ function updateMovesList() {
if (curMove) curMove.classList.add('current');
}
// Scroll to bottom
movesList.scrollTop = movesList.scrollHeight;
// Scroll to bottom nur wenn Benutzer nicht manuell navigiert hat
if (!userScrolledMoves) {
movesList.scrollTop = movesList.scrollHeight;
}
}
/**
@@ -308,6 +355,7 @@ function updateAllGamesList() {
entry.addEventListener('click', () => {
userSelectedGame = true;
userScrolledMoves = false;
currentGame = game;
updateBoard();
updatePlayerInfo();
@@ -354,7 +402,7 @@ function startAutoRefresh() {
if (countdown <= 0) {
countdown = REFRESH_INTERVAL / 1000;
loadPGN();
loadPGN(false);
}
}, 1000);
}