24335a2677
* feat(gateway): hashed key store with grant + hot reload Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * feat(gateway): channel registry + aiohttp app (keyed auth, whoami, per-channel ws/proxy) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * feat(gateway): manage_keys CLI (add/list/revoke) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * feat(gateway): retire srebot_external, run relay-gateway under PM2 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * feat(gateway): point ecosystem + README at relay-gateway Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * feat(tss): replay outbox producer for relay gateway Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * feat(tss): forward processed games to relay outbox Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * feat(tss-api): db helpers, app skeleton, info endpoint, fixtures Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * feat(tss-api): player, games, history, search endpoints Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * feat(tss-api): live, match, scoreboard, matches-search, maps Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * feat(tss-api): filter-required leaderboards (players/vehicles/stats) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * feat(tss-api): tournament list/detail/standings/matches Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * feat: wire tss upstream through gateway + tssbot-api PM2 app Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
70 lines
3.2 KiB
Python
70 lines
3.2 KiB
Python
from __future__ import annotations
|
|
|
|
from fastapi import APIRouter, HTTPException
|
|
|
|
from web.api import db
|
|
|
|
router = APIRouter()
|
|
|
|
|
|
def _window(start_date, end_date, tournament_id):
|
|
"""Return (where_sql, params) or raise 400 if no filter given."""
|
|
clauses: list[str] = []
|
|
params: list = []
|
|
if tournament_id is not None:
|
|
clauses.append("p.session_id IN (SELECT session_id FROM match_summary WHERE tournament_id = ?)")
|
|
params.append(tournament_id)
|
|
if start_date is not None:
|
|
clauses.append("p.endtime_unix >= ?"); params.append(start_date)
|
|
if end_date is not None:
|
|
clauses.append("p.endtime_unix <= ?"); params.append(end_date)
|
|
if not clauses:
|
|
raise HTTPException(status_code=400, detail={
|
|
"error": "a date window (start_date/end_date) or tournament_id is required",
|
|
"code": "FILTER_REQUIRED"})
|
|
return "WHERE " + " AND ".join(clauses), params
|
|
|
|
|
|
@router.get("/api/tss/leaderboard/players")
|
|
async def leaderboard_players(start_date: int | None = None, end_date: int | None = None,
|
|
tournament_id: int | None = None, limit: int = 100) -> dict:
|
|
where, params = _window(start_date, end_date, tournament_id)
|
|
params.append(max(1, min(limit, 500)))
|
|
rows = await db.query(db.battles_path(), f"""
|
|
SELECT p.UID uid, MAX(p.nick) nick, COUNT(*) battles,
|
|
SUM(CASE WHEN p.victor_bool='Win' THEN 1 ELSE 0 END) wins,
|
|
SUM(p.air_kills+p.ground_kills) kills, SUM(p.deaths) deaths, SUM(p.score) score
|
|
FROM player_games_hist p {where}
|
|
GROUP BY p.UID ORDER BY score DESC LIMIT ?""", tuple(params))
|
|
for r in rows:
|
|
r["uid"] = str(r["uid"])
|
|
return {"players": rows}
|
|
|
|
|
|
@router.get("/api/tss/leaderboard/vehicles")
|
|
async def leaderboard_vehicles(start_date: int | None = None, end_date: int | None = None,
|
|
tournament_id: int | None = None, limit: int = 100) -> dict:
|
|
where, params = _window(start_date, end_date, tournament_id)
|
|
params.append(max(1, min(limit, 500)))
|
|
rows = await db.query(db.battles_path(), f"""
|
|
SELECT p.vehicle_internal, MAX(p.vehicle) vehicle, COUNT(*) battles,
|
|
SUM(p.air_kills+p.ground_kills) kills, SUM(p.deaths) deaths, SUM(p.score) score
|
|
FROM player_games_hist p {where}
|
|
GROUP BY p.vehicle_internal ORDER BY battles DESC LIMIT ?""", tuple(params))
|
|
return {"vehicles": rows}
|
|
|
|
|
|
@router.get("/api/tss/leaderboard/stats")
|
|
async def leaderboard_stats(start_date: int | None = None, end_date: int | None = None,
|
|
tournament_id: int | None = None) -> dict:
|
|
where, params = _window(start_date, end_date, tournament_id)
|
|
totals = await db.query_one(db.battles_path(), f"""
|
|
SELECT COUNT(*) battles, COUNT(DISTINCT p.UID) players,
|
|
SUM(p.air_kills+p.ground_kills) kills, SUM(p.score) score
|
|
FROM player_games_hist p {where}""", tuple(params))
|
|
top = await db.query(db.battles_path(), f"""
|
|
SELECT p.vehicle_internal, MAX(p.vehicle) vehicle, COUNT(*) battles
|
|
FROM player_games_hist p {where}
|
|
GROUP BY p.vehicle_internal ORDER BY battles DESC LIMIT 10""", tuple(params))
|
|
return {"totals": totals or {}, "top_vehicles": top}
|