revert: remove Statistics tab and all associated changes
CI / Build & Test (push) Successful in 3m7s
CI / Build & Test (push) Successful in 3m7s
This commit is contained in:
@@ -16,7 +16,6 @@ importScripts(
|
||||
'background/logs.js',
|
||||
'background/state.js',
|
||||
'background/proxy.js',
|
||||
'background/stats-tracker.js',
|
||||
'background/native-messaging.js',
|
||||
'background/tab-lifecycle.js',
|
||||
'background/message-router.js'
|
||||
|
||||
@@ -123,9 +123,7 @@ browser.runtime.onMessage.addListener((message, sender, sendResponse) => {
|
||||
sshConnections: payload.sshConnections || [],
|
||||
rdpConnections: payload.rdpConnections || [],
|
||||
trafficStats: payload.trafficStats || null,
|
||||
stats: { ...extensionState.stats, ...payload.stats },
|
||||
statsHistory: getStatsHistory(),
|
||||
activeConnections: Array.from(activeConnections.values()),
|
||||
stats: { ...extensionState.stats, ...payload.stats }
|
||||
}
|
||||
});
|
||||
} else {
|
||||
@@ -141,9 +139,7 @@ browser.runtime.onMessage.addListener((message, sender, sendResponse) => {
|
||||
proxyPort: null,
|
||||
connectProxyPort: null,
|
||||
caInstalled: false,
|
||||
stats: extensionState.stats,
|
||||
statsHistory: getStatsHistory(),
|
||||
activeConnections: Array.from(activeConnections.values()),
|
||||
stats: extensionState.stats
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -162,9 +158,7 @@ browser.runtime.onMessage.addListener((message, sender, sendResponse) => {
|
||||
proxyPort: null,
|
||||
connectProxyPort: null,
|
||||
caInstalled: false,
|
||||
stats: extensionState.stats,
|
||||
statsHistory: getStatsHistory(),
|
||||
activeConnections: Array.from(activeConnections.values()),
|
||||
stats: extensionState.stats
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -182,9 +176,7 @@ browser.runtime.onMessage.addListener((message, sender, sendResponse) => {
|
||||
proxyPort: null,
|
||||
connectProxyPort: null,
|
||||
caInstalled: false,
|
||||
stats: extensionState.stats,
|
||||
statsHistory: getStatsHistory(),
|
||||
activeConnections: Array.from(activeConnections.values()),
|
||||
stats: extensionState.stats
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -155,7 +155,6 @@ function connect() {
|
||||
peerKey: peerInfo.publicKey || '',
|
||||
createdAt: Date.now()
|
||||
});
|
||||
onConnection();
|
||||
updateExtensionState();
|
||||
} else if (msg.event === 'error') {
|
||||
const connId = payload.connId;
|
||||
@@ -230,7 +229,6 @@ function connect() {
|
||||
log('Native host disconnected');
|
||||
extensionState.hostConnected = false;
|
||||
activeConnections.clear();
|
||||
onDisconnect();
|
||||
updateExtensionState();
|
||||
clearProxy();
|
||||
port = null;
|
||||
|
||||
@@ -1,61 +0,0 @@
|
||||
/**
|
||||
* Extension-side statistics tracker.
|
||||
* Maintains a cumulative connection counter, reconnect history, and a
|
||||
* per-minute connection-rate ring buffer (last 30 buckets) for sparkline display.
|
||||
* Depends on nothing — must be loaded before native-messaging.js.
|
||||
*/
|
||||
|
||||
const BUCKET_INTERVAL_MS = 60 * 1000; // 1 minute
|
||||
const MAX_BUCKETS = 30;
|
||||
|
||||
const _statsHistory = {
|
||||
totalConnectionsEver: 0,
|
||||
reconnectCount: 0,
|
||||
lastDisconnectAt: null,
|
||||
connectionRateHistory: [], // [{t: epochMs, count: number}] newest last
|
||||
_bucketCount: 0,
|
||||
_bucketStart: Date.now(),
|
||||
};
|
||||
|
||||
function _flushBucket() {
|
||||
const now = Date.now();
|
||||
// How many full minutes have elapsed since the bucket started?
|
||||
const elapsed = now - _statsHistory._bucketStart;
|
||||
const fullBuckets = Math.floor(elapsed / BUCKET_INTERVAL_MS);
|
||||
if (fullBuckets === 0) return;
|
||||
|
||||
for (let i = 0; i < fullBuckets; i++) {
|
||||
const t = _statsHistory._bucketStart + i * BUCKET_INTERVAL_MS;
|
||||
// Only the first bucket gets the accumulated count; the rest are zero (idle minutes)
|
||||
_statsHistory.connectionRateHistory.push({ t, count: i === 0 ? _statsHistory._bucketCount : 0 });
|
||||
}
|
||||
|
||||
// Trim to last MAX_BUCKETS entries
|
||||
if (_statsHistory.connectionRateHistory.length > MAX_BUCKETS) {
|
||||
_statsHistory.connectionRateHistory = _statsHistory.connectionRateHistory.slice(-MAX_BUCKETS);
|
||||
}
|
||||
|
||||
_statsHistory._bucketCount = 0;
|
||||
_statsHistory._bucketStart = _statsHistory._bucketStart + fullBuckets * BUCKET_INTERVAL_MS;
|
||||
}
|
||||
|
||||
function onConnection() {
|
||||
_flushBucket();
|
||||
_statsHistory._bucketCount++;
|
||||
_statsHistory.totalConnectionsEver++;
|
||||
}
|
||||
|
||||
function onDisconnect() {
|
||||
_statsHistory.lastDisconnectAt = Date.now();
|
||||
_statsHistory.reconnectCount++;
|
||||
}
|
||||
|
||||
function getStatsHistory() {
|
||||
_flushBucket();
|
||||
return {
|
||||
totalConnectionsEver: _statsHistory.totalConnectionsEver,
|
||||
reconnectCount: _statsHistory.reconnectCount,
|
||||
lastDisconnectAt: _statsHistory.lastDisconnectAt,
|
||||
connectionRateHistory: _statsHistory.connectionRateHistory.slice(),
|
||||
};
|
||||
}
|
||||
@@ -14,7 +14,6 @@ const PAGE_TITLES = {
|
||||
rdp: 'Remote Desktop',
|
||||
backups: 'Backups',
|
||||
logs: 'Logs',
|
||||
stats: 'Statistics',
|
||||
settings: 'Settings'
|
||||
};
|
||||
|
||||
|
||||
@@ -106,14 +106,6 @@
|
||||
</svg>
|
||||
Logs
|
||||
</div>
|
||||
<div class="nav-item" data-page="stats">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<line x1="18" y1="20" x2="18" y2="10"/>
|
||||
<line x1="12" y1="20" x2="12" y2="4"/>
|
||||
<line x1="6" y1="20" x2="6" y2="14"/>
|
||||
</svg>
|
||||
Statistics
|
||||
</div>
|
||||
<div class="nav-item" data-page="settings">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<circle cx="12" cy="12" r="3"/>
|
||||
@@ -560,10 +552,6 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ── Statistics page ────────────────────────── -->
|
||||
<div class="page" id="page-stats">
|
||||
</div>
|
||||
|
||||
<!-- ── Settings page ──────────────────────────── -->
|
||||
<div class="page" id="page-settings">
|
||||
<div class="card">
|
||||
@@ -1298,7 +1286,6 @@
|
||||
<script src="pages/logs.js"></script>
|
||||
<script src="pages/ssh.js"></script>
|
||||
<script src="pages/rdp.js"></script>
|
||||
<script src="pages/stats.js"></script>
|
||||
|
||||
<!-- Orchestration -->
|
||||
<script src="refresh.js"></script>
|
||||
|
||||
@@ -242,5 +242,4 @@ function setupEvents() {
|
||||
setupServerEvents();
|
||||
setupServiceTunnelEvents();
|
||||
setupLogsEvents();
|
||||
setupStatsEvents();
|
||||
}
|
||||
|
||||
@@ -1,398 +0,0 @@
|
||||
/**
|
||||
* 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 `
|
||||
<div style="display:flex;align-items:center;gap:8px;flex:1;">
|
||||
<div style="flex:1;height:6px;background:var(--border);border-radius:99px;overflow:hidden;">
|
||||
<div style="width:${pct}%;height:100%;background:${color};border-radius:99px;transition:width 0.4s ease;"></div>
|
||||
</div>
|
||||
<span style="font-size:11px;color:var(--text3);min-width:32px;text-align:right;">${pct}%</span>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
function _sparkline(history) {
|
||||
if (!history || history.length < 2) {
|
||||
return `<span style="font-size:11px;color:var(--text4);">Not enough data yet</span>`;
|
||||
}
|
||||
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 `
|
||||
<svg width="${w}" height="${h}" viewBox="0 0 ${w} ${h}" style="overflow:visible;display:block;">
|
||||
<defs>
|
||||
<linearGradient id="sparkGrad" x1="0" y1="0" x2="0" y2="1">
|
||||
<stop offset="0%" stop-color="var(--cyan)" stop-opacity="0.25"/>
|
||||
<stop offset="100%" stop-color="var(--cyan)" stop-opacity="0"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<polygon points="${fillPts}" fill="url(#sparkGrad)"/>
|
||||
<polyline points="${pts}" fill="none" stroke="var(--cyan)" stroke-width="1.5" stroke-linejoin="round" stroke-linecap="round"/>
|
||||
</svg>`;
|
||||
}
|
||||
|
||||
function _statChip(label, value, color) {
|
||||
return `
|
||||
<div style="display:flex;flex-direction:column;align-items:center;gap:2px;padding:8px 14px;background:var(--elevated);border:1px solid var(--border);border-radius:var(--radius-sm);min-width:72px;">
|
||||
<span style="font-size:15px;font-weight:700;color:${color || 'var(--text)'};">${value}</span>
|
||||
<span style="font-size:10px;color:var(--text4);text-transform:uppercase;letter-spacing:0.06em;">${label}</span>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
function _sectionCard(title, iconSvg, content) {
|
||||
return `
|
||||
<div class="card" style="margin-bottom:16px;">
|
||||
<div class="card-header" style="display:flex;align-items:center;gap:8px;">
|
||||
<span style="color:var(--cyan);display:flex;">${iconSvg}</span>
|
||||
<span class="card-title">${title}</span>
|
||||
</div>
|
||||
<div style="padding:0 16px 16px;">${content}</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
// ── 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 = `
|
||||
<div style="display:grid;grid-template-columns:1fr 1fr;gap:12px;margin-bottom:12px;">
|
||||
<div>
|
||||
<div style="font-size:11px;color:var(--text3);margin-bottom:4px;">Heap Used / Total</div>
|
||||
<div style="display:flex;align-items:center;gap:8px;">
|
||||
<span style="font-size:13px;font-weight:600;color:var(--text);">${_fmtBytes(mem.heapUsed)} / ${_fmtBytes(mem.heapTotal)}</span>
|
||||
</div>
|
||||
<div style="margin-top:6px;">${_memBar(mem.heapUsed, mem.heapTotal)}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div style="font-size:11px;color:var(--text3);margin-bottom:4px;">RSS (Physical)</div>
|
||||
<div style="font-size:13px;font-weight:600;color:var(--text);">${_fmtBytes(mem.rss)}</div>
|
||||
<div style="font-size:11px;color:var(--text4);margin-top:2px;">External: ${_fmtBytes(mem.external)}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div style="display:flex;gap:8px;flex-wrap:wrap;margin-bottom:4px;">
|
||||
${_statChip('PID', ps.process.pid, 'var(--text2)')}
|
||||
${_statChip('Uptime', _fmtUptime(ps.process.uptimeMs), 'var(--cyan)')}
|
||||
${_statChip('SW Uptime', swUptime, 'var(--text2)')}
|
||||
</div>`;
|
||||
} else if (!connected) {
|
||||
processRows = `<div style="color:var(--text4);font-size:13px;padding:8px 0;">Native host is not connected.</div>`;
|
||||
} else {
|
||||
processRows = `<div style="color:var(--text4);font-size:13px;padding:8px 0;">Loading process stats…</div>`;
|
||||
}
|
||||
|
||||
const reconnectInfo = `
|
||||
<div style="display:flex;gap:16px;margin-top:12px;padding-top:12px;border-top:1px solid var(--border);">
|
||||
<div>
|
||||
<div style="font-size:11px;color:var(--text3);">Reconnects</div>
|
||||
<div style="font-size:14px;font-weight:600;color:${(sh.reconnectCount || 0) > 0 ? 'var(--amber)' : 'var(--text)'};">${sh.reconnectCount || 0}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div style="font-size:11px;color:var(--text3);">Last Disconnect</div>
|
||||
<div style="font-size:13px;color:var(--text2);">${_fmtTime(sh.lastDisconnectAt)}</div>
|
||||
</div>
|
||||
</div>`;
|
||||
|
||||
const statusBadge = connected
|
||||
? `<span class="badge badge-green" style="margin-left:auto;">Connected</span>`
|
||||
: `<span class="badge" style="margin-left:auto;background:var(--red-dim);color:var(--red);">Disconnected</span>`;
|
||||
|
||||
const icon = `<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="2" y="3" width="6" height="6" rx="1"/><rect x="16" y="3" width="6" height="6" rx="1"/><rect x="9" y="15" width="6" height="6" rx="1"/><line x1="5" y1="9" x2="12" y2="15"/><line x1="19" y1="9" x2="12" y2="15"/></svg>`;
|
||||
|
||||
return _sectionCard(
|
||||
`<span style="display:flex;align-items:center;gap:8px;width:100%;">Native Host Process ${statusBadge}</span>`,
|
||||
icon,
|
||||
processRows + reconnectInfo
|
||||
);
|
||||
}
|
||||
|
||||
function _renderSystemResources(ps) {
|
||||
if (!ps || !ps.os) {
|
||||
return _sectionCard('System Resources',
|
||||
`<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="2" y="3" width="20" height="14" rx="2"/><line x1="8" y1="21" x2="16" y2="21"/><line x1="12" y1="17" x2="12" y2="21"/></svg>`,
|
||||
`<div style="color:var(--text4);font-size:13px;padding:8px 0;">Waiting for native host…</div>`
|
||||
);
|
||||
}
|
||||
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 = `
|
||||
<div style="margin-bottom:14px;">
|
||||
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:6px;">
|
||||
<span style="font-size:12px;color:var(--text3);">System Memory</span>
|
||||
<span style="font-size:12px;color:var(--text2);">${_fmtBytes(usedMem)} / ${_fmtBytes(o.totalMemory)}</span>
|
||||
</div>
|
||||
${_memBar(usedMem, o.totalMemory)}
|
||||
</div>
|
||||
<div style="margin-bottom:14px;">
|
||||
<div style="font-size:12px;color:var(--text3);margin-bottom:8px;">CPU Load Average</div>
|
||||
<div style="display:flex;gap:8px;flex-wrap:wrap;">
|
||||
${_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)')}
|
||||
</div>
|
||||
</div>
|
||||
<div style="display:flex;gap:16px;font-size:12px;color:var(--text3);">
|
||||
<span>Platform: <strong style="color:var(--text2);">${escapeHtml(o.platform || '—')}</strong></span>
|
||||
<span>Arch: <strong style="color:var(--text2);">${escapeHtml(o.arch || '—')}</strong></span>
|
||||
</div>`;
|
||||
|
||||
return _sectionCard('System Resources',
|
||||
`<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="2" y="3" width="20" height="14" rx="2"/><line x1="8" y1="21" x2="16" y2="21"/><line x1="12" y1="17" x2="12" y2="21"/></svg>`,
|
||||
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 `
|
||||
<div style="display:flex;align-items:center;justify-content:space-between;padding:8px 0;border-bottom:1px solid var(--border);">
|
||||
<span style="font-size:13px;color:var(--text2);">${label}</span>
|
||||
<span style="font-size:12px;color:var(--text4);">None</span>
|
||||
</div>`;
|
||||
return `
|
||||
<div style="display:flex;align-items:center;justify-content:space-between;padding:8px 0;border-bottom:1px solid var(--border);gap:8px;flex-wrap:wrap;">
|
||||
<span style="font-size:13px;font-weight:500;color:var(--text);">${label} <span style="color:var(--text3);font-weight:400;">(${total})</span></span>
|
||||
<div style="display:flex;gap:6px;flex-wrap:wrap;">
|
||||
${c.ready ? `<span class="badge badge-green">${c.ready} ready</span>` : ''}
|
||||
${c.connecting ? `<span class="badge" style="background:var(--amber-dim);color:var(--amber);">${c.connecting} connecting</span>` : ''}
|
||||
${c.error ? `<span class="badge" style="background:var(--red-dim);color:var(--red);">${c.error} error</span>` : ''}
|
||||
${c.closed ? `<span class="badge badge-neutral">${c.closed} closed</span>` : ''}
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
// 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 = `
|
||||
<div style="display:flex;align-items:center;justify-content:space-between;padding:8px 0 0;flex-wrap:wrap;gap:8px;">
|
||||
<span style="font-size:12px;color:var(--text3);">All tunnels (${total})</span>
|
||||
<div style="display:flex;gap:6px;flex-wrap:wrap;">
|
||||
${b.ready ? `<span class="badge badge-green">${b.ready} ready</span>` : ''}
|
||||
${b.connecting ? `<span class="badge" style="background:var(--amber-dim);color:var(--amber);">${b.connecting} connecting</span>` : ''}
|
||||
${b.error ? `<span class="badge" style="background:var(--red-dim);color:var(--red);">${b.error} error</span>` : ''}
|
||||
${b.closed ? `<span class="badge badge-neutral">${b.closed} closed</span>` : ''}
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
const content = `
|
||||
${stateRow('Server Tunnels', servers)}
|
||||
${stateRow('Virtual Hosts', vhosts)}
|
||||
${stateRow('Service Tunnels', svcTunnels)}
|
||||
${aggregateRow}`;
|
||||
|
||||
return _sectionCard('Tunnel Health',
|
||||
`<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 2L2 7l10 5 10-5-10-5z"/><path d="M2 17l10 5 10-5"/><path d="M2 12l10 5 10-5"/></svg>`,
|
||||
content
|
||||
);
|
||||
}
|
||||
|
||||
function _renderTraffic(state) {
|
||||
const ts = state.trafficStats;
|
||||
if (!ts) return '';
|
||||
|
||||
const content = `
|
||||
<div style="display:flex;gap:8px;flex-wrap:wrap;">
|
||||
${_statChip('Bytes In', _fmtBytes(ts.bytesIn), 'var(--green)')}
|
||||
${_statChip('Bytes Out', _fmtBytes(ts.bytesOut), 'var(--cyan)')}
|
||||
${_statChip('Requests', (ts.requests ?? 0).toLocaleString(), 'var(--text2)')}
|
||||
</div>`;
|
||||
|
||||
return _sectionCard('Proxy Traffic',
|
||||
`<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="22,12 18,12 15,21 9,3 6,12 2,12"/></svg>`,
|
||||
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 => `
|
||||
<tr>
|
||||
<td class="mono" style="font-size:11px;color:var(--text3);">${escapeHtml(truncate(c.swarmId || '—', 18))}</td>
|
||||
<td class="mono" style="font-size:11px;color:var(--cyan);">${escapeHtml(truncate(c.peerKey || '—', 18))}</td>
|
||||
<td style="font-size:12px;color:var(--text2);">${_fmtDuration(c.createdAt)}</td>
|
||||
</tr>`).join('');
|
||||
tableHtml = `
|
||||
<div style="margin-top:14px;overflow-x:auto;">
|
||||
<table style="width:100%;border-collapse:collapse;">
|
||||
<thead>
|
||||
<tr style="font-size:11px;color:var(--text4);text-transform:uppercase;letter-spacing:0.06em;">
|
||||
<th style="text-align:left;padding:4px 8px 6px 0;border-bottom:1px solid var(--border);">Swarm ID</th>
|
||||
<th style="text-align:left;padding:4px 8px 6px 0;border-bottom:1px solid var(--border);">Peer Key</th>
|
||||
<th style="text-align:left;padding:4px 0 6px 0;border-bottom:1px solid var(--border);">Duration</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>${rows}</tbody>
|
||||
</table>
|
||||
</div>`;
|
||||
} else {
|
||||
tableHtml = `<div style="font-size:12px;color:var(--text4);margin-top:12px;">No active peer connections.</div>`;
|
||||
}
|
||||
|
||||
const content = `
|
||||
<div style="display:flex;gap:8px;flex-wrap:wrap;margin-bottom:14px;">
|
||||
${_statChip('Active', liveCount, liveCount > 0 ? 'var(--green)' : 'var(--text2)')}
|
||||
${_statChip('Total Ever', sh.totalConnectionsEver || 0, 'var(--text2)')}
|
||||
</div>
|
||||
<div style="margin-bottom:4px;">
|
||||
<div style="font-size:11px;color:var(--text3);margin-bottom:6px;">Connections / minute (last 30 min)</div>
|
||||
${sparkHtml}
|
||||
</div>
|
||||
${tableHtml}`;
|
||||
|
||||
return _sectionCard('Peer Connections',
|
||||
`<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="2"/><circle cx="4" cy="6" r="2"/><circle cx="20" cy="6" r="2"/><circle cx="4" cy="18" r="2"/><circle cx="20" cy="18" r="2"/><line x1="6" y1="6" x2="10" y2="11"/><line x1="18" y1="6" x2="14" y2="11"/><line x1="6" y1="18" x2="10" y2="13"/><line x1="18" y1="18" x2="14" y2="13"/></svg>`,
|
||||
content
|
||||
);
|
||||
}
|
||||
|
||||
function _renderProxyInfo(state) {
|
||||
let version = '—';
|
||||
try { version = 'v' + chrome.runtime.getManifest().version; } catch (_) {}
|
||||
|
||||
const content = `
|
||||
<div style="display:flex;gap:8px;flex-wrap:wrap;">
|
||||
${_statChip('Extension', version, 'var(--text2)')}
|
||||
${_statChip('HTTPS Port', state.proxyPort ?? '—', 'var(--cyan)')}
|
||||
${_statChip('CONNECT Port', state.connectProxyPort ?? '—', 'var(--cyan)')}
|
||||
</div>`;
|
||||
|
||||
return _sectionCard('Extension & Proxy',
|
||||
`<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><line x1="2" y1="12" x2="22" y2="12"/><path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"/></svg>`,
|
||||
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 = `
|
||||
<div style="max-width:860px;">
|
||||
${_renderHostProcess(state, ps)}
|
||||
${_renderSystemResources(ps)}
|
||||
${_renderTunnelHealth(state, ps)}
|
||||
${_renderTraffic(state)}
|
||||
${_renderConnections(state)}
|
||||
${_renderProxyInfo(state)}
|
||||
</div>`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
}
|
||||
@@ -102,7 +102,6 @@ async function refresh() {
|
||||
updateTabsTable(state);
|
||||
updateServiceTunnelsTable(state);
|
||||
updateSettingsUI();
|
||||
updateStatsPage(state);
|
||||
}
|
||||
const sshCountEl = $('sshCount');
|
||||
if (sshCountEl) sshCountEl.textContent = sshConnections.length;
|
||||
|
||||
@@ -9,23 +9,6 @@ const { STORAGE_PATH } = require('./paths.js');
|
||||
const { log, debugLog } = require('./logger.js');
|
||||
const { initStartup, getProxiesReadyPromise, getTunnelsRestoredPromise, setTunnelsRestoredPromise, restorePersistedTunnels } = require('./startup.js');
|
||||
|
||||
const os = require('os');
|
||||
const _hostStartTime = Date.now();
|
||||
|
||||
function _computeTunnelStateCounts(manager) {
|
||||
const counts = { ready: 0, error: 0, closed: 0, connecting: 0 };
|
||||
const all = [
|
||||
...(manager.getServers() || []),
|
||||
...(manager.getVirtualHosts() || []),
|
||||
...(manager.getServiceTunnels() || []),
|
||||
];
|
||||
for (const t of all) {
|
||||
const s = t.state || 'connecting';
|
||||
if (s in counts) counts[s]++;
|
||||
else counts.connecting++;
|
||||
}
|
||||
return counts;
|
||||
}
|
||||
|
||||
const holesailManager = require('../holesail-manager.js');
|
||||
holesailManager.setStoragePath(STORAGE_PATH);
|
||||
@@ -348,50 +331,6 @@ async function handleMessageAsync(send, msg) {
|
||||
break;
|
||||
}
|
||||
|
||||
case 'getProcessStats': {
|
||||
const mem = process.memoryUsage();
|
||||
let loadAvg = [0, 0, 0];
|
||||
let cpuCount = 1;
|
||||
let platform = '';
|
||||
let arch = '';
|
||||
let totalMemory = 0;
|
||||
let freeMemory = 0;
|
||||
try { const la = os.loadavg(); loadAvg = Array.isArray(la) ? la : [0, 0, 0]; } catch (_) {}
|
||||
try { cpuCount = os.cpus().length; } catch (_) {}
|
||||
try { platform = os.platform(); } catch (_) {}
|
||||
try { arch = os.arch(); } catch (_) {}
|
||||
try { totalMemory = os.totalmem(); } catch (_) {}
|
||||
try { freeMemory = os.freemem(); } catch (_) {}
|
||||
reply({
|
||||
ok: true,
|
||||
process: {
|
||||
memoryUsage: {
|
||||
rss: mem.rss || 0,
|
||||
heapTotal: mem.heapTotal || 0,
|
||||
heapUsed: mem.heapUsed || 0,
|
||||
external: mem.external || 0,
|
||||
},
|
||||
uptimeMs: Date.now() - _hostStartTime,
|
||||
pid: process.pid,
|
||||
},
|
||||
os: {
|
||||
totalMemory,
|
||||
freeMemory,
|
||||
cpuCount,
|
||||
loadAvg,
|
||||
platform,
|
||||
arch,
|
||||
},
|
||||
tunnels: {
|
||||
serverCount: holesailManager.getServers().length,
|
||||
virtualHostCount: holesailManager.getVirtualHosts().length,
|
||||
serviceTunnelCount: holesailManager.getServiceTunnels().length,
|
||||
byState: _computeTunnelStateCounts(holesailManager),
|
||||
},
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
reply({ ok: false, error: `Unknown command: ${type}` });
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user