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:
3
.gitignore
vendored
3
.gitignore
vendored
@@ -1 +1,4 @@
|
||||
cache/
|
||||
__pycache__/
|
||||
*.pyc
|
||||
lara-chess/
|
||||
|
||||
73
README.md
73
README.md
@@ -59,6 +59,79 @@ Live-Überwachung von Lara Kiesewetters Partien bei der **ODJM (Offene Deutsche
|
||||
| Proxy-Server | Python http.server (ohne externe Abhängigkeiten) |
|
||||
| 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
|
||||
|
||||
Die wichtigsten Konstanten in `app.js`:
|
||||
|
||||
72
app.js
72
app.js
@@ -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);
|
||||
}
|
||||
|
||||
@@ -14,11 +14,11 @@ import time
|
||||
import json
|
||||
from datetime import datetime
|
||||
|
||||
PGN_URL = "https://www.deutsche-schachjugend.de/2026/odjm-d/partien/gesamt-utf8.pgn"
|
||||
PORT = 8111
|
||||
PGN_URL = os.environ.get("PGN_URL", "https://www.deutsche-schachjugend.de/2026/odjm-d/partien/gesamt-utf8.pgn")
|
||||
PORT = int(os.environ.get("PORT", 8111))
|
||||
CACHE_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "cache")
|
||||
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)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user