import './styles.css' // Configuration const UPDATE_INTERVAL = 250 // ms const HISTORY_DURATION = 300 // seconds const MAX_HISTORY_POINTS = Math.ceil((HISTORY_DURATION * 1000) / UPDATE_INTERVAL) // 1200 points const REQUEST_TIMEOUT = 5000 // ms // Hosts to monitor (IPv4 preferred endpoints) const HOSTS = [ { name: 'Google Cloud Console', url: 'https://console.cloud.google.com', color: '#4285f4' }, { name: 'AWS Console', url: 'https://console.aws.amazon.com', color: '#ff9900' }, { name: 'GitHub', url: 'https://github.com', color: '#f0f6fc' }, { name: 'Cloudflare', url: 'https://www.cloudflare.com', color: '#f38020' }, { name: 'Microsoft Azure', url: 'https://portal.azure.com', color: '#0078d4' }, { name: 'DigitalOcean', url: 'https://www.digitalocean.com', color: '#0080ff' }, { name: 'Fastly CDN', url: 'https://www.fastly.com', color: '#ff282d' }, { name: 'Akamai', url: 'https://www.akamai.com', color: '#0096d6' }, { name: 'Local Gateway', url: 'http://192.168.100.1', color: '#a855f7' }, { name: 'datavi.be', url: 'https://datavi.be', color: '#10b981' }, ] // State: history for each host const hostState = HOSTS.map(host => ({ ...host, history: [], // Array of { timestamp, latency } or { timestamp, error } lastLatency: null, status: 'pending', // 'online', 'offline', 'pending', 'error' })) // Measure latency using HEAD request async function measureLatency(url) { const controller = new AbortController() const timeoutId = setTimeout(() => controller.abort(), REQUEST_TIMEOUT) const start = performance.now() try { // Add cache-busting parameter const targetUrl = new URL(url) targetUrl.searchParams.set('_cb', Date.now().toString()) await fetch(targetUrl.toString(), { method: 'HEAD', mode: 'no-cors', // Allow cross-origin without CORS headers cache: 'no-store', signal: controller.signal, }) const end = performance.now() clearTimeout(timeoutId) return { latency: Math.round(end - start), error: null } } catch (err) { const end = performance.now() clearTimeout(timeoutId) 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' } } } // Get latency color based on value function getLatencyColor(latency, status) { if (status === 'offline' || status === 'error' || latency === null) { return 'var(--color-latency-offline)' } if (latency < 50) return 'var(--color-latency-excellent)' if (latency < 100) return 'var(--color-latency-good)' if (latency < 200) return 'var(--color-latency-moderate)' if (latency < 500) return 'var(--color-latency-poor)' return 'var(--color-latency-bad)' } // Get Tailwind class for latency function getLatencyClass(latency, status) { if (status === 'offline' || status === 'error' || latency === null) { return 'text-gray-500' } if (latency < 50) return 'text-green-500' if (latency < 100) return 'text-lime-500' if (latency < 200) return 'text-yellow-500' if (latency < 500) return 'text-orange-500' return 'text-red-500' } // Draw sparkline on canvas function drawSparkline(canvas, history, hostColor) { const ctx = canvas.getContext('2d') const width = canvas.width const height = canvas.height const padding = 4 // Clear canvas ctx.clearRect(0, 0, width, height) // Filter valid latency points const validPoints = history.filter(p => p.latency !== null) if (validPoints.length < 2) { // Draw placeholder line ctx.strokeStyle = 'rgba(255,255,255,0.1)' ctx.lineWidth = 1 ctx.beginPath() ctx.moveTo(padding, height / 2) ctx.lineTo(width - padding, height / 2) ctx.stroke() return } // Calculate min/max for scaling const latencies = validPoints.map(p => p.latency) const minLatency = Math.min(...latencies) const maxLatency = Math.max(...latencies) const range = maxLatency - minLatency || 1 // Draw background grid lines ctx.strokeStyle = 'rgba(255,255,255,0.05)' ctx.lineWidth = 1 for (let i = 0; i <= 4; i++) { const y = padding + (i / 4) * (height - 2 * padding) ctx.beginPath() ctx.moveTo(0, y) ctx.lineTo(width, y) ctx.stroke() } // Draw the sparkline ctx.strokeStyle = hostColor ctx.lineWidth = 1.5 ctx.lineCap = 'round' ctx.lineJoin = 'round' ctx.beginPath() const pointWidth = (width - 2 * padding) / (MAX_HISTORY_POINTS - 1) // 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 normalizedY = (point.latency - minLatency) / range const y = height - padding - normalizedY * (height - 2 * padding) if (firstPoint) { ctx.moveTo(x, y) firstPoint = false } else { ctx.lineTo(x, y) } }) ctx.stroke() // Draw latest point indicator const lastValidPoint = [...history].reverse().find(p => p.latency !== null) if (lastValidPoint) { const lastIndex = history.lastIndexOf(lastValidPoint) const x = padding + lastIndex * pointWidth const normalizedY = (lastValidPoint.latency - minLatency) / range const y = height - padding - normalizedY * (height - 2 * padding) ctx.fillStyle = hostColor ctx.beginPath() ctx.arc(x, y, 3, 0, Math.PI * 2) ctx.fill() } // Draw error regions (red zones where connectivity was lost) ctx.fillStyle = 'rgba(239, 68, 68, 0.3)' let inErrorRegion = false let errorStart = 0 history.forEach((point, i) => { const x = padding + i * pointWidth if (point.latency === null && !inErrorRegion) { inErrorRegion = true errorStart = x } else if (point.latency !== null && inErrorRegion) { inErrorRegion = false ctx.fillRect(errorStart, 0, x - errorStart, height) } }) if (inErrorRegion) { ctx.fillRect(errorStart, 0, width - errorStart, height) } } // Create the UI function createUI() { const app = document.getElementById('app') app.innerHTML = `

