add balance/mean-reversion signal and Cloudflare visitor logging

Balance signal (15% weight) favors under-represented chairs over last 50
games. Visitor middleware captures real IPs from CF headers, batched into
ClickHouse with 90-day TTL.
This commit is contained in:
2026-02-26 09:59:27 +05:00
parent 5fd4894599
commit 86865166ef
4 changed files with 135 additions and 9 deletions

View File

@@ -30,7 +30,65 @@ class WebServer:
def __init__(self):
self.app = web.Application()
self.clients: set[web.WebSocketResponse] = set()
self._visitor_buffer: list[dict] = []
self._visitor_lock = asyncio.Lock()
self._setup_routes()
self.app.middlewares.append(self._make_visitor_middleware())
def _make_visitor_middleware(self):
server = self
@web.middleware
async def visitor_middleware(request: web.Request, handler):
response = await handler(request)
path = request.path
# Skip static files and WebSocket upgrades
if path.startswith("/static/") or request.headers.get("Upgrade", "").lower() == "websocket":
return response
ip = (
request.headers.get("CF-Connecting-IP")
or (request.headers.get("X-Forwarded-For", "").split(",")[0].strip())
or request.remote
or ""
)
visitor = {
"ip": ip,
"country": request.headers.get("CF-IPCountry", ""),
"path": path,
"method": request.method,
"user_agent": request.headers.get("User-Agent", ""),
"referer": request.headers.get("Referer", ""),
"accept_lang": request.headers.get("Accept-Language", ""),
}
batch = None
async with server._visitor_lock:
server._visitor_buffer.append(visitor)
if len(server._visitor_buffer) >= 20:
batch = server._visitor_buffer[:]
server._visitor_buffer.clear()
if batch:
try:
await _run_sync(db.insert_visitors, batch)
except Exception as e:
log.warning("Visitor insert failed: %s", e)
return response
return visitor_middleware
async def _flush_visitors(self):
"""Periodically flush visitor buffer so low-traffic visits aren't lost."""
while True:
await asyncio.sleep(30)
batch = None
async with self._visitor_lock:
if self._visitor_buffer:
batch = self._visitor_buffer[:]
self._visitor_buffer.clear()
if batch:
try:
await _run_sync(db.insert_visitors, batch)
except Exception as e:
log.warning("Visitor flush failed: %s", e)
def _setup_routes(self):
self.app.router.add_get("/", self._handle_index)
@@ -219,6 +277,7 @@ class WebServer:
site = web.TCPSite(runner, "0.0.0.0", config.WEB_PORT)
await site.start()
log.info("Web server listening on http://0.0.0.0:%s", config.WEB_PORT)
flush_task = asyncio.create_task(self._flush_visitors())
# Keep running until cancelled
try:
while True:
@@ -226,4 +285,5 @@ class WebServer:
except asyncio.CancelledError:
pass
finally:
flush_task.cancel()
await runner.cleanup()