/**
* 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 `
`;
}
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 `
`;
}
// ── 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 = `
Swarm ID
Peer Key
Duration
${rows}
`;
} 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.
}