ai generated solutions to our ai generated problems

This commit is contained in:
2026-06-19 23:33:01 +01:00
parent 6745a2ff81
commit 9263d6f8eb
4 changed files with 278 additions and 1 deletions
+2
View File
@@ -15,11 +15,13 @@ It currently exposes:
- `GET /health`
- `GET /api/tss/leaderboard/teams?limit=100`
- `GET /api/tss/leaderboard/players?limit=100`
- `GET /api/tss/teams/resolve?name=...`
- `GET /api/tss/teams/search?q=...&limit=10`
- `GET /api/tss/teams/:team`
- `GET /api/tss/teams/:team/history`
- `GET /api/tss/teams/:team/games`
- `GET /api/tss/player/:uid`
## Local development
+130
View File
@@ -103,6 +103,11 @@ struct LeaderboardResponse {
teams: Vec<TeamLeaderboardRow>,
}
#[derive(Serialize)]
struct PlayerLeaderboardResponse {
players: Vec<PlayerLeaderboardRow>,
}
#[derive(Serialize)]
struct RecentGamesResponse {
matches: Vec<GameRow>,
@@ -138,6 +143,26 @@ struct TeamLeaderboardRow {
total_kills: i64,
}
#[derive(Serialize)]
struct PlayerLeaderboardRow {
uid: String,
nick: Option<String>,
total_battles: i64,
wins: i64,
losses: i64,
win_rate: f64,
ground_kills: i64,
air_kills: i64,
total_kills: i64,
assists: i64,
captures: i64,
deaths: i64,
score: i64,
kdr: f64,
teams_seen: i64,
last_seen: i64,
}
#[derive(Serialize)]
struct TeamDetail {
team_id: i64,
@@ -323,6 +348,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
let app = Router::new()
.route("/health", get(health))
.route("/api/tss/leaderboard/teams", get(leaderboard))
.route("/api/tss/leaderboard/players", get(player_leaderboard))
.route("/api/tss/games/recent", get(recent_games))
.route("/api/tss/games/{session_id}", get(game_detail))
.route("/api/tss/teams/resolve", get(resolve_team))
@@ -413,6 +439,16 @@ async fn leaderboard(
Ok(Json(LeaderboardResponse { teams }))
}
async fn player_leaderboard(
State(state): State<Arc<AppState>>,
Query(query): Query<LimitQuery>,
) -> ApiResult<PlayerLeaderboardResponse> {
let limit = i64::from(query.limit.unwrap_or(100).clamp(1, 100));
let battles_conn = open_db(&state.battles_db)?;
let players = player_leaderboard_rows(&battles_conn, limit)?;
Ok(Json(PlayerLeaderboardResponse { players }))
}
fn leaderboard_teams(state: &AppState, limit: usize) -> Result<Vec<TeamRecord>, ApiError> {
let teams_conn = open_db(&state.teams_db)?;
// Deduplicate teams by name across tournaments — pick the highest team_id
@@ -951,6 +987,100 @@ fn player_summaries_for(
Ok(out)
}
fn player_leaderboard_rows(
conn: &Connection,
limit: i64,
) -> Result<Vec<PlayerLeaderboardRow>, ApiError> {
let mut stmt = conn
.prepare(
"WITH per_session AS (
SELECT
UID,
session_id,
MAX(victor_bool) AS victor_bool,
MAX(ground_kills) AS ground_kills,
MAX(air_kills) AS air_kills,
MAX(assists) AS assists,
MAX(captures) AS captures,
MAX(deaths) AS deaths,
MAX(score) AS score,
MAX(endtime_unix) AS endtime_unix,
MAX(team_name) AS team_name
FROM player_games_hist
WHERE UID IS NOT NULL AND UID != ''
GROUP BY UID, session_id
),
latest_names AS (
SELECT UID, nick
FROM (
SELECT
UID,
nick,
ROW_NUMBER() OVER (
PARTITION BY UID
ORDER BY endtime_unix DESC, nick COLLATE NOCASE ASC
) AS rn
FROM player_games_hist
WHERE nick IS NOT NULL AND nick != ''
)
WHERE rn = 1
)
SELECT
p.UID,
n.nick,
COUNT(*) AS battles,
COALESCE(SUM(CASE WHEN UPPER(p.victor_bool) = 'WIN' THEN 1 ELSE 0 END), 0) AS wins,
COALESCE(SUM(CASE WHEN UPPER(p.victor_bool) = 'LOSS' THEN 1 ELSE 0 END), 0) AS losses,
COALESCE(SUM(p.ground_kills), 0) AS ground_kills,
COALESCE(SUM(p.air_kills), 0) AS air_kills,
COALESCE(SUM(p.assists), 0) AS assists,
COALESCE(SUM(p.captures), 0) AS captures,
COALESCE(SUM(p.deaths), 0) AS deaths,
COALESCE(SUM(p.score), 0) AS score,
COUNT(DISTINCT p.team_name) AS teams_seen,
MAX(p.endtime_unix) AS last_seen
FROM per_session p
LEFT JOIN latest_names n ON n.UID = p.UID
GROUP BY p.UID
ORDER BY score DESC, (ground_kills + air_kills) DESC, battles DESC, p.UID ASC
LIMIT ?1",
)
.map_err(db_error)?;
let players = stmt
.query_map(params![limit], |row| {
let ground: i64 = row.get(5)?;
let air: i64 = row.get(6)?;
let battles: i64 = row.get(2)?;
let wins: i64 = row.get(3)?;
let deaths: i64 = row.get(9)?;
let total_kills = ground + air;
Ok(PlayerLeaderboardRow {
uid: row.get(0)?,
nick: row.get(1)?,
total_battles: battles,
wins,
losses: row.get(4)?,
win_rate: percent(wins, battles),
ground_kills: ground,
air_kills: air,
total_kills,
assists: row.get(7)?,
captures: row.get(8)?,
deaths,
score: row.get(10)?,
kdr: ratio(total_kills, deaths),
teams_seen: row.get(11)?,
last_seen: row.get(12)?,
})
})
.map_err(db_error)?
.collect::<Result<Vec<_>, _>>()
.map_err(db_error)?;
Ok(players)
}
fn player_search(conn: &Connection, query: &str, limit: i64) -> Result<Vec<PlayerRef>, ApiError> {
let like = format!("%{}%", escape_like(query));
let mut stmt = conn
+131 -1
View File
@@ -15,6 +15,7 @@ const apiEndpoints = {
viewerEvent: '/api/viewers/event',
viewerDelete: '/api/viewers/delete',
teams: '/api/tss/leaderboard/teams?limit=100',
players: '/api/tss/leaderboard/players?limit=100',
homeTeams: '/api/tss/leaderboard/teams?limit=4',
teamsHealth: '/api/tss/leaderboard/teams?limit=1',
recentGames: '/api/tss/games/recent?limit=50',
@@ -29,6 +30,7 @@ const apiEndpoints = {
const navItems = [
{ path: '/', label: 'Home' },
{ path: '/teams', label: 'Team Leaderboard' },
{ path: '/players', label: 'Player Leaderboard' },
{ path: '/battle-logs', label: 'Battle Logs' },
{ path: '/viewers', label: 'Viewers' },
{ path: '/docs', label: 'Setup' },
@@ -74,6 +76,7 @@ async function fetchJson(path, signal) {
function parseRoute(pathname = window.location.pathname) {
if (pathname === '/') return { page: 'home', teamName: '' }
if (pathname === '/teams') return { page: 'teams', teamName: '' }
if (pathname === '/players') return { page: 'players', teamName: '' }
if (pathname === '/uptime') return { page: 'uptime', teamName: '' }
if (pathname === '/viewers') return { page: 'viewers', teamName: '' }
if (pathname === '/privacy') return { page: 'privacy', teamName: '' }
@@ -102,6 +105,10 @@ function gamePath(gameId) {
return `/games/${encodeURIComponent(gameId)}`
}
function playerPath(uid) {
return `/players/${encodeURIComponent(uid)}`
}
function formatNumber(value) {
return numberFormat.format(Number(value || 0))
}
@@ -420,6 +427,7 @@ function deviceType() {
function routeLabel(route) {
if (route.page === 'team' && route.teamName) return `Team: ${route.teamName}`
if (route.page === 'teams') return 'Team Leaderboard'
if (route.page === 'players') return 'Player Leaderboard'
if (route.page === 'battle-logs') return 'Battle Logs'
if (route.page === 'game') return route.gameId ? `Game ${route.gameId}` : 'Game'
if (route.page === 'uptime') return 'Uptime'
@@ -437,6 +445,7 @@ function currentPublicOrigin() {
function canonicalPathForRoute(route) {
if (route.page === 'team' && route.teamName) return teamPath(route.teamName)
if (route.page === 'teams') return '/teams'
if (route.page === 'players') return '/players'
if (route.page === 'battle-logs') return '/battle-logs'
if (route.page === 'game' && route.gameId) return gamePath(route.gameId)
if (route.page === 'uptime') return '/uptime'
@@ -496,6 +505,12 @@ function seoForRoute(route, profileDetail = null) {
robots: 'index, follow',
path: '/teams',
},
players: {
title: "TSS Player Leaderboard | Toothless' TSS Bot",
description: 'Browse the TSS player leaderboard, compare War Thunder player score, kills, win rate, KDR, and battle activity.',
robots: 'index, follow',
path: '/players',
},
'battle-logs': {
title: "TSS Battle Logs | Toothless' TSS Bot",
description: 'Read recent TSS battle logs with team names, map history, player counts, battle times, and War Thunder match context.',
@@ -912,6 +927,7 @@ function App() {
function AppContent() {
const [route, setRoute] = useState(() => parseRoute())
const [leaderboard, setLeaderboard] = useState({ status: 'idle', data: null, error: null })
const [playerLeaderboard, setPlayerLeaderboard] = useState({ status: 'idle', data: null, error: null })
const [homeTeams, setHomeTeams] = useState({ status: 'idle', data: null, error: null })
const [live, setLive] = useState({ status: 'idle', data: null, error: null, updatedAt: 0 })
const [uptime, setUptime] = useState({ status: 'idle', checks: [], history: [], updatedAt: null })
@@ -935,6 +951,10 @@ function AppContent() {
() => leaderboard.data?.teams || leaderboard.data?.squadrons || [],
[leaderboard.data],
)
const players = useMemo(
() => playerLeaderboard.data?.players || [],
[playerLeaderboard.data],
)
const teamsToWatch = useMemo(
() => homeTeams.data?.teams || homeTeams.data?.squadrons || teams.slice(0, 4),
[homeTeams.data, teams],
@@ -1187,6 +1207,24 @@ function AppContent() {
return () => controller.abort()
}, [leaderboard.status, route.page])
useEffect(() => {
if (route.page !== 'players') return
if (playerLeaderboard.status === 'ready' || playerLeaderboard.status === 'loading') return
const controller = new AbortController()
setPlayerLeaderboard({ status: 'loading', data: null, error: null })
fetchJson(apiEndpoints.players, controller.signal)
.then((data) => setPlayerLeaderboard({ status: 'ready', data, error: null }))
.catch((error) => {
if (!controller.signal.aborted) {
setPlayerLeaderboard({ status: 'error', data: null, error: error.message })
}
})
return () => controller.abort()
}, [playerLeaderboard.status, route.page])
useEffect(() => {
if (route.page !== 'home') return
if (homeTeams.status === 'ready' || homeTeams.status === 'loading') return
@@ -1558,6 +1596,8 @@ function AppContent() {
const activeNavPath =
route.page === 'team'
? '/teams'
: route.page === 'player' || route.page === 'players'
? '/players'
: route.page === 'battle-logs' || route.page === 'game'
? '/battle-logs'
: route.page === 'viewers'
@@ -1586,6 +1626,26 @@ function AppContent() {
}
}, [route.page])
useEffect(() => {
if (route.page !== 'players') return
const controller = new AbortController()
const timer = window.setInterval(() => {
fetchJson(apiEndpoints.players, controller.signal)
.then((data) => setPlayerLeaderboard({ status: 'ready', data, error: null }))
.catch((error) => {
if (!controller.signal.aborted) {
setPlayerLeaderboard((current) => ({ ...current, error: error.message }))
}
})
}, 60000)
return () => {
window.clearInterval(timer)
controller.abort()
}
}, [route.page])
return (
<main className="min-h-screen bg-bg text-text">
<header
@@ -1654,6 +1714,9 @@ function AppContent() {
{route.page === 'teams' ? (
<TeamsPage leaderboard={leaderboard} navigate={navigate} teams={teams} />
) : null}
{route.page === 'players' ? (
<PlayersPage leaderboard={playerLeaderboard} navigate={navigate} players={players} />
) : null}
{route.page === 'team' ? (
<TeamProfilePage
navigate={navigate}
@@ -2723,6 +2786,73 @@ function TeamsPage({ leaderboard, navigate, teams }) {
)
}
function PlayersPage({ leaderboard, navigate, players }) {
return (
<section className="space-y-6 pt-24 sm:pt-28">
<div>
<h1 className="text-3xl font-bold">Player Leaderboard</h1>
<p className="mt-2 text-sm text-text-soft">
{leaderboard.status === 'loading'
? 'Loading player leaderboard'
: leaderboard.error || `${players.length} players returned`}
</p>
</div>
<div className="overflow-hidden rounded-lg border border-border bg-fury-white shadow-sm">
<div className="overflow-x-auto">
<div className="min-w-[900px]">
{players.length ? (
<div className="grid grid-cols-[4rem_minmax(220px,1fr)_repeat(7,92px)] gap-3 border-b border-surface px-5 py-3 text-xs font-semibold uppercase tracking-wide text-text-soft">
<p>Rank</p>
<p>Player</p>
<p className="text-right">Score</p>
<p className="text-right">Battles</p>
<p className="text-right">Kills</p>
<p className="text-right">Assists</p>
<p className="text-right">WR</p>
<p className="text-right">KDR</p>
<p className="text-right">Teams</p>
</div>
) : null}
{players.map((player, index) => (
<button
className="grid w-full grid-cols-[4rem_minmax(220px,1fr)_repeat(7,92px)] gap-3 border-b border-surface px-5 py-4 text-left text-sm transition hover:bg-surface"
key={player.uid}
onClick={() => navigate(playerPath(player.uid))}
type="button"
>
<p className="font-semibold text-fury-cyan">#{index + 1}</p>
<div className="min-w-0">
<p className="truncate text-base font-semibold">{player.nick || player.uid}</p>
<p className="truncate text-xs text-text-soft">
{player.uid} · last seen {formatDate(player.last_seen)}
</p>
</div>
<p className="text-right font-semibold">{formatNumber(player.score)}</p>
<p className="text-right">{formatNumber(player.total_battles)}</p>
<p className="text-right">{formatNumber(player.total_kills)}</p>
<p className="text-right">{formatNumber(player.assists)}</p>
<p className="text-right">{Number(player.win_rate || 0).toFixed(1)}%</p>
<p className="text-right">{Number(player.kdr || 0).toFixed(2)}</p>
<p className="text-right">{formatNumber(player.teams_seen)}</p>
</button>
))}
</div>
</div>
{!players.length ? (
<p className="px-5 py-10 text-sm text-text-soft">
{leaderboard.status === 'loading'
? 'Loading player leaderboard'
: leaderboard.error || 'No players returned'}
</p>
) : null}
</div>
</section>
)
}
function TeamProfilePage({ navigate, profile, requestedTeam, teams }) {
const detail = profile.detail.data
const summary = detail?.team_summary || detail?.squadron_summary
@@ -2872,7 +3002,7 @@ function GamePage({ gameId, navigate }) {
<button
className="grid w-full grid-cols-[minmax(220px,1fr)_80px_repeat(5,88px)] gap-3 px-5 py-2 text-left text-sm transition hover:bg-surface"
key={player.uid}
onClick={() => navigate(`/players/${encodeURIComponent(player.uid)}`)}
onClick={() => navigate(playerPath(player.uid))}
type="button"
>
<div className="min-w-0 pl-8 sm:pl-12">
+15
View File
@@ -1572,6 +1572,15 @@ function allowedApiTarget(req) {
return url
}
if (pathname === '/api/tss/leaderboard/players') {
const keys = [...params.keys()]
const limit = Number(params.get('limit') || 100)
if (keys.some((key) => key !== 'limit') || !Number.isInteger(limit) || limit < 1 || limit > 100) {
return null
}
return url
}
if (pathname === '/api/tss/games/recent') {
const keys = [...params.keys()]
const limit = Number(params.get('limit') || 50)
@@ -1805,6 +1814,12 @@ function routeSeo(pathname) {
robots: 'index, follow',
path: '/teams',
},
'/players': {
title: "TSS Player Leaderboard | Toothless' TSS Bot",
description: 'Browse the TSS player leaderboard, compare War Thunder player score, kills, win rate, KDR, and battle activity.',
robots: 'index, follow',
path: '/players',
},
'/battle-logs': {
title: "TSS Battle Logs | Toothless' TSS Bot",
description: 'Read recent TSS battle logs with team names, map history, player counts, battle times, and War Thunder match context.',