diff --git a/public/assets/app.js b/public/assets/app.js index 08dfcbd..3918066 100644 --- a/public/assets/app.js +++ b/public/assets/app.js @@ -388,7 +388,7 @@ async function refreshStatus() { const response = await fetch('/api/status', { cache: 'no-store' }); const status = await response.json(); if (!response.ok) throw new Error('status failed'); - setStat('status', status.ok ? 'ONLINE' : 'DEGRADED'); + setStat('status', `${status.ok ? 'ONLINE' : 'DEGRADED'} ${formatUptime(status.uptimeSeconds)}`); setStat('latency', `${Math.max(1, Math.round(performance.now() - started))}MS`); setStat('nodes', formatCount(status.liveClients)); setStat('memes', formatCount(status.memeCount)); @@ -406,6 +406,21 @@ function setStat(name, value) { } } +function formatUptime(value) { + const seconds = Math.max(0, Number.isFinite(value) ? value : 0); + const minutes = Math.floor(seconds / 60); + if (minutes < 1) return `UP ${seconds}S`; + if (minutes < 60) return `UP ${minutes}M`; + + const hours = Math.floor(minutes / 60); + const remainingMinutes = minutes % 60; + if (hours < 24) return `UP ${hours}H${remainingMinutes > 0 ? ` ${remainingMinutes}M` : ''}`; + + const days = Math.floor(hours / 24); + const remainingHours = hours % 24; + return `UP ${days}D${remainingHours > 0 ? ` ${remainingHours}H` : ''}`; +} + function updateScrollIndicator() { const segments = [...scrollIndicator.querySelectorAll('span')]; const maxScroll = Math.max(1, document.documentElement.scrollHeight - window.innerHeight); diff --git a/server.js b/server.js index 4c5d66b..f9a5163 100644 --- a/server.js +++ b/server.js @@ -18,6 +18,7 @@ const DATA_DIR = process.env.DATA_DIR || './data'; const PAGE_SIZE_MAX = 48; const UPLOAD_MAX_BYTES = 5 * 1024 * 1024; const REQUEST_MAX_BYTES = 6 * 1024 * 1024; +const SSE_HEARTBEAT_MS = 25_000; const events = new Set(); const DISCOVERY_ROUTES = new Set([ '/robots.txt', @@ -135,11 +136,19 @@ const server = http.createServer(async (req, res) => { res.writeHead(200, { 'Content-Type': 'text/event-stream; charset=utf-8', 'Cache-Control': 'no-cache, no-transform', - Connection: 'keep-alive' + Connection: 'keep-alive', + 'X-Accel-Buffering': 'no' }); + res.write('retry: 5000\n'); res.write('event: ready\ndata: {}\n\n'); events.add(res); - req.on('close', () => events.delete(res)); + const heartbeat = setInterval(() => { + res.write(': keep-alive\n\n'); + }, SSE_HEARTBEAT_MS); + req.on('close', () => { + clearInterval(heartbeat); + events.delete(res); + }); return; }