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:
Greg Hendrickson
2026-01-27 21:29:35 +00:00
parent 6b6626e2bc
commit cd12435a95
4 changed files with 146 additions and 0 deletions

View File

@@ -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

View 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
View 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")

View File

@@ -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: