diff --git a/src/main.js b/src/main.js index f6ede17..c38fd66 100644 --- a/src/main.js +++ b/src/main.js @@ -4,7 +4,7 @@ import './styles.css' const UPDATE_INTERVAL = 2000 // ms const HISTORY_DURATION = 300 // seconds const MAX_HISTORY_POINTS = Math.ceil((HISTORY_DURATION * 1000) / UPDATE_INTERVAL) // 150 points -const REQUEST_TIMEOUT = 5000 // ms +const REQUEST_TIMEOUT = 1000 // ms // Pause state let isPaused = false @@ -32,39 +32,55 @@ const hostState = HOSTS.map(host => ({ status: 'pending', // 'online', 'offline', 'pending', 'error' })) -// Measure latency using HEAD request +// Measure latency using HEAD request with Resource Timing API async function measureLatency(url) { const controller = new AbortController() const timeoutId = setTimeout(() => controller.abort(), REQUEST_TIMEOUT) - const start = performance.now() + // Add cache-busting parameter + const targetUrl = new URL(url) + const cacheBuster = `${Date.now()}-${Math.random().toString(36).slice(2)}` + targetUrl.searchParams.set('_cb', cacheBuster) + const finalUrl = targetUrl.toString() try { - // Add cache-busting parameter - const targetUrl = new URL(url) - targetUrl.searchParams.set('_cb', Date.now().toString()) - - await fetch(targetUrl.toString(), { + await fetch(finalUrl, { method: 'HEAD', - mode: 'no-cors', // Allow cross-origin without CORS headers + mode: 'no-cors', cache: 'no-store', signal: controller.signal, }) - const end = performance.now() clearTimeout(timeoutId) - return { latency: Math.round(end - start), error: null } + // Get accurate timing from Resource Timing API + const entries = performance.getEntriesByName(finalUrl, 'resource') + if (entries.length > 0) { + const entry = entries[entries.length - 1] + // Use responseEnd - requestStart for actual network time + // Fall back to duration if detailed timing not available (cross-origin) + let latency + if (entry.requestStart > 0 && entry.responseEnd > 0) { + latency = entry.responseEnd - entry.requestStart + } else { + // For opaque responses, duration includes queuing time but is still more accurate + latency = entry.duration + } + // Clean up the entry + performance.clearResourceTimings() + return { latency: Math.round(latency), error: null } + } + + // Fallback if no timing entry found + return { latency: null, error: null } } catch (err) { - const end = performance.now() clearTimeout(timeoutId) + performance.clearResourceTimings() if (err.name === 'AbortError') { return { latency: null, error: 'timeout' } } - // For no-cors mode, network errors indicate unreachable - // But successful completion means we got through return { latency: null, error: 'unreachable' } } } @@ -93,7 +109,7 @@ function getLatencyClass(latency, status) { return 'text-red-500' } -// Draw sparkline on canvas +// Draw sparkline on canvas - latest point at right edge, growing left function drawSparkline(canvas, history, hostColor) { const ctx = canvas.getContext('2d') const width = canvas.width @@ -134,7 +150,7 @@ function drawSparkline(canvas, history, hostColor) { ctx.stroke() } - // Draw the sparkline + // Draw the sparkline - right-aligned (latest at right edge) ctx.strokeStyle = hostColor ctx.lineWidth = 1.5 ctx.lineCap = 'round' @@ -142,13 +158,17 @@ function drawSparkline(canvas, history, hostColor) { ctx.beginPath() const pointWidth = (width - 2 * padding) / (MAX_HISTORY_POINTS - 1) + const historyLen = history.length + + // Calculate x position: latest point at right edge + const getX = (i) => width - padding - (historyLen - 1 - i) * pointWidth // Map all history points to x positions let firstPoint = true history.forEach((point, i) => { if (point.latency === null) return - const x = padding + i * pointWidth + const x = getX(i) const normalizedY = (point.latency - minLatency) / range const y = height - padding - normalizedY * (height - 2 * padding) @@ -162,11 +182,11 @@ function drawSparkline(canvas, history, hostColor) { ctx.stroke() - // Draw latest point indicator + // Draw latest point indicator (always at right edge) const lastValidPoint = [...history].reverse().find(p => p.latency !== null) if (lastValidPoint) { const lastIndex = history.lastIndexOf(lastValidPoint) - const x = padding + lastIndex * pointWidth + const x = getX(lastIndex) const normalizedY = (lastValidPoint.latency - minLatency) / range const y = height - padding - normalizedY * (height - 2 * padding) @@ -182,7 +202,7 @@ function drawSparkline(canvas, history, hostColor) { let errorStart = 0 history.forEach((point, i) => { - const x = padding + i * pointWidth + const x = getX(i) if (point.latency === null && !inErrorRegion) { inErrorRegion = true @@ -194,7 +214,8 @@ function drawSparkline(canvas, history, hostColor) { }) if (inErrorRegion) { - ctx.fillRect(errorStart, 0, width - errorStart, height) + const lastX = getX(historyLen - 1) + ctx.fillRect(errorStart, 0, lastX - errorStart + padding, height) } }