import { useEffect, useMemo, useRef, useState } from 'react' import L from 'leaflet' import 'leaflet/dist/leaflet.css' import Tree from '../Tree/Tree' import FallingLeaves from '../Tree/FallingLeaves' const numberFormat = new Intl.NumberFormat('en-GB') const dateFormat = new Intl.DateTimeFormat('en-GB', { dateStyle: 'medium', timeStyle: 'short', }) const apiEndpoints = { health: '/health', uptime: '/api/uptime', viewers: '/api/viewers', viewerEvent: '/api/viewers/event', viewerDelete: '/api/viewers/delete', teams: '/api/tss/leaderboard/teams?limit=100', teamsHealth: '/api/tss/leaderboard/teams?limit=1', resolve: (name) => `/api/tss/teams/resolve?name=${encodeURIComponent(name)}`, detail: (name) => `/api/tss/teams/${encodeURIComponent(name)}`, history: (name) => `/api/tss/teams/${encodeURIComponent(name)}/history`, games: (name) => `/api/tss/teams/${encodeURIComponent(name)}/games`, } const navItems = [ { path: '/', label: 'Home' }, { path: '/teams', label: 'Team leaderboard' }, { path: '/battle-logs', label: 'Battle Logs' }, { path: '/viewers', label: 'Viewers' }, ] const analyticsConsentKey = 'tssbot.analyticsConsent' const analyticsPreferencesKey = 'tssbot.analyticsPreferences' const analyticsPreferencesCookie = 'tssbot_analytics_preferences' const analyticsVisitorKey = 'tssbot.analyticsVisitor' const analyticsConsentVersion = 3 const defaultAnalyticsPreferences = { chosen: false, analytics: false, device: false, display: false, locale: false, referrer: false, diagnostics: false, version: analyticsConsentVersion, } async function fetchJson(path, signal) { const response = await fetch(path, { signal, headers: { Accept: 'application/json' }, }) const body = await response.json().catch(() => null) if (!response.ok) { throw new Error(body?.error || `Request failed with ${response.status}`) } return body } function parseRoute(pathname = window.location.pathname) { if (pathname === '/') return { page: 'home', teamName: '' } if (pathname === '/teams') return { page: 'teams', teamName: '' } if (pathname === '/uptime') return { page: 'uptime', teamName: '' } if (pathname === '/viewers') return { page: 'viewers', teamName: '' } if (pathname === '/privacy') return { page: 'privacy', teamName: '' } if (pathname.startsWith('/teams/')) { const teamName = decodeURIComponent(pathname.slice('/teams/'.length)) return { page: 'team', teamName } } if (pathname === '/battle-logs' || pathname === '/live') return { page: 'battle-logs', teamName: '' } return { page: 'home', teamName: '' } } function teamPath(name) { return `/teams/${encodeURIComponent(name)}` } function formatNumber(value) { return numberFormat.format(Number(value || 0)) } function formatDate(timestamp) { if (!timestamp) return 'Unknown time' return dateFormat.format(new Date(Number(timestamp) * 1000)) } function bestTeamName(team) { return team?.tag_name || team?.short_name || team?.long_name || '' } function normalizeAnalyticsPreferences(value) { if (!value || typeof value !== 'object') return { ...defaultAnalyticsPreferences } return { chosen: Boolean(value.chosen) && value.version === analyticsConsentVersion, analytics: Boolean(value.analytics), device: Boolean(value.device), display: Boolean(value.display), locale: Boolean(value.locale), referrer: Boolean(value.referrer), diagnostics: Boolean(value.diagnostics), version: analyticsConsentVersion, } } function browserPrivacySignalEnabled() { return Boolean( navigator.globalPrivacyControl || navigator.doNotTrack === '1' || window.doNotTrack === '1', ) } function browserConnectionDetails() { const connection = navigator.connection || navigator.mozConnection || navigator.webkitConnection || null if (!connection) return {} return { effective_type: connection.effectiveType || '', downlink_mbps: Number.isFinite(connection.downlink) ? connection.downlink : null, rtt_ms: Number.isFinite(connection.rtt) ? connection.rtt : null, save_data: Boolean(connection.saveData), } } function browserDiagnosticsMetadata() { return { cookies_enabled: navigator.cookieEnabled, do_not_track: navigator.doNotTrack || window.doNotTrack || '', global_privacy_control: Boolean(navigator.globalPrivacyControl), online: navigator.onLine, platform: navigator.platform || '', vendor: navigator.vendor || '', max_touch_points: navigator.maxTouchPoints || 0, hardware_concurrency: navigator.hardwareConcurrency || null, device_memory_gb: navigator.deviceMemory || null, webdriver: Boolean(navigator.webdriver), history_length: window.history.length, visibility_state: document.visibilityState, connection: browserConnectionDetails(), } } function readCookie(name) { try { const match = document.cookie .split('; ') .find((item) => item.startsWith(`${encodeURIComponent(name)}=`)) return match ? decodeURIComponent(match.split('=').slice(1).join('=')) : '' } catch { return '' } } function writeCookie(name, value) { try { const sixMonths = 60 * 60 * 24 * 180 document.cookie = `${encodeURIComponent(name)}=${encodeURIComponent(value)}; Max-Age=${sixMonths}; Path=/; SameSite=Lax` } catch { // Some browsers block cookies; local storage and in-memory state still work. } } function storedAnalyticsPreferences() { const cookieValue = readCookie(analyticsPreferencesCookie) if (cookieValue) { try { return normalizeAnalyticsPreferences(JSON.parse(cookieValue)) } catch { // Fall back through the older storage formats below. } } try { const stored = window.localStorage.getItem(analyticsPreferencesKey) if (stored) return normalizeAnalyticsPreferences(JSON.parse(stored)) } catch { // Fall back through the older consent flag below. } try { const legacyConsent = window.localStorage.getItem(analyticsConsentKey) || '' if (legacyConsent === 'analytics') { return { ...defaultAnalyticsPreferences, chosen: true, analytics: true, device: true, display: true, locale: true, referrer: true, diagnostics: true, } } if (legacyConsent === 'declined') { return { ...defaultAnalyticsPreferences, chosen: true, analytics: false } } } catch { // Use the default prompt state. } return { ...defaultAnalyticsPreferences } } function persistAnalyticsPreferences(preferences) { const normalized = normalizeAnalyticsPreferences(preferences) const serialized = JSON.stringify(normalized) writeCookie(analyticsPreferencesCookie, serialized) try { window.localStorage.setItem(analyticsPreferencesKey, serialized) window.localStorage.setItem( analyticsConsentKey, normalized.analytics ? 'analytics' : 'declined', ) } catch { // Local storage can be blocked; the cookie and in-memory choice still apply. } return normalized } function stableId(storageKey) { try { const existing = window.localStorage.getItem(storageKey) if (existing) return existing const id = window.crypto?.randomUUID?.() || `${Date.now().toString(36)}${Math.random().toString(36).slice(2)}` window.localStorage.setItem(storageKey, id) return id } catch { return `${Date.now().toString(36)}${Math.random().toString(36).slice(2)}` } } function storedVisitorId(storageKey) { try { return window.localStorage.getItem(storageKey) || '' } catch { return '' } } function forgetVisitorId(storageKey) { try { window.localStorage.removeItem(storageKey) } catch { // Storage may be unavailable; nothing else to clear locally. } } const analyticsSessionId = window.crypto?.randomUUID?.() || `${Date.now().toString(36)}${Math.random().toString(36).slice(2)}` function browserName() { const ua = navigator.userAgent if (ua.includes('Edg/')) return 'Microsoft Edge' if (ua.includes('OPR/')) return 'Opera' if (ua.includes('Firefox/')) return 'Firefox' if (ua.includes('Chrome/') && !ua.includes('Chromium/')) return 'Chrome' if (ua.includes('Safari/') && ua.includes('Version/')) return 'Safari' return 'Unknown' } function operatingSystem() { const ua = navigator.userAgent if (ua.includes('Windows NT')) return 'Windows' if (ua.includes('Android')) return 'Android' if (/iPhone|iPad|iPod/.test(ua)) return 'iOS' if (ua.includes('Mac OS X')) return 'macOS' if (ua.includes('Linux')) return 'Linux' return 'Unknown' } function deviceType() { const ua = navigator.userAgent if (/Mobi|Android|iPhone|iPod/.test(ua)) return 'Mobile' if (/iPad|Tablet/.test(ua)) return 'Tablet' return 'Desktop' } function routeLabel(route) { if (route.page === 'team' && route.teamName) return `Team: ${route.teamName}` if (route.page === 'teams') return 'Team leaderboard' if (route.page === 'battle-logs') return 'Battle Logs' if (route.page === 'uptime') return 'Uptime' if (route.page === 'viewers') return 'viewers' if (route.page === 'privacy') return 'Privacy notice' return 'Home' } async function fetchRecentTssGames(teams, signal) { const teamNames = teams.map(bestTeamName).filter(Boolean).slice(0, 12) if (!teamNames.length) { return { matches: [] } } const responses = await Promise.allSettled( teamNames.map((name) => fetchJson(apiEndpoints.games(name), signal).then((data) => ({ name, data }))), ) const bySession = new Map() responses.forEach((result) => { if (result.status !== 'fulfilled') return const { name, data } = result.value ;(data.games || []).forEach((game) => { if (!game.session_id) return const existing = bySession.get(game.session_id) const currentTimestamp = Number(game.timestamp || 0) if (existing && Number(existing.timestamp || 0) >= currentTimestamp) return bySession.set(game.session_id, { ...game, team_name: data.tag_name || name, long_name: data.long_name || '', }) }) }) return { matches: Array.from(bySession.values()) .sort((a, b) => Number(b.timestamp || 0) - Number(a.timestamp || 0)) .slice(0, 50), } } function Stat({ label, value }) { return (

{label}

{value}

) } function App() { const [route, setRoute] = useState(() => parseRoute()) const [leaderboard, setLeaderboard] = useState({ status: 'idle', data: null, error: null }) const [live, setLive] = useState({ status: 'idle', data: null, error: null }) const [uptime, setUptime] = useState({ status: 'idle', checks: [], history: [], updatedAt: null }) const [viewers, setViewers] = useState({ status: 'idle', data: null, error: null, updatedAt: null }) const [analyticsPreferences, setAnalyticsPreferences] = useState(() => storedAnalyticsPreferences()) const [showFloatingNav, setShowFloatingNav] = useState(() => window.scrollY > 40) const [teamQuery, setTeamQuery] = useState('') const [searchHint, setSearchHint] = useState({ status: 'idle', name: '' }) const [profile, setProfile] = useState({ teamName: '', detail: { status: 'idle', data: null, error: null }, history: { status: 'idle', data: null, error: null }, games: { status: 'idle', data: null, error: null }, }) const teams = useMemo( () => leaderboard.data?.teams || leaderboard.data?.squadrons || [], [leaderboard.data], ) const matches = live.data?.matches || [] function navigate(path) { window.history.pushState({}, '', path) setRoute(parseRoute(path)) } useEffect(() => { const onPopState = () => setRoute(parseRoute()) window.addEventListener('popstate', onPopState) return () => window.removeEventListener('popstate', onPopState) }, []) useEffect(() => { const onScroll = () => setShowFloatingNav(window.scrollY > 40) onScroll() window.addEventListener('scroll', onScroll, { passive: true }) return () => window.removeEventListener('scroll', onScroll) }, []) useEffect(() => { const title = route.page === 'team' && route.teamName ? `${route.teamName} | Toothless' TSS Bot` : route.page === 'teams' ? "Team leaderboard | Toothless' TSS Bot" : route.page === 'battle-logs' ? "Battle Logs | Toothless' TSS Bot" : route.page === 'uptime' ? "Uptime | Toothless' TSS Bot" : route.page === 'viewers' ? "Viewers | Toothless' TSS Bot" : route.page === 'privacy' ? "Privacy notice | Toothless' TSS Bot" : "Toothless' TSS Bot" document.title = title }, [route.page, route.teamName]) useEffect(() => { if (!analyticsPreferences.analytics) return const visitorId = stableId(analyticsVisitorKey) let stopped = false const deviceDetails = analyticsPreferences.device const displayDetails = analyticsPreferences.display const localeDetails = analyticsPreferences.locale const referrerDetails = analyticsPreferences.referrer const diagnosticsDetails = analyticsPreferences.diagnostics function sendViewerEvent(eventType) { if (stopped) return const metadata = { preferences: { device: deviceDetails, display: displayDetails, locale: localeDetails, referrer: referrerDetails, diagnostics: diagnosticsDetails, }, } if (deviceDetails) { metadata.device = { user_agent_length: navigator.userAgent.length, platform: navigator.platform || '', vendor: navigator.vendor || '', max_touch_points: navigator.maxTouchPoints || 0, } } if (displayDetails) { metadata.display = { color_depth: window.screen.colorDepth, pixel_depth: window.screen.pixelDepth, viewport: `${window.innerWidth}x${window.innerHeight}`, device_pixel_ratio: window.devicePixelRatio || 1, orientation: window.screen.orientation?.type || '', } } if (localeDetails) { metadata.locale = { languages: Array.isArray(navigator.languages) ? navigator.languages.slice(0, 8) : [], timezone_offset_minutes: new Date().getTimezoneOffset(), } } if (diagnosticsDetails) { metadata.diagnostics = browserDiagnosticsMetadata() } const body = { consent: 'analytics', event_type: eventType, visitor_id: visitorId, session_id: analyticsSessionId, page_path: window.location.pathname, page_title: routeLabel(route), referrer: referrerDetails ? document.referrer : '', user_agent: deviceDetails ? navigator.userAgent : 'Not shared', browser: deviceDetails ? browserName() : 'Not shared', os: deviceDetails ? operatingSystem() : 'Not shared', device: deviceDetails ? deviceType() : 'Not shared', screen: displayDetails ? `${window.screen.width}x${window.screen.height}` : '', language: localeDetails ? navigator.language : '', timezone: localeDetails ? Intl.DateTimeFormat().resolvedOptions().timeZone : '', metadata, } fetch(apiEndpoints.viewerEvent, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify(body), keepalive: true, }).catch(() => {}) } sendViewerEvent('page_view') const timer = window.setInterval(() => sendViewerEvent('heartbeat'), 30000) const onVisibilityChange = () => { if (document.visibilityState === 'visible') sendViewerEvent('heartbeat') } document.addEventListener('visibilitychange', onVisibilityChange) return () => { stopped = true window.clearInterval(timer) document.removeEventListener('visibilitychange', onVisibilityChange) } }, [analyticsPreferences, route]) useEffect(() => { const onKeyDown = (event) => { if (event.key === 'Escape') { navigate('/') } } window.addEventListener('keydown', onKeyDown) return () => window.removeEventListener('keydown', onKeyDown) }, []) useEffect(() => { const query = teamQuery.trim() if (query.length < 2) { setSearchHint({ status: 'idle', name: '' }) return } const controller = new AbortController() const timer = window.setTimeout(() => { setSearchHint({ status: 'loading', name: '' }) fetchJson(apiEndpoints.resolve(query), controller.signal) .then((data) => { const name = data.tag_name || data.short_name || data.long_name || query setSearchHint({ status: 'ready', name }) }) .catch(() => { if (!controller.signal.aborted) { setSearchHint({ status: 'error', name: '' }) } }) }, 350) return () => { window.clearTimeout(timer) controller.abort() } }, [teamQuery]) useEffect(() => { if (!['home', 'teams', 'team', 'battle-logs'].includes(route.page)) return if (leaderboard.status === 'ready' || leaderboard.status === 'loading') return const controller = new AbortController() setLeaderboard({ status: 'loading', data: null, error: null }) fetchJson(apiEndpoints.teams, controller.signal) .then((data) => setLeaderboard({ status: 'ready', data, error: null })) .catch((error) => { if (!controller.signal.aborted) { setLeaderboard({ status: 'error', data: null, error: error.message }) } }) return () => controller.abort() }, [leaderboard.status, route.page]) useEffect(() => { if (!['home', 'teams', 'team', 'battle-logs'].includes(route.page)) return const controller = new AbortController() const timer = window.setInterval(() => { fetchJson(apiEndpoints.teams, controller.signal) .then((data) => setLeaderboard({ status: 'ready', data, error: null })) .catch((error) => { if (!controller.signal.aborted) { setLeaderboard((current) => ({ ...current, error: error.message })) } }) }, 60000) return () => { window.clearInterval(timer) controller.abort() } }, [route.page]) useEffect(() => { if (!['home', 'battle-logs'].includes(route.page)) return if (!teams.length) return const controller = new AbortController() setLive((current) => current.status === 'ready' ? current : { status: 'loading', data: null, error: null }, ) fetchRecentTssGames(teams, controller.signal) .then((data) => setLive({ status: 'ready', data, error: null })) .catch((error) => { if (!controller.signal.aborted) { setLive({ status: 'error', data: null, error: error.message }) } }) return () => controller.abort() }, [route.page, teams]) useEffect(() => { if (!['home', 'battle-logs'].includes(route.page)) return if (!teams.length) return const controller = new AbortController() const timer = window.setInterval(() => { fetchRecentTssGames(teams, controller.signal) .then((data) => setLive({ status: 'ready', data, error: null })) .catch((error) => { if (!controller.signal.aborted) { setLive((current) => ({ ...current, error: error.message })) } }) }, 15000) return () => { window.clearInterval(timer) controller.abort() } }, [route.page, teams]) useEffect(() => { if (route.page !== 'team' || !route.teamName) return const controller = new AbortController() setProfile({ teamName: route.teamName, detail: { status: 'loading', data: null, error: null }, history: { status: 'loading', data: null, error: null }, games: { status: 'loading', data: null, error: null }, }) Promise.allSettled([ fetchJson(apiEndpoints.detail(route.teamName), controller.signal), fetchJson(apiEndpoints.history(route.teamName), controller.signal), fetchJson(apiEndpoints.games(route.teamName), controller.signal), ]).then(([detailResult, historyResult, gamesResult]) => { if (controller.signal.aborted) return setProfile({ teamName: route.teamName, detail: detailResult.status === 'fulfilled' ? { status: 'ready', data: detailResult.value, error: null } : { status: 'error', data: null, error: detailResult.reason.message }, history: historyResult.status === 'fulfilled' ? { status: 'ready', data: historyResult.value, error: null } : { status: 'error', data: null, error: historyResult.reason.message }, games: gamesResult.status === 'fulfilled' ? { status: 'ready', data: gamesResult.value, error: null } : { status: 'error', data: null, error: gamesResult.reason.message }, }) }) return () => controller.abort() }, [route.page, route.teamName]) useEffect(() => { if (route.page !== 'uptime') return const controller = new AbortController() async function loadUptime() { setUptime((current) => ({ status: current.status === 'ready' ? 'refreshing' : 'loading', checks: current.checks, history: current.history, updatedAt: current.updatedAt, })) fetchJson(apiEndpoints.uptime, controller.signal) .then((data) => { const latest = data.latest const checks = latest ? [ { name: 'Website', detail: 'App shell and static assets', ok: latest.website_ok, label: latest.details?.website?.label || (latest.website_ok ? 'Online' : 'Issue'), latency: latest.latency_ms, }, { name: 'Health endpoint', detail: apiEndpoints.health, ok: latest.health_ok, label: latest.details?.health?.label || (latest.health_ok ? 'Operational' : 'Issue'), latency: latest.latency_ms, }, { name: 'TSS data proxy', detail: apiEndpoints.teamsHealth, ok: latest.tss_ok, label: latest.details?.tss?.label || (latest.tss_ok ? 'Operational' : 'Issue'), latency: latest.details?.tss?.latency_ms || latest.latency_ms, }, ] : [] setUptime({ status: 'ready', checks, history: (data.history || []).map((sample) => ({ timestamp: new Date(sample.checked_at).getTime(), onlineChecks: [sample.website_ok, sample.health_ok, sample.tss_ok].filter(Boolean).length, totalChecks: 3, ok: sample.ok, })), updatedAt: latest ? new Date(latest.checked_at).getTime() : null, configured: data.configured, }) }) .catch((error) => { if (controller.signal.aborted) return setUptime({ status: 'error', updatedAt: null, configured: false, history: [], checks: [ { name: 'Website', detail: 'App shell and static assets', ok: true, label: 'Online', latency: 0, }, { name: 'Health endpoint', detail: apiEndpoints.health, ok: false, label: error.message, latency: 0, }, { name: 'TSS data proxy', detail: apiEndpoints.teamsHealth, ok: false, label: 'Uptime history unavailable', latency: 0, }, ], }) }) } loadUptime() const timer = window.setInterval(loadUptime, 60000) return () => { window.clearInterval(timer) controller.abort() } }, [route.page]) useEffect(() => { if (route.page !== 'viewers') return const controller = new AbortController() function loadViewers() { setViewers((current) => ({ status: current.status === 'ready' ? 'refreshing' : 'loading', data: current.data, error: null, updatedAt: current.updatedAt, })) fetchJson(apiEndpoints.viewers, controller.signal) .then((data) => { setViewers({ status: 'ready', data, error: null, updatedAt: Date.now(), }) }) .catch((error) => { if (!controller.signal.aborted) { setViewers((current) => ({ ...current, status: 'error', error: error.message })) } }) } loadViewers() const timer = window.setInterval(loadViewers, 5000) return () => { window.clearInterval(timer) controller.abort() } }, [route.page]) useEffect(() => { if (route.page !== 'team' || !route.teamName) return const controller = new AbortController() const timer = window.setInterval(() => { Promise.allSettled([ fetchJson(apiEndpoints.detail(route.teamName), controller.signal), fetchJson(apiEndpoints.history(route.teamName), controller.signal), fetchJson(apiEndpoints.games(route.teamName), controller.signal), ]).then(([detailResult, historyResult, gamesResult]) => { if (controller.signal.aborted) return setProfile((current) => ({ teamName: route.teamName, detail: detailResult.status === 'fulfilled' ? { status: 'ready', data: detailResult.value, error: null } : current.detail, history: historyResult.status === 'fulfilled' ? { status: 'ready', data: historyResult.value, error: null } : current.history, games: gamesResult.status === 'fulfilled' ? { status: 'ready', data: gamesResult.value, error: null } : current.games, })) }) }, 60000) return () => { window.clearInterval(timer) controller.abort() } }, [route.page, route.teamName]) const topTeamName = bestTeamName(teams[0]) const searchPlaceholder = searchHint.status === 'ready' ? `Found ${searchHint.name}` : topTeamName || 'Search teams' async function handleTeamSearch(event) { event.preventDefault() const name = teamQuery.trim() if (!name) return try { const resolved = await fetchJson(apiEndpoints.resolve(name)) navigate(teamPath(resolved.tag_name || resolved.short_name || resolved.long_name || name)) } catch { navigate(teamPath(name)) } } function chooseAnalyticsConsent(preferences) { const previousVisitorId = storedVisitorId(analyticsVisitorKey) const nextPreferences = persistAnalyticsPreferences({ ...preferences, chosen: true }) if (!nextPreferences.analytics) { forgetVisitorId(analyticsVisitorKey) if (previousVisitorId) { fetch(apiEndpoints.viewerDelete, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ visitor_id: previousVisitorId, session_id: analyticsSessionId, }), keepalive: true, }).catch(() => {}) } } setAnalyticsPreferences(nextPreferences) } const activeNavPath = route.page === 'team' ? '/teams' : route.page === 'battle-logs' ? '/battle-logs' : route.page === 'viewers' ? '/viewers' : window.location.pathname return (
{route.page === 'home' ? ( ) : null} {route.page === 'teams' ? ( ) : null} {route.page === 'team' ? ( ) : null} {route.page === 'battle-logs' ? : null} {route.page === 'uptime' ? : null} {route.page === 'viewers' ? : null} {route.page === 'privacy' ? : null}
) } function Footer({ navigate }) { return ( ) } function ConsentBanner({ preferences, onChoose }) { const [isOpen, setIsOpen] = useState(() => !preferences.chosen) const [isConfiguring, setIsConfiguring] = useState(() => Boolean(preferences.chosen)) const [draft, setDraft] = useState(() => normalizeAnalyticsPreferences(preferences)) const privacySignalEnabled = browserPrivacySignalEnabled() useEffect(() => { setDraft(normalizeAnalyticsPreferences(preferences)) if (!preferences.chosen) { setIsOpen(true) setIsConfiguring(false) } }, [preferences]) useEffect(() => { if (!isOpen) return undefined const previousOverflow = document.body.style.overflow document.body.style.overflow = 'hidden' return () => { document.body.style.overflow = previousOverflow } }, [isOpen]) function updateDraft(key, value) { setDraft((current) => ({ ...current, [key]: value })) } function savePreferences(nextPreferences) { onChoose(normalizeAnalyticsPreferences(nextPreferences)) setIsOpen(false) } if (!isOpen) { return ( ) } return (

We use a necessary cookie to remember these choices. You can also allow analytics for the public viewers page and choose which details are included.

{privacySignalEnabled ? (

Your browser is signalling a privacy preference, so optional analytics stay off unless you explicitly allow them.

) : null}
{isConfiguring ? ( <>
updateDraft('analytics', value)} /> updateDraft('device', value)} /> updateDraft('display', value)} /> updateDraft('locale', value)} /> updateDraft('referrer', value)} /> updateDraft('diagnostics', value)} />
) : (
)}
) } function PreferenceToggle({ checked, description, disabled = false, label, onChange }) { return ( ) } function PrivacyPage() { return (

Privacy notice

How we handle your data

Controller: Heidi

Contact through Discord: clippiii

Necessary cookies store your cookie and analytics choices. If you opt in to viewer analytics, we collect page views, live viewing status, a pseudonymous visitor ID, and a session ID. Optional settings control whether browser/device, screen, language/timezone, referrer, and technical diagnostics are included. Technical diagnostics can include HTTP version, request protocol details, content negotiation headers, browser privacy signals, network quality, touch support, CPU/memory hints, and similar debugging fields. Necessary cookies are used to remember your privacy choices. Optional viewer analytics are used only to power the public viewers page, understand basic site activity, and keep abuse low. Necessary cookies are used because they are needed to remember your consent preferences. Optional analytics only run with your consent, and you can withdraw that consent from the Cookie settings button at any time. Analytics events are automatically deleted after the configured retention period, currently documented as 30 days by default. Active viewer sessions expire after a short inactivity window. If you decline analytics after previously allowing them, the site asks the server to delete records linked to your current visitor and session IDs, and removes the local visitor ID from your browser. Viewer analytics are stored by this site and are not sold. Public viewer pages show only consented analytics fields and never show raw IP addresses. Hosting providers may process server logs as part of running the site. You can ask for access, correction, deletion, restriction, objection, portability, or withdrawal of consent by contacting Heidi on Discord at clippiii. If you are in the UK, you can also complain to the ICO. If you are in the EU or EEA, you can complain to your local data protection authority.
) } function PrivacySection({ children, title }) { return (

{title}

{children}

) } function Landing({ live, matches, navigate, onTeamSearch, searchPlaceholder, setTeamQuery, teams, teamQuery, }) { const treeRef = useRef(null) return (

BorisBot got nothin on THIS

Toothless' TSS Bot

Powered by Spectra. TSS analytics.

Visit SREBOT
setTeamQuery(event.target.value)} />
Scroll
) } function LandingOverview({ teams, matches, navigate }) { const activeTeams = teams.slice(0, 4) const totalPlayers = matches.reduce((sum, match) => sum + Number(match.player_count || 0), 0) const latestMatch = matches[0] return (

What it does

A clean window into TSS activity

Track teams, inspect recent battles, and jump from a leaderboard name to a profile without touching the god awful TSS website.

navigate('/teams')} title="Team discovery" /> navigate('/battle-logs')} title="Battle context" /> navigate('/uptime')} title="Operational status" /> navigate('/viewers')} title="Viewer pulse" />

