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>
77 lines
2.8 KiB
Python
77 lines
2.8 KiB
Python
from __future__ import annotations
|
|
|
|
from fastapi import APIRouter, HTTPException
|
|
|
|
from web.api import db
|
|
|
|
router = APIRouter()
|
|
|
|
|
|
def _nullify(row: dict, keys: tuple[str, ...]) -> dict:
|
|
for k in keys:
|
|
if row.get(k) == "":
|
|
row[k] = None
|
|
return row
|
|
|
|
|
|
@router.get("/api/tss/tournaments")
|
|
async def tournaments(status: str | None = None, game_mode: str | None = None,
|
|
cluster: str | None = None, limit: int = 100) -> dict:
|
|
clauses: list[str] = []
|
|
params: list = []
|
|
if status:
|
|
clauses.append("status = ?"); params.append(status)
|
|
if game_mode:
|
|
clauses.append("game_mode = ?"); params.append(game_mode)
|
|
if cluster:
|
|
clauses.append("cluster = ?"); params.append(cluster)
|
|
where = ("WHERE " + " AND ".join(clauses)) if clauses else ""
|
|
params.append(max(1, min(limit, 500)))
|
|
rows = await db.query(db.tournaments_path(),
|
|
f"SELECT * FROM tournaments {where} ORDER BY date_end DESC LIMIT ?", tuple(params))
|
|
return {"total": len(rows), "tournaments": rows}
|
|
|
|
|
|
async def _matches(tid: int) -> list[dict]:
|
|
rows = await db.query(db.tournaments_path(),
|
|
"SELECT * FROM tournament_matches WHERE tournament_id = ? ORDER BY round, position",
|
|
(tid,))
|
|
battles = await db.query(db.tournaments_path(),
|
|
"SELECT match_id, type_bracket, session_hex FROM tournament_battles WHERE tournament_id = ?",
|
|
(tid,))
|
|
by_match: dict[tuple, list[str]] = {}
|
|
for b in battles:
|
|
by_match.setdefault((b["match_id"], b["type_bracket"]), []).append(b["session_hex"])
|
|
out = []
|
|
for m in rows:
|
|
_nullify(m, ("round", "position"))
|
|
m["session_ids"] = by_match.get((m["match_id"], m["type_bracket"]), [])
|
|
out.append(m)
|
|
return out
|
|
|
|
|
|
@router.get("/api/tss/tournament/{tid}")
|
|
async def tournament(tid: int) -> dict:
|
|
meta = await db.query_one(db.tournaments_path(),
|
|
"SELECT * FROM tournaments WHERE tournament_id = ?", (tid,))
|
|
if not meta:
|
|
raise HTTPException(status_code=404, detail=f"tournament {tid} not found")
|
|
standings = await db.query(db.tournaments_path(),
|
|
"SELECT * FROM tournament_standings WHERE tournament_id = ? ORDER BY group_index, rank",
|
|
(tid,))
|
|
return {"tournament": meta, "standings": standings, "matches": await _matches(tid)}
|
|
|
|
|
|
@router.get("/api/tss/tournament/{tid}/standings")
|
|
async def standings(tid: int) -> dict:
|
|
rows = await db.query(db.tournaments_path(),
|
|
"SELECT * FROM tournament_standings WHERE tournament_id = ? ORDER BY group_index, rank",
|
|
(tid,))
|
|
return {"tournament_id": tid, "standings": rows}
|
|
|
|
|
|
@router.get("/api/tss/tournament/{tid}/matches")
|
|
async def tournament_matches(tid: int) -> dict:
|
|
rows = await _matches(tid)
|
|
return {"tournament_id": tid, "total": len(rows), "matches": rows}
|