import "./styles.css"; // --- Configuration ----------------------------------------------------------- // Timing, axis labels, and display constants. Latency above maxLatency is // clamped to "unreachable". The sparkline Y-axis is capped at // graphMaxLatency — values above it pin to the top of the chart but still // display their real value in the latency figure. The history buffer holds // maxHistoryPoints samples (historyDuration / updateInterval). const CONFIG = Object.freeze({ updateInterval: 2000, historyDuration: 300, requestTimeout: 1500, maxLatency: 1500, graphMaxLatency: 1000, yAxisTicks: [0, 250, 500, 750, 1000], xAxisTicks: [0, 60, 120, 180, 240, 300], canvasHeight: 80, get maxHistoryPoints() { return Math.ceil((this.historyDuration * 1000) / this.updateInterval); }, }); // WAN endpoints to monitor. These are used for the aggregate health/stats // display (min/max/avg). Ordered: personal, LLM APIs, big-3 cloud, then // CDN/hosting/other. const WAN_HOSTS = [ { name: "datavi.be", url: "https://datavi.be" }, { name: "Anthropic API", url: "https://api.anthropic.com" }, { name: "OpenAI API", url: "https://api.openai.com" }, { name: "AWS Console", url: "https://console.aws.amazon.com" }, { name: "Google Cloud Console", url: "https://console.cloud.google.com" }, { name: "Microsoft Azure", url: "https://portal.azure.com" }, { name: "Cloudflare", url: "https://www.cloudflare.com" }, { name: "Fastly CDN", url: "https://www.fastly.com" }, { name: "Akamai", url: "https://www.akamai.com" }, { name: "GitHub", url: "https://github.com" }, { name: "B2", url: "https://api.backblazeb2.com" }, { name: "S3 af-south-1 Cape Town (Africa)", url: "https://s3.af-south-1.amazonaws.com", }, { name: "S3 eu-west-2 London (Europe)", url: "https://s3.eu-west-2.amazonaws.com", }, { name: "S3 me-south-1 Bahrain (Middle East)", url: "https://s3.me-south-1.amazonaws.com", }, { name: "S3 ap-northeast-1 Tokyo (Asia)", url: "https://s3.ap-northeast-1.amazonaws.com", }, { name: "S3 ap-southeast-2 Sydney (Oceania)", url: "https://s3.ap-southeast-2.amazonaws.com", }, { name: "S3 us-west-2 Oregon (North America)", url: "https://s3.us-west-2.amazonaws.com", }, { name: "S3 sa-east-1 São Paulo (South America)", url: "https://s3.sa-east-1.amazonaws.com", }, // GCS locational endpoints — compare GCP routing vs AWS per-continent { name: "GCS us-central1 Iowa (North America)", url: "https://storage.us-central1.rep.googleapis.com", }, { name: "GCS europe-west1 Belgium (Europe)", url: "https://storage.europe-west1.rep.googleapis.com", }, { name: "GCS asia-southeast1 Singapore (Asia)", url: "https://storage.asia-southeast1.rep.googleapis.com", }, { name: "GCS australia-southeast1 Sydney (Oceania)", url: "https://storage.australia-southeast1.rep.googleapis.com", }, ]; // The cable modem / CPE upstream of the local gateway — always monitored. const LOCAL_CPE = { name: "Local CPE", url: "http://192.168.100.1" }; // Common default gateway addresses. On startup we probe each one and use // whichever responds first as the "Local Gateway" monitor target. // // NOTE: Modern browsers enforce Private Network Access (PNA) restrictions // that block pages served from public origins from making requests to // RFC1918 addresses. These local targets will likely only work when // NetWatch is served from localhost or another private address. const GATEWAY_CANDIDATES = [ "http://192.168.1.1", "http://192.168.0.1", "http://192.168.8.1", "http://10.0.0.1", ]; // --- Gateway Detection ------------------------------------------------------- // Probe each gateway candidate with a short timeout. Returns the first one // that responds, or null if none do. We race them all in parallel and take // whichever wins. async function detectGateway() { try { const result = await Promise.any( GATEWAY_CANDIDATES.map(async (url) => { const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), 1500); try { await fetch(url, { method: "HEAD", mode: "no-cors", cache: "no-store", signal: controller.signal, }); clearTimeout(timeoutId); return { name: "Local Gateway", url }; } catch { clearTimeout(timeoutId); throw new Error("no response"); } }), ); return result; } catch { // All candidates failed return null; } } // --- App State --------------------------------------------------------------- class HostState { constructor(host) { this.name = host.name; this.url = host.url; this.history = []; // { timestamp, latency, paused } this.lastLatency = null; this.status = "pending"; // 'online' | 'offline' | 'error' | 'pending' } pushSample(timestamp, result) { this.history.push({ timestamp, latency: result.latency, error: result.error, }); this._trim(); this.lastLatency = result.latency; if (result.error === "timeout") this.status = "error"; else if (result.error) this.status = "offline"; else this.status = "online"; } pushPaused(timestamp) { this.history.push({ timestamp, latency: null, paused: true }); this._trim(); } averageLatency() { const valid = this.history.filter((p) => p.latency !== null); if (valid.length === 0) return null; return Math.round( valid.reduce((s, p) => s + p.latency, 0) / valid.length, ); } _trim() { while (this.history.length > CONFIG.maxHistoryPoints) this.history.shift(); } } class AppState { constructor(localHosts) { this.wan = WAN_HOSTS.map((h) => new HostState(h)); this.local = localHosts.map((h) => new HostState(h)); this.paused = false; } get allHosts() { return [...this.wan, ...this.local]; } /** WAN-only stats from latest sample (excludes local) */ wanStats() { const reachable = this.wan.filter((h) => h.lastLatency !== null); const latencies = reachable.map((h) => h.lastLatency); const total = this.wan.length; if (latencies.length === 0) return { reachable: 0, total, min: null, max: null, avg: null }; return { reachable: latencies.length, total, min: Math.min(...latencies), max: Math.max(...latencies), avg: Math.round( latencies.reduce((a, b) => a + b, 0) / latencies.length, ), }; } /** Overall health: true = healthy (more than half WAN reachable) */ isHealthy() { const reachable = this.wan.filter((h) => h.lastLatency !== null).length; return reachable > this.wan.length / 2; } } // --- Latency Measurement ----------------------------------------------------- async function measureLatency(url) { const controller = new AbortController(); const timeoutId = setTimeout( () => controller.abort(), CONFIG.requestTimeout, ); const targetUrl = new URL(url); targetUrl.searchParams.set("_cb", Date.now().toString()); const start = performance.now(); try { await fetch(targetUrl.toString(), { method: "HEAD", mode: "no-cors", cache: "no-store", signal: controller.signal, }); const latency = Math.round(performance.now() - start); clearTimeout(timeoutId); if (latency > CONFIG.maxLatency) return { latency: null, error: "timeout" }; return { latency, error: null }; } catch (err) { clearTimeout(timeoutId); if (err.name === "AbortError") return { latency: null, error: "timeout" }; return { latency: null, error: "unreachable" }; } } // --- Color Helpers ----------------------------------------------------------- function latencyHex(latency) { if (latency === null) return "#6b7280"; if (latency < 50) return "#22c55e"; if (latency < 100) return "#84cc16"; if (latency < 200) return "#eab308"; if (latency < 500) return "#f97316"; return "#ef4444"; } function latencyClass(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"; } // --- Sparkline Renderer ------------------------------------------------------ class SparklineRenderer { static MARGIN = { left: 35, right: 10, top: 5, bottom: 18 }; static draw(canvas, history) { const ctx = canvas.getContext("2d"); const dpr = window.devicePixelRatio || 1; const w = canvas.width / dpr; const h = canvas.height / dpr; const m = SparklineRenderer.MARGIN; const cw = w - m.left - m.right; const ch = h - m.top - m.bottom; ctx.clearRect(0, 0, w, h); SparklineRenderer._drawYAxis(ctx, w, h, m, ch); SparklineRenderer._drawXAxis(ctx, w, h, m, cw); const len = history.length; const pw = cw / (CONFIG.maxHistoryPoints - 1); const getX = (i) => m.left + cw - (len - 1 - i) * pw; const getY = (lat) => m.top + ch - (Math.min(lat, CONFIG.graphMaxLatency) / CONFIG.graphMaxLatency) * ch; SparklineRenderer._drawErrors(ctx, history, getX, m.top, ch); SparklineRenderer._drawLine(ctx, history, getX, getY); SparklineRenderer._drawTip(ctx, history, getX, getY); } static _drawYAxis(ctx, w, h, m, ch) { ctx.font = "9px monospace"; ctx.textAlign = "right"; ctx.textBaseline = "middle"; for (const tick of CONFIG.yAxisTicks) { const y = m.top + ch - (tick / CONFIG.graphMaxLatency) * ch; ctx.strokeStyle = "rgba(255,255,255,0.1)"; ctx.lineWidth = 1; ctx.beginPath(); ctx.moveTo(m.left, y); ctx.lineTo(w - m.right, y); ctx.stroke(); ctx.fillStyle = "rgba(255,255,255,0.5)"; ctx.fillText(`${tick}`, m.left - 4, y); } } static _drawXAxis(ctx, w, h, m, cw) { ctx.textAlign = "center"; ctx.textBaseline = "top"; for (const tick of CONFIG.xAxisTicks) { const x = m.left + cw - (tick / CONFIG.historyDuration) * cw; ctx.fillStyle = "rgba(255,255,255,0.5)"; ctx.fillText(`-${tick}s`, x, h - m.bottom + 4); } } static _drawErrors(ctx, history, getX, top, ch) { ctx.fillStyle = "rgba(239, 68, 68, 0.2)"; let inErr = false, start = 0; for (let i = 0; i < history.length; i++) { const p = history[i]; const x = getX(i); // Only real errors, not paused gaps const isError = p.latency === null && !p.paused; if (isError && !inErr) { inErr = true; start = x; } else if (!isError && inErr) { inErr = false; ctx.fillRect(start, top, x - start, ch); } } if (inErr) ctx.fillRect(start, top, getX(history.length - 1) - start + 5, ch); } static _drawLine(ctx, history, getX, getY) { ctx.lineWidth = 2; ctx.lineCap = "round"; ctx.lineJoin = "round"; let prev = null; for (let i = 0; i < history.length; i++) { const p = history[i]; if (p.latency === null) { prev = null; continue; } const x = getX(i), y = getY(p.latency); if (prev) { ctx.strokeStyle = latencyHex(p.latency); ctx.beginPath(); ctx.moveTo(prev.x, prev.y); ctx.lineTo(x, y); ctx.stroke(); } prev = { x, y }; } } static _drawTip(ctx, history, getX, getY) { for (let i = history.length - 1; i >= 0; i--) { if (history[i].latency !== null) { const x = getX(i), y = getY(history[i].latency); ctx.fillStyle = latencyHex(history[i].latency); ctx.beginPath(); ctx.arc(x, y, 3, 0, Math.PI * 2); ctx.fill(); return; } } } static sizeCanvas(canvas) { const dpr = window.devicePixelRatio || 1; const rect = canvas.getBoundingClientRect(); canvas.width = rect.width * dpr; canvas.height = CONFIG.canvasHeight * dpr; const ctx = canvas.getContext("2d"); ctx.setTransform(1, 0, 0, 1, 0, 0); ctx.scale(dpr, dpr); } } // --- UI Renderer ------------------------------------------------------------- function hostRowHTML(host, index) { return `
${host.name}
${host.url}
---
waiting...
`; } function buildUI(state) { const app = document.getElementById("app"); app.innerHTML = `

