From 1fe22e41120a0de020c17e23a65272d7d9433141 Mon Sep 17 00:00:00 2001 From: Heidi Date: Thu, 14 May 2026 20:52:54 +0100 Subject: [PATCH] feat:/ added uptime page --- src/App.jsx | 153 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 153 insertions(+) diff --git a/src/App.jsx b/src/App.jsx index 0bbc0cd..cb34d5b 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -9,7 +9,9 @@ const dateFormat = new Intl.DateTimeFormat('en-GB', { }) const apiEndpoints = { + health: '/health', 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`, @@ -39,6 +41,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 === '/uptime') return { page: 'uptime', teamName: '' } if (pathname.startsWith('/teams/')) { const teamName = decodeURIComponent(pathname.slice('/teams/'.length)) return { page: 'team', teamName } @@ -117,6 +120,7 @@ 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: [], updatedAt: null }) const [teamQuery, setTeamQuery] = useState('') const [searchHint, setSearchHint] = useState({ status: 'idle', name: '' }) const [profile, setProfile] = useState({ @@ -150,6 +154,8 @@ function App() { ? "Team leaderboard | Toothless' TSS Bot" : route.page === 'battle-logs' ? "Battle Logs | Toothless' TSS Bot" + : route.page === 'uptime' + ? "Uptime | Toothless' TSS Bot" : "Toothless' TSS Bot" document.title = title @@ -311,6 +317,70 @@ function App() { return () => controller.abort() }, [route.page, route.teamName]) + useEffect(() => { + if (route.page !== 'uptime') return + + const controller = new AbortController() + + async function checkUptime() { + setUptime((current) => ({ + status: current.status === 'ready' ? 'refreshing' : 'loading', + checks: current.checks, + updatedAt: current.updatedAt, + })) + + const startedAt = performance.now() + const healthResult = await fetchJson(apiEndpoints.health, controller.signal) + .then(() => ({ ok: true, label: 'Operational' })) + .catch((error) => ({ ok: false, label: error.message })) + + const apiResult = await fetchJson(apiEndpoints.teamsHealth, controller.signal) + .then((data) => { + const teamCount = data.teams?.length || data.squadrons?.length || 0 + return { ok: true, label: `${teamCount} sample team${teamCount === 1 ? '' : 's'} returned` } + }) + .catch((error) => ({ ok: false, label: error.message })) + + if (controller.signal.aborted) return + + setUptime({ + status: 'ready', + updatedAt: Date.now(), + checks: [ + { + name: 'Website', + detail: 'App shell and static assets', + ok: true, + label: 'Online', + latency: Math.round(performance.now() - startedAt), + }, + { + name: 'Health endpoint', + detail: apiEndpoints.health, + ok: healthResult.ok, + label: healthResult.label, + latency: Math.round(performance.now() - startedAt), + }, + { + name: 'TSS data proxy', + detail: apiEndpoints.teamsHealth, + ok: apiResult.ok, + label: apiResult.label, + latency: Math.round(performance.now() - startedAt), + }, + ], + }) + } + + checkUptime() + const timer = window.setInterval(checkUptime, 30000) + + return () => { + window.clearInterval(timer) + controller.abort() + } + }, [route.page]) + useEffect(() => { if (route.page !== 'team' || !route.teamName) return @@ -434,11 +504,30 @@ function App() { /> ) : null} {route.page === 'battle-logs' ? : null} + {route.page === 'uptime' ? : null} +