ai generated solutions to our ai generated problems
This commit is contained in:
@@ -86,12 +86,12 @@ Vehicle icon PNGs are served statically at `/vehicle-icons` from `VEHICLE_ICONS_
|
||||
|
||||
The proxy blocks cross-origin/API-navigation requests, strips CORS headers from
|
||||
the upstream response, rate limits callers, and caches successful GET responses.
|
||||
Public TSS reads are written to a bounded JSON snapshot cache and served at both
|
||||
their `/api/tss/*` route and matching `/data/*` path. The frontend uses `/data/*`
|
||||
by default for public pages. Fresh snapshots return without touching the backend;
|
||||
stale snapshots are served immediately while the server refreshes them in the
|
||||
background. Missing `/data/*` snapshots are filled from the matching upstream API
|
||||
with a short timeout, then written atomically for future requests. All responses
|
||||
Public TSS reads are written to a bounded JSON snapshot cache and served through
|
||||
their normal `/api/tss/*` route. Fresh snapshots return without touching the
|
||||
backend; stale snapshots are served immediately while the server refreshes them
|
||||
in the background. Matching `/data/*` paths are also available for diagnostics or
|
||||
static-first experiments, but the frontend uses `/api/tss/*` by default so the
|
||||
site stays dynamic. All responses
|
||||
ship `X-Content-Type-Options`, `X-Frame-Options: DENY`, `Referrer-Policy`,
|
||||
`Permissions-Policy`, `Cross-Origin-Opener-Policy`, `Cross-Origin-Resource-Policy`,
|
||||
HSTS (over HTTPS), and HTML responses include a Content Security Policy that
|
||||
@@ -119,7 +119,7 @@ PUBLIC_DATA_CACHE_FRESH_MS=300000
|
||||
PUBLIC_DATA_CACHE_STALE_MS=86400000
|
||||
PUBLIC_DATA_PREWARM_INTERVAL_MS=300000
|
||||
PUBLIC_DATA_COLD_TIMEOUT_MS=8000
|
||||
VITE_STATIC_DATA=true
|
||||
VITE_STATIC_DATA=false
|
||||
VITE_SITE_GATE=false
|
||||
API_RATE_LIMIT_WINDOW_MS=60000
|
||||
API_RATE_LIMIT_MAX=120
|
||||
|
||||
+1
-1
@@ -63,4 +63,4 @@ DISCORD_INCLUDE_PATCH=true
|
||||
VITE_TURNSTILE_SITE_KEY=
|
||||
TURNSTILE_SECRET_KEY=
|
||||
VITE_SITE_GATE=false
|
||||
VITE_STATIC_DATA=true
|
||||
VITE_STATIC_DATA=false
|
||||
|
||||
@@ -38,7 +38,6 @@
|
||||
} catch {}
|
||||
|
||||
window.__TSS_BOOT_PREFERENCES__ = { analyticsPreferences, theme }
|
||||
window.__TSS_BOOT_DATA__ = __TSS_BOOT_DATA__
|
||||
document.documentElement.dataset.theme = theme
|
||||
document.documentElement.style.colorScheme = theme
|
||||
document.querySelector('meta[name="theme-color"]')?.setAttribute(
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
# Static Public Data
|
||||
|
||||
The frontend tries these JSON snapshots before falling back to the live API by
|
||||
default. The web server serves `/data/*` from the public data cache and fills
|
||||
missing snapshots from the matching `/api/tss/*` route with a short timeout.
|
||||
The web server serves `/data/*` from the same public data cache used by
|
||||
`/api/tss/*`, and fills missing snapshots from the matching API route with a
|
||||
short timeout. The frontend uses `/api/tss/*` by default; set
|
||||
`VITE_STATIC_DATA=true` only for static-first experiments.
|
||||
|
||||
- `/data/leaderboard-teams.json`
|
||||
- `/data/leaderboard-players.json`
|
||||
|
||||
@@ -52,7 +52,7 @@ const siteVersion = '1.0.1'
|
||||
const turnstileSiteKey = import.meta.env.VITE_TURNSTILE_SITE_KEY || ''
|
||||
const siteGateEnabled = String(import.meta.env.VITE_SITE_GATE || 'false').toLowerCase() === 'true'
|
||||
const staticDataBase = (import.meta.env.VITE_STATIC_DATA_BASE || '/data').replace(/\/+$/, '')
|
||||
const staticDataEnabled = String(import.meta.env.VITE_STATIC_DATA || 'true').toLowerCase() !== 'false'
|
||||
const staticDataEnabled = String(import.meta.env.VITE_STATIC_DATA || 'false').toLowerCase() === 'true'
|
||||
const missingStaticDataPaths = new Set()
|
||||
|
||||
const defaultAnalyticsPreferences = {
|
||||
|
||||
+50
-1
@@ -2,4 +2,53 @@ import { createRoot } from 'react-dom/client'
|
||||
import './styles.css'
|
||||
import App from './App.jsx'
|
||||
|
||||
createRoot(document.getElementById('root')).render(<App />)
|
||||
const root = document.getElementById('root')
|
||||
|
||||
async function fetchBootJson(path) {
|
||||
const response = await fetch(path, { headers: { Accept: 'application/json' } })
|
||||
if (!response.ok) throw new Error(`Boot request failed with ${response.status}`)
|
||||
return response.json()
|
||||
}
|
||||
|
||||
async function preloadBootData() {
|
||||
if (!root?.dataset.tssFallback) return
|
||||
|
||||
const pathname = window.location.pathname
|
||||
const timeout = new Promise((_, reject) => {
|
||||
window.setTimeout(() => reject(new Error('Boot preload timed out')), 1500)
|
||||
})
|
||||
|
||||
const load = async () => {
|
||||
if (pathname === '/') {
|
||||
const [homeTeams, live] = await Promise.all([
|
||||
fetchBootJson('/data/home-teams.json'),
|
||||
fetchBootJson('/data/recent-games.json'),
|
||||
])
|
||||
return { homeTeams, live }
|
||||
}
|
||||
|
||||
if (pathname === '/teams') {
|
||||
return { leaderboard: await fetchBootJson('/data/leaderboard-teams.json') }
|
||||
}
|
||||
|
||||
if (pathname === '/players') {
|
||||
return { playerLeaderboard: await fetchBootJson('/data/leaderboard-players.json') }
|
||||
}
|
||||
|
||||
if (pathname === '/battle-logs' || pathname === '/live') {
|
||||
const [live, leaderboard] = await Promise.all([
|
||||
fetchBootJson('/data/recent-games.json'),
|
||||
fetchBootJson('/data/leaderboard-teams.json'),
|
||||
])
|
||||
return { live, leaderboard }
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
window.__TSS_BOOT_DATA__ = await Promise.race([load(), timeout]).catch(() => null)
|
||||
}
|
||||
|
||||
preloadBootData().finally(() => {
|
||||
createRoot(root).render(<App />)
|
||||
})
|
||||
|
||||
+155
-21
@@ -2294,28 +2294,156 @@ function readPublicDataSnapshot(relativePath) {
|
||||
}
|
||||
}
|
||||
|
||||
function routeBootData(pathname) {
|
||||
const boot = {}
|
||||
|
||||
if (pathname === '/') {
|
||||
const homeTeams = readPublicDataSnapshot('home-teams.json')
|
||||
const live = readPublicDataSnapshot('recent-games.json')
|
||||
if (homeTeams) boot.homeTeams = homeTeams
|
||||
if (live) boot.live = live
|
||||
} else if (pathname === '/teams') {
|
||||
const leaderboard = readPublicDataSnapshot('leaderboard-teams.json')
|
||||
if (leaderboard) boot.leaderboard = leaderboard
|
||||
} else if (pathname === '/players') {
|
||||
const playerLeaderboard = readPublicDataSnapshot('leaderboard-players.json')
|
||||
if (playerLeaderboard) boot.playerLeaderboard = playerLeaderboard
|
||||
} else if (pathname === '/battle-logs' || pathname === '/live') {
|
||||
const live = readPublicDataSnapshot('recent-games.json')
|
||||
const leaderboard = readPublicDataSnapshot('leaderboard-teams.json')
|
||||
if (live) boot.live = live
|
||||
if (leaderboard) boot.leaderboard = leaderboard
|
||||
function fallbackNumber(value) {
|
||||
return new Intl.NumberFormat('en-GB').format(Number(value || 0))
|
||||
}
|
||||
|
||||
return Object.keys(boot).length ? boot : null
|
||||
function fallbackDate(timestamp) {
|
||||
if (!timestamp) return 'Unknown time'
|
||||
return new Intl.DateTimeFormat('en-GB', { dateStyle: 'medium', timeStyle: 'short' }).format(new Date(Number(timestamp) * 1000))
|
||||
}
|
||||
|
||||
function fallbackShell(title, meta, body) {
|
||||
return `
|
||||
<main class="min-h-screen bg-bg px-5 py-24 text-text sm:px-8 sm:pt-28">
|
||||
<section class="mx-auto w-full max-w-7xl space-y-6">
|
||||
<div>
|
||||
<h1 class="text-3xl font-bold">${escapeHtml(title)}</h1>
|
||||
<p class="mt-2 text-sm text-text-soft">${escapeHtml(meta)}</p>
|
||||
</div>
|
||||
${body}
|
||||
</section>
|
||||
</main>
|
||||
`
|
||||
}
|
||||
|
||||
function playersFallbackHtml() {
|
||||
const players = readPublicDataSnapshot('leaderboard-players.json')?.players || []
|
||||
if (!players.length) return ''
|
||||
|
||||
const rows = players.slice(0, 100).map((player, index) => `
|
||||
<a class="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" href="/players/${encodeURIComponent(player.uid)}">
|
||||
<p class="font-semibold text-fury-cyan">#${index + 1}</p>
|
||||
<div class="min-w-0">
|
||||
<p class="truncate text-base font-semibold">${escapeHtml(player.nick || player.uid)}</p>
|
||||
<p class="truncate text-xs text-text-soft">${escapeHtml(player.uid)} · last seen ${escapeHtml(fallbackDate(player.last_seen))}</p>
|
||||
</div>
|
||||
<p class="text-right font-semibold">${fallbackNumber(player.score)}</p>
|
||||
<p class="text-right">${fallbackNumber(player.total_battles)}</p>
|
||||
<p class="text-right">${fallbackNumber(player.total_kills)}</p>
|
||||
<p class="text-right">${fallbackNumber(player.assists)}</p>
|
||||
<p class="text-right">${Number(player.win_rate || 0).toFixed(1)}%</p>
|
||||
<p class="text-right">${Number(player.kdr || 0).toFixed(2)}</p>
|
||||
<p class="text-right">${fallbackNumber(player.teams_seen)}</p>
|
||||
</a>
|
||||
`).join('')
|
||||
|
||||
return fallbackShell(
|
||||
'Player Leaderboard',
|
||||
`${players.length} players returned`,
|
||||
`<div class="overflow-hidden rounded-lg border border-border bg-fury-white shadow-sm">
|
||||
<div class="overflow-x-auto">
|
||||
<div class="min-w-[900px]">
|
||||
<div class="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 class="text-right">Score</p><p class="text-right">Battles</p><p class="text-right">Kills</p><p class="text-right">Assists</p><p class="text-right">WR</p><p class="text-right">KDR</p><p class="text-right">Teams</p>
|
||||
</div>
|
||||
${rows}
|
||||
</div>
|
||||
</div>
|
||||
</div>`,
|
||||
)
|
||||
}
|
||||
|
||||
function teamsFallbackHtml() {
|
||||
const teams = readPublicDataSnapshot('leaderboard-teams.json')?.teams || []
|
||||
if (!teams.length) return ''
|
||||
|
||||
const rows = teams.slice(0, 100).map((team, index) => {
|
||||
const name = team.name || ''
|
||||
return `
|
||||
<a class="grid w-full gap-4 border-b border-surface px-5 py-4 text-left transition hover:bg-surface md:grid-cols-[4rem_1fr_repeat(4,auto)] md:items-center" href="/teams/${encodeURIComponent(name)}">
|
||||
<span class="text-sm font-semibold text-fury-cyan">#${index + 1}</span>
|
||||
<span class="min-w-0"><span class="block truncate text-lg font-semibold">${escapeHtml(name)}</span></span>
|
||||
<span class="text-sm">${fallbackNumber(team.player_count)} players</span>
|
||||
<span class="text-sm">${fallbackNumber(team.total_battles)} battles</span>
|
||||
<span class="text-sm">${Number(team.win_rate || 0).toFixed(1)}% WR</span>
|
||||
<span class="text-sm font-semibold">${fallbackNumber(team.points?.total_points || team.total_kills)}</span>
|
||||
</a>
|
||||
`
|
||||
}).join('')
|
||||
|
||||
return fallbackShell(
|
||||
'Team Leaderboard',
|
||||
`${teams.length} teams returned`,
|
||||
`<div class="overflow-hidden rounded-lg border border-border bg-fury-white shadow-sm">${rows}</div>`,
|
||||
)
|
||||
}
|
||||
|
||||
function battleLogsFallbackHtml() {
|
||||
const matches = readPublicDataSnapshot('recent-games.json')?.matches || []
|
||||
if (!matches.length) return ''
|
||||
|
||||
const rows = matches.slice(0, 50).map((match) => {
|
||||
const winner = match.winning_team || match.team_name || ''
|
||||
const loser = match.losing_team || ''
|
||||
return `
|
||||
<a class="grid w-full gap-x-6 gap-y-2 border-b border-surface px-5 py-4 text-left transition hover:bg-surface md:grid-cols-[minmax(0,1fr)_minmax(18rem,0.9fr)_auto] md:items-center" href="/games/${encodeURIComponent(match.session_id)}">
|
||||
<div class="min-w-0">
|
||||
<p class="truncate font-semibold">${escapeHtml(match.map_name || 'Unknown map')}</p>
|
||||
<p class="text-xs text-text-soft">${escapeHtml(fallbackDate(match.timestamp))} · ${escapeHtml(match.session_id)}</p>
|
||||
</div>
|
||||
<div class="mx-auto grid w-full max-w-xl min-w-0 grid-cols-[minmax(0,1fr)_auto_minmax(0,1fr)] items-center gap-x-4">
|
||||
<span class="min-w-0 truncate text-right text-sm font-semibold text-win">${escapeHtml(winner)}</span>
|
||||
<span class="shrink-0 text-xs font-semibold uppercase tracking-wide text-text-muted">vs</span>
|
||||
<span class="min-w-0 truncate text-left text-sm font-semibold text-loss">${escapeHtml(loser)}</span>
|
||||
</div>
|
||||
<p class="text-sm">${fallbackNumber(match.player_count)}v${fallbackNumber(match.player_count)}</p>
|
||||
</a>
|
||||
`
|
||||
}).join('')
|
||||
|
||||
return fallbackShell(
|
||||
'Battle Logs',
|
||||
`${matches.length} battles returned`,
|
||||
`<div class="overflow-hidden rounded-lg border border-border bg-fury-white shadow-sm">${rows}</div>`,
|
||||
)
|
||||
}
|
||||
|
||||
function homeFallbackHtml() {
|
||||
const teams = readPublicDataSnapshot('home-teams.json')?.teams || []
|
||||
const matches = readPublicDataSnapshot('recent-games.json')?.matches || []
|
||||
|
||||
return `
|
||||
<main class="min-h-screen bg-bg px-5 py-24 text-text sm:px-8 sm:pt-28">
|
||||
<section class="mx-auto grid w-full max-w-7xl gap-8 lg:grid-cols-[1.05fr_0.95fr] lg:items-center">
|
||||
<div class="max-w-3xl">
|
||||
<p class="text-base font-semibold uppercase tracking-wide text-fury-cyan">BorisBot got nothin on THIS</p>
|
||||
<h1 class="mt-3 text-6xl font-bold tracking-normal sm:text-7xl lg:text-8xl">Toothless' TSS Bot</h1>
|
||||
<p class="mt-6 max-w-2xl text-xl leading-9 text-text-soft">Powered by Spectra. TSS analytics.</p>
|
||||
<div class="mt-8 grid gap-4 sm:grid-cols-3">
|
||||
<a class="apricot-button-text min-h-15 rounded-lg bg-text px-5 py-4 text-center text-base font-semibold text-bg" href="/teams">Team Leaderboard</a>
|
||||
<a class="min-h-15 rounded-lg border-2 border-ring px-5 py-4 text-center text-base font-semibold text-fury-cyan" href="/battle-logs">Battle Logs</a>
|
||||
<a class="min-h-15 rounded-lg border-2 border-text bg-fury-white px-5 py-4 text-center text-base font-semibold text-text" href="/players">Player Leaderboard</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="rounded-lg border border-border bg-fury-white p-5 shadow-sm">
|
||||
<p class="text-sm font-semibold uppercase tracking-wide text-fury-cyan">Cached now</p>
|
||||
<div class="mt-4 grid gap-3">
|
||||
<p class="text-sm text-text-soft">${fallbackNumber(teams.length)} featured teams · ${fallbackNumber(matches.length)} recent games</p>
|
||||
${teams.slice(0, 4).map((team) => `<a class="rounded-md border border-border bg-bg px-3 py-2 text-sm font-semibold" href="/teams/${encodeURIComponent(team.name || '')}">${escapeHtml(team.name || '')}</a>`).join('')}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
`
|
||||
}
|
||||
|
||||
function routeFallbackHtml(pathname) {
|
||||
if (pathname === '/players') return playersFallbackHtml()
|
||||
if (pathname === '/teams') return teamsFallbackHtml()
|
||||
if (pathname === '/battle-logs' || pathname === '/live') return battleLogsFallbackHtml()
|
||||
if (pathname === '/') return homeFallbackHtml()
|
||||
return ''
|
||||
}
|
||||
|
||||
function htmlWithSeo(req, data) {
|
||||
@@ -2330,7 +2458,14 @@ function htmlWithSeo(req, data) {
|
||||
const seo = routeSeo(pathname)
|
||||
const canonicalUrl = `${origin}${seo.path}`
|
||||
|
||||
const fallback = routeFallbackHtml(pathname)
|
||||
|
||||
const rootHtml = fallback
|
||||
? `<div id="root" data-tss-fallback="1">${fallback}</div>`
|
||||
: '<div id="root"></div>'
|
||||
|
||||
return data.toString('utf8')
|
||||
.replace('<div id="root"></div>', rootHtml)
|
||||
.replace(/\s+integrity=(["'])sha(?:256|384|512)-[^"']+\1/g, '')
|
||||
.replaceAll('__PUBLIC_ORIGIN__', origin)
|
||||
.replaceAll('__SEO_TITLE__', escapeHtml(seo.title))
|
||||
@@ -2338,7 +2473,6 @@ function htmlWithSeo(req, data) {
|
||||
.replaceAll('__SEO_ROBOTS__', escapeHtml(seo.robots))
|
||||
.replaceAll('__SEO_CANONICAL__', escapeHtml(canonicalUrl))
|
||||
.replaceAll('__SEO_JSON_LD__', routeStructuredData(origin, seo, canonicalUrl).replace(/</g, '\\u003c'))
|
||||
.replaceAll('__TSS_BOOT_DATA__', JSON.stringify(routeBootData(pathname)).replace(/</g, '\\u003c'))
|
||||
.replaceAll('__TURNSTILE_SESSION__', isTurnstileSessionVerified(req) ? 'verified' : 'required')
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user