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

3
.gitignore vendored
View File

@@ -1 +1,4 @@
cache/ cache/
__pycache__/
*.pyc
lara-chess/

View File

@@ -59,6 +59,79 @@ Live-Überwachung von Lara Kiesewetters Partien bei der **ODJM (Offene Deutsche
| Proxy-Server | Python http.server (ohne externe Abhängigkeiten) | | Proxy-Server | Python http.server (ohne externe Abhängigkeiten) |
| PGN-Quelle | deutsche-schachjugend.de (ODJM 2026) | | PGN-Quelle | deutsche-schachjugend.de (ODJM 2026) |
## Deployment
Die Anwendung kann als eigenständiger Server betrieben werden.
### Produktivbetrieb (systemd Linux)
```bash
# Service-Datei erstellen
sudo tee /etc/systemd/system/lara-chess.service <<EOF
[Unit]
Description=Lara Kiesewetter Live Schachturnier
After=network.target
[Service]
Type=simple
User=www-data
WorkingDirectory=/opt/lara-chess
ExecStart=/usr/bin/python3 /opt/lara-chess/server.py
Restart=always
RestartSec=10
[Install]
WantedBy=multi-user.target
EOF
# Aktivieren und starten
sudo systemctl daemon-reload
sudo systemctl enable --now lara-chess
```
Die App läuft dann auf `http://SERVER_IP:8111`.
### Docker
```dockerfile
# Dockerfile
FROM python:3-slim
WORKDIR /app
COPY . .
EXPOSE 8111
CMD ["python", "server.py"]
```
```bash
docker build -t lara-chess .
docker run -d --name lara-chess -p 8111:8111 --restart unless-stopped lara-chess
```
### Reverse Proxy (nginx)
```nginx
server {
listen 80;
server_name chess.example.com;
location / {
proxy_pass http://127.0.0.1:8111;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
}
```
### Umgebungsvariablen (optional)
In `server.py` können Port und PGN-URL über Umgebungsvariablen überschrieben werden:
| Variable | Default | Beschreibung |
|---|---|---|
| `PORT` | `8111` | Server-Port |
| `PGN_URL` | (siehe server.py) | Quelle der PGN-Daten |
| `CACHE_TTL` | `30` | Cache-Gültigkeit in Sekunden |
## Konfiguration ## Konfiguration
Die wichtigsten Konstanten in `app.js`: Die wichtigsten Konstanten in `app.js`:

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 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'; const PLAYER_NAME = 'Kiesewetter, Lara';
let board = null; let board = null;
@@ -17,12 +17,13 @@ let serverLastFetch = null;
let laraColor = null; let laraColor = null;
let currentMoveIndex = -1; let currentMoveIndex = -1;
let userSelectedGame = false; let userSelectedGame = false;
let userScrolledMoves = false;
/** /**
* Lädt die PGN-Datei und aktualisiert die Anzeige * Lädt die PGN-Datei und aktualisiert die Anzeige
*/ */
async function loadPGN() { async function loadPGN(showOverlay = true) {
showLoading(true); if (showOverlay) showLoading(true);
hideError(); hideError();
try { try {
@@ -50,7 +51,11 @@ async function loadPGN() {
// Nur automatisch wechseln, wenn der Benutzer keine andere Partie ausgewählt hat // Nur automatisch wechseln, wenn der Benutzer keine andere Partie ausgewählt hat
if (!userSelectedGame) { if (!userSelectedGame) {
const liveGame = getLiveGame(allLaraGames); const liveGame = getLiveGame(allLaraGames);
currentGame = liveGame || getLatestGame(allLaraGames); const newGame = liveGame || getLatestGame(allLaraGames);
if (newGame !== currentGame) {
userScrolledMoves = false;
}
currentGame = newGame;
} }
updateBoard(); updateBoard();
updatePlayerInfo(); updatePlayerInfo();
@@ -89,7 +94,19 @@ function updateBoard() {
// Ignoriere ungültige Züge // 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) { if (board) {
board.position(chess.fen(), true); board.position(chess.fen(), true);
@@ -122,6 +139,7 @@ function goToMove(index) {
if (index < -1) index = -1; if (index < -1) index = -1;
if (index >= nonResultMoves.length) index = nonResultMoves.length - 1; if (index >= nonResultMoves.length) index = nonResultMoves.length - 1;
userScrolledMoves = index < nonResultMoves.length - 1;
currentMoveIndex = index; currentMoveIndex = index;
chess = new Chess(); chess = new Chess();
@@ -135,6 +153,7 @@ function goToMove(index) {
board.position(chess.fen(), true); board.position(chess.fen(), true);
highlightActivePlayer(); highlightActivePlayer();
updateClocks(index);
document.querySelectorAll('#moves-list .move').forEach(el => el.classList.remove('current')); document.querySelectorAll('#moves-list .move').forEach(el => el.classList.remove('current'));
if (index >= 0) { if (index >= 0) {
@@ -153,26 +172,24 @@ function updatePlayerInfo() {
const laraPlayer = laraIsWhite ? currentGame.white : currentGame.black; const laraPlayer = laraIsWhite ? currentGame.white : currentGame.black;
const laraElo = laraIsWhite ? currentGame.whiteElo : currentGame.blackElo; const laraElo = laraIsWhite ? currentGame.whiteElo : currentGame.blackElo;
const laraClock = laraIsWhite ? currentGame.whiteClock : currentGame.blackClock;
const laraEmoji = laraIsWhite ? '⬜' : '⬛'; const laraEmoji = laraIsWhite ? '⬜' : '⬛';
const oppPlayer = laraIsWhite ? currentGame.black : currentGame.white; const oppPlayer = laraIsWhite ? currentGame.black : currentGame.white;
const oppElo = laraIsWhite ? currentGame.blackElo : currentGame.whiteElo; const oppElo = laraIsWhite ? currentGame.blackElo : currentGame.whiteElo;
const oppClock = laraIsWhite ? currentGame.blackClock : currentGame.whiteClock;
const oppEmoji = laraIsWhite ? '⬛' : '⬜'; const oppEmoji = laraIsWhite ? '⬛' : '⬜';
// Top-Panel (player-black) = Gegner // Top-Panel (player-black) = Gegner
document.getElementById('black-name').textContent = oppPlayer; document.getElementById('black-name').textContent = oppPlayer;
document.getElementById('black-elo').textContent = oppElo ? `(ELO: ${oppElo})` : ''; document.getElementById('black-elo').textContent = oppElo ? `(ELO: ${oppElo})` : '';
document.getElementById('black-clock').textContent = formatClock(oppClock);
document.querySelector('#player-black .player-avatar').textContent = oppEmoji; document.querySelector('#player-black .player-avatar').textContent = oppEmoji;
// Bottom-Panel (player-white) = Lara // Bottom-Panel (player-white) = Lara
document.getElementById('white-name').textContent = laraPlayer; document.getElementById('white-name').textContent = laraPlayer;
document.getElementById('white-elo').textContent = laraElo ? `(ELO: ${laraElo})` : ''; document.getElementById('white-elo').textContent = laraElo ? `(ELO: ${laraElo})` : '';
document.getElementById('white-clock').textContent = formatClock(laraClock);
document.querySelector('#player-white .player-avatar').textContent = laraEmoji; document.querySelector('#player-white .player-avatar').textContent = laraEmoji;
updateClocks(currentMoveIndex);
// Round info // Round info
const roundInfo = `Runde ${currentGame.round} ${currentGame.event || 'Turnier'}`; const roundInfo = `Runde ${currentGame.round} ${currentGame.event || 'Turnier'}`;
document.getElementById('round-info').textContent = roundInfo; 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 * Highlight den Spieler, der gerade am Zug ist
*/ */
@@ -267,8 +312,10 @@ function updateMovesList() {
if (curMove) curMove.classList.add('current'); if (curMove) curMove.classList.add('current');
} }
// Scroll to bottom // Scroll to bottom nur wenn Benutzer nicht manuell navigiert hat
movesList.scrollTop = movesList.scrollHeight; if (!userScrolledMoves) {
movesList.scrollTop = movesList.scrollHeight;
}
} }
/** /**
@@ -308,6 +355,7 @@ function updateAllGamesList() {
entry.addEventListener('click', () => { entry.addEventListener('click', () => {
userSelectedGame = true; userSelectedGame = true;
userScrolledMoves = false;
currentGame = game; currentGame = game;
updateBoard(); updateBoard();
updatePlayerInfo(); updatePlayerInfo();
@@ -354,7 +402,7 @@ function startAutoRefresh() {
if (countdown <= 0) { if (countdown <= 0) {
countdown = REFRESH_INTERVAL / 1000; countdown = REFRESH_INTERVAL / 1000;
loadPGN(); loadPGN(false);
} }
}, 1000); }, 1000);
} }

View File

@@ -14,11 +14,11 @@ import time
import json import json
from datetime import datetime from datetime import datetime
PGN_URL = "https://www.deutsche-schachjugend.de/2026/odjm-d/partien/gesamt-utf8.pgn" PGN_URL = os.environ.get("PGN_URL", "https://www.deutsche-schachjugend.de/2026/odjm-d/partien/gesamt-utf8.pgn")
PORT = 8111 PORT = int(os.environ.get("PORT", 8111))
CACHE_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "cache") CACHE_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "cache")
CACHE_FILE = os.path.join(CACHE_DIR, "gesamt-utf8.pgn") CACHE_FILE = os.path.join(CACHE_DIR, "gesamt-utf8.pgn")
CACHE_TTL = 30 # Sekunden CACHE_TTL = int(os.environ.get("CACHE_TTL", 30)) # Sekunden
os.makedirs(CACHE_DIR, exist_ok=True) os.makedirs(CACHE_DIR, exist_ok=True)