Eval-Bar mit Stockfish-Streaming, LESS entfernt, Move-Lock, AGENTS-Regeln

This commit is contained in:
2026-05-25 20:09:09 +02:00
parent 3b1a4ce4e4
commit ec1ebaa525
8 changed files with 412 additions and 595 deletions

1
.gitignore vendored
View File

@@ -4,3 +4,4 @@ __pycache__/
lara-chess/
node_modules/
package-lock.json
stockfish.exe

View File

@@ -3,5 +3,5 @@
- Nach jeder Änderung am Proxy (server.py) oder an den Requests (app.js) muss der Proxy neu gestartet werden: erst alten Prozess killen (`Get-Process -Name python | Stop-Process -Force`), dann unsichtbar starten.
- Commits und Push erfolgen nur durch expliziten Befehl des Nutzers.
- Der Server (server.py) wird immer unsichtbar gestartet: `Start-Process -NoNewWindow -FilePath "python" -ArgumentList "server.py"`
- **LESS-Workflow**: CSS-Quelldatei ist `style.less`. Bei Serverstart wird automatisch `style.css` via `node build-less.js` kompiliert. Bei manuellen Änderungen an `style.less`: `node build-less.js` ausführen, dann Server neustarten.
- `style.css` ist auto-generiert keine manuellen Änderungen dort vornehmen.
- Bei Updates (Polling/Code-Änderungen) niemals den aktuell angezeigten Zug wechseln. Der Benutzer bleibt auf dem von ihm ausgewählten Zug.

View File

@@ -5,9 +5,7 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>🏆 Lara Kiesewetter Live Schachturnier</title>
<link rel="stylesheet" href="https://unpkg.com/@chrisoakman/chessboardjs@1.0.0/dist/chessboard-1.0.0.min.css">
<link rel="stylesheet/less" href="less/style.less">
<script>less = { env: 'development' };</script>
<script src="https://cdn.jsdelivr.net/npm/less@4.6.4/dist/less.min.js"></script>
<link rel="stylesheet" href="style.css">
</head>
<body>
<div id="app">
@@ -32,7 +30,18 @@
</div>
<div class="player-clock" id="black-clock">--:--:--</div>
</div>
<div id="board-row">
<div id="board"></div>
<div id="eval-bar-container">
<div id="eval-bar">
<div id="eval-bar-fill"></div>
<div id="eval-bar-marker"></div>
</div>
<div id="eval-info">
<span id="eval-score">0.00</span>
</div>
</div>
</div>
<div id="player-white" class="player-info">
<div class="player-avatar"></div>
<div class="player-details">

163
js/app.js
View File

