Auto merge dev → main (#1353)

* 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>
This commit is contained in:
NotSoToothless
2026-06-28 03:38:20 -07:00
committed by GitHub
parent f2a1a33c28
commit 24335a2677
18 changed files with 777 additions and 0 deletions
+69
View File
@@ -0,0 +1,69 @@
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}