NetWatch

Real-time network latency monitor | Updates every ${CONFIG.updateInterval / 1000}s | History: ${CONFIG.historyDuration}s | Running

Waiting for data...
Reachable: --/-- | Min: --ms | Max: --ms | Avg: --ms
${state.wan.map((h, i) => hostRowHTML(h, i)).join("")}

Local Network

${state.local.map((h, i) => hostRowHTML(h, state.wan.length + i)).join("")}
`; requestAnimationFrame(() => { document .querySelectorAll(".sparkline-canvas") .forEach((c) => SparklineRenderer.sizeCanvas(c)); }); } // --- UI Updaters ------------------------------------------------------------- function updateHostRow(host, 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; if (host.lastLatency !== null) { const cls = latencyClass(host.lastLatency, host.status); latencyEl.innerHTML = `${host.lastLatency}ms`; } else if (host.status === "offline" || host.status === "error") { latencyEl.innerHTML = `---`; } const avg = host.averageLatency(); if (host.status === "online" && avg !== null) { statusEl.textContent = `avg: ${avg}ms`; statusEl.className = `status-text text-xs mt-1 ${latencyClass(avg, "online")}`; } 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"; } SparklineRenderer.draw(canvas, host.history); } function updateSummary(state) { const stats = state.wanStats(); const reachableEl = document.getElementById("summary-reachable"); const minEl = document.getElementById("summary-min"); const maxEl = document.getElementById("summary-max"); const avgEl = document.getElementById("summary-avg"); if (!reachableEl) return; reachableEl.textContent = `${stats.reachable}/${stats.total}`; reachableEl.className = stats.reachable === stats.total ? "text-green-400" : stats.reachable === 0 ? "text-red-400" : "text-yellow-400"; if (stats.min !== null) { minEl.textContent = `${stats.min}ms`; minEl.className = latencyClass(stats.min, "online"); maxEl.textContent = `${stats.max}ms`; maxEl.className = latencyClass(stats.max, "online"); avgEl.textContent = `${stats.avg}ms`; avgEl.className = latencyClass(stats.avg, "online"); } else { for (const el of [minEl, maxEl, avgEl]) { el.textContent = "--ms"; el.className = "text-gray-500"; } } } function updateHealthBox(state) { const el = document.getElementById("health-text"); const box = document.getElementById("health-box"); if (!el || !box) return; const anyData = state.wan.some((h) => h.status !== "pending"); if (!anyData) return; const healthy = state.isHealthy(); el.textContent = healthy ? "HEALTHY" : "DEGRADED"; el.className = healthy ? "text-green-400 font-bold" : "text-red-400 font-bold"; box.className = `mt-4 p-3 rounded-lg border font-mono text-sm text-center ${ healthy ? "bg-green-900/20 border-green-700/50" : "bg-red-900/20 border-red-700/50" }`; } // --- Main Loop --------------------------------------------------------------- async function tick(state) { const ts = Date.now(); if (state.paused) { // No probes — just push a paused marker so the chart keeps scrolling for (const host of state.allHosts) { host.pushPaused(ts); } // Redraw sparklines only state.allHosts.forEach((host, i) => { const canvas = document.querySelector( `.sparkline-canvas[data-host="${i}"]`, ); if (canvas) SparklineRenderer.draw(canvas, host.history); }); return; } const results = await Promise.all( state.allHosts.map((h) => measureLatency(h.url)), ); state.allHosts.forEach((host, i) => { host.pushSample(ts, results[i]); updateHostRow(host, i); }); updateSummary(state); updateHealthBox(state); } // --- Pause / Resume ---------------------------------------------------------- function togglePause(state) { state.paused = !state.paused; const pauseIcon = document.getElementById("pause-icon"); const playIcon = document.getElementById("play-icon"); const pauseText = document.getElementById("pause-text"); const indicator = document.getElementById("status-indicator"); if (state.paused) { pauseIcon.classList.add("hidden"); playIcon.classList.remove("hidden"); pauseText.textContent = "Resume"; indicator.textContent = "Paused"; indicator.className = "text-yellow-400"; } else { pauseIcon.classList.remove("hidden"); playIcon.classList.add("hidden"); pauseText.textContent = "Pause"; indicator.textContent = "Running"; indicator.className = "text-green-400"; } } // --- Resize ------------------------------------------------------------------ function handleResize(state) { document.querySelectorAll(".sparkline-canvas").forEach((canvas, i) => { SparklineRenderer.sizeCanvas(canvas); const host = state.allHosts[i]; if (host) SparklineRenderer.draw(canvas, host.history); }); } // --- Bootstrap --------------------------------------------------------------- async function init() { // Probe common gateway IPs to find the local router const gateway = await detectGateway(); const localHosts = [LOCAL_CPE]; if (gateway) localHosts.push(gateway); const state = new AppState(localHosts); buildUI(state); document .getElementById("pause-btn") .addEventListener("click", () => togglePause(state)); tick(state); setInterval(() => tick(state), CONFIG.updateInterval); window.addEventListener("resize", () => handleResize(state)); setTimeout(() => handleResize(state), 100); } if (document.readyState === "loading") { document.addEventListener("DOMContentLoaded", init); } else { init(); }