ai generated solutions to our ai generated problems
This commit is contained in:
@@ -125,6 +125,12 @@ API_RATE_LIMIT_WINDOW_MS=60000
|
|||||||
API_RATE_LIMIT_MAX=120
|
API_RATE_LIMIT_MAX=120
|
||||||
```
|
```
|
||||||
|
|
||||||
|
On startup, the web server preloads the critical public snapshots before
|
||||||
|
signalling PM2 `ready`: team leaderboard, player leaderboard, home teams, and
|
||||||
|
recent games. `/health` includes a `public_data` block with the latest preload
|
||||||
|
status. A same-origin `POST /api/cache/prewarm` refreshes those snapshots on
|
||||||
|
demand.
|
||||||
|
|
||||||
## Reverse proxy / Cloudflare
|
## Reverse proxy / Cloudflare
|
||||||
|
|
||||||
The server only honours `CF-Connecting-IP`, `X-Forwarded-For`, `X-Forwarded-Proto`,
|
The server only honours `CF-Connecting-IP`, `X-Forwarded-For`, `X-Forwarded-Proto`,
|
||||||
|
|||||||
+4
-21
@@ -1074,28 +1074,11 @@ function GatedAppContent() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function AppContent() {
|
function AppContent() {
|
||||||
const bootData = window.__TSS_BOOT_DATA__ || {}
|
|
||||||
const [route, setRoute] = useState(() => parseRoute())
|
const [route, setRoute] = useState(() => parseRoute())
|
||||||
const [leaderboard, setLeaderboard] = useState(() => (
|
const [leaderboard, setLeaderboard] = useState({ status: 'idle', data: null, error: null })
|
||||||
bootData.leaderboard
|
const [playerLeaderboard, setPlayerLeaderboard] = useState({ status: 'idle', data: null, error: null })
|
||||||
? { status: 'ready', data: bootData.leaderboard, error: null }
|
const [homeTeams, setHomeTeams] = useState({ status: 'idle', data: null, error: null })
|
||||||
: { status: 'idle', data: null, error: null }
|
const [live, setLive] = useState({ status: 'idle', data: null, error: null, updatedAt: 0 })
|
||||||
))
|
|
||||||
const [playerLeaderboard, setPlayerLeaderboard] = useState(() => (
|
|
||||||
bootData.playerLeaderboard
|
|
||||||
? { status: 'ready', data: bootData.playerLeaderboard, error: null }
|
|
||||||
: { status: 'idle', data: null, error: null }
|
|
||||||
))
|
|
||||||
const [homeTeams, setHomeTeams] = useState(() => (
|
|
||||||
bootData.homeTeams
|
|
||||||
? { status: 'ready', data: bootData.homeTeams, error: null }
|
|
||||||
: { status: 'idle', data: null, error: null }
|
|
||||||
))
|
|
||||||
const [live, setLive] = useState(() => (
|
|
||||||
bootData.live
|
|
||||||
? { status: 'ready', data: bootData.live, error: null, updatedAt: Date.now() }
|
|
||||||
: { status: 'idle', data: null, error: null, updatedAt: 0 }
|
|
||||||
))
|
|
||||||
const [uptime, setUptime] = useState({ status: 'idle', checks: [], history: [], updatedAt: null })
|
const [uptime, setUptime] = useState({ status: 'idle', checks: [], history: [], updatedAt: null })
|
||||||
const [viewers, setViewers] = useState({ status: 'idle', data: null, error: null, updatedAt: null })
|
const [viewers, setViewers] = useState({ status: 'idle', data: null, error: null, updatedAt: null })
|
||||||
const [analyticsPreferences, setAnalyticsPreferences] = useState(() => storedAnalyticsPreferences())
|
const [analyticsPreferences, setAnalyticsPreferences] = useState(() => storedAnalyticsPreferences())
|
||||||
|
|||||||
+1
-50
@@ -2,53 +2,4 @@ import { createRoot } from 'react-dom/client'
|
|||||||
import './styles.css'
|
import './styles.css'
|
||||||
import App from './App.jsx'
|
import App from './App.jsx'
|
||||||
|
|
||||||
const root = document.getElementById('root')
|
createRoot(document.getElementById('root')).render(<App />)
|
||||||
|
|
||||||
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 />)
|
|
||||||
})
|
|
||||||
|
|||||||
+75
-184
@@ -212,6 +212,7 @@ let uptimeDb = null
|
|||||||
let analyticsDb = null
|
let analyticsDb = null
|
||||||
let latestUptimeSnapshot = null
|
let latestUptimeSnapshot = null
|
||||||
let publicDataPrewarmTimer = null
|
let publicDataPrewarmTimer = null
|
||||||
|
let publicDataStartupStatus = { ready: false, checked_at: null, results: [] }
|
||||||
|
|
||||||
function sendJson(res, status, body, headers = {}) {
|
function sendJson(res, status, body, headers = {}) {
|
||||||
send(res, status, JSON.stringify(body), { ...jsonHeaders, ...headers })
|
send(res, status, JSON.stringify(body), { ...jsonHeaders, ...headers })
|
||||||
@@ -464,13 +465,14 @@ function refreshPublicData(filePath, target) {
|
|||||||
|
|
||||||
function publicDataPrewarmTargets() {
|
function publicDataPrewarmTargets() {
|
||||||
return [
|
return [
|
||||||
'/api/tss/leaderboard/teams?limit=100',
|
{ name: 'team leaderboard', path: '/api/tss/leaderboard/teams?limit=100' },
|
||||||
'/api/tss/leaderboard/players?limit=100',
|
{ name: 'player leaderboard', path: '/api/tss/leaderboard/players?limit=100' },
|
||||||
'/api/tss/leaderboard/teams?limit=4',
|
{ name: 'home teams', path: '/api/tss/leaderboard/teams?limit=4' },
|
||||||
'/api/tss/games/recent?limit=50',
|
{ name: 'recent games', path: '/api/tss/games/recent?limit=50' },
|
||||||
].map((requestPath) => {
|
].map(({ name, path: requestPath }) => {
|
||||||
const requestUrl = new URL(requestPath, 'http://localhost')
|
const requestUrl = new URL(requestPath, 'http://localhost')
|
||||||
return {
|
return {
|
||||||
|
name,
|
||||||
requestUrl,
|
requestUrl,
|
||||||
target: new URL(requestPath, API_UPSTREAM),
|
target: new URL(requestPath, API_UPSTREAM),
|
||||||
filePath: publicDataCachePathForUrl(requestUrl),
|
filePath: publicDataCachePathForUrl(requestUrl),
|
||||||
@@ -478,14 +480,60 @@ function publicDataPrewarmTargets() {
|
|||||||
}).filter((entry) => entry.filePath)
|
}).filter((entry) => entry.filePath)
|
||||||
}
|
}
|
||||||
|
|
||||||
function prewarmPublicDataCache() {
|
async function prewarmPublicDataCache({ force = false, log = false } = {}) {
|
||||||
const jobs = []
|
const jobs = []
|
||||||
for (const { filePath, target } of publicDataPrewarmTargets()) {
|
for (const { name, filePath, target } of publicDataPrewarmTargets()) {
|
||||||
const current = cachedPublicData(filePath)
|
const current = cachedPublicData(filePath)
|
||||||
if (current?.fresh) continue
|
if (current?.fresh && !force) {
|
||||||
jobs.push(fillPublicData(filePath, target, PUBLIC_DATA_COLD_TIMEOUT_MS))
|
jobs.push(Promise.resolve({ name, filePath, ok: true, cached: true, bytes: fs.statSync(filePath).size }))
|
||||||
|
continue
|
||||||
}
|
}
|
||||||
return Promise.allSettled(jobs)
|
|
||||||
|
const startedAt = Date.now()
|
||||||
|
jobs.push(
|
||||||
|
fillPublicData(filePath, target, PUBLIC_DATA_COLD_TIMEOUT_MS)
|
||||||
|
.then((ok) => ({
|
||||||
|
name,
|
||||||
|
filePath,
|
||||||
|
ok,
|
||||||
|
cached: false,
|
||||||
|
ms: Date.now() - startedAt,
|
||||||
|
bytes: fs.existsSync(filePath) ? fs.statSync(filePath).size : 0,
|
||||||
|
}))
|
||||||
|
.catch((error) => ({
|
||||||
|
name,
|
||||||
|
filePath,
|
||||||
|
ok: false,
|
||||||
|
cached: false,
|
||||||
|
ms: Date.now() - startedAt,
|
||||||
|
error: error.message,
|
||||||
|
bytes: fs.existsSync(filePath) ? fs.statSync(filePath).size : 0,
|
||||||
|
})),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
const results = await Promise.all(jobs)
|
||||||
|
publicDataStartupStatus = {
|
||||||
|
ready: results.every((result) => result.ok || result.bytes > 0),
|
||||||
|
checked_at: new Date().toISOString(),
|
||||||
|
results: results.map((result) => ({
|
||||||
|
name: result.name,
|
||||||
|
ok: Boolean(result.ok || result.bytes > 0),
|
||||||
|
cached: Boolean(result.cached),
|
||||||
|
bytes: result.bytes || 0,
|
||||||
|
ms: result.ms || 0,
|
||||||
|
error: result.error || '',
|
||||||
|
})),
|
||||||
|
}
|
||||||
|
|
||||||
|
if (log) {
|
||||||
|
for (const result of publicDataStartupStatus.results) {
|
||||||
|
const state = result.ok ? (result.cached ? 'cached' : 'warmed') : 'failed'
|
||||||
|
const detail = result.error ? ` (${result.error})` : ''
|
||||||
|
console.log(`public data ${state}: ${result.name} ${result.bytes} bytes in ${result.ms}ms${detail}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return publicDataStartupStatus
|
||||||
}
|
}
|
||||||
|
|
||||||
function startPublicDataPrewarmer() {
|
function startPublicDataPrewarmer() {
|
||||||
@@ -2281,171 +2329,6 @@ function routeStructuredData(origin, seo, canonicalUrl) {
|
|||||||
])
|
])
|
||||||
}
|
}
|
||||||
|
|
||||||
function readPublicDataSnapshot(relativePath) {
|
|
||||||
try {
|
|
||||||
const filePath = path.resolve(PUBLIC_DATA_CACHE_DIR, relativePath)
|
|
||||||
const relativeToCache = path.relative(PUBLIC_DATA_CACHE_DIR, filePath)
|
|
||||||
if (relativeToCache.startsWith('..') || path.isAbsolute(relativeToCache)) return null
|
|
||||||
const stat = fs.statSync(filePath)
|
|
||||||
if (!stat.isFile() || Date.now() - stat.mtimeMs > PUBLIC_DATA_CACHE_STALE_MS) return null
|
|
||||||
return JSON.parse(fs.readFileSync(filePath, 'utf8'))
|
|
||||||
} catch {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function fallbackNumber(value) {
|
|
||||||
return new Intl.NumberFormat('en-GB').format(Number(value || 0))
|
|
||||||
}
|
|
||||||
|
|
||||||
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) {
|
function htmlWithSeo(req, data) {
|
||||||
const origin = pagePublicOrigin(req)
|
const origin = pagePublicOrigin(req)
|
||||||
let pathname = '/'
|
let pathname = '/'
|
||||||
@@ -2458,14 +2341,7 @@ function htmlWithSeo(req, data) {
|
|||||||
const seo = routeSeo(pathname)
|
const seo = routeSeo(pathname)
|
||||||
const canonicalUrl = `${origin}${seo.path}`
|
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')
|
return data.toString('utf8')
|
||||||
.replace('<div id="root"></div>', rootHtml)
|
|
||||||
.replace(/\s+integrity=(["'])sha(?:256|384|512)-[^"']+\1/g, '')
|
.replace(/\s+integrity=(["'])sha(?:256|384|512)-[^"']+\1/g, '')
|
||||||
.replaceAll('__PUBLIC_ORIGIN__', origin)
|
.replaceAll('__PUBLIC_ORIGIN__', origin)
|
||||||
.replaceAll('__SEO_TITLE__', escapeHtml(seo.title))
|
.replaceAll('__SEO_TITLE__', escapeHtml(seo.title))
|
||||||
@@ -2854,7 +2730,7 @@ const server = http.createServer((req, res) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (req.url === '/health') {
|
if (req.url === '/health') {
|
||||||
sendJson(res, 200, { ok: true })
|
sendJson(res, 200, { ok: true, public_data: publicDataStartupStatus })
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2869,6 +2745,21 @@ const server = http.createServer((req, res) => {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (req.method === 'POST' && req.url === '/api/cache/prewarm') {
|
||||||
|
if (!isSameOriginRequest(req)) {
|
||||||
|
sendJson(res, 403, { error: 'Cache prewarm is restricted to this site' })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (isRateLimited(req)) {
|
||||||
|
sendJson(res, 429, { error: 'Too many requests' }, { 'retry-after': String(Math.ceil(API_RATE_LIMIT_WINDOW_MS / 1000)) })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
prewarmPublicDataCache({ force: true, log: true })
|
||||||
|
.then((status) => sendJson(res, status.ready ? 200 : 207, status))
|
||||||
|
.catch((error) => sendJson(res, 500, { error: 'Cache prewarm failed', detail: error.message }))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if (req.method === 'GET' && req.url === '/api/viewers') {
|
if (req.method === 'GET' && req.url === '/api/viewers') {
|
||||||
if (isRateLimited(req)) {
|
if (isRateLimited(req)) {
|
||||||
sendJson(res, 429, { error: 'Too many requests' }, { 'retry-after': String(Math.ceil(API_RATE_LIMIT_WINDOW_MS / 1000)) })
|
sendJson(res, 429, { error: 'Too many requests' }, { 'retry-after': String(Math.ceil(API_RATE_LIMIT_WINDOW_MS / 1000)) })
|
||||||
@@ -3030,7 +2921,7 @@ server.listen(PORT, '0.0.0.0', async () => {
|
|||||||
if (RUN_BACKGROUND_JOBS) {
|
if (RUN_BACKGROUND_JOBS) {
|
||||||
startUptimeSampler()
|
startUptimeSampler()
|
||||||
fs.mkdirSync(PUBLIC_DATA_CACHE_DIR, { recursive: true })
|
fs.mkdirSync(PUBLIC_DATA_CACHE_DIR, { recursive: true })
|
||||||
await prewarmPublicDataCache()
|
await prewarmPublicDataCache({ log: true })
|
||||||
startPublicDataPrewarmer()
|
startPublicDataPrewarmer()
|
||||||
}
|
}
|
||||||
process.send?.('ready')
|
process.send?.('ready')
|
||||||
|
|||||||
Reference in New Issue
Block a user