diff --git a/src/main.js b/src/main.js index 2940d34..4eeb18b 100644 --- a/src/main.js +++ b/src/main.js @@ -1,43 +1,109 @@ import './styles.css' -// Configuration -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 = 1000 // ms +// --- Configuration ----------------------------------------------------------- -// Pause state -let isPaused = false -let intervalId = null +const CONFIG = Object.freeze({ + updateInterval: 2000, + historyDuration: 300, + requestTimeout: 1000, + maxLatency: 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) + }, +}) -// 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' }, +const WAN_HOSTS = [ + { name: 'Google Cloud Console', url: 'https://console.cloud.google.com' }, + { name: 'AWS Console', url: 'https://console.aws.amazon.com' }, + { name: 'GitHub', url: 'https://github.com' }, + { name: 'Cloudflare', url: 'https://www.cloudflare.com' }, + { name: 'Microsoft Azure', url: 'https://portal.azure.com' }, + { name: 'DigitalOcean', url: 'https://www.digitalocean.com' }, + { name: 'Fastly CDN', url: 'https://www.fastly.com' }, + { name: 'Akamai', url: 'https://www.akamai.com' }, + { name: 'datavi.be', url: 'https://datavi.be' }, ] -// 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' -})) +const LOCAL_HOSTS = [ + { name: 'Local Gateway', url: 'http://192.168.100.1' }, +] + +// --- 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() { + this.wan = WAN_HOSTS.map(h => new HostState(h)) + this.local = LOCAL_HOSTS.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 ----------------------------------------------------- -// Measure latency using HEAD request async function measureLatency(url) { const controller = new AbortController() - const timeoutId = setTimeout(() => controller.abort(), REQUEST_TIMEOUT) + const timeoutId = setTimeout(() => controller.abort(), CONFIG.requestTimeout) - // Add cache-busting parameter const targetUrl = new URL(url) targetUrl.searchParams.set('_cb', Date.now().toString()) @@ -50,209 +116,196 @@ async function measureLatency(url) { cache: 'no-store', signal: controller.signal, }) - const latency = Math.round(performance.now() - start) clearTimeout(timeoutId) - // Clamp >1000ms to unreachable - if (latency > 1000) { - return { latency: null, error: 'timeout' } - } + 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' } - } - + if (err.name === 'AbortError') return { latency: null, error: 'timeout' } return { latency: null, error: 'unreachable' } } } -// Get latency color (raw hex for canvas) -function getLatencyColor(latency) { - if (latency === null) return '#6b7280' // gray - if (latency < 50) return '#22c55e' // green - if (latency < 100) return '#84cc16' // lime - if (latency < 200) return '#eab308' // yellow - if (latency < 500) return '#f97316' // orange - return '#ef4444' // red +// --- 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' } -// 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' +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' } -// Fixed axis constants -const Y_AXIS_MAX = 1000 // ms -const Y_AXIS_TICKS = [0, 250, 500, 750, 1000] -const X_AXIS_TICKS = [0, 60, 120, 180, 240, 300] // seconds ago +// --- Sparkline Renderer ------------------------------------------------------ -// Draw sparkline on canvas - latest point at right edge, growing left -function drawSparkline(canvas, history) { - const ctx = canvas.getContext('2d') - const dpr = window.devicePixelRatio || 1 - const width = canvas.width / dpr - const height = canvas.height / dpr +class SparklineRenderer { + static MARGIN = { left: 35, right: 10, top: 5, bottom: 18 } - // Margins for axis labels - const marginLeft = 35 - const marginRight = 10 - const marginTop = 5 - const marginBottom = 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 - const chartWidth = width - marginLeft - marginRight - const chartHeight = height - marginTop - marginBottom + ctx.clearRect(0, 0, w, h) + SparklineRenderer._drawYAxis(ctx, w, h, m, ch) + SparklineRenderer._drawXAxis(ctx, w, h, m, cw) - // Clear canvas - ctx.clearRect(0, 0, width, height) + 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 - (lat / CONFIG.maxLatency) * ch - // Draw Y-axis labels and grid lines - ctx.font = '9px monospace' - ctx.textAlign = 'right' - ctx.textBaseline = 'middle' - - Y_AXIS_TICKS.forEach(tick => { - const y = marginTop + chartHeight - (tick / Y_AXIS_MAX) * chartHeight - - // Grid line - ctx.strokeStyle = 'rgba(255,255,255,0.1)' - ctx.lineWidth = 1 - ctx.beginPath() - ctx.moveTo(marginLeft, y) - ctx.lineTo(width - marginRight, y) - ctx.stroke() - - // Label - ctx.fillStyle = 'rgba(255,255,255,0.5)' - ctx.fillText(`${tick}`, marginLeft - 4, y) - }) - - // Draw X-axis labels - ctx.textAlign = 'center' - ctx.textBaseline = 'top' - - X_AXIS_TICKS.forEach(tick => { - // tick is seconds ago, 0 = now (right edge), 300 = 5min ago (left edge) - const x = marginLeft + chartWidth - (tick / HISTORY_DURATION) * chartWidth - - // Label - ctx.fillStyle = 'rgba(255,255,255,0.5)' - ctx.fillText(`-${tick}s`, x, height - marginBottom + 4) - }) - - // Helper functions for chart coordinates - const historyLen = history.length - const pointWidth = chartWidth / (MAX_HISTORY_POINTS - 1) - - const getX = (i) => marginLeft + chartWidth - (historyLen - 1 - i) * pointWidth - const getY = (latency) => marginTop + chartHeight - (latency / Y_AXIS_MAX) * chartHeight - - // Draw error regions first (behind the line) - ctx.fillStyle = 'rgba(239, 68, 68, 0.2)' - let inErrorRegion = false - let errorStart = 0 - - history.forEach((point, i) => { - const x = getX(i) - - if (point.latency === null && !inErrorRegion) { - inErrorRegion = true - errorStart = x - } else if (point.latency !== null && inErrorRegion) { - inErrorRegion = false - ctx.fillRect(errorStart, marginTop, x - errorStart, chartHeight) - } - }) - - if (inErrorRegion) { - const lastX = getX(historyLen - 1) - ctx.fillRect(errorStart, marginTop, lastX - errorStart + 5, chartHeight) + SparklineRenderer._drawErrors(ctx, history, getX, m.top, ch) + SparklineRenderer._drawLine(ctx, history, getX, getY) + SparklineRenderer._drawTip(ctx, history, getX, getY) } - // Draw the sparkline segments - color coded by latency - ctx.lineWidth = 2 - ctx.lineCap = 'round' - ctx.lineJoin = 'round' - - let prevPoint = null - let prevX = 0 - let prevY = 0 - - history.forEach((point, i) => { - if (point.latency === null) { - prevPoint = null - return + 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.maxLatency) * 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) } + } - const x = getX(i) - const y = getY(point.latency) - - if (prevPoint !== null) { - ctx.strokeStyle = getLatencyColor(point.latency) - ctx.beginPath() - ctx.moveTo(prevX, prevY) - ctx.lineTo(x, y) - ctx.stroke() + 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) } + } - prevPoint = point - prevX = x - prevY = y - }) + 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) + } - // Draw latest point indicator - const lastValidPoint = [...history].reverse().find(p => p.latency !== null) - if (lastValidPoint) { - const lastIndex = history.lastIndexOf(lastValidPoint) - const x = getX(lastIndex) - const y = getY(lastValidPoint.latency) + 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 } + } + } - ctx.fillStyle = getLatencyColor(lastValidPoint.latency) - ctx.beginPath() - ctx.arc(x, y, 3, 0, Math.PI * 2) - ctx.fill() + 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) } } -// Create the UI -function createUI() { - const app = document.getElementById('app') +// --- 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 ${UPDATE_INTERVAL / 1000}s | - History: ${HISTORY_DURATION}s | + Updates every ${CONFIG.updateInterval / 1000}s | + History: ${CONFIG.historyDuration}s | Running