NetWatch

Real-time network latency monitor | Updates every ${UPDATE_INTERVAL}ms | History: ${HISTORY_DURATION}s

${hostState.map((host, index) => `
${host.name}
${host.url}
---
waiting...
`).join('')}
` // Set up canvas sizes after DOM is ready requestAnimationFrame(() => { document.querySelectorAll('.sparkline-canvas').forEach(canvas => { const rect = canvas.getBoundingClientRect() canvas.width = rect.width * window.devicePixelRatio canvas.height = 60 * window.devicePixelRatio canvas.getContext('2d').scale(window.devicePixelRatio, window.devicePixelRatio) }) }) } // Update single host display function updateHostDisplay(index) { const host = hostState[index] const latencyEl = document.querySelector(`.latency-value[data-host="${index}"]`) const statusEl = document.querySelector(`.status-text[data-host="${index}"]`) const canvas = document.querySelector(`.sparkline-canvas[data-host="${index}"]`) if (!latencyEl || !statusEl || !canvas) return // Update latency value if (host.lastLatency !== null) { const colorClass = getLatencyClass(host.lastLatency, host.status) latencyEl.innerHTML = `${host.lastLatency}ms` } else if (host.status === 'offline' || host.status === 'error') { latencyEl.innerHTML = `---` } // Update status text const avgLatency = calculateAverageLatency(host.history) if (host.status === 'online' && avgLatency !== null) { statusEl.textContent = `avg: ${avgLatency}ms` statusEl.className = 'status-text text-xs text-gray-400 mt-1' } else if (host.status === 'offline') { statusEl.textContent = 'unreachable' statusEl.className = 'status-text text-xs text-red-400 mt-1' } else if (host.status === 'error') { statusEl.textContent = 'timeout' statusEl.className = 'status-text text-xs text-orange-400 mt-1' } else { statusEl.textContent = 'connecting...' statusEl.className = 'status-text text-xs text-gray-500 mt-1' } // Update sparkline const canvasRect = canvas.getBoundingClientRect() drawSparkline(canvas, host.history, host.color) } // Calculate average latency from history function calculateAverageLatency(history) { const validPoints = history.filter(p => p.latency !== null) if (validPoints.length === 0) return null const sum = validPoints.reduce((acc, p) => acc + p.latency, 0) return Math.round(sum / validPoints.length) } // Main measurement loop async function measureAll() { const timestamp = Date.now() // Measure all hosts in parallel const results = await Promise.all( hostState.map(host => measureLatency(host.url)) ) // Update state results.forEach((result, index) => { const host = hostState[index] // Add to history host.history.push({ timestamp, latency: result.latency, error: result.error, }) // Trim history to max size while (host.history.length > MAX_HISTORY_POINTS) { host.history.shift() } // Update current state host.lastLatency = result.latency if (result.error === 'timeout') { host.status = 'error' } else if (result.error) { host.status = 'offline' } else { host.status = 'online' } // Update display updateHostDisplay(index) }) } // Handle window resize function handleResize() { document.querySelectorAll('.sparkline-canvas').forEach((canvas, index) => { const rect = canvas.getBoundingClientRect() canvas.width = rect.width * window.devicePixelRatio canvas.height = 60 * window.devicePixelRatio const ctx = canvas.getContext('2d') ctx.setTransform(1, 0, 0, 1, 0, 0) ctx.scale(window.devicePixelRatio, window.devicePixelRatio) // Redraw if (hostState[index]) { drawSparkline(canvas, hostState[index].history, hostState[index].color) } }) } // Initialize function init() { createUI() // Start measurement loop measureAll() setInterval(measureAll, UPDATE_INTERVAL) // Handle resize window.addEventListener('resize', handleResize) // Initial resize setup after a short delay setTimeout(handleResize, 100) } // Run when DOM is ready if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', init) } else { init() }