@@ -11,10 +11,13 @@ let currentGame = null;
let allLaraGames = [];
let currentMoveIndex = -1;
let userSelectedGame = false;
let userScrolledMoves = false;
let evalAbortController = null;
let lastEvalFen = null;
let currentRound = 0;
let roundPgns = {};
let pollId = 0;
let pollInterval = null;
let updateTimer = null;
async function fetchRoundPGN(round) {
const res = await fetch(`https://www.deutsche-schachjugend.de/2026/odjm-d/partien/${round}.pgn?t=${Date.now()}`);
@@ -54,11 +57,7 @@ async function loadPGN(showOverlay = true) {
if (!userSelectedGame) {
const liveGame = getLiveGame(allLaraGames);
const newGame = liveGame || getLatestGame(allLaraGames);
if (newGame !== currentGame) {
userScrolledMoves = false;
}
currentGame = newGame;
currentGame = liveGame || getLatestGame(allLaraGames);
}
updateBoard();
updatePlayerInfo();
@@ -82,26 +81,20 @@ async function loadPGN(showOverlay = true) {
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';
// 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 {
// Ignoriere ungültige Züge
}
}
if (!userScrolledMoves) {
// Aktuellen Move beibehalten — nie automatisch zum letzten Zug springen
if (currentMoveIndex >= nonResultMoves.length || currentMoveIndex < -1) {
currentMoveIndex = nonResultMoves.length - 1;
} else if (currentMoveIndex >= 0) {
// Benutzer hat navigiert zeige Brett an seinem ausgewählten Zug
}
chess = new Chess();
if (currentMoveIndex >= 0) {
for (let i = 0; i <= currentMoveIndex && i < nonResultMoves.length; i++) {
try {
chess.move(nonResultMoves[i].san);
@@ -131,6 +124,105 @@ function updateBoard() {
// Highlight active player
highlightActivePlayer();
syncEvalBarHeight();
updateEvaluation();
}
function syncEvalBarHeight() {
const boardEl = document.getElementById('board');
const evalContainer = document.getElementById('eval-bar-container');
if (boardEl && evalContainer) {
const h = boardEl.offsetHeight;
if (h > 100) evalContainer.style.minHeight = h + 'px';
}
}
/**
* Sendet FEN an Stockfish und aktualisiert die Eval-Bar
*/
async function updateEvaluation() {
if (!chess) return;
const fen = chess.fen();
if (lastEvalFen === fen && evalAbortController) return;
lastEvalFen = fen;
if (evalAbortController) {
evalAbortController.abort();
}
evalAbortController = new AbortController();
try {
const res = await fetch('/evaluate', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ fen }),
signal: evalAbortController.signal,
});
if (!res.ok) throw new Error('Eval fehlgeschlagen');
const reader = res.body.getReader();
const decoder = new TextDecoder();
let buffer = '';
while (true) {
const { done, value } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });
const lines = buffer.split('\n');
buffer = lines.pop();
for (const line of lines) {
if (!line.trim()) continue;
try {
displayEval(JSON.parse(line));
} catch {}
}
}
if (buffer.trim()) {
try { displayEval(JSON.parse(buffer)); } catch {}
}
} catch (e) {
if (e.name === 'AbortError') return;
const el = document.getElementById('eval-score');
if (el) el.textContent = '?';
}
}
function displayEval(data) {
const evalScore = document.getElementById('eval-score');
const barFill = document.getElementById('eval-bar-fill');
const barMarker = document.getElementById('eval-bar-marker');
let scoreText = '0.00';
let fillPercent = 50;
if (data.scoreMate !== null && data.scoreMate !== undefined && data.scoreMate !== 0) {
const mate = parseInt(data.scoreMate);
const whiteMate = chess && chess.turn() === 'w' ? mate : -mate;
if (whiteMate > 0) {
scoreText = '#' + whiteMate;
fillPercent = 100;
} else {
scoreText = '#' + Math.abs(whiteMate);
fillPercent = 0;
}
} else if (data.scoreCp !== null && data.scoreCp !== undefined) {
const cp = parseInt(data.scoreCp);
const whiteCp = chess && chess.turn() === 'w' ? cp : -cp;
const pawns = (whiteCp / 100).toFixed(2);
scoreText = (whiteCp > 0 ? '+' : '') + pawns;
fillPercent = 50 + whiteCp / 14;
fillPercent = Math.max(0, Math.min(100, fillPercent));
}
evalScore.textContent = scoreText;
if (barFill) barFill.style.height = fillPercent + '%';
if (barMarker) barMarker.style.top = (100 - fillPercent) + '%';
}
/**
@@ -144,7 +236,6 @@ 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();
@@ -160,6 +251,8 @@ function goToMove(index) {
highlightActivePlayer();
highlightLastMove();
updateClocks(index);
syncEvalBarHeight();
updateEvaluation();
document.querySelectorAll('#moves-list .move').forEach(el => el.classList.remove('current'));
if (index >= 0) {
@@ -382,11 +475,6 @@ function updateMovesList() {
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;
}
}
/**
@@ -426,7 +514,6 @@ function updateAllGamesList() {
entry.addEventListener('click', () => {
userSelectedGame = true;
userScrolledMoves = false;
currentGame = game;
updateBoard();
updatePlayerInfo();
@@ -531,9 +618,11 @@ function updateTimestamp() {
* Startet Auto-Refresh alle 30 Sekunden
*/
function startAutoRefresh() {
clearInterval(pollInterval);
clearInterval(updateTimer);
const myId = ++pollId;
let lastUpdate = Date.now();
let pollInterval, timer;
document.getElementById('refresh-timer').textContent = '0s';
document.getElementById('refresh-timer').style.color = '#4ade80';
@@ -541,13 +630,13 @@ function startAutoRefresh() {
loadPGN(true);
pollInterval = setInterval(() => {
if (pollId !== myId) { clearInterval(pollInterval); clearInterval(timer); return; }
if (pollId !== myId) { clearInterval(pollInterval); clearInterval(updateTimer); return; }
loadPGN(false);
lastUpdate = Date.now();
}, 30000);
timer = setInterval(() => {
if (pollId !== myId) { clearInterval(pollInterval); clearInterval(timer); return; }
updateTimer = setInterval(() => {
if (pollId !== myId) { clearInterval(pollInterval); clearInterval(updateTimer); return; }
const elapsed = Date.now() - lastUpdate;
const remaining = Math.max(0, 30000 - elapsed);
const s = Math.floor(remaining / 1000);
@@ -626,8 +715,24 @@ if (window.ResizeObserver) {
ro.observe(document.getElementById('board'));
}
/**
* Page Visibility API Timer pausieren wenn Tab unsichtbar
*/
document.addEventListener('visibilitychange', () => {
if (document.hidden) {
clearInterval(pollInterval);
clearInterval(updateTimer);
} else {
startAutoRefresh();
}
});
/**
* Init
*/
loadPGN();
startAutoRefresh();
// Eval-Bar initialisieren
chess = new Chess();
updateEvaluation();

View File

@@ -1,544 +0,0 @@
// Farbvariablen
@primary: #e94560;
@primary-glow: rgba(233, 69, 96, 0.3);
@bg-dark: rgba(0, 0, 0, 0.3);
@bg-darker: rgba(0, 0, 0, 0.4);
@text-muted: #aaa;
@text-light: #e0e0e0;
@text-white: #fff;
@accent-green: #4ade80;
@gold: #ffd700;
@font-mono: 'Courier New', monospace;
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background: linear-gradient(135deg, #1a1a2e 0%, #16213e 50%, #0f3460 100%);
color: @text-light;
min-height: 100vh;
}
header {
background: rgba(0, 0, 0, 0.4);
padding: 16px 24px;
display: flex;
justify-content: space-between;
align-items: center;
flex-wrap: wrap;
gap: 12px;
border-bottom: 2px solid @primary;
h1 {
font-size: 1.5rem;
color: @text-white;
text-shadow: 0 0 10px rgba(233, 69, 96, 0.5);
}
}
#status-bar {
display: flex;
align-items: center;
gap: 16px;
font-size: 0.85rem;
color: @text-muted;
}
#last-update, #refresh-timer {
font-family: @font-mono;
font-size: 0.9rem;
color: @accent-green;
background: @bg-darker;
padding: 4px 10px;
border-radius: 6px;
border: 1px solid rgba(74, 222, 128, 0.2);
}
#refresh-btn {
background: @primary;
border: none;
color: white;
width: 36px;
height: 36px;
border-radius: 50%;
cursor: pointer;
font-size: 1.2rem;
transition: transform 0.3s, background 0.3s;
&:hover {
background: #ff6b6b;
transform: rotate(180deg);
}
}
#main-content {
display: flex;
gap: 24px;
padding: 24px;
max-width: 1400px;
margin: 0 auto;
align-items: flex-start;
}
/* Board Section */
#board-section {
flex: 1;
min-width: 0;
display: flex;
flex-direction: column;
align-items: center;
gap: 12px;
}
.player-info {
display: flex;
align-items: center;
gap: 12px;
width: 100%;
max-width: 500px;
padding: 12px 16px;
background: @bg-dark;
border-radius: 12px;
border: 2px solid transparent;
transition: border-color 0.3s, box-shadow 0.3s;
&.active {
border-color: @primary;
box-shadow: 0 0 15px @primary-glow;
}
}
.player-avatar {
font-size: 2rem;
width: 48px;
height: 48px;
display: flex;
align-items: center;
justify-content: center;
background: rgba(255, 255, 255, 0.1);
border-radius: 50%;
flex-shrink: 0;
}
.player-details {
flex: 1;
display: flex;
flex-direction: column;
gap: 2px;
}
.player-name {
font-weight: 600;
font-size: 1.1rem;
color: @text-white;
}
.player-elo {
font-size: 0.85rem;
color: @text-muted;
}
.player-clock {
font-family: @font-mono;
font-size: 1.3rem;
font-weight: bold;
background: rgba(0, 0, 0, 0.5);
padding: 6px 12px;
border-radius: 8px;
min-width: 100px;
text-align: center;
color: @accent-green;
}
#board {
width: 100%;
max-width: 500px;
[data-square].last-move-highlight {
box-shadow: inset 0 0 3px 3px rgba(255, 200, 0, 0.7);
}
}
#pgn-panel, #pgn-panel-mobile {
width: 100%;
max-width: 500px;
}
#pgn-panel-mobile {
display: none;
}
.pgn-panel--desktop,
.pgn-panel--mobile {
background: @bg-dark;
border-radius: 12px;
}
.pgn-panel--desktop {
padding: 12px 16px;
}
.pgn-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 8px;
color: @primary;
font-weight: bold;
font-size: 0.9rem;
}
.copy-pgn-btn {
background: rgba(255, 255, 255, 0.1);
border: none;
color: #ccc;
width: 32px;
height: 32px;
border-radius: 6px;
cursor: pointer;
font-size: 1rem;
transition: background 0.2s;
&:hover {
background: rgba(255, 255, 255, 0.2);
}
}
.pgn-text {
font-family: @font-mono;
font-size: 0.78rem;
line-height: 1.5;
color: @text-muted;
white-space: pre-wrap;
word-break: break-all;
max-height: 160px;
overflow-y: auto;
padding: 8px;
background: @bg-darker;
border-radius: 6px;
-webkit-user-select: text;
user-select: text;
}
/* Info Section */
#info-section {
flex: 0 0 380px;
display: flex;
flex-direction: column;
gap: 20px;
}
#game-info h2 {
font-size: 1.3rem;
color: @primary;
margin-bottom: 8px;
}
#result-info {
font-size: 1.1rem;
padding: 8px 12px;
background: @bg-dark;
border-radius: 8px;
}
#moves-panel, #all-games-panel, #standings-panel, #pgn-panel-mobile {
background: @bg-dark;
border-radius: 12px;
padding: 16px;
h3 {
margin-bottom: 12px;
color: @primary;
font-size: 1rem;
}
}
#standings-content {
font-size: 0.9rem;
line-height: 1.6;
.standings-loading {
color: #888;
font-style: italic;
}
}
.standings-row {
display: flex;
justify-content: space-between;
padding: 6px 0;
border-bottom: 1px solid rgba(255, 255, 255, 0.05);
&:last-child {
border-bottom: none;
}
}
.standings-label {
color: @text-muted;
}
.standings-value {
color: @text-white;
font-weight: 600;
}
.standings-rank {
font-size: 2rem;
font-weight: bold;
color: @primary;
text-align: center;
padding: 8px 0;
}
.standings-rank-label {
font-size: 0.8rem;
color: #888;
text-align: center;
}
.standings-header {
text-align: center;
margin-bottom: 8px;
color: @gold;
font-size: 0.85rem;
}
#moves-list {
max-height: 300px;
overflow-y: auto;
font-family: @font-mono;
font-size: 0.95rem;
line-height: 1.8;
display: flex;
flex-wrap: wrap;
gap: 4px;
.move-number {
color: #888;
font-weight: bold;
}
.move {
color: @text-light;
cursor: pointer;
padding: 2px 6px;
border-radius: 4px;
transition: background 0.2s;
&:hover {
background: @primary-glow;
}
&.current {
background: @primary;
color: @text-white;
}
}
.lara-move {
color: @gold;
font-weight: bold;
}
.opp-move {
color: @text-muted;
}
}
#all-games-list {
display: flex;
flex-direction: column;
gap: 8px;
}
.game-entry {
padding: 10px 12px;
background: rgba(255, 255, 255, 0.05);
border-radius: 8px;
cursor: pointer;
transition: background 0.2s;
border-left: 3px solid transparent;
&:hover {
background: rgba(255, 255, 255, 0.1);
}
&.active {
border-left-color: @primary;
background: rgba(233, 69, 96, 0.15);
}
.game-round {
font-weight: bold;
color: @primary;
font-size: 0.85rem;
}
.game-players {
font-size: 0.9rem;
color: #ccc;
margin-top: 2px;
}
.game-result {
font-size: 0.8rem;
color: #888;
margin-top: 2px;
}
}
/* Overlays */
#loading-overlay, #error-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(26, 26, 46, 0.95);
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
z-index: 1000;
gap: 16px;
p {
font-size: 1.2rem;
}
}
#error-overlay button {
padding: 10px 24px;
background: @primary;
color: white;
border: none;
border-radius: 8px;
cursor: pointer;
font-size: 1rem;
margin-top: 8px;
}
.spinner {
width: 50px;
height: 50px;
border: 4px solid @primary-glow;
border-top-color: @primary;
border-radius: 50%;
animation: spin 0.8s linear infinite;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
/* Scrollbar */
::-webkit-scrollbar {
width: 6px;
}
::-webkit-scrollbar-track {
background: rgba(0, 0, 0, 0.2);
border-radius: 3px;
}
::-webkit-scrollbar-thumb {
background: @primary;
border-radius: 3px;
}
.round-info-mobile {
display: none;
}
/* Responsive */
@media (max-width: 900px) {
#main-content {
flex-direction: column;
align-items: center;
padding: 12px;
}
#info-section {
flex: none;
width: 100%;
max-width: 500px;
}
html, body {
overflow-x: hidden;
}
#board-section {
width: 100%;
align-items: stretch;
}
#board,
.player-info {
max-width: 100%;
width: 100%;
}
#board {
overflow: hidden;
}
#board > div {
max-width: 100%;
box-sizing: border-box;
}
.round-info-mobile {
display: block;
width: 100%;
max-width: 500px;
font-size: 1.3rem;
color: @primary;
margin: 0 auto;
}
#round-info {
display: none;
}
#result-info {
display: none;
}
.pgn-panel--desktop {
display: none;
}
#pgn-panel-mobile {
display: block;
}
.player-info {
padding: 8px 12px;
}
.player-avatar {
width: 36px;
height: 36px;
font-size: 1.4rem;
}
.player-name {
font-size: 0.95rem;
}
.player-clock {
font-size: 1.1rem;
min-width: 80px;
padding: 4px 8px;
}
header h1 {
font-size: 1.2rem;
}
}