-
+
+ Waiting for data... +
+
Reachable: --/-- | Min: --ms @@ -263,40 +316,13 @@ function createUI() {
-
- ${hostState.map((host, index) => ` -
-
- -
-
-
- ${host.name} -
-
${host.url}
-
+
+ ${state.wan.map((h, i) => hostRowHTML(h, i)).join('')} +
- -
-
- --- -
-
- waiting... -
-
- - -
- -
-
-
- `).join('')} +

Local Network

+
+ ${state.local.map((h, i) => hostRowHTML(h, state.wan.length + i)).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 = 80 * window.devicePixelRatio - canvas.getContext('2d').scale(window.devicePixelRatio, window.devicePixelRatio) - }) + document.querySelectorAll('.sparkline-canvas').forEach(c => SparklineRenderer.sizeCanvas(c)) }) } -// Update single host display -function updateHostDisplay(index) { - const host = hostState[index] +// --- 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 - // Update latency value if (host.lastLatency !== null) { - const colorClass = getLatencyClass(host.lastLatency, host.status) - latencyEl.innerHTML = `${host.lastLatency}ms` + const cls = latencyClass(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` + const avg = host.averageLatency() + if (host.status === 'online' && avg !== null) { + statusEl.textContent = `avg: ${avg}ms` statusEl.className = 'status-text text-xs text-gray-400 mt-1' } else if (host.status === 'offline') { statusEl.textContent = 'unreachable' @@ -357,170 +373,123 @@ function updateHostDisplay(index) { statusEl.className = 'status-text text-xs text-gray-500 mt-1' } - // Update sparkline - const canvasRect = canvas.getBoundingClientRect() - drawSparkline(canvas, host.history) + SparklineRenderer.draw(canvas, host.history) } -// 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) -} - -// Update summary line with current host stats -function updateSummary() { - const reachableHosts = hostState.filter(h => h.lastLatency !== null) - const reachableCount = reachableHosts.length - const totalCount = hostState.length +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 = `${reachableCount}/${totalCount}` - reachableEl.className = reachableCount === totalCount ? 'text-green-400' : - reachableCount === 0 ? 'text-red-400' : 'text-yellow-400' + 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 (reachableCount > 0) { - const latencies = reachableHosts.map(h => h.lastLatency) - const min = Math.min(...latencies) - const max = Math.max(...latencies) - const avg = Math.round(latencies.reduce((a, b) => a + b, 0) / latencies.length) - - minEl.textContent = `${min}ms` - minEl.className = getLatencyClass(min, 'online') - maxEl.textContent = `${max}ms` - maxEl.className = getLatencyClass(max, 'online') - avgEl.textContent = `${avg}ms` - avgEl.className = getLatencyClass(avg, 'online') + 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 { - minEl.textContent = '--ms' - minEl.className = 'text-gray-500' - maxEl.textContent = '--ms' - maxEl.className = 'text-gray-500' - avgEl.textContent = '--ms' - avgEl.className = 'text-gray-500' + for (const el of [minEl, maxEl, avgEl]) { el.textContent = '--ms'; el.className = 'text-gray-500' } } } -// Main measurement loop -async function measureAll() { - const timestamp = Date.now() +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 + } - // Measure all hosts in parallel const results = await Promise.all( - hostState.map(host => measureLatency(host.url)) + state.allHosts.map(h => measureLatency(h.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) + state.allHosts.forEach((host, i) => { + host.pushSample(ts, results[i]) + updateHostRow(host, i) }) - // Update summary - updateSummary() + updateSummary(state) + updateHealthBox(state) } -// Handle window resize -function handleResize() { - document.querySelectorAll('.sparkline-canvas').forEach((canvas, index) => { - const rect = canvas.getBoundingClientRect() - canvas.width = rect.width * window.devicePixelRatio - canvas.height = 80 * 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) - } - }) -} - -// Toggle pause state -function togglePause() { - isPaused = !isPaused +// --- 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 statusIndicator = document.getElementById('status-indicator') + const indicator = document.getElementById('status-indicator') - if (isPaused) { - // Stop the interval - if (intervalId) { - clearInterval(intervalId) - intervalId = null - } - pauseIcon.classList.add('hidden') - playIcon.classList.remove('hidden') + if (state.paused) { + pauseIcon.classList.add('hidden'); playIcon.classList.remove('hidden') pauseText.textContent = 'Resume' - statusIndicator.textContent = 'Paused' - statusIndicator.className = 'text-yellow-400' + indicator.textContent = 'Paused'; indicator.className = 'text-yellow-400' } else { - // Resume the interval - measureAll() - intervalId = setInterval(measureAll, UPDATE_INTERVAL) - pauseIcon.classList.remove('hidden') - playIcon.classList.add('hidden') + pauseIcon.classList.remove('hidden'); playIcon.classList.add('hidden') pauseText.textContent = 'Pause' - statusIndicator.textContent = 'Running' - statusIndicator.className = 'text-green-400' + indicator.textContent = 'Running'; indicator.className = 'text-green-400' } } -// Initialize -function init() { - createUI() +// --- Resize ------------------------------------------------------------------ - // Wire up pause button - document.getElementById('pause-btn').addEventListener('click', togglePause) - - // Start measurement loop - measureAll() - intervalId = setInterval(measureAll, UPDATE_INTERVAL) - - // Handle resize - window.addEventListener('resize', handleResize) - - // Initial resize setup after a short delay - setTimeout(handleResize, 100) +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 --------------------------------------------------------------- + +function init() { + const state = new AppState() + 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) } -// Run when DOM is ready if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', init) } else {