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
+77
View File
@@ -0,0 +1,77 @@
from __future__ import annotations
from fastapi import APIRouter, HTTPException
from web.api import db
router = APIRouter()
@router.get("/api/tss/player/{uid}")
async def player(uid: str) -> dict:
bp = db.battles_path()
summary = await db.query_one(bp, """
SELECT COUNT(*) battles,
SUM(CASE WHEN victor_bool='Win' THEN 1 ELSE 0 END) wins,
SUM(CASE WHEN victor_bool!='Win' THEN 1 ELSE 0 END) losses,
SUM(ground_kills) ground_kills, SUM(air_kills) air_kills,
SUM(assists) assists, SUM(captures) captures, SUM(deaths) deaths,
SUM(score) score, AVG(pvp_ratio) avg_pvp_ratio
FROM player_games_hist WHERE UID = ?""", (uid,))
if not summary or summary["battles"] == 0:
raise HTTPException(status_code=404, detail=f"player {uid} not found")
vehicles = await db.query(bp, """
SELECT vehicle, vehicle_internal, COUNT(*) battles, SUM(air_kills+ground_kills) kills,
SUM(deaths) deaths, SUM(score) score
FROM player_games_hist WHERE UID = ?
GROUP BY vehicle_internal ORDER BY battles DESC""", (uid,))
nicks = await db.query(bp, """
SELECT nick, MIN(endtime_unix) first_seen, MAX(endtime_unix) last_seen
FROM player_games_hist WHERE UID = ? GROUP BY nick ORDER BY last_seen DESC""", (uid,))
team_history = await db.query(bp, """
SELECT team_id, team_name, tss_role, COUNT(*) battles,
MIN(endtime_unix) first_seen, MAX(endtime_unix) last_seen
FROM player_games_hist WHERE UID = ?
GROUP BY team_id, team_name, tss_role ORDER BY last_seen DESC""", (uid,))
win_rate = round(summary["wins"] / summary["battles"], 4) if summary["battles"] else 0.0
return {"uid": str(uid), "summary": {**summary, "win_rate": win_rate},
"vehicles": vehicles, "nicks": nicks, "team_history": team_history}
@router.get("/api/tss/player/{uid}/games")
async def player_games(uid: str, limit: int = 100,
time_from: int | None = None, time_to: int | None = None) -> dict:
clauses = ["p.UID = ?"]
params: list = [uid]
if time_from is not None:
clauses.append("p.endtime_unix >= ?"); params.append(time_from)
if time_to is not None:
clauses.append("p.endtime_unix <= ?"); params.append(time_to)
params.append(max(1, min(limit, 1000)))
rows = await db.query(db.battles_path(), f"""
SELECT p.*, m.mission_name, m.tournament_name
FROM player_games_hist p LEFT JOIN match_summary m ON m.session_id = p.session_id
WHERE {' AND '.join(clauses)} ORDER BY p.endtime_unix DESC LIMIT ?""", tuple(params))
return {"uid": str(uid), "total": len(rows), "games": rows}
@router.get("/api/tss/player/{uid}/history")
async def player_history(uid: str) -> dict:
rows = await db.query(db.battles_path(), """
SELECT date(endtime_unix, 'unixepoch') day, COUNT(*) battles,
SUM(CASE WHEN victor_bool='Win' THEN 1 ELSE 0 END) wins,
SUM(air_kills+ground_kills) kills, SUM(deaths) deaths, SUM(score) score
FROM player_games_hist WHERE UID = ? GROUP BY day ORDER BY day DESC""", (uid,))
return {"uid": str(uid), "days_with_battles_only": True, "history": rows}
@router.get("/api/tss/search/{nick}")
async def search(nick: str, limit: int = 25) -> dict:
rows = await db.query(db.battles_path(), """
SELECT UID uid, nick, MAX(endtime_unix) last_seen, COUNT(*) battles
FROM player_games_hist WHERE nick LIKE ? COLLATE NOCASE
GROUP BY UID, nick ORDER BY last_seen DESC LIMIT ?""",
(f"%{nick}%", max(1, min(limit, 100))))
for r in rows:
r["uid"] = str(r["uid"])
return {"query": nick, "players": rows}