/** * Statistics page — real-time native host process metrics, system resources, * tunnel health, traffic stats, and peer connection history. * Depends on: core/utils.js ($, escapeHtml, truncate), core/messaging.js (sendToNative), * ui/toast.js (showToast) */ // ── Helpers ────────────────────────────────────────────────────────────────── function _fmtBytes(n) { if (n == null || isNaN(n)) return '—'; if (n >= 1073741824) return (n / 1073741824).toFixed(2) + ' GB'; if (n >= 1048576) return (n / 1048576).toFixed(1) + ' MB'; if (n >= 1024) return (n / 1024).toFixed(1) + ' KB'; return n + ' B'; } function _fmtUptime(ms) { if (!ms || ms < 0) return '—'; const s = Math.floor(ms / 1000); const d = Math.floor(s / 86400); const h = Math.floor((s % 86400) / 3600); const m = Math.floor((s % 3600) / 60); const sec = s % 60; if (d > 0) return `${d}d ${h}h ${m}m`; if (h > 0) return `${h}h ${m}m ${sec}s`; if (m > 0) return `${m}m ${sec}s`; return `${sec}s`; } function _fmtTime(epochMs) { if (!epochMs) return '—'; return new Date(epochMs).toLocaleString(); } function _fmtDuration(createdAt) { if (!createdAt) return '—'; return _fmtUptime(Date.now() - createdAt); } function _memBar(used, total) { const pct = total ? Math.min(100, Math.round((used / total) * 100)) : 0; const color = pct > 85 ? 'var(--red)' : pct > 65 ? 'var(--amber)' : 'var(--cyan)'; return `
${pct}%
`; } function _sparkline(history) { if (!history || history.length < 2) { return `Not enough data yet`; } const counts = history.map(b => b.count); const w = 240, h = 44; const max = Math.max(...counts, 1); const pts = counts.map((v, i) => { const x = (i / (counts.length - 1)) * w; const y = h - 4 - (v / max) * (h - 8); return `${x.toFixed(1)},${y.toFixed(1)}`; }).join(' '); const fillPts = `0,${h} ` + pts + ` ${w},${h}`; return ` `; } function _statChip(label, value, color) { return `
${value} ${label}
`; } function _sectionCard(title, iconSvg, content) { return `
${iconSvg} ${title}
${content}
`; } // ── Section renderers ───────────────────────────────────────────────────────── function _renderHostProcess(state, ps) { const connected = state.hostConnected; const sh = state.statsHistory || {}; const swUptime = state.stats?.uptime ? _fmtUptime(Date.now() - state.stats.uptime) : '—'; let processRows = ''; if (connected && ps && ps.process) { const mem = ps.process.memoryUsage; processRows = `
Heap Used / Total
${_fmtBytes(mem.heapUsed)} / ${_fmtBytes(mem.heapTotal)}
${_memBar(mem.heapUsed, mem.heapTotal)}
RSS (Physical)
${_fmtBytes(mem.rss)}
External: ${_fmtBytes(mem.external)}
${_statChip('PID', ps.process.pid, 'var(--text2)')} ${_statChip('Uptime', _fmtUptime(ps.process.uptimeMs), 'var(--cyan)')} ${_statChip('SW Uptime', swUptime, 'var(--text2)')}
`; } else if (!connected) { processRows = `
Native host is not connected.
`; } else { processRows = `
Loading process stats…
`; } const reconnectInfo = `
Reconnects
${sh.reconnectCount || 0}
Last Disconnect
${_fmtTime(sh.lastDisconnectAt)}
`; const statusBadge = connected ? `Connected` : `Disconnected`; const icon = ``; return _sectionCard( `Native Host Process ${statusBadge}`, icon, processRows + reconnectInfo ); } function _renderSystemResources(ps) { if (!ps || !ps.os) { return _sectionCard('System Resources', ``, `
Waiting for native host…
` ); } const o = ps.os; const usedMem = o.totalMemory - o.freeMemory; const loadAvg = Array.isArray(o.loadAvg) ? o.loadAvg : [0, 0, 0]; const loadColors = loadAvg.map(l => l > (o.cpuCount * 0.8) ? 'var(--red)' : l > (o.cpuCount * 0.5) ? 'var(--amber)' : 'var(--green)'); const content = `
System Memory ${_fmtBytes(usedMem)} / ${_fmtBytes(o.totalMemory)}
${_memBar(usedMem, o.totalMemory)}
CPU Load Average
${_statChip('1 min', loadAvg[0]?.toFixed(2) ?? '—', loadColors[0])} ${_statChip('5 min', loadAvg[1]?.toFixed(2) ?? '—', loadColors[1])} ${_statChip('15 min', loadAvg[2]?.toFixed(2) ?? '—', loadColors[2])} ${_statChip('Cores', o.cpuCount, 'var(--text2)')}
Platform: ${escapeHtml(o.platform || '—')} Arch: ${escapeHtml(o.arch || '—')}
`; return _sectionCard('System Resources', ``, content ); } function _renderTunnelHealth(state, ps) { const servers = state.servers || []; const vhosts = state.virtualHosts || []; const svcTunnels = state.serviceTunnels || []; function stateCounts(arr) { const c = { ready: 0, error: 0, closed: 0, connecting: 0 }; for (const t of arr) { const s = t.state || 'connecting'; if (s in c) c[s]++; else c.connecting++; } return c; } function stateRow(label, arr) { const c = stateCounts(arr); const total = arr.length; if (total === 0) return `
${label} None
`; return `
${label} (${total})
${c.ready ? `${c.ready} ready` : ''} ${c.connecting ? `${c.connecting} connecting` : ''} ${c.error ? `${c.error} error` : ''} ${c.closed ? `${c.closed} closed` : ''}
`; } // Aggregate byState from native host if available let aggregateRow = ''; if (ps && ps.tunnels && ps.tunnels.byState) { const b = ps.tunnels.byState; const total = (ps.tunnels.serverCount || 0) + (ps.tunnels.virtualHostCount || 0) + (ps.tunnels.serviceTunnelCount || 0); aggregateRow = `
All tunnels (${total})
${b.ready ? `${b.ready} ready` : ''} ${b.connecting ? `${b.connecting} connecting` : ''} ${b.error ? `${b.error} error` : ''} ${b.closed ? `${b.closed} closed` : ''}
`; } const content = ` ${stateRow('Server Tunnels', servers)} ${stateRow('Virtual Hosts', vhosts)} ${stateRow('Service Tunnels', svcTunnels)} ${aggregateRow}`; return _sectionCard('Tunnel Health', ``, content ); } function _renderTraffic(state) { const ts = state.trafficStats; if (!ts) return ''; const content = `
${_statChip('Bytes In', _fmtBytes(ts.bytesIn), 'var(--green)')} ${_statChip('Bytes Out', _fmtBytes(ts.bytesOut), 'var(--cyan)')} ${_statChip('Requests', (ts.requests ?? 0).toLocaleString(), 'var(--text2)')}
`; return _sectionCard('Proxy Traffic', ``, content ); } function _renderConnections(state) { const sh = state.statsHistory || {}; const active = state.activeConnections || []; const liveCount = state.stats?.totalConnections ?? active.length; const sparkHtml = _sparkline(sh.connectionRateHistory); let tableHtml = ''; if (active.length > 0) { const rows = active.map(c => ` ${escapeHtml(truncate(c.swarmId || '—', 18))} ${escapeHtml(truncate(c.peerKey || '—', 18))} ${_fmtDuration(c.createdAt)} `).join(''); tableHtml = `
${rows}
Swarm ID Peer Key Duration
`; } else { tableHtml = `
No active peer connections.
`; } const content = `
${_statChip('Active', liveCount, liveCount > 0 ? 'var(--green)' : 'var(--text2)')} ${_statChip('Total Ever', sh.totalConnectionsEver || 0, 'var(--text2)')}
Connections / minute (last 30 min)
${sparkHtml}
${tableHtml}`; return _sectionCard('Peer Connections', ``, content ); } function _renderProxyInfo(state) { let version = '—'; try { version = 'v' + chrome.runtime.getManifest().version; } catch (_) {} const content = `
${_statChip('Extension', version, 'var(--text2)')} ${_statChip('HTTPS Port', state.proxyPort ?? '—', 'var(--cyan)')} ${_statChip('CONNECT Port', state.connectProxyPort ?? '—', 'var(--cyan)')}
`; return _sectionCard('Extension & Proxy', ``, content ); } // ── Main render ─────────────────────────────────────────────────────────────── let _lastProcessStats = null; /** * Re-render the Statistics page with the latest state. * Also fires a getProcessStats request to the native host for live CPU/memory. * @param {object} state - Full extension state from fetchState(). */ function updateStatsPage(state) { const el = $('page-stats'); if (!el || !el.classList.contains('active')) return; // Fire async request for process stats; re-render when it arrives if (state.hostConnected) { chrome.runtime.sendMessage( { target: 'holesail-native', action: 'send', payload: { type: 'getProcessStats', payload: {} } }, (response) => { if (chrome.runtime.lastError) return; if (response && response.ok) { _lastProcessStats = response; _renderStatsPage(state, _lastProcessStats); } } ); } else { _lastProcessStats = null; } _renderStatsPage(state, _lastProcessStats); } function _renderStatsPage(state, ps) { const el = $('page-stats'); if (!el) return; el.innerHTML = `
${_renderHostProcess(state, ps)} ${_renderSystemResources(ps)} ${_renderTunnelHealth(state, ps)} ${_renderTraffic(state)} ${_renderConnections(state)} ${_renderProxyInfo(state)}
`; } /** * Attach event listeners for the Statistics page. * Called once during dashboard initialisation. */ function setupStatsEvents() { // No modals or interactive controls on this page — all data is read-only. }