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/
|
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) |
|
| 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
72
app.js
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user