add bet impact simulator, visitor log page, and fix console logging
- Bet impact simulator on /predictions shows rank headroom and safe bet amounts - Password-protected /visitors page with visitor log table and stats - Console now logs real visitor IPs instead of Cloudflare tunnel IPs
This commit is contained in:
24
app/db.py
24
app/db.py
@@ -177,6 +177,30 @@ def insert_visitors(batch: list[dict]):
|
||||
)
|
||||
|
||||
|
||||
@_with_lock
|
||||
def get_recent_visitors(limit: int = 200) -> list[dict]:
|
||||
"""Get recent visitor log entries."""
|
||||
client = get_client()
|
||||
result = client.query(
|
||||
"SELECT ip, country, path, method, user_agent, referer, accept_lang, created_at "
|
||||
"FROM visitors ORDER BY created_at DESC LIMIT {limit:UInt32}",
|
||||
parameters={"limit": limit},
|
||||
)
|
||||
visitors = []
|
||||
for row in result.result_rows:
|
||||
visitors.append({
|
||||
"ip": row[0],
|
||||
"country": row[1],
|
||||
"path": row[2],
|
||||
"method": row[3],
|
||||
"user_agent": row[4],
|
||||
"referer": row[5],
|
||||
"accept_lang": row[6],
|
||||
"created_at": str(row[7]),
|
||||
})
|
||||
return visitors
|
||||
|
||||
|
||||
@_with_lock
|
||||
def get_recent_games(n: int = 50) -> list[dict]:
|
||||
"""Get last N completed games."""
|
||||
|
||||
@@ -5,6 +5,7 @@ All blocking DB calls run in a thread executor to avoid blocking the event loop.
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import base64
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
@@ -51,6 +52,8 @@ class WebServer:
|
||||
or request.remote
|
||||
or ""
|
||||
)
|
||||
log.info('%s %s %s %d "%s"', ip, request.method, path, response.status,
|
||||
request.headers.get("User-Agent", "-"))
|
||||
visitor = {
|
||||
"ip": ip,
|
||||
"country": request.headers.get("CF-IPCountry", ""),
|
||||
@@ -90,6 +93,28 @@ class WebServer:
|
||||
except Exception as e:
|
||||
log.warning("Visitor flush failed: %s", e)
|
||||
|
||||
@staticmethod
|
||||
def _check_basic_auth(request: web.Request) -> bool:
|
||||
auth = request.headers.get("Authorization", "")
|
||||
if not auth.startswith("Basic "):
|
||||
return False
|
||||
try:
|
||||
decoded = base64.b64decode(auth[6:]).decode()
|
||||
return decoded == "sk:hakunamatata2020"
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
@staticmethod
|
||||
def _require_auth(request: web.Request) -> web.Response | None:
|
||||
"""Return a 401 response if auth fails, else None."""
|
||||
if WebServer._check_basic_auth(request):
|
||||
return None
|
||||
return web.Response(
|
||||
status=401,
|
||||
headers={"WWW-Authenticate": 'Basic realm="3pmonitor"'},
|
||||
text="Unauthorized",
|
||||
)
|
||||
|
||||
def _setup_routes(self):
|
||||
self.app.router.add_get("/", self._handle_index)
|
||||
self.app.router.add_get("/api/history", self._handle_history)
|
||||
@@ -104,6 +129,8 @@ class WebServer:
|
||||
self.app.router.add_get("/predictions", self._handle_predictions_page)
|
||||
self.app.router.add_get("/api/predictions", self._handle_predictions)
|
||||
self.app.router.add_get("/api/prediction-history", self._handle_prediction_history)
|
||||
self.app.router.add_get("/visitors", self._handle_visitors_page)
|
||||
self.app.router.add_get("/api/visitors", self._handle_visitors)
|
||||
self.app.router.add_get("/ws", self._handle_ws)
|
||||
self.app.router.add_static("/static/", STATIC_DIR, name="static")
|
||||
|
||||
@@ -186,6 +213,25 @@ class WebServer:
|
||||
log.error("Prediction history query failed: %s", e)
|
||||
return web.json_response({"error": str(e)}, status=500)
|
||||
|
||||
async def _handle_visitors_page(self, request: web.Request) -> web.Response:
|
||||
denied = self._require_auth(request)
|
||||
if denied:
|
||||
return denied
|
||||
path = os.path.join(STATIC_DIR, "visitors.html")
|
||||
return web.FileResponse(path)
|
||||
|
||||
async def _handle_visitors(self, request: web.Request) -> web.Response:
|
||||
denied = self._require_auth(request)
|
||||
if denied:
|
||||
return denied
|
||||
limit = min(int(request.query.get("limit", 200)), 1000)
|
||||
try:
|
||||
visitors = await _run_sync(db.get_recent_visitors, limit)
|
||||
return web.json_response(visitors)
|
||||
except Exception as e:
|
||||
log.error("Visitors query failed: %s", e)
|
||||
return web.json_response([], status=500)
|
||||
|
||||
async def _handle_analytics(self, request: web.Request) -> web.Response:
|
||||
period = request.query.get("period", "all")
|
||||
if period not in ("1h", "6h", "24h", "7d", "all"):
|
||||
@@ -272,7 +318,7 @@ class WebServer:
|
||||
log.warning("push_refresh failed: %s", e)
|
||||
|
||||
async def run(self):
|
||||
runner = web.AppRunner(self.app)
|
||||
runner = web.AppRunner(self.app, access_log=None)
|
||||
await runner.setup()
|
||||
site = web.TCPSite(runner, "0.0.0.0", config.WEB_PORT)
|
||||
await site.start()
|
||||
|
||||
Reference in New Issue
Block a user