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: .
|
||||
ports:
|
||||
- "22:22"
|
||||
- "8080:8080"
|
||||
environment:
|
||||
- SHELLMATE_SSH_PORT=22
|
||||
- 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)
|
||||
|
||||
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_forever()
|
||||
except KeyboardInterrupt:
|
||||
|
||||
Reference in New Issue
Block a user