feat:/ added uptime page
This commit is contained in:
+153
@@ -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' ? <BattleLogsPage live={live} matches={matches} /> : null}
|
||||
{route.page === 'uptime' ? <UptimePage uptime={uptime} /> : null}
|
||||
</section>
|
||||
<Footer navigate={navigate} />
|
||||
</main>
|
||||
)
|
||||
}
|
||||
|
||||
function Footer({ navigate }) {
|
||||
return (
|
||||
<footer className="mt-14 border-t border-border bg-fury-white">
|
||||
<div className="mx-auto flex w-full max-w-7xl flex-col gap-3 px-5 py-6 text-sm text-text-soft sm:flex-row sm:items-center sm:justify-between sm:px-8">
|
||||
<p>Toothless' TSS Bot</p>
|
||||
<button
|
||||
className="w-fit font-semibold text-fury-cyan transition hover:text-text"
|
||||
onClick={() => navigate('/uptime')}
|
||||
type="button"
|
||||
>
|
||||
Uptime
|
||||
</button>
|
||||
</div>
|
||||
</footer>
|
||||
)
|
||||
}
|
||||
|
||||
function Landing({ live, matches, navigate }) {
|
||||
const treeRef = useRef(null)
|
||||
|
||||
@@ -968,4 +1057,68 @@ function BattleLogsPage({ live, matches }) {
|
||||
)
|
||||
}
|
||||
|
||||
function UptimePage({ uptime }) {
|
||||
const checks = uptime.checks
|
||||
const operationalCount = checks.filter((check) => check.ok).length
|
||||
const allOperational = checks.length > 0 && operationalCount === checks.length
|
||||
const updatedAt = uptime.updatedAt ? dateFormat.format(new Date(uptime.updatedAt)) : 'Not checked yet'
|
||||
|
||||
return (
|
||||
<section className="space-y-6">
|
||||
<div className="rounded-lg border border-border bg-fury-white p-6 shadow-sm">
|
||||
<div className="flex flex-col gap-4 lg:flex-row lg:items-start lg:justify-between">
|
||||
<div>
|
||||
<p className="text-sm font-semibold uppercase tracking-wide text-fury-cyan">
|
||||
Website uptime
|
||||
</p>
|
||||
<h1 className="mt-1 text-4xl font-bold">
|
||||
{allOperational ? 'All systems operational' : 'Status check'}
|
||||
</h1>
|
||||
<p className="mt-2 text-sm text-text-soft">
|
||||
Last checked {updatedAt}. Refreshes every 30 seconds while this page is open.
|
||||
</p>
|
||||
</div>
|
||||
<span
|
||||
className={`w-fit rounded-md px-3 py-2 text-sm font-semibold ${allOperational
|
||||
? 'bg-surface text-fury-cyan'
|
||||
: 'bg-fury-ice text-fury-violet'
|
||||
}`}
|
||||
>
|
||||
{uptime.status === 'loading' ? 'Checking' : `${operationalCount}/${checks.length || 3} online`}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-4 lg:grid-cols-3">
|
||||
{checks.map((check) => (
|
||||
<article className="rounded-lg border border-border bg-fury-white p-5 shadow-sm" key={check.name}>
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
<div>
|
||||
<h2 className="text-lg font-semibold">{check.name}</h2>
|
||||
<p className="mt-1 text-sm text-text-soft">{check.detail}</p>
|
||||
</div>
|
||||
<span
|
||||
className={`shrink-0 rounded-md px-2 py-1 text-xs font-semibold ${check.ok
|
||||
? 'bg-surface text-fury-cyan'
|
||||
: 'bg-fury-ice text-fury-violet'
|
||||
}`}
|
||||
>
|
||||
{check.ok ? 'Online' : 'Issue'}
|
||||
</span>
|
||||
</div>
|
||||
<p className="mt-5 text-sm font-semibold">{check.label}</p>
|
||||
<p className="mt-1 text-xs text-text-soft">{formatNumber(check.latency)} ms response window</p>
|
||||
</article>
|
||||
))}
|
||||
|
||||
{!checks.length ? (
|
||||
<p className="rounded-lg border border-border bg-fury-white px-5 py-10 text-sm text-text-soft shadow-sm lg:col-span-3">
|
||||
Checking website status
|
||||
</p>
|
||||
) : null}
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
export default App
|
||||
|
||||
Reference in New Issue
Block a user