View File

@@ -1,14 +1,11 @@
{
"scripts": {
"build:css": "node build-less.js"
},
"scripts": {},
"devDependencies": {
"@eslint/css": "^1.2.0",
"@eslint/js": "^10.0.1",
"@eslint/json": "^1.2.0",
"@eslint/markdown": "^8.0.2",
"eslint": "^10.4.0",
"globals": "^17.6.0",
"less": "^4.6.4"
"globals": "^17.6.0"
}
}

205
server.py
View File

@@ -1,27 +1,144 @@
#!/usr/bin/env python3
"""
Lokaler Server für Lara Schachturnier
Serviert statische Dateien direkt.
Serviert statische Dateien und bietet Stockfish-Analyse.
"""
import http.server
import socketserver
import os
import subprocess
import json
import re
import sys
import threading
import socket
PORT = int(os.environ.get("PORT", 8111))
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
STOCKFISH_PATH = os.environ.get("STOCKFISH_PATH")
if not STOCKFISH_PATH:
win_path = os.path.join(BASE_DIR, "stockfish.exe")
if os.path.exists(win_path):
STOCKFISH_PATH = win_path
else:
STOCKFISH_PATH = "stockfish"
STOCKFISH_DEPTH = int(os.environ.get("STOCKFISH_DEPTH", 15))
_stockfish_lock = threading.Lock()
class StockfishEngine:
def __init__(self, path):
self.path = path
self.proc = None
def start(self):
if self.proc:
return
flags = 0
if sys.platform == "win32":
flags = subprocess.CREATE_NO_WINDOW
self.proc = subprocess.Popen(
[self.path],
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
bufsize=1,
creationflags=flags,
)
self._send("uci")
self._read_until("uciok")
self._send("isready")
self._read_until("readyok")
print(f"[STOCKFISH] Engine gestartet: {self.path}")
def _send(self, cmd):
self.proc.stdin.write(cmd + "\n")
self.proc.stdin.flush()
def _read_until(self, marker):
while True:
line = self.proc.stdout.readline().strip()
if line == marker:
return
def evaluate(self, fen):
self._send(f"position fen {fen}")
self._send(f"go movetime 10000")
score_cp = None
score_mate = None
bestmove = None
pv = None
last_depth = 0
while True:
line = self.proc.stdout.readline().strip()
if not line:
continue
if "score cp" in line:
m = re.search(r"score cp (-?\d+)", line)
if m:
score_cp = int(m.group(1))
if "score mate" in line:
m = re.search(r"score mate (-?\d+)", line)
if m:
score_mate = int(m.group(1))
if " pv " in line:
pv = line.split(" pv ", 1)[1]
depth_m = re.search(r"depth (\d+)", line)
if depth_m and score_cp is not None:
new_depth = int(depth_m.group(1))
if new_depth != last_depth:
last_depth = new_depth
yield {
"scoreCp": score_cp,
"scoreMate": score_mate,
"bestMove": None,
"pv": pv,
"depth": new_depth,
}
if line.startswith("bestmove"):
parts = line.split()
bestmove = parts[1] if len(parts) > 1 else None
break
yield {
"scoreCp": score_cp,
"scoreMate": score_mate,
"bestMove": bestmove,
"pv": pv,
}
def stop(self):
if self.proc:
self.proc.terminate()
self.proc = None
_engine = StockfishEngine(STOCKFISH_PATH)
class Handler(http.server.BaseHTTPRequestHandler):
class StaticHandler(http.server.BaseHTTPRequestHandler):
def do_GET(self):
if self.path == "/":
self.path = "/index.html"
filepath = os.path.join(os.path.dirname(os.path.abspath(__file__)), self.path.lstrip("/"))
filepath = os.path.join(BASE_DIR, self.path.lstrip("/"))
if os.path.isfile(filepath):
content_types = {
".html": "text/html",
".css": "text/css",
".less": "text/css",
".js": "application/javascript",
".json": "application/json",
".png": "image/png",
@@ -37,29 +154,101 @@ class StaticHandler(http.server.BaseHTTPRequestHandler):
self.send_response(200)
self.send_header("Content-Type", f"{content_type}; charset=utf-8")
self.send_header("Access-Control-Allow-Origin", "*")
self.send_header("Cache-Control", "no-cache")
self.end_headers()
self.wfile.write(content)
else:
self.send_response(404)
self.end_headers()
def do_POST(self):
if self.path == "/evaluate":
length = int(self.headers.get("Content-Length", 0))
body = self.rfile.read(length) if length else b"{}"
try:
data = json.loads(body)
fen = data.get("fen", "")
except (json.JSONDecodeError, TypeError):
self._send_json({"error": "Invalid JSON"}, 400)
return
if not fen:
self._send_json({"error": "Missing fen"}, 400)
return
self.send_response(200)
self.send_header("Content-Type", "application/x-ndjson; charset=utf-8")
self.send_header("Access-Control-Allow-Origin", "*")
self.send_header("Cache-Control", "no-cache")
self.send_header("Connection", "close")
self.end_headers()
self.wfile.flush()
# TCP_NODELAY für sofortiges Senden
try:
self.connection.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
except Exception:
pass
self.close_connection = True
try:
with _stockfish_lock:
try:
_engine.start()
except FileNotFoundError:
self.wfile.write(
json.dumps({"error": "Stockfish nicht gefunden"}).encode("utf-8") + b"\n"
)
self.wfile.flush()
return
for result in _engine.evaluate(fen):
data = json.dumps(result).encode("utf-8") + b"\n"
self.wfile.write(data)
self.wfile.flush()
except Exception as e:
print(f"[STOCKFISH] Fehler: {e}")
self.wfile.write(
json.dumps({"error": str(e)}).encode("utf-8") + b"\n"
)
self.wfile.flush()
else:
self._send_json({"error": "Not found"}, 404)
def _send_json(self, obj, status=200):
self.send_response(status)
self.send_header("Content-Type", "application/json; charset=utf-8")
self.send_header("Access-Control-Allow-Origin", "*")
self.send_header("Cache-Control", "no-cache")
self.end_headers()
self.wfile.write(json.dumps(obj).encode("utf-8"))
def log_message(self, format, *args):
print(f"[{self.log_date_time_string()}] {args[0]}")
def main():
print("=" * 50)
print(" [TROPHY] Lara Kiesewetter Live Schachturnier")
print(" [TROPHY] Lara Kiesewetter - Live Schachturnier")
print("=" * 50)
print(f" Server läuft auf: http://localhost:{PORT}")
print(f" Drücke Ctrl+C zum Beenden")
print(f" Server laeuft auf: http://localhost:{PORT}")
if os.path.exists(STOCKFISH_PATH) or STOCKFISH_PATH == "stockfish":
print(f" Stockfish-Analyse aktiv (depth={STOCKFISH_DEPTH})")
else:
print(f" Stockfish nicht gefunden unter: {STOCKFISH_PATH}")
print(f" Druecke Ctrl+C zum Beenden")
print("=" * 50)
with socketserver.TCPServer(("", PORT), StaticHandler) as httpd:
socketserver.ThreadingTCPServer.allow_reuse_address = True
with socketserver.ThreadingTCPServer(("", PORT), Handler) as httpd:
print(f"\n[SERVER] Server gestartet: http://localhost:{PORT}\n")
try:
httpd.serve_forever()
except KeyboardInterrupt:
print("\n[BYE] Server gestoppt.")
_engine.stop()
if __name__ == "__main__":
main()

View File

@@ -133,6 +133,66 @@ header h1 {
#board [data-square].last-move-highlight {
box-shadow: inset 0 0 3px 3px rgba(255, 200, 0, 0.7);
}
/* Board Row: Board + Eval-Bar nebeneinander */
#board-row {
display: flex;
gap: 8px;
width: 100%;
max-width: 540px;
}
#eval-bar-container {
display: flex;
flex-direction: column;
align-items: center;
gap: 4px;
flex-shrink: 0;
width: 36px;
min-height: 350px;
}
#eval-bar {
flex: 1;
width: 16px;
background: #1a1a1a;
border-radius: 8px;
position: relative;
overflow: hidden;
border: 1px solid rgba(255, 255, 255, 0.15);
}
#eval-bar-fill {
position: absolute;
bottom: 0;
width: 100%;
background: linear-gradient(to top, #f0f0f0 0%, #d0d0d0 100%);
transition: height 0.3s ease;
border-radius: 0 0 7px 7px;
height: 50%;
}
#eval-bar-marker {
position: absolute;
left: -2px;
right: -2px;
height: 2px;
background: #000;
transition: top 0.3s ease;
z-index: 2;
box-shadow: 0 0 3px rgba(0, 0, 0, 0.5);
top: 50%;
}
#eval-info {
display: flex;
flex-direction: column;
align-items: center;
gap: 1px;
flex-shrink: 0;
}
#eval-score {
font-family: 'Courier New', monospace;
font-size: 0.75rem;
font-weight: bold;
color: #fff;
text-align: center;
white-space: nowrap;
}
#pgn-panel,
#pgn-panel-mobile {
width: 100%;