Heartbeat and nicer status line
This commit is contained in:
+16
-1
@@ -388,7 +388,7 @@ async function refreshStatus() {
|
|||||||
const response = await fetch('/api/status', { cache: 'no-store' });
|
const response = await fetch('/api/status', { cache: 'no-store' });
|
||||||
const status = await response.json();
|
const status = await response.json();
|
||||||
if (!response.ok) throw new Error('status failed');
|
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('latency', `${Math.max(1, Math.round(performance.now() - started))}MS`);
|
||||||
setStat('nodes', formatCount(status.liveClients));
|
setStat('nodes', formatCount(status.liveClients));
|
||||||
setStat('memes', formatCount(status.memeCount));
|
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() {
|
function updateScrollIndicator() {
|
||||||
const segments = [...scrollIndicator.querySelectorAll('span')];
|
const segments = [...scrollIndicator.querySelectorAll('span')];
|
||||||
const maxScroll = Math.max(1, document.documentElement.scrollHeight - window.innerHeight);
|
const maxScroll = Math.max(1, document.documentElement.scrollHeight - window.innerHeight);
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ const DATA_DIR = process.env.DATA_DIR || './data';
|
|||||||
const PAGE_SIZE_MAX = 48;
|
const PAGE_SIZE_MAX = 48;
|
||||||
const UPLOAD_MAX_BYTES = 5 * 1024 * 1024;
|
const UPLOAD_MAX_BYTES = 5 * 1024 * 1024;
|
||||||
const REQUEST_MAX_BYTES = 6 * 1024 * 1024;
|
const REQUEST_MAX_BYTES = 6 * 1024 * 1024;
|
||||||
|
const SSE_HEARTBEAT_MS = 25_000;
|
||||||
const events = new Set();
|
const events = new Set();
|
||||||
const DISCOVERY_ROUTES = new Set([
|
const DISCOVERY_ROUTES = new Set([
|
||||||
'/robots.txt',
|
'/robots.txt',
|
||||||
@@ -135,11 +136,19 @@ const server = http.createServer(async (req, res) => {
|
|||||||
res.writeHead(200, {
|
res.writeHead(200, {
|
||||||
'Content-Type': 'text/event-stream; charset=utf-8',
|
'Content-Type': 'text/event-stream; charset=utf-8',
|
||||||
'Cache-Control': 'no-cache, no-transform',
|
'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');
|
res.write('event: ready\ndata: {}\n\n');
|
||||||
events.add(res);
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user