mirror of
https://github.com/ghndrx/shellmate.git
synced 2026-02-10 06:45:02 +00:00
Add leaderboard API and update website
- /api/leaderboard - returns top 10 players - /api/stats - returns total players/games - /api/health - health check - Website now shows live leaderboard with auto-refresh - Nginx proxies /api to container port 8080
This commit is contained in:
@@ -3,6 +3,7 @@ services:
|
|||||||
build: .
|
build: .
|
||||||
ports:
|
ports:
|
||||||
- "22:22"
|
- "22:22"
|
||||||
|
- "8080:8080"
|
||||||
environment:
|
environment:
|
||||||
- SHELLMATE_SSH_PORT=22
|
- SHELLMATE_SSH_PORT=22
|
||||||
- SHELLMATE_REDIS_URL=redis://redis:6379
|
- SHELLMATE_REDIS_URL=redis://redis:6379
|
||||||
|
|||||||
5
src/shellmate/api/__init__.py
Normal file
5
src/shellmate/api/__init__.py
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
"""Simple HTTP API for ShellMate."""
|
||||||
|
|
||||||
|
from shellmate.api.server import start_api_server
|
||||||
|
|
||||||
|
__all__ = ["start_api_server"]
|
||||||
135
src/shellmate/api/server.py
Normal file
135
src/shellmate/api/server.py
Normal file
@@ -0,0 +1,135 @@
|
|||||||
|
"""Simple HTTP API server for leaderboard data."""
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
import json
|
||||||
|
import logging
|
||||||
|
from http.server import BaseHTTPRequestHandler, HTTPServer
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class APIHandler(BaseHTTPRequestHandler):
|
||||||
|
"""HTTP request handler for API endpoints."""
|
||||||
|
|
||||||
|
db = None # Will be set by server
|
||||||
|
|
||||||
|
def do_GET(self):
|
||||||
|
"""Handle GET requests."""
|
||||||
|
if self.path == "/api/leaderboard":
|
||||||
|
self.handle_leaderboard()
|
||||||
|
elif self.path == "/api/stats":
|
||||||
|
self.handle_stats()
|
||||||
|
elif self.path == "/api/health":
|
||||||
|
self.handle_health()
|
||||||
|
else:
|
||||||
|
self.send_error(404, "Not Found")
|
||||||
|
|
||||||
|
def do_OPTIONS(self):
|
||||||
|
"""Handle CORS preflight."""
|
||||||
|
self.send_response(200)
|
||||||
|
self.send_cors_headers()
|
||||||
|
self.end_headers()
|
||||||
|
|
||||||
|
def send_cors_headers(self):
|
||||||
|
"""Add CORS headers."""
|
||||||
|
self.send_header("Access-Control-Allow-Origin", "*")
|
||||||
|
self.send_header("Access-Control-Allow-Methods", "GET, OPTIONS")
|
||||||
|
self.send_header("Access-Control-Allow-Headers", "Content-Type")
|
||||||
|
|
||||||
|
def send_json(self, data: Any, status: int = 200):
|
||||||
|
"""Send JSON response."""
|
||||||
|
self.send_response(status)
|
||||||
|
self.send_header("Content-Type", "application/json")
|
||||||
|
self.send_cors_headers()
|
||||||
|
self.end_headers()
|
||||||
|
self.wfile.write(json.dumps(data).encode())
|
||||||
|
|
||||||
|
def handle_leaderboard(self):
|
||||||
|
"""Return leaderboard data."""
|
||||||
|
try:
|
||||||
|
# Run async query in sync context
|
||||||
|
loop = asyncio.new_event_loop()
|
||||||
|
asyncio.set_event_loop(loop)
|
||||||
|
try:
|
||||||
|
entries = loop.run_until_complete(self._get_leaderboard())
|
||||||
|
finally:
|
||||||
|
loop.close()
|
||||||
|
|
||||||
|
data = {
|
||||||
|
"leaderboard": [
|
||||||
|
{
|
||||||
|
"rank": e.rank,
|
||||||
|
"username": e.username,
|
||||||
|
"elo": e.elo,
|
||||||
|
"games": e.games_played,
|
||||||
|
"wins": e.wins,
|
||||||
|
"losses": e.losses,
|
||||||
|
"winrate": e.winrate,
|
||||||
|
}
|
||||||
|
for e in entries
|
||||||
|
]
|
||||||
|
}
|
||||||
|
self.send_json(data)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Leaderboard error: {e}")
|
||||||
|
self.send_json({"error": str(e)}, 500)
|
||||||
|
|
||||||
|
def handle_stats(self):
|
||||||
|
"""Return overall stats."""
|
||||||
|
try:
|
||||||
|
loop = asyncio.new_event_loop()
|
||||||
|
asyncio.set_event_loop(loop)
|
||||||
|
try:
|
||||||
|
stats = loop.run_until_complete(self._get_stats())
|
||||||
|
finally:
|
||||||
|
loop.close()
|
||||||
|
|
||||||
|
self.send_json(stats)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Stats error: {e}")
|
||||||
|
self.send_json({"error": str(e)}, 500)
|
||||||
|
|
||||||
|
def handle_health(self):
|
||||||
|
"""Health check endpoint."""
|
||||||
|
self.send_json({"status": "ok"})
|
||||||
|
|
||||||
|
async def _get_leaderboard(self):
|
||||||
|
"""Fetch leaderboard from database."""
|
||||||
|
from shellmate.db import Database
|
||||||
|
db = await Database.get_instance()
|
||||||
|
return await db.get_leaderboard(10)
|
||||||
|
|
||||||
|
async def _get_stats(self):
|
||||||
|
"""Fetch stats from database."""
|
||||||
|
from shellmate.db import Database
|
||||||
|
db = await Database.get_instance()
|
||||||
|
return {
|
||||||
|
"total_players": await db.get_total_players(),
|
||||||
|
"total_games": await db.get_total_games(),
|
||||||
|
}
|
||||||
|
|
||||||
|
def log_message(self, format, *args):
|
||||||
|
"""Suppress default logging."""
|
||||||
|
logger.debug(f"API: {args[0]}")
|
||||||
|
|
||||||
|
|
||||||
|
def start_api_server(host: str = "0.0.0.0", port: int = 8080):
|
||||||
|
"""Start the API server (blocking)."""
|
||||||
|
server = HTTPServer((host, port), APIHandler)
|
||||||
|
logger.info(f"API server listening on {host}:{port}")
|
||||||
|
server.serve_forever()
|
||||||
|
|
||||||
|
|
||||||
|
async def start_api_server_async(host: str = "0.0.0.0", port: int = 8080):
|
||||||
|
"""Start the API server in a thread."""
|
||||||
|
import threading
|
||||||
|
|
||||||
|
def run_server():
|
||||||
|
server = HTTPServer((host, port), APIHandler)
|
||||||
|
logger.info(f"API server listening on {host}:{port}")
|
||||||
|
server.serve_forever()
|
||||||
|
|
||||||
|
thread = threading.Thread(target=run_server, daemon=True)
|
||||||
|
thread.start()
|
||||||
|
logger.info("API server thread started")
|
||||||
@@ -801,6 +801,11 @@ def main() -> None:
|
|||||||
asyncio.set_event_loop(loop)
|
asyncio.set_event_loop(loop)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
# Start API server in background thread
|
||||||
|
from shellmate.api.server import start_api_server_async
|
||||||
|
loop.run_until_complete(start_api_server_async(port=8080))
|
||||||
|
|
||||||
|
# Start SSH server
|
||||||
loop.run_until_complete(start_server())
|
loop.run_until_complete(start_server())
|
||||||
loop.run_forever()
|
loop.run_forever()
|
||||||
except KeyboardInterrupt:
|
except KeyboardInterrupt:
|
||||||
|
|||||||
Reference in New Issue
Block a user