Latest signal

{latestMatch?.map_name || 'Waiting for the next battle'}

{latestMatch ? `${latestMatch.team_name || 'A TSS team'} played with ${formatNumber(latestMatch.player_count)} players.` : 'Latest match data will appear here once the data proxy returns games.'}

Teams to watch

{activeTeams.length ? ( activeTeams.map((team) => { const name = bestTeamName(team) return ( ) }) ) : (

Team data is still loading.

)}
) } function LandingMetric({ label, value }) { return (

{value}

{label}

) } function LandingFeature({ action, description, onClick, title }) { return (

{title}

{description}

) } function LandingTrustSection({ navigate }) { return (

Brought to you by...

Built by the team behind SREBot

TSSBot is built alongside SREBot, both providing different views into different aspects of "competitive" War Thunder.

Visit SREBOT
) } function RecentGamesSection({ live, matches, navigate }) { const recentMatches = matches.slice(0, 6) return (

Recent activity

Latest games

{recentMatches.map((match) => (

{match.map_name || 'Unknown map'}

{formatDate(match.timestamp)}

{match.result || 'Unknown'}

{match.team_name || 'TSS team'}

{formatNumber(match.player_count)} players

{formatNumber(match.stats?.ground_kills)} ground {formatNumber(match.stats?.air_kills)} air {formatNumber(match.stats?.deaths)} deaths
))}
{!recentMatches.length ? (

{live.status === 'loading' ? 'Loading latest games' : live.error || 'No games returned'}

) : null}
) } function PixelMountains() { const canvasRef = useRef(null) useEffect(() => { const canvas = canvasRef.current const ctx = canvas.getContext('2d') const WORLD_W = 1920 const WORLD_H = 900 function interpolate(points, x) { for (let i = 0; i < points.length - 1; i++) { const [x0, y0] = points[i] const [x1, y1] = points[i + 1] if (x >= x0 && x <= x1) { const t = (x - x0) / Math.max(1, x1 - x0) return y0 + (y1 - y0) * t } } return points.at(-1)[1] } function drawMountain(points, color, jitter = 0) { const width = WORLD_W const height = WORLD_H ctx.fillStyle = color for (let x = 0; x < width; x++) { const wave = jitter ? Math.sin(x * 0.08) * jitter + Math.sin(x * 0.021) * jitter * 1.8 : 0 const y = Math.round(interpolate(points, x) + wave) ctx.fillRect(x, y, 1, height - y) } } function draw() { const width = WORLD_W const height = WORLD_H canvas.width = WORLD_W canvas.height = WORLD_H ctx.imageSmoothingEnabled = false ctx.clearRect(0, 0, WORLD_W, WORLD_H) drawMountain( [ [0, height * 0.82], [width * 0.12, height * 0.73], [width * 0.26, height * 0.66], [width * 0.39, height * 0.76], [width * 0.55, height * 0.58], [width * 0.64, height * 0.46], [width * 0.73, height * 0.62], [width * 0.86, height * 0.56], [width, height * 0.64], ], '#fcfbcf', 1.1, ) drawMountain( [ [0, height * 0.86], [width * 0.15, height * 0.78], [width * 0.31, height * 0.81], [width * 0.43, height * 0.69], [width * 0.52, height * 0.76], [width * 0.62, height * 0.43], [width * 0.69, height * 0.31], [width * 0.78, height * 0.48], [width, height * 0.5], ], '#fff2e6', 1.6, ) drawMountain( [ [0, height * 0.94], [width * 0.17, height * 0.9], [width * 0.32, height * 0.92], [width * 0.47, height * 0.83], [width * 0.58, height * 0.89], [width * 0.66, height * 0.61], [width * 0.71, height * 0.5], [width * 0.84, height * 0.7], [width, height * 0.68], ], '#fee5cd', 2.1, ) drawMountain( [ [0, height * 0.98], [width * 0.17, height * 0.96], [width * 0.34, height * 0.99], [width * 0.48, height * 0.93], [width * 0.62, height * 0.97], [width * 0.72, height * 0.88], [width * 0.86, height * 0.95], [width, height * 0.93], ], '#fdca9b', 1.4, ) } draw() }, []) return